@openfactura/cli 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +395 -2
  3. package/dist/index.js +2137 -0
  4. package/package.json +51 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xaviigna
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,396 @@
1
- # Temporary Holding Version
1
+ # @openfactura/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Cliente de línea de comandos de [Open Factura](../../README.md), la plataforma abierta de facturación
4
+ electrónica de El Salvador (DTE, Ministerio de Hacienda). Emite, consulta e invalida documentos desde la
5
+ terminal o desde un script, e incluye un servidor MCP por stdio para Claude y otros asistentes.
6
+
7
+ La CLI habla con un servidor de Open Factura únicamente por su API REST: no firma ni transmite por su
8
+ cuenta, y no necesita el certificado de Hacienda en su equipo.
9
+
10
+ ## Instalación
11
+
12
+ Requiere Node.js 20 o superior (también funciona con Bun).
13
+
14
+ ```bash
15
+ npm i -g @openfactura/cli
16
+ openfactura --version
17
+
18
+ # o sin instalar
19
+ npx @openfactura/cli --help
20
+ ```
21
+
22
+ ## Inicio de sesión
23
+
24
+ Cree una clave de API en el panel web (Desarrolladores → Nueva clave). Las claves `of_test_…` trabajan contra
25
+ el ambiente de pruebas del MH y las `of_live_…` contra producción.
26
+
27
+ ```bash
28
+ openfactura login --url https://factura.su-empresa.sv
29
+ # Clave de API (of_test_… u of_live_…): ← se teclea sin mostrarse
30
+ ```
31
+
32
+ `login` verifica la clave contra el servidor y la guarda en `~/.config/openfactura/config.json`
33
+ (o en `$XDG_CONFIG_HOME/openfactura/config.json`) con permisos `0600`. La clave completa nunca se
34
+ imprime: la CLI muestra sólo sus primeros 14 caracteres.
35
+
36
+ ```bash
37
+ openfactura login --url https://factura.su-empresa.sv --key of_test_… # sin preguntar
38
+ echo "$CLAVE" | openfactura login # desde una tubería
39
+ openfactura login --profile produccion # otro perfil
40
+ openfactura logout # borra la clave guardada
41
+ ```
42
+
43
+ Evite `--key` en equipos compartidos: el valor queda en el historial de la terminal.
44
+
45
+ En un servidor de integración continua no hace falta `login`; basta con las variables de entorno:
46
+
47
+ ```bash
48
+ export OPENFACTURA_URL=https://factura.su-empresa.sv
49
+ export OPENFACTURA_API_KEY=of_test_…
50
+ openfactura whoami
51
+ ```
52
+
53
+ ### De dónde sale cada valor
54
+
55
+ | Valor | Orden de prioridad |
56
+ | ---------------- | ----------------------------------------------------------------------- |
57
+ | URL del servidor | `--url` → `OPENFACTURA_URL` → perfil guardado → `http://localhost:8788` |
58
+ | Clave de API | `OPENFACTURA_API_KEY` → perfil guardado |
59
+ | Perfil | `--profile` → `OPENFACTURA_PROFILE` → `default` |
60
+
61
+ ## Comandos
62
+
63
+ Todos aceptan `--json` (imprime el JSON de la API en stdout, para scripts), `--url`, `--profile` y
64
+ `--help`.
65
+
66
+ ### Sesión y emisor
67
+
68
+ ```bash
69
+ openfactura whoami # actor, permisos, ambiente, servidor y prefijo de la clave
70
+ openfactura emisor # datos fiscales, establecimientos, puntos de venta y credenciales cargadas
71
+ ```
72
+
73
+ ```text
74
+ $ openfactura whoami
75
+ Actor: clave
76
+ Organización: aaFgOwpa4md9Tl6Ct9e42AJ2jNnAltjh
77
+ Ambiente: 00 Pruebas (simulador del MH)
78
+ Servidor: http://localhost:8788 (defecto)
79
+ Clave: of_test_0d2aEw… (entorno)
80
+ Perfil: default
81
+ Permisos: dte:read, dte:emit, dte:invalidate, clientes:read, …
82
+ ```
83
+
84
+ ### Clientes
85
+
86
+ ```bash
87
+ openfactura clientes list
88
+ openfactura clientes list --q "contribuyente"
89
+
90
+ openfactura clientes add --file cliente.json
91
+ openfactura clientes add \
92
+ --tipo-documento 36 --num-documento 0614-290786-103-1 --nrc 234567-8 \
93
+ --nombre "CLIENTE CONTRIBUYENTE, S.A. DE C.V." \
94
+ --cod-actividad 46900 --desc-actividad "Venta al por mayor de otros productos" \
95
+ --departamento 06 --municipio 23 --distrito 14 --complemento "Av. Olímpica #100" \
96
+ --telefono 22445566 --correo compras@cliente.com.sv
97
+ ```
98
+
99
+ `--tipo-documento`: `36` NIT, `13` DUI, `03` pasaporte, `02` carné de residente, `37` otro. También
100
+ existe `--nombre-comercial`. El archivo de `--file` lleva los mismos campos que la API
101
+ (`tipoDocumento`, `numDocumento`, `nombre`, …); `--file -` lee la entrada estándar.
102
+
103
+ ### Productos
104
+
105
+ ```bash
106
+ openfactura productos list --q licencia
107
+
108
+ openfactura productos add --file producto.json
109
+ openfactura productos add --codigo LIC-001 --descripcion "Licencia anual" --precio 100 --tipo-item 2
110
+ ```
111
+
112
+ `--precio` va **sin IVA**. `--tipo-item`: `1` bien (por defecto), `2` servicio, `3` ambos, `4` otros
113
+ tributos. `--uni-medida`: código de CAT-014 (por defecto `59`, unidad). `--tipo-venta`: `gravada`
114
+ (por defecto), `exenta` o `noSujeta`.
115
+
116
+ ### Catálogos del MH
117
+
118
+ ```bash
119
+ openfactura catalogos # lista de catálogos
120
+ openfactura catalogos CAT-012 # departamentos
121
+ openfactura catalogos CAT-013 --departamento 06 # municipios de San Salvador
122
+ openfactura catalogos CAT-019 --q "programación" # actividades económicas
123
+ ```
124
+
125
+ ### DTE
126
+
127
+ El archivo de emisión es el mismo cuerpo de `POST /api/v1/dte`:
128
+
129
+ ```json
130
+ {
131
+ "tipoDte": "01",
132
+ "datos": {
133
+ "items": [
134
+ { "descripcion": "Servicio de consultoría", "cantidad": 2, "precioUni": 11.3, "tipoItem": 2 }
135
+ ]
136
+ }
137
+ }
138
+ ```
139
+
140
+ - `tipoDte`: `01` Factura, `03` Crédito Fiscal, `04` Nota de Remisión, `05` Nota de Crédito, `06` Nota de
141
+ Débito, `07` Comprobante de Retención, `11` Factura de Exportación, `14` Sujeto Excluido.
142
+ - `datos`: `items`, `receptor`, `condicionOperacion`, `pagos`, `documentosRelacionados`… En Factura,
143
+ `precioUni` **incluye** IVA; en Crédito Fiscal, notas y remisión va **sin** IVA.
144
+ - `clienteId` (opcional): usa un cliente guardado como receptor. `puntoVentaId` (opcional): punto de
145
+ venta que emite; por defecto el primero.
146
+
147
+ Un Crédito Fiscal para un cliente guardado:
148
+
149
+ ```json
150
+ {
151
+ "tipoDte": "03",
152
+ "clienteId": "c6de4c6c-d772-4f19-811a-5419655dcd06",
153
+ "datos": { "items": [{ "descripcion": "Licencia anual", "cantidad": 1, "precioUni": 100 }] }
154
+ }
155
+ ```
156
+
157
+ #### Validar
158
+
159
+ ```bash
160
+ openfactura dte validar factura.json
161
+ cat factura.json | openfactura dte validar -
162
+ ```
163
+
164
+ Construye el documento y lo valida contra el esquema del MH sin consumir número de control, sin firmar y
165
+ sin transmitir. Imprime los totales y los problemas; termina con salida `1` si hay problemas.
166
+
167
+ #### Emitir
168
+
169
+ ```bash
170
+ openfactura dte emitir factura.json
171
+ generar-factura | openfactura dte emitir - --json > emitido.json
172
+ ```
173
+
174
+ ```text
175
+ $ openfactura dte emitir factura.json
176
+ Idempotency-Key: 853c48f2-f488-45d5-af00-0369495f0c05
177
+ Estado: procesado
178
+ Tipo: 01 Factura
179
+ Número de control: DTE-01-M001P001-000000000000002
180
+ Código de generación: 0862382B-0BB8-42A0-BB6B-B42DCCDF1EA4
181
+ Total: $22.60
182
+ Sello recibido: 20265EE94C1B2C4CBB56F56121E37F5AC8CA7D27
183
+ ```
184
+
185
+ Cada emisión lleva una `Idempotency-Key`. Si no la indica, la CLI genera un UUID y lo muestra (por
186
+ stderr) **antes** de enviar. Si la red falla y no sabe si el documento se emitió, repita el comando con
187
+ esa misma clave: el servidor devuelve el resultado original y no emite dos veces.
188
+
189
+ ```bash
190
+ openfactura dte emitir factura.json --idempotency-key 853c48f2-f488-45d5-af00-0369495f0c05
191
+ openfactura dte emitir factura.json --idempotency-key pedido-1042 # su propio identificador
192
+ ```
193
+
194
+ Si el MH no responde, el documento queda firmado en estado `contingencia` y se transmite después con
195
+ `openfactura contingencia transmitir`.
196
+
197
+ #### Consultar
198
+
199
+ ```bash
200
+ openfactura dte list
201
+ openfactura dte list --tipo 01 --estado procesado --desde 2026-11-01 --hasta 2026-11-30
202
+ openfactura dte list --q "cliente contribuyente" --pagina 2
203
+
204
+ openfactura dte get 0862382B-0BB8-42A0-BB6B-B42DCCDF1EA4 # estado, sello, respuesta del MH, eventos
205
+
206
+ openfactura dte json 0862382B-0BB8-42A0-BB6B-B42DCCDF1EA4 # JSON oficial a stdout
207
+ openfactura dte json 0862382B-0BB8-42A0-BB6B-B42DCCDF1EA4 -o dte.json
208
+
209
+ openfactura dte pdf 0862382B-0BB8-42A0-BB6B-B42DCCDF1EA4 # guarda <codigo>.pdf aquí
210
+ openfactura dte pdf 0862382B-0BB8-42A0-BB6B-B42DCCDF1EA4 -o factura.pdf
211
+ ```
212
+
213
+ `--estado`: `pendiente`, `procesado`, `rechazado`, `contingencia` o `invalidado`.
214
+
215
+ #### Invalidar
216
+
217
+ ```bash
218
+ openfactura dte invalidar 0862382B-0BB8-42A0-BB6B-B42DCCDF1EA4 --tipo 2 --motivo "Operación rescindida"
219
+ openfactura dte invalidar <codigo> --tipo 1 --reemplazo <codigo-del-dte-que-lo-corrige>
220
+ ```
221
+
222
+ `--tipo`: `1` error en la información (requiere `--reemplazo`), `2` rescindir la operación, `3` otro
223
+ (requiere `--motivo` y `--reemplazo`). La invalidación es irreversible. El mismo comando invalida un
224
+ evento de retorno u operaciones especiales si recibe su código. Un DTE con un retorno vigente no se
225
+ invalida hasta invalidar antes el retorno.
226
+
227
+ #### Retorno (evento 18)
228
+
229
+ Devolución de bienes, reembolso o envases sobre una Factura (01), Factura de Exportación (11) o Sujeto
230
+ Excluido (14) procesados, sin invalidar el documento. Plazo: 3 meses desde la fecha del documento.
231
+
232
+ ```bash
233
+ echo '{ "items": [{ "descripcion": "Silla", "cantidad": 1, "precioUni": 22.6 }] }' \
234
+ | openfactura dte retorno 0862382B-0BB8-42A0-BB6B-B42DCCDF1EA4 -
235
+ ```
236
+
237
+ `precioUni` va como en el documento original: en Factura con IVA, en 11 y 14 sin IVA. Como `dte emitir`,
238
+ lleva `Idempotency-Key` y pide confirmación con una clave `of_live_…`.
239
+
240
+ ### Eventos
241
+
242
+ ```bash
243
+ openfactura eventos operaciones-especiales simplificadas.json # evento 17
244
+ openfactura eventos list --tipo retorno
245
+ openfactura eventos get <codigo>
246
+ openfactura eventos json <codigo> -o retorno.json
247
+ openfactura eventos pdf <codigo> -o retorno.pdf
248
+ ```
249
+
250
+ El archivo de operaciones especiales informa facturas simplificadas (`"tipoDocumento": "02"`, por rangos)
251
+ o comprobantes de control interno (`"97"`):
252
+
253
+ ```json
254
+ {
255
+ "tipoDocumento": "02",
256
+ "fecha": "2026-10-31",
257
+ "items": [
258
+ { "descripcion": "Facturas simplificadas", "docDel": "1", "docAl": "25", "precioUni": 10 }
259
+ ]
260
+ }
261
+ ```
262
+
263
+ ### Contingencia
264
+
265
+ ```bash
266
+ openfactura contingencia transmitir
267
+ ```
268
+
269
+ ### Webhooks
270
+
271
+ Su sistema recibe un `POST` firmado cuando un documento cambia de estado. La clave necesita el permiso
272
+ `webhooks:manage`.
273
+
274
+ ```bash
275
+ openfactura webhooks add https://erp.su-empresa.sv/hooks/openfactura \
276
+ --eventos dte.procesado,dte.rechazado --descripcion "ERP" # sin --eventos: todos
277
+ openfactura webhooks list
278
+ openfactura webhooks test <id> # envía webhook.prueba; salida 1 si no se entrega
279
+ openfactura webhooks deliveries <id> # últimas 50 entregas, con intentos y errores
280
+ openfactura webhooks rotate <id> # secreto nuevo; el anterior deja de firmar
281
+ openfactura webhooks remove <id>
282
+ ```
283
+
284
+ `add` y `rotate` muestran el secreto de firma (`whsec_…`) una sola vez. Eventos: `dte.procesado`,
285
+ `dte.rechazado`, `dte.contingencia`, `dte.invalidado`, `retorno.procesado`,
286
+ `operaciones_especiales.procesado`. Cómo verificar la cabecera `OpenFactura-Signature`: sección
287
+ «Webhooks» de `docs/api.md` en el repositorio del servidor.
288
+
289
+ ### Confirmación en producción
290
+
291
+ Con una clave `of_live_…`, `dte emitir`, `dte retorno`, `eventos operaciones-especiales`,
292
+ `dte invalidar` y `contingencia transmitir` piden confirmación
293
+ porque tienen efecto fiscal. Use `--yes` para omitirla. Si no hay una terminal interactiva (un script,
294
+ una tubería, `dte emitir -`), la CLI no pregunta: exige `--yes` y termina con salida `2` si falta.
295
+
296
+ ## Uso en scripts
297
+
298
+ `--json` imprime en stdout exactamente el JSON de la API; los avisos y errores van a stderr.
299
+
300
+ ```bash
301
+ codigo=$(openfactura dte emitir factura.json --json | jq -r .codigoGeneracion)
302
+ openfactura dte pdf "$codigo" -o "facturas/$codigo.pdf"
303
+ ```
304
+
305
+ Los errores se imprimen en stderr como `Error [codigo]: mensaje`, seguidos del detalle: cada problema de
306
+ `dte_invalido` como `ruta: mensaje`, o las observaciones del MH en `dte_rechazado`. Con `--json`, stderr
307
+ recibe el JSON de error de la API (`{ "error": { "code", "message", "details" } }`); en un `429` se le
308
+ agrega el campo `retryAfter` con los segundos de espera.
309
+
310
+ ### Códigos de salida
311
+
312
+ | Código | Significado |
313
+ | ------ | -------------------------------------------------------------------------------------- |
314
+ | `0` | Todo bien |
315
+ | `1` | Error de la API o de validación (incluye `dte validar` con problemas y errores de red) |
316
+ | `2` | Uso incorrecto: opción o argumento inválido, archivo ilegible, falta `--yes` |
317
+ | `3` | Sin autenticar: falta la clave, o es inválida, fue revocada o expiró |
318
+ | `4` | Límite de peticiones excedido; se imprime `Retry-After` |
319
+
320
+ ### Variables de entorno
321
+
322
+ | Variable | Uso |
323
+ | --------------------- | ---------------------------------------------------------- |
324
+ | `OPENFACTURA_API_KEY` | Clave de API. Tiene prioridad sobre la clave guardada |
325
+ | `OPENFACTURA_URL` | URL del servidor |
326
+ | `OPENFACTURA_PROFILE` | Perfil de configuración (por defecto `default`) |
327
+ | `XDG_CONFIG_HOME` | Carpeta base de la configuración (por defecto `~/.config`) |
328
+
329
+ ## Servidor MCP (stdio)
330
+
331
+ `openfactura mcp` es un servidor [MCP](https://modelcontextprotocol.io) por stdio. Sirve para los
332
+ clientes que no pueden enviar un encabezado HTTP fijo al servidor MCP remoto (`/mcp`). Expone las mismas
333
+ herramientas que el remoto y las ejecuta llamando a la API REST con su clave:
334
+
335
+ `emisor_obtener`, `catalogo_buscar`, `clientes_buscar`, `cliente_crear`, `productos_buscar`,
336
+ `producto_crear`, `dte_validar`, `dte_emitir`, `dte_listar`, `dte_obtener`, `dte_invalidar`,
337
+ `dte_retorno`, `operaciones_especiales_informar`, `eventos_listar`, `evento_obtener`,
338
+ `webhooks_listar`, `webhook_crear`, `webhook_eliminar`, `webhook_probar`.
339
+
340
+ Cada herramienta exige el mismo permiso que su ruta REST, así que la clave decide qué puede hacer el
341
+ asistente. Empiece con una clave `of_test_…`; `dte_emitir`, `dte_retorno`, `operaciones_especiales_informar` y
342
+ `dte_invalidar` tienen efecto fiscal con una
343
+ clave `of_live_…`, y aquí no hay confirmación de la CLI: la da usted en su cliente MCP al aprobar la
344
+ llamada.
345
+
346
+ ### Claude Code
347
+
348
+ ```bash
349
+ claude mcp add openfactura \
350
+ --env OPENFACTURA_API_KEY=of_test_… \
351
+ --env OPENFACTURA_URL=https://factura.su-empresa.sv \
352
+ -- npx -y @openfactura/cli mcp
353
+ ```
354
+
355
+ ### Claude Desktop
356
+
357
+ En `claude_desktop_config.json` (o en un `.mcp.json` de proyecto para Claude Code):
358
+
359
+ ```json
360
+ {
361
+ "mcpServers": {
362
+ "openfactura": {
363
+ "command": "npx",
364
+ "args": ["-y", "@openfactura/cli", "mcp"],
365
+ "env": {
366
+ "OPENFACTURA_API_KEY": "of_test_…",
367
+ "OPENFACTURA_URL": "https://factura.su-empresa.sv"
368
+ }
369
+ }
370
+ }
371
+ }
372
+ ```
373
+
374
+ Si ya ejecutó `openfactura login` en ese equipo puede omitir `env`: el servidor usa el perfil guardado.
375
+
376
+ Si su cliente sí admite encabezados HTTP, puede conectarse al servidor remoto sin instalar nada:
377
+
378
+ ```bash
379
+ claude mcp add --transport http openfactura https://factura.su-empresa.sv/mcp \
380
+ --header "Authorization: Bearer of_test_…"
381
+ ```
382
+
383
+ ## Desarrollo
384
+
385
+ ```bash
386
+ bun install
387
+ bun run dev -- whoami # ejecuta desde el código fuente
388
+ bun run build # dist/index.js
389
+ bun run test
390
+ bun run type-check
391
+ ```
392
+
393
+ ## Licencia
394
+
395
+ MIT. La CLI es un cliente independiente del servidor de Open Factura (AGPL-3.0): se comunica con él
396
+ únicamente por HTTP y no incluye su código.