¿Qué hace d1-cli — Tiendas D1 desde la línea de comandos?
D1 es la cadena de supermercados hard-discount más grande de Colombia. Su tienda en línea funciona con VTEX IO (cuenta d1tiendas), lo que significa que la API pública del sitio es la superficie de integración — sin scraping, sin credenciales de administrador.
Invocar
cd skills/commerce/d1-cli
# 1. Resuelve tu punto de entrega PRIMERO — todo lo demás depende de esto
bun run src/cli.ts region --lat 4.6486 --lng=-74.0628
# 2. Luego busca: los precios y el stock ahora son de tu tienda, no del país
bun run src/cli.ts search leche entera --count 5
bun run src/cli.ts search --facets category-1/lacteos-y-huevos --sort price:asc
# 3. Cotiza una canasta sin tocar tu carrito
bun run src/cli.ts quote 262:2 892:1
# 3b. O deja que un presupuesto decida qué incluir — términos, no SKUs
bun run src/cli.ts basket --budget 30000 arroz leche aceite huevos
# 4. O construye una real, luego pásala a un humano para pagar
bun run src/cli.ts cart add 262 --qty 2
bun run src/cli.ts cart deliver-to
bun run src/cli.ts cart checkout # imprime una URL; no paga
Agrega --json a cualquier comando para obtener salida legible por el agente. Los códigos de salida son un contrato, y esta lista es la única copia de él:
| código | significado | ¿reintentar? |
|---|---|---|
0 | funcionó | — |
1 | D1 rechazó o fue inalcanzable — punto no entregable, SKU no disponible o desconocido, interrupción | sí, puede ayudar |
2 | el comando se invocó incorrectamente | nunca ayuda |
3 | funcionó y la respuesta es "ninguno" (substitute, basket) | nunca ayuda |
Un agente que no pueda separar estos o reintenta un error tipográfico para siempre o abandona una interrupción transitoria. 3 existe porque una categoría vacía no es ninguna de las otras.
Ten en cuenta que cart add establece la línea a --qty; no agrega a ella. Esa es la semántica propia de D1, verificada en vivo.
Las dos cosas que hacen que la automatización ingenua de D1 sea incorrecta
La disponibilidad está regionalizada. Si consultas el catálogo sin región, D1 responde desde el catálogo nacional, reportando cómodamente 10,000 unidades contra el vendedor ficticio 1. Pon el mismo SKU a través de una simulación de checkout para una dirección real y regresa withoutStock, porque la tienda física que lo enviaría no lo tiene. Siempre ejecuta d1 region primero; el CLI advierte en texto plano cada vez que te muestra precios nacionales.
Resolver una región también es la parte menos adivinable de la API: las coordenadas se envían como geoCoordinates={lon};{lat} — separadas por punto y coma, longitud primero. Una coma, que es lo que los propios cuerpos JSON de VTEX usan para el mismo par, se rechaza con CHK0119 ("las direcciones deben tener un código postal o geocoordenadas") — un mensaje que parece indicar que el parámetro falta en lugar de estar mal formado. test/region.test.ts fija esto para que una limpieza no pueda deshacerlo.
Los precios llegan en dos unidades. Para el SKU 262 (Leche Entera Latti 900 ml, COP 3,500 en el estante):
| API | Campo | Valor | Unidad |
|---|---|---|---|
| intelligent-search | Price | 3500 | pesos enteros |
| catalog_system | Price | 3500.0 | pesos enteros |
| checkout orderForm | sellingPrice | 350000 | centésimas |
Ninguno etiqueta su unidad. catalog.ts normaliza todo a centésimas en el límite para que el resto del código tenga exactamente una unidad, y una prueba afirma que un precio obtenido de búsqueda se renderiza idéntico a uno obtenido de checkout.
Lo que no hará
d1 construye y cotiza canastas. No paga. d1 cart checkout imprime la URL donde un humano revisa el total y completa el pago.
Ese límite se verifica en dos lugares desde una sola lista (src/endpoints.ts): en tiempo de ejecución, D1Client.request rechaza cualquier solicitud cuya URL resuelta no sea el origen propio de D1 más uno de los 18 caminos aprobados antes de que la solicitud salga del proceso; estáticamente, test/safety.test.ts falla si algún literal /api/ en cualquier lugar bajo src/ no está aprobado. Agregar un endpoint — de pago o de otro tipo — falla hasta que se liste, por lo que la decisión se toma en lugar de asumirse por defecto.
La mitad en tiempo de ejecución es crítica y se agregó después de que una revisión mostrara que un escaneo estático no es suficiente: --facets '../../../..' resolvió fuera del endpoint de búsqueda hacia la liquidación de pedidos mientras todos los literales de origen permanecían aprobados.
Dicho con precisión, porque versiones anteriores de este archivo exageraban: nada aquí impide que un colaborador con acceso de escritura edite la lista de permitidos. Ninguna prueba dentro del repositorio puede hacerlo.
La única credencial que almacena es un token de sesión de la tienda — lo mismo que tiene un navegador con sesión iniciada, limitado a los pedidos y carrito de su propietario — escrito en ~/.config/d1-cli/session.json con modo 0600. Ningún appKey/appToken de administrador de VTEX se lee en ningún lado. El inicio de sesión es mediante un código enviado por correo electrónico de un solo uso; la autenticación por contraseña no está implementada deliberadamente.
La puerta de checkout — termina siempre aquí
Finaliza cada canasta con d1 cart checkout y actúa según su código de salida. No es una formalidad: es el único comando que vuelve a verificar todo el carrito contra D1 tal como está ahora, y los carritos se vuelven obsoletos de maneras que nada más revela.
0 cada línea es entregable — la URL es segura para entregar
1 al menos una línea NO se puede entregar; la URL se imprime pero el carrito está roto
2 el comando se invocó incorrectamente
Esto existe porque cada una de estas situaciones ocurrió en una canasta real:
- Una línea se agotó durante la noche.
PAN ARTESANAL INTEGRALquedóno tiene inventariohoras después de agregarse. El carrito todavía se renderizaba, todavía tenía un total — un total que silenciosamente era 6.490 más bajo, porque la línea muerta había desaparecido de él. - Un cambio de dirección dejó varadas todas las líneas. Nueve artículos regresaron
cannotBeDeliveredmientras cada uno aún tenía un SLA válido y el carrito aún mostraba un total pagable. - El envío se subestimó 12 veces por una asignación por línea que parecía un total.
En los tres casos, el carrito parecía estar bien. Una cotización tomada antes en una sesión no es evidencia sobre el carrito ahora, así que nunca entregues una URL de checkout que obtuviste antes de la última mutación — vuelve a ejecutar la puerta.
Cuando devuelve 1, --json da undeliverable[] nombrando las líneas, y readyToCheckout: false. Elimina o reemplaza esas líneas y ejecútalo de nuevo.
Reemplazar una línea que D1 no puede suministrar
d1 substitute 192 --limit 5 # clasifica productos en stock de la misma categoría
Salida 0 significa que hay algo que proponer; 3 significa que no lo hay — uno de los dos comandos que usan 3, junto con d1 basket.
Tres, no uno, y la distinción es el punto: 1 significa "D1 rechazó o no pudo ser contactado" en cualquier otro lugar de este CLI, e invita a reintentar. Una categoría vacía nunca se vuelve no vacía al reintentar, por lo que un agente con una política de reintentar en 1 se repetiría para siempre. La salida 0 con una lista vacía es la otra respuesta incorrecta — se lee como éxito y se actúa sobre ella.
Cada candidato nombra qué cambia: marca, tamaño de empaque, $/kg o $/L, precio del empaque, y cualquier advertencia de la Ley 2120 ganada o perdida. Eso es la salida, no la puntuación. Un -12% por litro para los mismos 900 ml y la misma etiqueta es una propuesta diferente de una que llega con Exceso en Azúcares adjunto, y solo los deltas las distinguen.
Propónlo; no lo tomes. substitute nunca escribe — imprime el d1 cart add para que un humano lo apruebe. Esto no es precaución por sí misma: el cliente prefirió dos veces que se eliminara una línea en lugar de un sustituto incorrecto, lo dijo explícitamente, y tuvo razón en ambas ocasiones. Presenta los candidatos y las compensaciones; deja que la persona decida, y estate dispuesto a eliminar la línea.
Tres cosas que te dirá que vale la pena leer:
- "Esto todavía está en stock en tu tienda." La disponibilidad es por tienda, por lo que una línea que falló en otro lugar puede estar bien aquí. Verifica antes de reemplazar cualquier cosa. Sin
--lat/--lnglo dice en lugar de adivinar — no hay tienda sobre la cual hacer una afirmación por tienda. - "ampliado al nivel 2 de 3" — la categoría hoja no tenía nada en stock, por lo que las sugerencias provienen de un pasillo más amplio y son correspondientemente menos estrictas.
- "solo N fueron comparados" — la categoría es más grande que una página. Aumenta
--count. Esto se imprime también en el resultado vacío, porque "nada aquí está en stock" afirmado sobre un barrido parcial necesita la advertencia aún más, no menos.
Encontrar cosas cuando la solicitud es vaga
Para "necesitamos arroz" en lugar de "compra el SKU 1092", dos ejes importan y ninguno es el predeterminado:
Ordena por precio unitario, no por precio de empaque. --sort per-unit utiliza los datos PUM que la ley colombiana exige que D1 publique. Las dos clasificaciones realmente discrepan: ARROZ ESTÁNDAR 500 GRS es el empaque más barato a $1.550 y cuesta $3.100/kg, mientras que el mejor valor en los mismos resultados es $2.775/kg — en una bolsa de $5.550 que no aparece en ningún lugar del top cinco por precio de empaque. Ordenar por precio de empaque da una respuesta peor mientras parece correcta.
Debido a que la búsqueda de D1 no puede ordenar por esto, --sort per-unit ordena la página que obtuviste, no todo el conjunto de resultados. Aumenta --count (máx. 50) para ampliarlo. La salida lo dice en lugar de implicar un superlativo que no puede respaldar, y nombra cuántos resultados no publican tamaño y por lo tanto no se pueden comparar.
El tamaño declarado de un multipaquete es el empaque, no un artículo — no lo "corrijas". Un censo de todos los 1,600 productos encontró que entre los multipaquetes con nombre coincidente, 44 de los 46 donde D1 dice lo suficiente para decidir declaran el total del empaque. Analizar N UN de un nombre de producto y multiplicar corrompería esos 44 para arreglar 2. El recuento en un nombre no es evidencia sobre lo que significa el PUM.
resolvePackSize por lo tanto lee descripciones en lugar de nombres. En todos los 1,548 productos que llevan un PUM, actúa en los 10 cuya descripción establece tanto un tamaño por artículo como un recuento: tres se corrigen (SKU 718, 510, 1008), siete se confirman como ya declarando el empaque, y todo lo demás se deja como está.
Lee las etiquetas de advertencia. Los productos llevan las advertencias frontales de Colombia (Exceso en Azúcares, Exceso en sodio, Exceso en grasas saturadas, Exceso en grasas trans, Contiene Edulcorantes) en warnings[] bajo --json. La cobertura es parcial — aproximadamente 70–90% por categoría — por lo que la ausencia significa "no declarado", nunca "seguro".
Luego reduce con d1 facets <query> (categoría, marca, subcategoría con recuentos) antes de paginar: la búsqueda tiene un límite de 50 páginas, por lo que una consulta amplia es mejor cortarla por faceta que recorrerla.
Comandos
| Comando | Qué hace |
|---|---|
region --lat --lng | resuelve y recuerda el punto de entrega |
search <query> | encuentra productos (--facets --sort --count --page --available) |
suggest <parcial> · trending | autocompletar · qué está buscando Colombia |
categories · facets [query] | árbol de departamentos · filtros para una consulta |
substitute <sku> | qué comprar en su lugar, y qué cambia (--limit --count) |
quote <sku>[:qty]... | cotiza una canasta, sin mutar el carrito |
| `cart [show | add |
login --email / --from-cookie · whoami · logout | cuenta |
orders · order <id> | historial de pedidos (el detalle está redactado a una lista de permitidos; --raw opta por mostrarlo completo) |
stores near | tiendas D1 cerca de un punto — solo ubicaciones, D1 no ofrece recogida en tienda |
Tests
bun test # unidades monetarias, la trampa del punto y coma, cantidad en cotización,
# vacío de SKU desconocido, normalización de carrito, redacción de pedidos, y
# la lista de endpoints permitidos que limita la superficie de pago
bun run lint && bun run typecheck
Los códigos de salida son un contrato, no una decoración — la tabla bajo Invocar arriba es la única copia de él.
Consulta README.md para el mapa completo de endpoints, el comportamiento de los niveles de envío y lo que la API pública no te dará.