@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/dist/index.js ADDED
@@ -0,0 +1,2137 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { parseArgs } from "util";
5
+
6
+ // package.json
7
+ var package_default = {
8
+ name: "@openfactura/cli",
9
+ version: "0.2.0",
10
+ description: "Cliente de l\xEDnea de comandos y servidor MCP (stdio) de Open Factura: facturaci\xF3n electr\xF3nica de El Salvador (DTE).",
11
+ license: "MIT",
12
+ repository: {
13
+ type: "git",
14
+ url: "git+https://github.com/xaviigna/openfactura.git",
15
+ directory: "packages/cli"
16
+ },
17
+ homepage: "https://github.com/xaviigna/openfactura/tree/main/packages/cli#readme",
18
+ bugs: "https://github.com/xaviigna/openfactura/issues",
19
+ type: "module",
20
+ bin: {
21
+ openfactura: "dist/index.js"
22
+ },
23
+ files: [
24
+ "dist",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ engines: {
29
+ node: ">=20"
30
+ },
31
+ publishConfig: {
32
+ access: "public"
33
+ },
34
+ keywords: [
35
+ "dte",
36
+ "el-salvador",
37
+ "factura-electronica",
38
+ "hacienda",
39
+ "mcp",
40
+ "cli"
41
+ ],
42
+ scripts: {
43
+ build: "tsup",
44
+ dev: "tsx src/index.ts",
45
+ test: "vitest run",
46
+ "type-check": "tsc --noEmit"
47
+ },
48
+ dependencies: {
49
+ "@modelcontextprotocol/server": "^2.4.0",
50
+ zod: "^4.0.0"
51
+ },
52
+ devDependencies: {
53
+ "@types/node": "^22.0.0",
54
+ tsup: "^8.5.0",
55
+ tsx: "^4.20.0",
56
+ typescript: "^5.9.0",
57
+ vitest: "^4.1.0"
58
+ }
59
+ };
60
+
61
+ // src/comandos.ts
62
+ import { randomUUID } from "crypto";
63
+ import { readFile, writeFile } from "fs/promises";
64
+ import { basename, resolve } from "path";
65
+
66
+ // src/config.ts
67
+ import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
68
+ import { homedir } from "os";
69
+ import { dirname, isAbsolute, join } from "path";
70
+
71
+ // src/errores.ts
72
+ var SALIDA = { ok: 0, error: 1, uso: 2, noAutenticado: 3, limite: 4 };
73
+ var ErrorUso = class extends Error {
74
+ };
75
+ var ErrorCli = class extends Error {
76
+ constructor(message, code = "error", salida = SALIDA.error) {
77
+ super(message);
78
+ this.code = code;
79
+ this.salida = salida;
80
+ }
81
+ code;
82
+ salida;
83
+ get cuerpo() {
84
+ return { error: { code: this.code, message: this.message } };
85
+ }
86
+ };
87
+ var ErrorApi = class extends Error {
88
+ constructor(status, cuerpo, retryAfter = null) {
89
+ super(cuerpo.error.message);
90
+ this.status = status;
91
+ this.cuerpo = cuerpo;
92
+ this.retryAfter = retryAfter;
93
+ }
94
+ status;
95
+ cuerpo;
96
+ retryAfter;
97
+ get code() {
98
+ return this.cuerpo.error.code;
99
+ }
100
+ get details() {
101
+ return this.cuerpo.error.details;
102
+ }
103
+ get salida() {
104
+ if (this.status === 401) return SALIDA.noAutenticado;
105
+ if (this.status === 429) return SALIDA.limite;
106
+ return SALIDA.error;
107
+ }
108
+ };
109
+
110
+ // src/config.ts
111
+ var URL_POR_DEFECTO = "http://localhost:8788";
112
+ function rutaConfig(env) {
113
+ const xdg = env.XDG_CONFIG_HOME;
114
+ const base = xdg && isAbsolute(xdg) ? xdg : join(env.HOME || homedir(), ".config");
115
+ return join(base, "openfactura", "config.json");
116
+ }
117
+ function leerConfig(env) {
118
+ const ruta2 = rutaConfig(env);
119
+ let texto2;
120
+ try {
121
+ texto2 = readFileSync(ruta2, "utf8");
122
+ } catch (error) {
123
+ if (error.code === "ENOENT") return { perfiles: {} };
124
+ throw new ErrorCli(`No se pudo leer ${ruta2}: ${error.message}`, "config_ilegible");
125
+ }
126
+ try {
127
+ const datos = JSON.parse(texto2);
128
+ const perfiles = datos && typeof datos.perfiles === "object" && datos.perfiles ? datos.perfiles : {};
129
+ return { perfiles: { ...perfiles } };
130
+ } catch {
131
+ throw new ErrorCli(`${ruta2} no es un JSON v\xE1lido. Corr\xEDjalo o b\xF3rrelo.`, "config_ilegible");
132
+ }
133
+ }
134
+ function guardarConfig(env, config) {
135
+ const ruta2 = rutaConfig(env);
136
+ const carpeta = dirname(ruta2);
137
+ mkdirSync(carpeta, { recursive: true, mode: 448 });
138
+ chmodSync(carpeta, 448);
139
+ const temporal = `${ruta2}.${process.pid}.tmp`;
140
+ rmSync(temporal, { force: true });
141
+ writeFileSync(temporal, `${JSON.stringify(config, null, 2)}
142
+ `, { mode: 384, flag: "wx" });
143
+ chmodSync(temporal, 384);
144
+ renameSync(temporal, ruta2);
145
+ return ruta2;
146
+ }
147
+ function normalizarUrl(valor) {
148
+ let url;
149
+ try {
150
+ url = new URL(valor.trim());
151
+ } catch {
152
+ throw new ErrorUso(`URL del servidor inv\xE1lida: ${valor}`);
153
+ }
154
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
155
+ throw new ErrorUso(`La URL del servidor debe empezar con http:// o https://: ${valor}`);
156
+ }
157
+ if (url.username || url.password) {
158
+ throw new ErrorUso("La URL del servidor no debe llevar usuario ni contrase\xF1a");
159
+ }
160
+ const ruta2 = url.pathname.replace(/\/+$/, "").replace(/\/api\/v1$/, "");
161
+ return `${url.origin}${ruta2}`;
162
+ }
163
+ function nombrePerfil(env, opcion) {
164
+ const nombre = opcion || env.OPENFACTURA_PROFILE || "default";
165
+ if (!/^[\w.-]{1,64}$/.test(nombre) || nombre === "__proto__") {
166
+ throw new ErrorUso(`Nombre de perfil inv\xE1lido: ${nombre}`);
167
+ }
168
+ return nombre;
169
+ }
170
+ function resolver(env, opciones = {}) {
171
+ const perfil = nombrePerfil(env, opciones.perfil);
172
+ const guardado = leerConfig(env).perfiles[perfil] ?? {};
173
+ let url = URL_POR_DEFECTO;
174
+ let origenUrl = "defecto";
175
+ if (opciones.url) {
176
+ url = opciones.url;
177
+ origenUrl = "opci\xF3n";
178
+ } else if (env.OPENFACTURA_URL) {
179
+ url = env.OPENFACTURA_URL;
180
+ origenUrl = "entorno";
181
+ } else if (guardado.url) {
182
+ url = guardado.url;
183
+ origenUrl = "config";
184
+ }
185
+ const deEntorno = env.OPENFACTURA_API_KEY?.trim();
186
+ const apiKey = deEntorno || guardado.apiKey || null;
187
+ return {
188
+ perfil,
189
+ url: normalizarUrl(url),
190
+ origenUrl,
191
+ apiKey,
192
+ origenClave: deEntorno ? "entorno" : apiKey ? "config" : null
193
+ };
194
+ }
195
+ function enmascarar(clave) {
196
+ return `${clave.slice(0, 14)}\u2026`;
197
+ }
198
+ var esClaveValida = (clave) => /^of_(test|live)_\S+$/.test(clave);
199
+ var esClaveDeProduccion = (clave) => clave.startsWith("of_live_");
200
+
201
+ // src/esquemas.ts
202
+ import { z } from "zod";
203
+ var TIPOS_DTE = {
204
+ "01": "Factura",
205
+ "03": "Comprobante de Cr\xE9dito Fiscal",
206
+ "04": "Nota de Remisi\xF3n",
207
+ "05": "Nota de Cr\xE9dito",
208
+ "06": "Nota de D\xE9bito",
209
+ "07": "Comprobante de Retenci\xF3n",
210
+ "11": "Factura de Exportaci\xF3n",
211
+ "14": "Factura de Sujeto Excluido"
212
+ };
213
+ var ESTADOS_DTE = [
214
+ "pendiente",
215
+ "procesado",
216
+ "rechazado",
217
+ "contingencia",
218
+ "invalidado"
219
+ ];
220
+ var NOMBRE_AMBIENTE = { "00": "Pruebas", "01": "Producci\xF3n" };
221
+ var codigosTipo = Object.keys(TIPOS_DTE);
222
+ var texto = (max = 250) => z.string().trim().min(1).max(max);
223
+ var opcional = (max = 250) => z.string().trim().max(max).nullish().transform((v) => v || null);
224
+ var tipoDte = z.enum(codigosTipo);
225
+ var cliente = z.object({
226
+ tipoDocumento: z.enum(["36", "13", "03", "02", "37"]),
227
+ numDocumento: texto(20),
228
+ nrc: opcional(8),
229
+ nombre: texto(),
230
+ nombreComercial: opcional(150),
231
+ codActividad: opcional(6),
232
+ descActividad: opcional(150),
233
+ departamento: opcional(2),
234
+ municipio: opcional(2),
235
+ distrito: opcional(2),
236
+ complemento: opcional(200),
237
+ telefono: opcional(30),
238
+ correo: z.union([z.email().max(100), z.literal("")]).nullish().transform((v) => v || null)
239
+ });
240
+ var producto = z.object({
241
+ codigo: texto(25),
242
+ descripcion: texto(1e3),
243
+ precioUnitario: z.number().nonnegative().max(99999999999),
244
+ tipoItem: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]).default(1),
245
+ uniMedida: z.number().int().min(1).max(99).default(59),
246
+ tipoVenta: z.enum(["gravada", "exenta", "noSujeta"]).default("gravada")
247
+ });
248
+ var persona = z.object({
249
+ nombre: texto(100),
250
+ tipoDocumento: z.string().min(2).max(2),
251
+ numDocumento: texto(20)
252
+ });
253
+ var invalidacion = z.object({
254
+ tipoAnulacion: z.union([z.literal(1), z.literal(2), z.literal(3)]),
255
+ motivoAnulacion: opcional(250),
256
+ codigoGeneracionReemplazo: opcional(36),
257
+ solicitante: persona.optional(),
258
+ responsable: persona.optional()
259
+ });
260
+ var fecha = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use el formato AAAA-MM-DD");
261
+ var retorno = z.object({
262
+ items: z.array(
263
+ z.object({
264
+ descripcion: texto(1e3),
265
+ cantidad: z.number().positive(),
266
+ precioUni: z.number().nonnegative(),
267
+ tipoItem: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]).optional(),
268
+ codigo: opcional(25),
269
+ uniMedida: z.number().int().min(1).max(99).optional(),
270
+ descuento: z.number().nonnegative().optional(),
271
+ tipoVenta: z.enum(["gravada", "exenta", "noSujeta"]).optional(),
272
+ seguro: z.number().nonnegative().optional(),
273
+ flete: z.number().nonnegative().optional()
274
+ })
275
+ ).min(1).max(2e3).describe(
276
+ "Bienes que se retornan. precioUni: en Factura (01) CON IVA, como en la factura; en Exportaci\xF3n (11) y Sujeto Excluido (14) SIN IVA."
277
+ )
278
+ });
279
+ var operacionesEspeciales = z.object({
280
+ tipoDocumento: z.enum(["02", "97"]).describe("02 factura de venta simplificada (por rangos), 97 comprobante de control interno"),
281
+ fecha: fecha.optional().describe("Fecha del evento; define el periodo informado"),
282
+ items: z.array(
283
+ z.object({
284
+ descripcion: texto(1500),
285
+ precioUni: z.number().nonnegative().describe("SIN IVA"),
286
+ cantidad: z.number().int().positive().optional(),
287
+ numDocumento: opcional(36),
288
+ docDel: z.string().trim().min(1).max(36).optional(),
289
+ docAl: z.string().trim().min(1).max(36).optional(),
290
+ fechaEmision: fecha.optional(),
291
+ tipoVenta: z.enum(["gravada", "exenta", "noSujeta"]).optional(),
292
+ codigoGeneracionRef: z.string().length(36).optional()
293
+ })
294
+ ).min(1).max(2e3)
295
+ });
296
+ var EVENTOS_WEBHOOK = [
297
+ "dte.procesado",
298
+ "dte.rechazado",
299
+ "dte.contingencia",
300
+ "dte.invalidado",
301
+ "retorno.procesado",
302
+ "operaciones_especiales.procesado"
303
+ ];
304
+ var webhook = z.object({
305
+ url: z.string().max(2e3).describe("URL https:// p\xFAblica que recibir\xE1 los POST firmados"),
306
+ descripcion: opcional(200),
307
+ eventos: z.array(z.enum(EVENTOS_WEBHOOK)).min(1).describe("Eventos a los que se suscribe"),
308
+ activo: z.boolean().optional()
309
+ });
310
+
311
+ // src/http.ts
312
+ var TIEMPO_LIMITE_MS = 6e4;
313
+ var ClienteApi = class {
314
+ url;
315
+ opciones;
316
+ constructor(opciones) {
317
+ this.opciones = opciones;
318
+ this.url = opciones.url;
319
+ }
320
+ async pedir(metodo, ruta2, peticion = {}) {
321
+ const respuesta = await this.enviar(metodo, ruta2, peticion);
322
+ const texto2 = await respuesta.text();
323
+ let datos = null;
324
+ if (texto2) {
325
+ try {
326
+ datos = JSON.parse(texto2);
327
+ } catch {
328
+ throw new ErrorCli(
329
+ `El servidor respondi\xF3 algo que no es JSON (HTTP ${respuesta.status}). Revise la URL: ${this.url}`,
330
+ "respuesta_invalida"
331
+ );
332
+ }
333
+ }
334
+ return { status: respuesta.status, headers: respuesta.headers, datos };
335
+ }
336
+ /** Descarga binaria (PDF). */
337
+ async descargar(ruta2) {
338
+ const respuesta = await this.enviar("GET", ruta2, {});
339
+ return new Uint8Array(await respuesta.arrayBuffer());
340
+ }
341
+ async enviar(metodo, ruta2, { query, cuerpo, headers }) {
342
+ const destino = new URL(`${this.url}/api/v1${ruta2}`);
343
+ for (const [clave, valor] of Object.entries(query ?? {})) {
344
+ if (valor !== void 0 && valor !== null && valor !== "")
345
+ destino.searchParams.set(clave, String(valor));
346
+ }
347
+ const { apiKey, version, timeoutMs = TIEMPO_LIMITE_MS } = this.opciones;
348
+ const hacerFetch = this.opciones.fetch ?? fetch;
349
+ let respuesta;
350
+ try {
351
+ respuesta = await hacerFetch(destino, {
352
+ method: metodo,
353
+ headers: {
354
+ Authorization: `Bearer ${apiKey}`,
355
+ "User-Agent": `openfactura-cli/${version}`,
356
+ Accept: "application/json",
357
+ ...cuerpo === void 0 ? {} : { "Content-Type": "application/json" },
358
+ ...headers
359
+ },
360
+ body: cuerpo === void 0 ? void 0 : JSON.stringify(cuerpo),
361
+ signal: AbortSignal.timeout(timeoutMs)
362
+ });
363
+ } catch (error) {
364
+ const nombre = error?.name;
365
+ if (nombre === "TimeoutError" || nombre === "AbortError") {
366
+ throw new ErrorCli(
367
+ `El servidor no respondi\xF3 en ${Math.round(timeoutMs / 1e3)} s (${this.url})`,
368
+ "tiempo_agotado"
369
+ );
370
+ }
371
+ const causa = error?.cause;
372
+ const detalle = causa?.code || causa?.message || error?.message || "error de red";
373
+ throw new ErrorCli(`No se pudo conectar con ${this.url} (${detalle})`, "sin_conexion");
374
+ }
375
+ if (!respuesta.ok) throw await leerError(respuesta);
376
+ return respuesta;
377
+ }
378
+ };
379
+ async function leerError(respuesta) {
380
+ const texto2 = await respuesta.text().catch(() => "");
381
+ let cuerpo = null;
382
+ try {
383
+ const json = JSON.parse(texto2);
384
+ const e = json?.error;
385
+ if (e && typeof e.code === "string" && typeof e.message === "string") {
386
+ cuerpo = {
387
+ error: {
388
+ code: e.code,
389
+ message: e.message,
390
+ ...e.details === void 0 ? {} : { details: e.details }
391
+ }
392
+ };
393
+ }
394
+ } catch {
395
+ }
396
+ cuerpo ??= {
397
+ error: {
398
+ code: `http_${respuesta.status}`,
399
+ message: `El servidor respondi\xF3 HTTP ${respuesta.status}${respuesta.statusText ? ` ${respuesta.statusText}` : ""}`
400
+ }
401
+ };
402
+ const espera = Number.parseInt(respuesta.headers.get("Retry-After") ?? "", 10);
403
+ return new ErrorApi(respuesta.status, cuerpo, Number.isFinite(espera) ? espera : null);
404
+ }
405
+
406
+ // src/mcp.ts
407
+ import { McpServer } from "@modelcontextprotocol/server";
408
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
409
+ import { z as z2 } from "zod";
410
+ var listaTipos = Object.entries(TIPOS_DTE).map(([codigo, nombre]) => `${codigo} ${nombre}`).join(", ");
411
+ var DATOS_DTE = `Datos del documento. Campos habituales:
412
+ - items: [{ descripcion, cantidad, precioUni, tipoItem?: 1 bien | 2 servicio, codigo?, uniMedida?: 59 unidad, descuento?, tipoVenta?: 'gravada'|'exenta'|'noSujeta' }]. En Factura (01) precioUni INCLUYE IVA; en CCF (03), notas y remisi\xF3n va SIN IVA.
413
+ - receptor: { nit o tipoDocumento+numDocumento, nrc, nombre, codActividad, descActividad, direccion: { departamento, municipio, distrito, complemento }, telefono, correo }. En Factura es opcional bajo USD 25,000. Puede omitirse si env\xEDa clienteId.
414
+ - condicionOperacion?: 1 contado | 2 cr\xE9dito | 3 otro; pagos?; credito?: { plazo, periodo }.
415
+ - documentosRelacionados (notas de cr\xE9dito/d\xE9bito): [{ tipoDocumento, tipoGeneracion: 2, numeroDocumento: <c\xF3digo de generaci\xF3n>, fechaEmision }].
416
+ Los c\xF3digos de cat\xE1logo se consultan con catalogo_buscar.`;
417
+ var entradaEmision = {
418
+ tipoDte: tipoDte.describe(`Tipo de documento: ${listaTipos}`),
419
+ datos: z2.record(z2.string(), z2.unknown()).describe(DATOS_DTE),
420
+ clienteId: z2.string().optional().describe("Id de un cliente guardado para usarlo como receptor"),
421
+ puntoVentaId: z2.string().optional().describe("Punto de venta emisor; por defecto el primero")
422
+ };
423
+ async function responder(accion) {
424
+ try {
425
+ const { datos } = await accion();
426
+ return {
427
+ content: [{ type: "text", text: JSON.stringify(datos, null, 2) }],
428
+ structuredContent: { resultado: datos }
429
+ };
430
+ } catch (error) {
431
+ const cuerpo = error instanceof ErrorApi || error instanceof ErrorCli ? error.cuerpo : { error: { code: "error_interno", message: "Ocurri\xF3 un error inesperado" } };
432
+ return { content: [{ type: "text", text: JSON.stringify(cuerpo, null, 2) }], isError: true };
433
+ }
434
+ }
435
+ var lectura = { readOnlyHint: true, openWorldHint: false };
436
+ var ruta = (segmento2) => encodeURIComponent(segmento2);
437
+ function crearServidorMcp(api, version) {
438
+ const servidor = new McpServer({ name: "openfactura", version });
439
+ servidor.registerTool(
440
+ "emisor_obtener",
441
+ {
442
+ title: "Ver emisor",
443
+ description: "Datos fiscales del emisor, sus establecimientos y puntos de venta, el ambiente activo (00 pruebas, 01 producci\xF3n) y si las credenciales del MH est\xE1n cargadas.",
444
+ inputSchema: z2.object({}),
445
+ annotations: lectura
446
+ },
447
+ () => responder(() => api.pedir("GET", "/emisor"))
448
+ );
449
+ servidor.registerTool(
450
+ "catalogo_buscar",
451
+ {
452
+ title: "Buscar en cat\xE1logos del MH",
453
+ description: "Consulta un cat\xE1logo oficial (CAT-001 \u2026 CAT-033): CAT-012 departamentos, CAT-013 municipios, CAT-008 distritos, CAT-019 actividades econ\xF3micas, CAT-014 unidades de medida, CAT-017 formas de pago, CAT-022 tipos de documento. Sin `id` devuelve la lista de cat\xE1logos.",
454
+ inputSchema: z2.object({
455
+ id: z2.string().optional().describe("Por ejemplo CAT-019"),
456
+ q: z2.string().optional().describe("Texto a buscar en el c\xF3digo o la descripci\xF3n"),
457
+ departamento: z2.string().optional().describe("Filtra municipios o distritos por departamento")
458
+ }),
459
+ annotations: lectura
460
+ },
461
+ ({ id, q, departamento }) => responder(
462
+ () => id ? api.pedir("GET", `/catalogos/${ruta(id.toUpperCase())}`, {
463
+ query: { q, departamento, limite: 50 }
464
+ }) : api.pedir("GET", "/catalogos")
465
+ )
466
+ );
467
+ servidor.registerTool(
468
+ "clientes_buscar",
469
+ {
470
+ title: "Buscar clientes",
471
+ description: "Busca clientes guardados por nombre o n\xFAmero de documento.",
472
+ inputSchema: z2.object({ q: z2.string().optional() }),
473
+ annotations: lectura
474
+ },
475
+ ({ q }) => responder(() => api.pedir("GET", "/clientes", { query: { q, limite: 25 } }))
476
+ );
477
+ servidor.registerTool(
478
+ "cliente_crear",
479
+ {
480
+ title: "Crear cliente",
481
+ description: "Guarda un cliente para reutilizarlo como receptor. tipoDocumento: 36 NIT, 13 DUI, 03 pasaporte, 02 carn\xE9 de residente, 37 otro.",
482
+ inputSchema: cliente,
483
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
484
+ },
485
+ (datos) => responder(() => api.pedir("POST", "/clientes", { cuerpo: datos }))
486
+ );
487
+ servidor.registerTool(
488
+ "productos_buscar",
489
+ {
490
+ title: "Buscar productos",
491
+ description: "Busca productos y servicios guardados por c\xF3digo o descripci\xF3n. El precio guardado es SIN IVA.",
492
+ inputSchema: z2.object({ q: z2.string().optional() }),
493
+ annotations: lectura
494
+ },
495
+ ({ q }) => responder(() => api.pedir("GET", "/productos", { query: { q, limite: 25 } }))
496
+ );
497
+ servidor.registerTool(
498
+ "producto_crear",
499
+ {
500
+ title: "Crear producto",
501
+ description: "Guarda un producto o servicio. precioUnitario va SIN IVA.",
502
+ inputSchema: producto,
503
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
504
+ },
505
+ (datos) => responder(() => api.pedir("POST", "/productos", { cuerpo: datos }))
506
+ );
507
+ servidor.registerTool(
508
+ "dte_validar",
509
+ {
510
+ title: "Validar un DTE sin emitirlo",
511
+ description: "Construye el documento y lo valida contra el esquema oficial del MH. No consume n\xFAmero de control, no firma y no transmite. \xDAselo antes de dte_emitir.",
512
+ inputSchema: z2.object(entradaEmision),
513
+ annotations: lectura
514
+ },
515
+ (entrada) => responder(() => api.pedir("POST", "/dte/validar", { cuerpo: entrada }))
516
+ );
517
+ servidor.registerTool(
518
+ "dte_emitir",
519
+ {
520
+ title: "Emitir un DTE",
521
+ description: 'Emite un documento tributario electr\xF3nico: lo firma y lo transmite al Ministerio de Hacienda. Tiene efecto fiscal y consume un n\xFAmero de control. Confirme los datos con el usuario antes de llamar. Devuelve estado "procesado" con sello, o "contingencia" si el MH no respondi\xF3.',
522
+ inputSchema: z2.object({
523
+ ...entradaEmision,
524
+ idempotencyKey: z2.string().min(8).max(200).describe(
525
+ "Identificador \xFAnico de este intento. Repetirlo devuelve el mismo resultado sin emitir dos veces."
526
+ )
527
+ }),
528
+ annotations: {
529
+ readOnlyHint: false,
530
+ destructiveHint: false,
531
+ idempotentHint: true,
532
+ openWorldHint: true
533
+ }
534
+ },
535
+ ({ idempotencyKey, ...entrada }) => responder(
536
+ () => api.pedir("POST", "/dte", {
537
+ cuerpo: entrada,
538
+ headers: { "Idempotency-Key": idempotencyKey }
539
+ })
540
+ )
541
+ );
542
+ servidor.registerTool(
543
+ "dte_listar",
544
+ {
545
+ title: "Listar documentos",
546
+ description: "Lista los DTE del ambiente activo, del m\xE1s reciente al m\xE1s antiguo.",
547
+ inputSchema: z2.object({
548
+ tipo: tipoDte.optional(),
549
+ estado: z2.enum(ESTADOS_DTE).optional(),
550
+ q: z2.string().optional().describe("Receptor, n\xFAmero de control o c\xF3digo de generaci\xF3n"),
551
+ desde: z2.string().optional().describe("AAAA-MM-DD"),
552
+ hasta: z2.string().optional().describe("AAAA-MM-DD"),
553
+ pagina: z2.number().int().min(1).optional()
554
+ }),
555
+ annotations: lectura
556
+ },
557
+ (filtros) => responder(
558
+ () => api.pedir("GET", "/dte", { query: { pagina: 1, porPagina: 25, ...filtros } })
559
+ )
560
+ );
561
+ servidor.registerTool(
562
+ "dte_obtener",
563
+ {
564
+ title: "Ver un documento",
565
+ description: "Detalle de un DTE por c\xF3digo de generaci\xF3n: estado, sello, JSON oficial, respuesta del MH, eventos y enlace de consulta p\xFAblica.",
566
+ inputSchema: z2.object({ codigoGeneracion: z2.string() }),
567
+ annotations: lectura
568
+ },
569
+ ({ codigoGeneracion }) => responder(() => api.pedir("GET", `/dte/${ruta(codigoGeneracion)}`))
570
+ );
571
+ servidor.registerTool(
572
+ "dte_invalidar",
573
+ {
574
+ title: "Invalidar un DTE",
575
+ description: "Transmite el evento de invalidaci\xF3n de un DTE procesado, o de un evento de retorno u operaciones especiales (use su c\xF3digo de generaci\xF3n). Un DTE con retorno vigente exige invalidar antes el retorno. Es irreversible y tiene efecto fiscal: confirme con el usuario. tipoAnulacion: 1 error en la informaci\xF3n (requiere codigoGeneracionReemplazo), 2 rescindir la operaci\xF3n, 3 otro (requiere motivo y reemplazo).",
576
+ inputSchema: z2.object({ codigoGeneracion: z2.string(), ...invalidacion.shape }),
577
+ annotations: {
578
+ readOnlyHint: false,
579
+ destructiveHint: true,
580
+ idempotentHint: false,
581
+ openWorldHint: true
582
+ }
583
+ },
584
+ ({ codigoGeneracion, ...entrada }) => responder(
585
+ () => api.pedir("POST", `/dte/${ruta(codigoGeneracion)}/invalidar`, { cuerpo: entrada })
586
+ )
587
+ );
588
+ servidor.registerTool(
589
+ "dte_retorno",
590
+ {
591
+ title: "Registrar un retorno (evento 18)",
592
+ description: "Transmite un Evento de Retorno sobre una Factura (01), Factura de Exportaci\xF3n (11) o Sujeto Excluido (14) procesados: devoluci\xF3n de bienes, reembolso o envases, sin invalidar el documento. Plazo: 3 meses desde la fecha del DTE. La suma de retornos no puede superar el DTE. Tiene efecto fiscal: confirme con el usuario. Para un Cr\xE9dito Fiscal use una Nota de Cr\xE9dito (05).",
593
+ inputSchema: z2.object({
594
+ codigoGeneracion: z2.string().describe("C\xF3digo de generaci\xF3n del DTE original"),
595
+ ...retorno.shape,
596
+ idempotencyKey: z2.string().min(8).max(200).describe(
597
+ "Identificador \xFAnico de este intento. Repetirlo devuelve el mismo resultado sin transmitir dos veces."
598
+ )
599
+ }),
600
+ annotations: {
601
+ readOnlyHint: false,
602
+ destructiveHint: false,
603
+ idempotentHint: true,
604
+ openWorldHint: true
605
+ }
606
+ },
607
+ ({ codigoGeneracion, idempotencyKey, ...entrada }) => responder(
608
+ () => api.pedir("POST", `/dte/${ruta(codigoGeneracion)}/retorno`, {
609
+ cuerpo: entrada,
610
+ headers: { "Idempotency-Key": idempotencyKey }
611
+ })
612
+ )
613
+ );
614
+ servidor.registerTool(
615
+ "operaciones_especiales_informar",
616
+ {
617
+ title: "Informar operaciones especiales (evento 17)",
618
+ description: "Transmite un Evento de Operaciones Especiales: informa al MH las facturas simplificadas (02, siempre por rangos docDel\u2013docAl) o los comprobantes de control interno (97, uno a uno con numDocumento o por rangos, sin mezclar) de un periodo. precioUni SIN IVA. Los documentos anulados van en un evento aparte con codigoGeneracionRef. Tiene efecto fiscal: confirme con el usuario.",
619
+ inputSchema: z2.object({
620
+ ...operacionesEspeciales.shape,
621
+ idempotencyKey: z2.string().min(8).max(200).describe(
622
+ "Identificador \xFAnico de este intento. Repetirlo devuelve el mismo resultado sin transmitir dos veces."
623
+ )
624
+ }),
625
+ annotations: {
626
+ readOnlyHint: false,
627
+ destructiveHint: false,
628
+ idempotentHint: true,
629
+ openWorldHint: true
630
+ }
631
+ },
632
+ ({ idempotencyKey, ...entrada }) => responder(
633
+ () => api.pedir("POST", "/eventos/operaciones-especiales", {
634
+ cuerpo: entrada,
635
+ headers: { "Idempotency-Key": idempotencyKey }
636
+ })
637
+ )
638
+ );
639
+ servidor.registerTool(
640
+ "eventos_listar",
641
+ {
642
+ title: "Listar eventos",
643
+ description: "Eventos de retorno y de operaciones especiales del ambiente activo (los 50 m\xE1s recientes).",
644
+ inputSchema: z2.object({ tipo: z2.enum(["retorno", "operaciones_especiales"]).optional() }),
645
+ annotations: lectura
646
+ },
647
+ ({ tipo }) => responder(() => api.pedir("GET", "/eventos", { query: { tipo } }))
648
+ );
649
+ servidor.registerTool(
650
+ "evento_obtener",
651
+ {
652
+ title: "Ver un evento",
653
+ description: "Detalle de un evento por c\xF3digo de generaci\xF3n: estado, sello, JSON oficial y respuesta del MH.",
654
+ inputSchema: z2.object({ codigoGeneracion: z2.string() }),
655
+ annotations: lectura
656
+ },
657
+ ({ codigoGeneracion }) => responder(() => api.pedir("GET", `/eventos/${ruta(codigoGeneracion)}`))
658
+ );
659
+ servidor.registerTool(
660
+ "webhooks_listar",
661
+ {
662
+ title: "Listar webhooks",
663
+ description: "Webhooks de la organizaci\xF3n: URL, eventos suscritos y si est\xE1n activos. Nunca devuelve el secreto de firma.",
664
+ inputSchema: z2.object({}),
665
+ annotations: lectura
666
+ },
667
+ () => responder(() => api.pedir("GET", "/webhooks"))
668
+ );
669
+ servidor.registerTool(
670
+ "webhook_crear",
671
+ {
672
+ title: "Crear webhook",
673
+ description: `Registra una URL https:// que recibir\xE1 un POST firmado cuando cambie el estado de un documento. Eventos: ${EVENTOS_WEBHOOK.join(", ")}. Devuelve el secreto de firma (whsec_\u2026) UNA sola vez: entr\xE9gueselo al usuario para que verifique la firma OpenFactura-Signature.`,
674
+ inputSchema: webhook,
675
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
676
+ },
677
+ (datos) => responder(() => api.pedir("POST", "/webhooks", { cuerpo: datos }))
678
+ );
679
+ servidor.registerTool(
680
+ "webhook_eliminar",
681
+ {
682
+ title: "Eliminar webhook",
683
+ description: "Elimina un webhook y su historial de entregas. Deja de recibir notificaciones de inmediato. Confirme con el usuario.",
684
+ inputSchema: z2.object({ id: z2.string() }),
685
+ annotations: {
686
+ readOnlyHint: false,
687
+ destructiveHint: true,
688
+ idempotentHint: true,
689
+ openWorldHint: false
690
+ }
691
+ },
692
+ ({ id }) => responder(() => api.pedir("DELETE", `/webhooks/${ruta(id)}`))
693
+ );
694
+ servidor.registerTool(
695
+ "webhook_probar",
696
+ {
697
+ title: "Probar webhook",
698
+ description: "Env\xEDa ya un evento webhook.prueba firmado a la URL del webhook y devuelve si respondi\xF3 2xx, su estado HTTP o el error.",
699
+ inputSchema: z2.object({ id: z2.string() }),
700
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
701
+ },
702
+ ({ id }) => responder(() => api.pedir("POST", `/webhooks/${ruta(id)}/probar`))
703
+ );
704
+ return servidor;
705
+ }
706
+ function servirMcp(api, version) {
707
+ return new Promise((resolve2) => {
708
+ const conexion = serveStdio(() => crearServidorMcp(api, version), {
709
+ onerror: (error) => process.stderr.write(`openfactura mcp: ${error.message}
710
+ `)
711
+ });
712
+ const cerrar = () => void conexion.close().finally(resolve2);
713
+ process.stdin.once("end", cerrar);
714
+ process.stdin.once("close", cerrar);
715
+ });
716
+ }
717
+
718
+ // src/salida.ts
719
+ var ANCHO_MAXIMO = 44;
720
+ var celda = (valor) => {
721
+ if (valor === null || valor === void 0 || valor === "") return "\u2014";
722
+ const texto2 = String(valor).replace(/\s+/g, " ");
723
+ return texto2.length > ANCHO_MAXIMO ? `${texto2.slice(0, ANCHO_MAXIMO - 1)}\u2026` : texto2;
724
+ };
725
+ function tabla(encabezados, filas, derecha = []) {
726
+ const celdas = filas.map((fila) => fila.map(celda));
727
+ const anchos = encabezados.map(
728
+ (titulo, i) => Math.max(titulo.length, ...celdas.map((fila) => fila[i]?.length ?? 0))
729
+ );
730
+ const linea = (fila) => fila.map(
731
+ (texto2, i) => derecha.includes(i) ? texto2.padStart(anchos[i] ?? 0) : texto2.padEnd(anchos[i] ?? 0)
732
+ ).join(" ").trimEnd();
733
+ return [linea(encabezados.map((t) => t.toUpperCase())), ...celdas.map(linea)].join("\n");
734
+ }
735
+ function fichas(pares) {
736
+ const visibles = pares.filter(
737
+ ([, valor]) => valor !== null && valor !== void 0 && valor !== ""
738
+ );
739
+ const ancho = Math.max(0, ...visibles.map(([clave]) => clave.length));
740
+ return visibles.map(([clave, valor]) => `${`${clave}:`.padEnd(ancho + 2)}${String(valor)}`).join("\n");
741
+ }
742
+ var dinero = (valor) => typeof valor === "number" ? `$${valor.toFixed(2)}` : "\u2014";
743
+ var siNo = (valor) => valor ? "s\xED" : "no";
744
+
745
+ // src/comandos.ts
746
+ var nombreTipo = (codigo) => `${codigo} ${TIPOS_DTE[String(codigo)] ?? ""}`.trim();
747
+ var nombreAmbiente = (codigo) => `${codigo} ${NOMBRE_AMBIENTE[String(codigo)] ?? ""}`.trim();
748
+ var fecha2 = (ms) => typeof ms === "number" ? new Date(ms).toISOString().replace("T", " ").slice(0, 19) : ms;
749
+ var segmento = (texto2) => encodeURIComponent(texto2);
750
+ function mostrar(ctx, datos, legible) {
751
+ ctx.ent.stdout(ctx.json ? JSON.stringify(datos, null, 2) : legible());
752
+ }
753
+ async function leerJson(ctx, origen) {
754
+ let texto2;
755
+ if (origen === "-") {
756
+ texto2 = await ctx.ent.leerEntrada();
757
+ } else {
758
+ try {
759
+ texto2 = await readFile(resolve(ctx.ent.cwd, origen), "utf8");
760
+ } catch (error) {
761
+ throw new ErrorUso(`No se pudo leer ${origen}: ${error.message}`);
762
+ }
763
+ }
764
+ try {
765
+ return JSON.parse(texto2);
766
+ } catch (error) {
767
+ throw new ErrorUso(
768
+ `${origen === "-" ? "La entrada est\xE1ndar" : origen} no es un JSON v\xE1lido: ${error.message}`
769
+ );
770
+ }
771
+ }
772
+ async function leerEmision(ctx, origen) {
773
+ const cuerpo = await leerJson(ctx, origen);
774
+ const valido = cuerpo && typeof cuerpo === "object" && !Array.isArray(cuerpo) && "tipoDte" in cuerpo && "datos" in cuerpo;
775
+ if (!valido) {
776
+ throw new ErrorUso(
777
+ 'El archivo debe ser un objeto { "tipoDte": "01", "datos": { \u2026 }, "clienteId"?, "puntoVentaId"? }'
778
+ );
779
+ }
780
+ return cuerpo;
781
+ }
782
+ async function confirmar(ctx, accion, entradaOcupada = false) {
783
+ if (ctx.si || !ctx.cfg.apiKey || !esClaveDeProduccion(ctx.cfg.apiKey)) return;
784
+ if (!ctx.ent.esTTY || entradaOcupada) {
785
+ throw new ErrorUso(
786
+ `Va a ${accion} en PRODUCCI\xD3N y no hay una terminal para confirmar: agregue --yes.`
787
+ );
788
+ }
789
+ const respuesta = await ctx.ent.preguntar(
790
+ `Va a ${accion} en PRODUCCI\xD3N (clave of_live_). \xBFContinuar? [s/N] `
791
+ );
792
+ if (!/^(s|si|sí|y|yes)$/i.test(respuesta.trim()))
793
+ throw new ErrorCli("Operaci\xF3n cancelada.", "cancelado");
794
+ }
795
+ function entero(ctx, opcion, minimo = 1) {
796
+ const texto2 = ctx.op(opcion);
797
+ if (texto2 === void 0) return void 0;
798
+ const numero = Number(texto2);
799
+ if (!Number.isInteger(numero) || numero < minimo)
800
+ throw new ErrorUso(`--${opcion} debe ser un n\xFAmero entero (recibido: ${texto2})`);
801
+ return numero;
802
+ }
803
+ function fichaDocumento(doc) {
804
+ return fichas([
805
+ ["Estado", doc.estado],
806
+ ["Tipo", nombreTipo(doc.tipoDte)],
807
+ ["N\xFAmero de control", doc.numeroControl],
808
+ ["C\xF3digo de generaci\xF3n", doc.codigoGeneracion],
809
+ ["Ambiente", nombreAmbiente(doc.ambiente)],
810
+ ["Emisi\xF3n", `${doc.fechaEmision ?? ""} ${doc.horaEmision ?? ""}`.trim()],
811
+ ["Receptor", [doc.receptorNombre, doc.receptorDocumento].filter(Boolean).join(" \xB7 ")],
812
+ ["Total", dinero(doc.total)],
813
+ ["Sello recibido", doc.selloRecibido],
814
+ ["Origen", doc.origen],
815
+ ["Error", doc.error],
816
+ ["Consulta p\xFAblica", doc.urlConsulta]
817
+ ]);
818
+ }
819
+ async function cuerpoDeAlta(ctx, campos, numericos = []) {
820
+ const dados = Object.keys(campos).filter((opcion) => ctx.op(opcion) !== void 0);
821
+ const archivo = ctx.op("file");
822
+ if (archivo !== void 0) {
823
+ if (dados.length) throw new ErrorUso("Use --file o las opciones sueltas, no ambas.");
824
+ return leerJson(ctx, archivo);
825
+ }
826
+ if (!dados.length) {
827
+ throw new ErrorUso(
828
+ `Indique --file <archivo.json> o los datos con opciones: ${Object.keys(campos).map((o) => `--${o}`).join(", ")}`
829
+ );
830
+ }
831
+ const cuerpo = {};
832
+ for (const opcion of dados) {
833
+ const texto2 = ctx.op(opcion);
834
+ const campo = campos[opcion];
835
+ if (numericos.includes(opcion)) {
836
+ const numero = Number(texto2);
837
+ if (texto2.trim() === "" || !Number.isFinite(numero))
838
+ throw new ErrorUso(`--${opcion} debe ser un n\xFAmero (recibido: ${texto2})`);
839
+ cuerpo[campo] = numero;
840
+ } else {
841
+ cuerpo[campo] = texto2;
842
+ }
843
+ }
844
+ return cuerpo;
845
+ }
846
+ var CAMPOS_CLIENTE = {
847
+ "tipo-documento": "tipoDocumento",
848
+ "num-documento": "numDocumento",
849
+ nrc: "nrc",
850
+ nombre: "nombre",
851
+ "nombre-comercial": "nombreComercial",
852
+ "cod-actividad": "codActividad",
853
+ "desc-actividad": "descActividad",
854
+ departamento: "departamento",
855
+ municipio: "municipio",
856
+ distrito: "distrito",
857
+ complemento: "complemento",
858
+ telefono: "telefono",
859
+ correo: "correo"
860
+ };
861
+ var CAMPOS_PRODUCTO = {
862
+ codigo: "codigo",
863
+ descripcion: "descripcion",
864
+ precio: "precioUnitario",
865
+ "tipo-item": "tipoItem",
866
+ "uni-medida": "uniMedida",
867
+ "tipo-venta": "tipoVenta"
868
+ };
869
+ async function login(ctx) {
870
+ let clave = ctx.op("key");
871
+ if (clave === void 0) {
872
+ clave = ctx.ent.esTTY ? await ctx.ent.preguntar("Clave de API (of_test_\u2026 u of_live_\u2026): ", true) : (await ctx.ent.leerEntrada()).split("\n")[0];
873
+ }
874
+ clave = (clave ?? "").trim();
875
+ if (!esClaveValida(clave))
876
+ throw new ErrorUso("La clave de API debe empezar con of_test_ o of_live_.");
877
+ const api = new ClienteApi({
878
+ url: ctx.cfg.url,
879
+ apiKey: clave,
880
+ version: ctx.version,
881
+ fetch: ctx.ent.fetch
882
+ });
883
+ const { datos } = await api.pedir("GET", "/yo");
884
+ const config = leerConfig(ctx.ent.env);
885
+ config.perfiles[ctx.cfg.perfil] = { url: ctx.cfg.url, apiKey: clave };
886
+ const ruta2 = guardarConfig(ctx.ent.env, config);
887
+ mostrar(
888
+ ctx,
889
+ datos,
890
+ () => [
891
+ `Sesi\xF3n iniciada en ${ctx.cfg.url}`,
892
+ fichas([
893
+ ["Clave", enmascarar(clave)],
894
+ ["Ambiente", nombreAmbiente(datos.ambiente)],
895
+ ["Perfil", ctx.cfg.perfil],
896
+ ["Guardado en", ruta2]
897
+ ])
898
+ ].join("\n")
899
+ );
900
+ if (ctx.ent.env.OPENFACTURA_API_KEY) {
901
+ ctx.ent.stderr(
902
+ "Aviso: OPENFACTURA_API_KEY est\xE1 definida y tiene prioridad sobre la clave guardada."
903
+ );
904
+ }
905
+ return void 0;
906
+ }
907
+ async function logout(ctx) {
908
+ const config = leerConfig(ctx.ent.env);
909
+ const habia = ctx.cfg.perfil in config.perfiles;
910
+ if (habia) {
911
+ delete config.perfiles[ctx.cfg.perfil];
912
+ guardarConfig(ctx.ent.env, config);
913
+ }
914
+ mostrar(
915
+ ctx,
916
+ { perfil: ctx.cfg.perfil, eliminado: habia },
917
+ () => habia ? `Sesi\xF3n cerrada: se borr\xF3 la clave del perfil \xAB${ctx.cfg.perfil}\xBB (${rutaConfig(ctx.ent.env)}).` : `No hab\xEDa una sesi\xF3n guardada en el perfil \xAB${ctx.cfg.perfil}\xBB.`
918
+ );
919
+ if (ctx.ent.env.OPENFACTURA_API_KEY) {
920
+ ctx.ent.stderr("Aviso: OPENFACTURA_API_KEY sigue definida en el entorno.");
921
+ }
922
+ return void 0;
923
+ }
924
+ async function whoami(ctx) {
925
+ const { datos } = await ctx.api().pedir("GET", "/yo");
926
+ const usuario = datos.usuario;
927
+ mostrar(
928
+ ctx,
929
+ datos,
930
+ () => fichas([
931
+ ["Actor", datos.actor],
932
+ ["Usuario", usuario ? `${usuario.nombre} <${usuario.correo}>` : null],
933
+ ["Rol", datos.rol],
934
+ ["Organizaci\xF3n", datos.organizationId],
935
+ [
936
+ "Ambiente",
937
+ `${nombreAmbiente(datos.ambiente)}${datos.simulador ? " (simulador del MH)" : ""}`
938
+ ],
939
+ ["Servidor", `${ctx.cfg.url} (${ctx.cfg.origenUrl})`],
940
+ ["Clave", `${enmascarar(ctx.cfg.apiKey)} (${ctx.cfg.origenClave})`],
941
+ ["Perfil", ctx.cfg.perfil],
942
+ ["Permisos", Array.isArray(datos.permisos) ? datos.permisos.join(", ") : null]
943
+ ])
944
+ );
945
+ return void 0;
946
+ }
947
+ async function emisor(ctx) {
948
+ const { datos } = await ctx.api().pedir("GET", "/emisor");
949
+ mostrar(ctx, datos, () => {
950
+ const e = datos.emisor;
951
+ if (!e) return "El emisor a\xFAn no est\xE1 configurado. Complete sus datos en el panel web.";
952
+ const establecimientos = datos.establecimientos ?? [];
953
+ const credenciales = datos.credenciales ?? {};
954
+ return [
955
+ fichas([
956
+ ["Nombre", e.nombre],
957
+ ["Nombre comercial", e.nombreComercial],
958
+ ["NIT", e.nit],
959
+ ["NRC", e.nrc],
960
+ ["Actividad", `${e.codActividad} ${e.descActividad}`],
961
+ [
962
+ "Direcci\xF3n",
963
+ `${e.complemento} (depto. ${e.departamento}, mun. ${e.municipio}, dist. ${e.distrito})`
964
+ ],
965
+ ["Tel\xE9fono", e.telefono],
966
+ ["Correo", e.correo],
967
+ ["Ambiente activo", nombreAmbiente(datos.ambienteActivo)]
968
+ ]),
969
+ "",
970
+ "Establecimientos",
971
+ tabla(
972
+ ["c\xF3digo", "nombre", "tipo", "puntos de venta"],
973
+ establecimientos.map((est) => [
974
+ est.codMh,
975
+ est.nombre,
976
+ est.tipo,
977
+ (est.puntosVenta ?? []).map((pv) => `${pv.codMh} ${pv.nombre}`).join(", ")
978
+ ])
979
+ ),
980
+ "",
981
+ "Credenciales del MH",
982
+ tabla(
983
+ ["ambiente", "contrase\xF1a api", "certificado", "nit del certificado"],
984
+ Object.entries(credenciales).map(([ambiente, c]) => [
985
+ nombreAmbiente(ambiente),
986
+ siNo(c.apiPassword),
987
+ siNo(c.certificado),
988
+ c.certificadoNit
989
+ ])
990
+ )
991
+ ].join("\n");
992
+ });
993
+ return void 0;
994
+ }
995
+ async function catalogos(ctx) {
996
+ const id = ctx.args[0];
997
+ if (!id) {
998
+ if (ctx.op("q") !== void 0 || ctx.op("departamento") !== void 0) {
999
+ throw new ErrorUso(
1000
+ "--q y --departamento requieren el id del cat\xE1logo, por ejemplo: openfactura catalogos CAT-013 --departamento 06"
1001
+ );
1002
+ }
1003
+ const { datos: datos2 } = await ctx.api().pedir("GET", "/catalogos");
1004
+ mostrar(
1005
+ ctx,
1006
+ datos2,
1007
+ () => tabla(
1008
+ ["id", "nombre", "valores"],
1009
+ datos2.map((c) => [c.id, c.nombre, c.total]),
1010
+ [2]
1011
+ )
1012
+ );
1013
+ return void 0;
1014
+ }
1015
+ const { datos } = await ctx.api().pedir("GET", `/catalogos/${segmento(id.toUpperCase())}`, {
1016
+ query: { q: ctx.op("q"), departamento: ctx.op("departamento") }
1017
+ });
1018
+ mostrar(ctx, datos, () => {
1019
+ const conDepartamento = datos.valores.some((v) => v.departamento);
1020
+ const filas = datos.valores.map(
1021
+ (v) => conDepartamento ? [v.codigo, v.valor, v.departamento] : [v.codigo, v.valor]
1022
+ );
1023
+ return [
1024
+ `${datos.id} ${datos.nombre} \u2014 ${datos.valores.length} de ${datos.total} valores`,
1025
+ tabla(conDepartamento ? ["c\xF3digo", "valor", "departamento"] : ["c\xF3digo", "valor"], filas)
1026
+ ].join("\n");
1027
+ });
1028
+ return void 0;
1029
+ }
1030
+ async function clientesList(ctx) {
1031
+ const { datos } = await ctx.api().pedir("GET", "/clientes", { query: { q: ctx.op("q") } });
1032
+ mostrar(
1033
+ ctx,
1034
+ datos,
1035
+ () => datos.length ? tabla(
1036
+ ["id", "doc", "n\xFAmero", "nombre", "nrc", "correo"],
1037
+ datos.map((c) => [c.id, c.tipoDocumento, c.numDocumento, c.nombre, c.nrc, c.correo])
1038
+ ) : "No hay clientes."
1039
+ );
1040
+ return void 0;
1041
+ }
1042
+ async function clientesAdd(ctx) {
1043
+ const cuerpo = await cuerpoDeAlta(ctx, CAMPOS_CLIENTE);
1044
+ const { datos } = await ctx.api().pedir("POST", "/clientes", { cuerpo });
1045
+ mostrar(
1046
+ ctx,
1047
+ datos,
1048
+ () => [
1049
+ "Cliente creado.",
1050
+ fichas([
1051
+ ["Id", datos.id],
1052
+ ["Documento", `${datos.tipoDocumento} ${datos.numDocumento}`],
1053
+ ["Nombre", datos.nombre],
1054
+ ["NRC", datos.nrc],
1055
+ ["Correo", datos.correo]
1056
+ ])
1057
+ ].join("\n")
1058
+ );
1059
+ return void 0;
1060
+ }
1061
+ async function productosList(ctx) {
1062
+ const { datos } = await ctx.api().pedir("GET", "/productos", { query: { q: ctx.op("q") } });
1063
+ mostrar(
1064
+ ctx,
1065
+ datos,
1066
+ () => datos.length ? tabla(
1067
+ ["id", "c\xF3digo", "descripci\xF3n", "precio sin iva", "\xEDtem", "unidad", "venta"],
1068
+ datos.map((p) => [
1069
+ p.id,
1070
+ p.codigo,
1071
+ p.descripcion,
1072
+ dinero(p.precioUnitario),
1073
+ p.tipoItem,
1074
+ p.uniMedida,
1075
+ p.tipoVenta
1076
+ ]),
1077
+ [3]
1078
+ ) : "No hay productos."
1079
+ );
1080
+ return void 0;
1081
+ }
1082
+ async function productosAdd(ctx) {
1083
+ const cuerpo = await cuerpoDeAlta(ctx, CAMPOS_PRODUCTO, ["precio", "tipo-item", "uni-medida"]);
1084
+ const { datos } = await ctx.api().pedir("POST", "/productos", { cuerpo });
1085
+ mostrar(
1086
+ ctx,
1087
+ datos,
1088
+ () => [
1089
+ "Producto creado.",
1090
+ fichas([
1091
+ ["Id", datos.id],
1092
+ ["C\xF3digo", datos.codigo],
1093
+ ["Descripci\xF3n", datos.descripcion],
1094
+ ["Precio sin IVA", dinero(datos.precioUnitario)],
1095
+ ["Tipo de venta", datos.tipoVenta]
1096
+ ])
1097
+ ].join("\n")
1098
+ );
1099
+ return void 0;
1100
+ }
1101
+ async function dteValidar(ctx) {
1102
+ const cuerpo = await leerEmision(ctx, ctx.args[0]);
1103
+ const { datos } = await ctx.api().pedir("POST", "/dte/validar", { cuerpo });
1104
+ const problemas = datos.problemas ?? [];
1105
+ mostrar(ctx, datos, () => {
1106
+ const resumen = datos.dte?.resumen ?? {};
1107
+ const numero = (campo) => typeof resumen[campo] === "number" ? dinero(resumen[campo]) : null;
1108
+ return [
1109
+ fichas([
1110
+ ["Tipo", nombreTipo(cuerpo.tipoDte)],
1111
+ ["Receptor", datos.receptorNombre],
1112
+ ["Gravado", numero("totalGravada")],
1113
+ ["Exento", numero("totalExenta")],
1114
+ ["No sujeto", numero("totalNoSuj")],
1115
+ ["IVA", numero("totalIva")],
1116
+ ["Total", dinero(datos.total)],
1117
+ ["En letras", resumen.totalLetras]
1118
+ ]),
1119
+ "",
1120
+ problemas.length ? [
1121
+ `${problemas.length} problema(s):`,
1122
+ ...problemas.map((p) => ` ${p.ruta}: ${p.mensaje}`)
1123
+ ].join("\n") : "V\xE1lido: cumple el esquema del MH. No se emiti\xF3 nada."
1124
+ ].join("\n");
1125
+ });
1126
+ return problemas.length ? SALIDA.error : SALIDA.ok;
1127
+ }
1128
+ async function enviarUnaVez(ctx, ruta2, cuerpo, qu\u00E9, reintento, stdin = false) {
1129
+ const api = ctx.api();
1130
+ const clave = ctx.op("idempotency-key") ?? randomUUID();
1131
+ if (clave.length < 1 || clave.length > 200)
1132
+ throw new ErrorUso("--idempotency-key debe tener entre 1 y 200 caracteres.");
1133
+ await confirmar(ctx, qu\u00E9, stdin);
1134
+ ctx.ent.stderr(`Idempotency-Key: ${clave}`);
1135
+ try {
1136
+ const respuesta = await api.pedir("POST", ruta2, {
1137
+ cuerpo,
1138
+ headers: { "Idempotency-Key": clave }
1139
+ });
1140
+ if (respuesta.headers.get("Idempotent-Replayed") === "true") {
1141
+ ctx.ent.stderr(
1142
+ "Respuesta repetida: esta Idempotency-Key ya se hab\xEDa usado; no se transmiti\xF3 de nuevo."
1143
+ );
1144
+ }
1145
+ return respuesta.datos;
1146
+ } catch (error) {
1147
+ if (error instanceof ErrorCli && (error.code === "sin_conexion" || error.code === "tiempo_agotado")) {
1148
+ ctx.ent.stderr(
1149
+ `No se sabe si se transmiti\xF3. Reintente con la misma clave para no duplicarlo:
1150
+ openfactura ${reintento} --idempotency-key ${clave}`
1151
+ );
1152
+ }
1153
+ throw error;
1154
+ }
1155
+ }
1156
+ async function dteEmitir(ctx) {
1157
+ const origen = ctx.args[0];
1158
+ const cuerpo = await leerEmision(ctx, origen);
1159
+ const doc = await enviarUnaVez(
1160
+ ctx,
1161
+ "/dte",
1162
+ cuerpo,
1163
+ `emitir un DTE (${nombreTipo(cuerpo.tipoDte)})`,
1164
+ `dte emitir ${origen}`,
1165
+ origen === "-"
1166
+ );
1167
+ if (doc.estado === "contingencia") {
1168
+ ctx.ent.stderr(
1169
+ "El MH no respondi\xF3: el DTE qued\xF3 firmado en contingencia. Transm\xEDtalo con `openfactura contingencia transmitir`."
1170
+ );
1171
+ }
1172
+ mostrar(ctx, doc, () => fichaDocumento(doc));
1173
+ return void 0;
1174
+ }
1175
+ async function dteList(ctx) {
1176
+ const estado = ctx.op("estado");
1177
+ if (estado !== void 0 && !ESTADOS_DTE.includes(estado)) {
1178
+ throw new ErrorUso(`--estado debe ser uno de: ${ESTADOS_DTE.join(", ")}`);
1179
+ }
1180
+ for (const opcion of ["desde", "hasta"]) {
1181
+ const valor = ctx.op(opcion);
1182
+ if (valor !== void 0 && !/^\d{4}-\d{2}-\d{2}$/.test(valor))
1183
+ throw new ErrorUso(`--${opcion} debe tener el formato AAAA-MM-DD`);
1184
+ }
1185
+ const { datos } = await ctx.api().pedir("GET", "/dte", {
1186
+ query: {
1187
+ tipo: ctx.op("tipo"),
1188
+ estado,
1189
+ q: ctx.op("q"),
1190
+ desde: ctx.op("desde"),
1191
+ hasta: ctx.op("hasta"),
1192
+ pagina: entero(ctx, "pagina")
1193
+ }
1194
+ });
1195
+ mostrar(
1196
+ ctx,
1197
+ datos,
1198
+ () => [
1199
+ datos.items.length ? tabla(
1200
+ [
1201
+ "fecha",
1202
+ "tipo",
1203
+ "n\xFAmero de control",
1204
+ "estado",
1205
+ "receptor",
1206
+ "total",
1207
+ "c\xF3digo de generaci\xF3n"
1208
+ ],
1209
+ datos.items.map((d) => [
1210
+ d.fechaEmision,
1211
+ d.tipoDte,
1212
+ d.numeroControl,
1213
+ d.estado,
1214
+ d.receptorNombre,
1215
+ dinero(d.total),
1216
+ d.codigoGeneracion
1217
+ ]),
1218
+ [5]
1219
+ ) : "No hay documentos.",
1220
+ `P\xE1gina ${datos.pagina} \xB7 ${datos.items.length} de ${datos.total} documento(s) \xB7 ambiente ${nombreAmbiente(datos.ambiente)}`
1221
+ ].join("\n")
1222
+ );
1223
+ return void 0;
1224
+ }
1225
+ async function dteGet(ctx) {
1226
+ const { datos } = await ctx.api().pedir("GET", `/dte/${segmento(ctx.args[0])}`);
1227
+ mostrar(ctx, datos, () => {
1228
+ const mh = datos.respuestaMh;
1229
+ const eventos = datos.eventos ?? [];
1230
+ const observaciones = Array.isArray(mh?.observaciones) ? mh.observaciones.join("; ") : null;
1231
+ return [
1232
+ fichaDocumento(datos),
1233
+ ...mh ? [
1234
+ "",
1235
+ "Respuesta del MH",
1236
+ fichas([
1237
+ ["Estado", mh.estado],
1238
+ ["Mensaje", `${mh.codigoMsg ?? ""} ${mh.descripcionMsg ?? ""}`.trim()],
1239
+ ["Procesado", mh.fhProcesamiento],
1240
+ ["Observaciones", observaciones]
1241
+ ])
1242
+ ] : [],
1243
+ ...eventos.length ? [
1244
+ "",
1245
+ "Eventos",
1246
+ tabla(
1247
+ ["tipo", "estado", "fecha (UTC)", "c\xF3digo de generaci\xF3n", "sello"],
1248
+ eventos.map((e) => [
1249
+ e.tipo,
1250
+ e.estado,
1251
+ fecha2(e.creado),
1252
+ e.codigoGeneracion,
1253
+ e.selloRecibido
1254
+ ])
1255
+ )
1256
+ ] : []
1257
+ ].join("\n");
1258
+ });
1259
+ return void 0;
1260
+ }
1261
+ async function dteJson(ctx) {
1262
+ const { datos } = await ctx.api().pedir("GET", `/dte/${segmento(ctx.args[0])}/json`);
1263
+ const texto2 = JSON.stringify(datos, null, 2);
1264
+ const salida = ctx.op("output");
1265
+ if (salida === void 0) {
1266
+ ctx.ent.stdout(texto2);
1267
+ return void 0;
1268
+ }
1269
+ const destino = resolve(ctx.ent.cwd, salida);
1270
+ await writeFile(destino, `${texto2}
1271
+ `);
1272
+ mostrar(ctx, { archivo: destino }, () => `JSON guardado en ${destino}`);
1273
+ return void 0;
1274
+ }
1275
+ async function dtePdf(ctx) {
1276
+ const codigo = ctx.args[0];
1277
+ const bytes = await ctx.api().descargar(`/dte/${segmento(codigo)}/pdf`);
1278
+ const destino = resolve(ctx.ent.cwd, ctx.op("output") ?? `${basename(codigo)}.pdf`);
1279
+ await writeFile(destino, bytes);
1280
+ mostrar(
1281
+ ctx,
1282
+ { archivo: destino, bytes: bytes.length },
1283
+ () => `PDF guardado en ${destino} (${bytes.length} bytes)`
1284
+ );
1285
+ return void 0;
1286
+ }
1287
+ async function dteInvalidar(ctx) {
1288
+ const codigo = ctx.args[0];
1289
+ const tipo = ctx.op("tipo");
1290
+ if (tipo !== "1" && tipo !== "2" && tipo !== "3") {
1291
+ throw new ErrorUso(
1292
+ "Indique --tipo 1|2|3 (1 error en la informaci\xF3n, 2 rescindir la operaci\xF3n, 3 otro)."
1293
+ );
1294
+ }
1295
+ const cuerpo = { tipoAnulacion: Number(tipo) };
1296
+ if (ctx.op("motivo") !== void 0) cuerpo.motivoAnulacion = ctx.op("motivo");
1297
+ if (ctx.op("reemplazo") !== void 0) cuerpo.codigoGeneracionReemplazo = ctx.op("reemplazo");
1298
+ const api = ctx.api();
1299
+ await confirmar(ctx, `invalidar el DTE ${codigo} (irreversible)`);
1300
+ const { datos } = await api.pedir("POST", `/dte/${segmento(codigo)}/invalidar`, {
1301
+ cuerpo
1302
+ });
1303
+ mostrar(ctx, datos, () => `DTE invalidado.
1304
+ ${fichaDocumento(datos)}`);
1305
+ return void 0;
1306
+ }
1307
+ var TIPOS_EVENTO = ["retorno", "operaciones_especiales"];
1308
+ function fichaEvento(e) {
1309
+ return fichas([
1310
+ [
1311
+ "Evento",
1312
+ e.tipoEvento === "18" ? "18 Retorno" : e.tipoEvento === "17" ? "17 Operaciones especiales" : e.tipo
1313
+ ],
1314
+ ["Estado", e.estado],
1315
+ ["C\xF3digo de generaci\xF3n", e.codigoGeneracion],
1316
+ ["Ambiente", nombreAmbiente(e.ambiente)],
1317
+ ["Sello recibido", e.selloRecibido]
1318
+ ]);
1319
+ }
1320
+ async function leerObjeto(ctx, origen, forma) {
1321
+ const cuerpo = await leerJson(ctx, origen);
1322
+ if (!cuerpo || typeof cuerpo !== "object" || Array.isArray(cuerpo) || !("items" in cuerpo)) {
1323
+ throw new ErrorUso(`El archivo debe ser un objeto ${forma}`);
1324
+ }
1325
+ return cuerpo;
1326
+ }
1327
+ async function dteRetorno(ctx) {
1328
+ const [codigo, origen] = ctx.args;
1329
+ const cuerpo = await leerObjeto(
1330
+ ctx,
1331
+ origen,
1332
+ '{ "items": [{ "descripcion", "cantidad", "precioUni" }] }'
1333
+ );
1334
+ const evento = await enviarUnaVez(
1335
+ ctx,
1336
+ `/dte/${segmento(codigo)}/retorno`,
1337
+ cuerpo,
1338
+ `registrar un retorno sobre el DTE ${codigo}`,
1339
+ `dte retorno ${codigo} ${origen}`,
1340
+ origen === "-"
1341
+ );
1342
+ mostrar(ctx, evento, () => `Retorno registrado.
1343
+ ${fichaEvento(evento)}`);
1344
+ return void 0;
1345
+ }
1346
+ async function eventosOperacionesEspeciales(ctx) {
1347
+ const origen = ctx.args[0];
1348
+ const cuerpo = await leerObjeto(
1349
+ ctx,
1350
+ origen,
1351
+ '{ "tipoDocumento": "02" | "97", "fecha"?, "items": [{ "descripcion", "precioUni", "docDel", "docAl" }] }'
1352
+ );
1353
+ const evento = await enviarUnaVez(
1354
+ ctx,
1355
+ "/eventos/operaciones-especiales",
1356
+ cuerpo,
1357
+ "informar operaciones especiales",
1358
+ `eventos operaciones-especiales ${origen}`,
1359
+ origen === "-"
1360
+ );
1361
+ mostrar(ctx, evento, () => `Operaciones especiales informadas.
1362
+ ${fichaEvento(evento)}`);
1363
+ return void 0;
1364
+ }
1365
+ async function eventosList(ctx) {
1366
+ const tipo = ctx.op("tipo");
1367
+ if (tipo !== void 0 && !TIPOS_EVENTO.includes(tipo)) {
1368
+ throw new ErrorUso(`--tipo debe ser uno de: ${TIPOS_EVENTO.join(", ")}`);
1369
+ }
1370
+ const { datos } = await ctx.api().pedir("GET", "/eventos", { query: { tipo } });
1371
+ mostrar(
1372
+ ctx,
1373
+ datos,
1374
+ () => datos.length ? tabla(
1375
+ ["creado", "evento", "estado", "c\xF3digo de generaci\xF3n"],
1376
+ datos.map((e) => [
1377
+ new Date(e.creado).toISOString().slice(0, 10),
1378
+ e.tipoEvento,
1379
+ e.estado,
1380
+ e.codigoGeneracion
1381
+ ])
1382
+ ) : "No hay eventos."
1383
+ );
1384
+ return void 0;
1385
+ }
1386
+ async function eventosGet(ctx) {
1387
+ const { datos } = await ctx.api().pedir("GET", `/eventos/${segmento(ctx.args[0])}`);
1388
+ mostrar(ctx, datos, () => fichaEvento(datos));
1389
+ return void 0;
1390
+ }
1391
+ async function eventosJson(ctx) {
1392
+ const codigo = ctx.args[0];
1393
+ const { datos } = await ctx.api().pedir("GET", `/eventos/${segmento(codigo)}/json`);
1394
+ const texto2 = JSON.stringify(datos, null, 2);
1395
+ const salida = ctx.op("output");
1396
+ if (salida === void 0) {
1397
+ ctx.ent.stdout(texto2);
1398
+ return void 0;
1399
+ }
1400
+ const destino = resolve(ctx.ent.cwd, salida);
1401
+ await writeFile(destino, `${texto2}
1402
+ `);
1403
+ mostrar(ctx, { archivo: destino }, () => `JSON guardado en ${destino}`);
1404
+ return void 0;
1405
+ }
1406
+ async function eventosPdf(ctx) {
1407
+ const codigo = ctx.args[0];
1408
+ const bytes = await ctx.api().descargar(`/eventos/${segmento(codigo)}/pdf`);
1409
+ const destino = resolve(ctx.ent.cwd, ctx.op("output") ?? `${basename(codigo)}.pdf`);
1410
+ await writeFile(destino, bytes);
1411
+ mostrar(
1412
+ ctx,
1413
+ { archivo: destino, bytes: bytes.length },
1414
+ () => `PDF guardado en ${destino} (${bytes.length} bytes)`
1415
+ );
1416
+ return void 0;
1417
+ }
1418
+ async function contingenciaTransmitir(ctx) {
1419
+ const api = ctx.api();
1420
+ await confirmar(ctx, "transmitir los DTE en contingencia");
1421
+ const { datos } = await api.pedir("POST", "/contingencia/transmitir");
1422
+ mostrar(ctx, datos, () => {
1423
+ const procesados = datos.procesados ?? [];
1424
+ const rechazados = datos.rechazados ?? [];
1425
+ if (!datos.evento && !procesados.length && !rechazados.length)
1426
+ return "No hay documentos en contingencia.";
1427
+ const linea = (d) => ` ${typeof d === "string" ? d : JSON.stringify(d)}`;
1428
+ return [
1429
+ fichas([
1430
+ ["Procesados", procesados.length],
1431
+ ["Rechazados", rechazados.length],
1432
+ ["Restantes", datos.restantes]
1433
+ ]),
1434
+ ...rechazados.length ? ["", "Rechazados:", ...rechazados.map(linea)] : []
1435
+ ].join("\n");
1436
+ });
1437
+ return void 0;
1438
+ }
1439
+ function fichaWebhook(w) {
1440
+ return fichas([
1441
+ ["Id", w.id],
1442
+ ["URL", w.url],
1443
+ ["Descripci\xF3n", w.descripcion],
1444
+ ["Eventos", Array.isArray(w.eventos) ? w.eventos.join(", ") : null],
1445
+ ["Activo", siNo(w.activo)]
1446
+ ]);
1447
+ }
1448
+ function mostrarSecreto(ctx, w, titulo) {
1449
+ mostrar(
1450
+ ctx,
1451
+ w,
1452
+ () => [
1453
+ titulo,
1454
+ fichaWebhook(w),
1455
+ "",
1456
+ `Secreto de firma: ${w.secreto}`,
1457
+ "Gu\xE1rdelo ahora: no se vuelve a mostrar. Verifique con \xE9l la cabecera OpenFactura-Signature."
1458
+ ].join("\n")
1459
+ );
1460
+ }
1461
+ async function webhooksList(ctx) {
1462
+ const { datos } = await ctx.api().pedir("GET", "/webhooks");
1463
+ mostrar(
1464
+ ctx,
1465
+ datos,
1466
+ () => datos.length ? tabla(
1467
+ ["id", "url", "eventos", "activo", "descripci\xF3n"],
1468
+ datos.map((w) => [
1469
+ w.id,
1470
+ w.url,
1471
+ w.eventos.join(", "),
1472
+ siNo(w.activo),
1473
+ w.descripcion
1474
+ ])
1475
+ ) : "No hay webhooks."
1476
+ );
1477
+ return void 0;
1478
+ }
1479
+ async function webhooksAdd(ctx) {
1480
+ const texto2 = ctx.op("eventos");
1481
+ const eventos = texto2 ? texto2.split(",").map((e) => e.trim()).filter(Boolean) : [...EVENTOS_WEBHOOK];
1482
+ const desconocido = eventos.find((e) => !EVENTOS_WEBHOOK.includes(e));
1483
+ if (desconocido || !eventos.length) {
1484
+ throw new ErrorUso(`--eventos acepta, separados por coma: ${EVENTOS_WEBHOOK.join(", ")}`);
1485
+ }
1486
+ const cuerpo = { url: ctx.args[0], eventos };
1487
+ if (ctx.op("descripcion") !== void 0) cuerpo.descripcion = ctx.op("descripcion");
1488
+ const { datos } = await ctx.api().pedir("POST", "/webhooks", { cuerpo });
1489
+ mostrarSecreto(ctx, datos, "Webhook creado.");
1490
+ return void 0;
1491
+ }
1492
+ async function webhooksRemove(ctx) {
1493
+ const id = ctx.args[0];
1494
+ const { datos } = await ctx.api().pedir("DELETE", `/webhooks/${segmento(id)}`);
1495
+ mostrar(ctx, datos, () => `Webhook ${id} eliminado.`);
1496
+ return void 0;
1497
+ }
1498
+ async function webhooksTest(ctx) {
1499
+ const id = ctx.args[0];
1500
+ const { datos } = await ctx.api().pedir("POST", `/webhooks/${segmento(id)}/probar`);
1501
+ mostrar(
1502
+ ctx,
1503
+ datos,
1504
+ () => datos.entregado ? `Entregado: el destino respondi\xF3 HTTP ${datos.status}.` : `No se entreg\xF3: ${datos.error ?? "error desconocido"}.`
1505
+ );
1506
+ return datos.entregado ? SALIDA.ok : SALIDA.error;
1507
+ }
1508
+ async function webhooksDeliveries(ctx) {
1509
+ const id = ctx.args[0];
1510
+ const { datos } = await ctx.api().pedir("GET", `/webhooks/${segmento(id)}/entregas`);
1511
+ mostrar(
1512
+ ctx,
1513
+ datos,
1514
+ () => datos.length ? tabla(
1515
+ [
1516
+ "creado (UTC)",
1517
+ "evento",
1518
+ "estado",
1519
+ "intentos",
1520
+ "http",
1521
+ "error",
1522
+ "pr\xF3ximo intento",
1523
+ "id"
1524
+ ],
1525
+ datos.map((e) => [
1526
+ fecha2(e.creado),
1527
+ e.evento,
1528
+ e.estado,
1529
+ e.intentos,
1530
+ e.ultimoStatus,
1531
+ e.ultimoError,
1532
+ fecha2(e.proximoIntento),
1533
+ e.id
1534
+ ]),
1535
+ [3]
1536
+ ) : "Este webhook todav\xEDa no tiene entregas."
1537
+ );
1538
+ return void 0;
1539
+ }
1540
+ async function webhooksRotate(ctx) {
1541
+ const id = ctx.args[0];
1542
+ const { datos } = await ctx.api().pedir("POST", `/webhooks/${segmento(id)}/rotar-secreto`);
1543
+ mostrarSecreto(ctx, datos, "Secreto rotado: el anterior ya no firma.");
1544
+ return void 0;
1545
+ }
1546
+ async function mcp(ctx) {
1547
+ const api = ctx.api();
1548
+ ctx.ent.stderr(
1549
+ `openfactura mcp: servidor MCP por stdio \u2192 ${ctx.cfg.url} (clave ${enmascarar(ctx.cfg.apiKey)})`
1550
+ );
1551
+ await servirMcp(api, ctx.version);
1552
+ return void 0;
1553
+ }
1554
+ var GRUPOS = {
1555
+ clientes: "Clientes guardados",
1556
+ productos: "Productos y servicios guardados",
1557
+ dte: "Documentos tributarios electr\xF3nicos",
1558
+ eventos: "Eventos de retorno (18) y de operaciones especiales (17)",
1559
+ contingencia: "Documentos emitidos en contingencia",
1560
+ webhooks: "Notificaciones firmadas a su sistema cuando cambia un documento"
1561
+ };
1562
+ var COMANDOS = [
1563
+ {
1564
+ nombre: "login",
1565
+ uso: "login [--url <url>] [--key <clave>]",
1566
+ resumen: "Verifica una clave de API y la guarda",
1567
+ opciones: ["key"],
1568
+ args: [0, 0],
1569
+ accion: login
1570
+ },
1571
+ {
1572
+ nombre: "logout",
1573
+ uso: "logout",
1574
+ resumen: "Borra la clave guardada del perfil",
1575
+ opciones: [],
1576
+ args: [0, 0],
1577
+ accion: logout
1578
+ },
1579
+ {
1580
+ nombre: "whoami",
1581
+ uso: "whoami",
1582
+ resumen: "Muestra el actor, sus permisos, el ambiente y el servidor",
1583
+ opciones: [],
1584
+ args: [0, 0],
1585
+ accion: whoami
1586
+ },
1587
+ {
1588
+ nombre: "emisor",
1589
+ uso: "emisor",
1590
+ resumen: "Datos del emisor, establecimientos y credenciales cargadas",
1591
+ opciones: [],
1592
+ args: [0, 0],
1593
+ accion: emisor
1594
+ },
1595
+ {
1596
+ nombre: "clientes list",
1597
+ uso: "clientes list [--q <texto>]",
1598
+ resumen: "Lista o busca clientes",
1599
+ opciones: ["q"],
1600
+ args: [0, 0],
1601
+ accion: clientesList
1602
+ },
1603
+ {
1604
+ nombre: "clientes add",
1605
+ uso: "clientes add --file <cliente.json> | --tipo-documento <36|13|03|02|37> --num-documento <n> --nombre <nombre> [\u2026]",
1606
+ resumen: "Crea un cliente",
1607
+ opciones: ["file", ...Object.keys(CAMPOS_CLIENTE)],
1608
+ args: [0, 0],
1609
+ accion: clientesAdd
1610
+ },
1611
+ {
1612
+ nombre: "productos list",
1613
+ uso: "productos list [--q <texto>]",
1614
+ resumen: "Lista o busca productos",
1615
+ opciones: ["q"],
1616
+ args: [0, 0],
1617
+ accion: productosList
1618
+ },
1619
+ {
1620
+ nombre: "productos add",
1621
+ uso: "productos add --file <producto.json> | --codigo <c> --descripcion <d> --precio <sin IVA> [\u2026]",
1622
+ resumen: "Crea un producto o servicio",
1623
+ opciones: ["file", ...Object.keys(CAMPOS_PRODUCTO)],
1624
+ args: [0, 0],
1625
+ accion: productosAdd
1626
+ },
1627
+ {
1628
+ nombre: "catalogos",
1629
+ uso: "catalogos [<id>] [--q <texto>] [--departamento <06>]",
1630
+ resumen: "Cat\xE1logos oficiales del MH (CAT-012, CAT-019\u2026)",
1631
+ opciones: ["q", "departamento"],
1632
+ args: [0, 1],
1633
+ accion: catalogos
1634
+ },
1635
+ {
1636
+ nombre: "dte validar",
1637
+ uso: "dte validar <archivo.json | ->",
1638
+ resumen: "Valida un DTE sin emitirlo (salida 1 si hay problemas)",
1639
+ opciones: [],
1640
+ args: [1, 1],
1641
+ accion: dteValidar
1642
+ },
1643
+ {
1644
+ nombre: "dte emitir",
1645
+ uso: "dte emitir <archivo.json | -> [--idempotency-key <k>] [--yes]",
1646
+ resumen: "Emite un DTE: lo firma y lo transmite al MH",
1647
+ opciones: ["idempotency-key", "yes"],
1648
+ args: [1, 1],
1649
+ accion: dteEmitir
1650
+ },
1651
+ {
1652
+ nombre: "dte list",
1653
+ uso: "dte list [--tipo <01>] [--estado <estado>] [--q <texto>] [--desde <AAAA-MM-DD>] [--hasta <AAAA-MM-DD>] [--pagina <n>]",
1654
+ resumen: "Lista documentos, del m\xE1s reciente al m\xE1s antiguo",
1655
+ opciones: ["tipo", "estado", "q", "desde", "hasta", "pagina"],
1656
+ args: [0, 0],
1657
+ accion: dteList
1658
+ },
1659
+ {
1660
+ nombre: "dte get",
1661
+ uso: "dte get <codigoGeneracion>",
1662
+ resumen: "Detalle de un documento",
1663
+ opciones: [],
1664
+ args: [1, 1],
1665
+ accion: dteGet
1666
+ },
1667
+ {
1668
+ nombre: "dte json",
1669
+ uso: "dte json <codigoGeneracion> [-o <archivo.json>]",
1670
+ resumen: "JSON oficial del DTE (firma y sello)",
1671
+ opciones: ["output"],
1672
+ args: [1, 1],
1673
+ accion: dteJson
1674
+ },
1675
+ {
1676
+ nombre: "dte pdf",
1677
+ uso: "dte pdf <codigoGeneracion> [-o <archivo.pdf>]",
1678
+ resumen: "Descarga la representaci\xF3n gr\xE1fica en PDF",
1679
+ opciones: ["output"],
1680
+ args: [1, 1],
1681
+ accion: dtePdf
1682
+ },
1683
+ {
1684
+ nombre: "dte invalidar",
1685
+ uso: "dte invalidar <codigoGeneracion> --tipo <1|2|3> [--motivo <texto>] [--reemplazo <codigo>] [--yes]",
1686
+ resumen: "Invalida un DTE procesado, o un evento de retorno u operaciones especiales (irreversible)",
1687
+ opciones: ["tipo", "motivo", "reemplazo", "yes"],
1688
+ args: [1, 1],
1689
+ accion: dteInvalidar
1690
+ },
1691
+ {
1692
+ nombre: "dte retorno",
1693
+ uso: "dte retorno <codigoGeneracion> <archivo.json | -> [--idempotency-key <k>] [--yes]",
1694
+ resumen: "Registra un retorno (evento 18) sobre una Factura, Exportaci\xF3n o Sujeto Excluido",
1695
+ opciones: ["idempotency-key", "yes"],
1696
+ args: [2, 2],
1697
+ accion: dteRetorno
1698
+ },
1699
+ {
1700
+ nombre: "eventos operaciones-especiales",
1701
+ uso: "eventos operaciones-especiales <archivo.json | -> [--idempotency-key <k>] [--yes]",
1702
+ resumen: "Informa facturas simplificadas o comprobantes de control interno (evento 17)",
1703
+ opciones: ["idempotency-key", "yes"],
1704
+ args: [1, 1],
1705
+ accion: eventosOperacionesEspeciales
1706
+ },
1707
+ {
1708
+ nombre: "eventos list",
1709
+ uso: "eventos list [--tipo retorno|operaciones_especiales]",
1710
+ resumen: "Lista los eventos de retorno y operaciones especiales",
1711
+ opciones: ["tipo"],
1712
+ args: [0, 0],
1713
+ accion: eventosList
1714
+ },
1715
+ {
1716
+ nombre: "eventos get",
1717
+ uso: "eventos get <codigoGeneracion>",
1718
+ resumen: "Muestra un evento",
1719
+ opciones: [],
1720
+ args: [1, 1],
1721
+ accion: eventosGet
1722
+ },
1723
+ {
1724
+ nombre: "eventos json",
1725
+ uso: "eventos json <codigoGeneracion> [-o <archivo.json>]",
1726
+ resumen: "Descarga el archivo JSON oficial del evento",
1727
+ opciones: ["output"],
1728
+ args: [1, 1],
1729
+ accion: eventosJson
1730
+ },
1731
+ {
1732
+ nombre: "eventos pdf",
1733
+ uso: "eventos pdf <codigoGeneracion> [-o <archivo.pdf>]",
1734
+ resumen: "Descarga la representaci\xF3n gr\xE1fica del evento",
1735
+ opciones: ["output"],
1736
+ args: [1, 1],
1737
+ accion: eventosPdf
1738
+ },
1739
+ {
1740
+ nombre: "contingencia transmitir",
1741
+ uso: "contingencia transmitir [--yes]",
1742
+ resumen: "Transmite al MH los DTE en contingencia",
1743
+ opciones: ["yes"],
1744
+ args: [0, 0],
1745
+ accion: contingenciaTransmitir
1746
+ },
1747
+ {
1748
+ nombre: "webhooks list",
1749
+ uso: "webhooks list",
1750
+ resumen: "Lista los webhooks (sin su secreto)",
1751
+ opciones: [],
1752
+ args: [0, 0],
1753
+ accion: webhooksList
1754
+ },
1755
+ {
1756
+ nombre: "webhooks add",
1757
+ uso: "webhooks add <https://url> [--eventos <e1,e2>] [--descripcion <texto>]",
1758
+ resumen: "Crea un webhook y muestra su secreto de firma una sola vez",
1759
+ opciones: ["eventos", "descripcion"],
1760
+ args: [1, 1],
1761
+ accion: webhooksAdd
1762
+ },
1763
+ {
1764
+ nombre: "webhooks remove",
1765
+ uso: "webhooks remove <id>",
1766
+ resumen: "Elimina un webhook y su historial de entregas",
1767
+ opciones: [],
1768
+ args: [1, 1],
1769
+ accion: webhooksRemove
1770
+ },
1771
+ {
1772
+ nombre: "webhooks test",
1773
+ uso: "webhooks test <id>",
1774
+ resumen: "Env\xEDa un evento webhook.prueba (salida 1 si no se entrega)",
1775
+ opciones: [],
1776
+ args: [1, 1],
1777
+ accion: webhooksTest
1778
+ },
1779
+ {
1780
+ nombre: "webhooks deliveries",
1781
+ uso: "webhooks deliveries <id>",
1782
+ resumen: "\xDAltimas 50 entregas de un webhook",
1783
+ opciones: [],
1784
+ args: [1, 1],
1785
+ accion: webhooksDeliveries
1786
+ },
1787
+ {
1788
+ nombre: "webhooks rotate",
1789
+ uso: "webhooks rotate <id>",
1790
+ resumen: "Genera un secreto de firma nuevo y lo muestra una vez",
1791
+ opciones: [],
1792
+ args: [1, 1],
1793
+ accion: webhooksRotate
1794
+ },
1795
+ {
1796
+ nombre: "mcp",
1797
+ uso: "mcp",
1798
+ resumen: "Servidor MCP por stdio para Claude y otros clientes",
1799
+ opciones: [],
1800
+ args: [0, 0],
1801
+ accion: mcp
1802
+ }
1803
+ ];
1804
+
1805
+ // src/cli.ts
1806
+ var VERSION = package_default.version;
1807
+ var OPCIONES = {
1808
+ help: { type: "boolean", short: "h", ayuda: "Muestra esta ayuda" },
1809
+ version: { type: "boolean", short: "v", ayuda: "Muestra la versi\xF3n" },
1810
+ json: { type: "boolean", ayuda: "Imprime el JSON de la API (para scripts)" },
1811
+ url: { type: "string", ayuda: "URL del servidor (o OPENFACTURA_URL)" },
1812
+ profile: {
1813
+ type: "string",
1814
+ ayuda: "Perfil de configuraci\xF3n (o OPENFACTURA_PROFILE; por defecto \xABdefault\xBB)"
1815
+ },
1816
+ yes: { type: "boolean", short: "y", ayuda: "No pedir confirmaci\xF3n con claves de producci\xF3n" },
1817
+ key: { type: "string", ayuda: "Clave de API; si se omite, se pide sin mostrarla" },
1818
+ q: { type: "string", ayuda: "Texto a buscar" },
1819
+ file: {
1820
+ type: "string",
1821
+ short: "f",
1822
+ ayuda: "Archivo JSON con los datos (\xAB-\xBB lee la entrada est\xE1ndar)"
1823
+ },
1824
+ output: { type: "string", short: "o", ayuda: "Archivo de salida" },
1825
+ "idempotency-key": {
1826
+ type: "string",
1827
+ ayuda: "Clave de idempotencia; por defecto se genera un UUID"
1828
+ },
1829
+ tipo: {
1830
+ type: "string",
1831
+ ayuda: "Tipo de DTE (01, 03\u2026); al invalidar, tipo de anulaci\xF3n (1, 2, 3); en eventos, retorno | operaciones_especiales"
1832
+ },
1833
+ estado: {
1834
+ type: "string",
1835
+ ayuda: "pendiente | procesado | rechazado | contingencia | invalidado"
1836
+ },
1837
+ desde: { type: "string", ayuda: "Fecha inicial AAAA-MM-DD" },
1838
+ hasta: { type: "string", ayuda: "Fecha final AAAA-MM-DD" },
1839
+ pagina: { type: "string", ayuda: "N\xFAmero de p\xE1gina" },
1840
+ motivo: { type: "string", ayuda: "Motivo de la invalidaci\xF3n" },
1841
+ reemplazo: { type: "string", ayuda: "C\xF3digo de generaci\xF3n del DTE que reemplaza al invalidado" },
1842
+ "tipo-documento": {
1843
+ type: "string",
1844
+ ayuda: "36 NIT | 13 DUI | 03 pasaporte | 02 carn\xE9 de residente | 37 otro"
1845
+ },
1846
+ "num-documento": { type: "string", ayuda: "N\xFAmero del documento" },
1847
+ nrc: { type: "string", ayuda: "NRC del contribuyente" },
1848
+ nombre: { type: "string", ayuda: "Nombre o raz\xF3n social" },
1849
+ "nombre-comercial": { type: "string", ayuda: "Nombre comercial" },
1850
+ "cod-actividad": { type: "string", ayuda: "C\xF3digo de actividad econ\xF3mica (CAT-019)" },
1851
+ "desc-actividad": { type: "string", ayuda: "Descripci\xF3n de la actividad econ\xF3mica" },
1852
+ departamento: { type: "string", ayuda: "C\xF3digo de departamento (CAT-012)" },
1853
+ municipio: { type: "string", ayuda: "C\xF3digo de municipio (CAT-013)" },
1854
+ distrito: { type: "string", ayuda: "C\xF3digo de distrito (CAT-008)" },
1855
+ complemento: { type: "string", ayuda: "Direcci\xF3n" },
1856
+ telefono: { type: "string", ayuda: "Tel\xE9fono" },
1857
+ correo: { type: "string", ayuda: "Correo electr\xF3nico" },
1858
+ codigo: { type: "string", ayuda: "C\xF3digo del producto" },
1859
+ descripcion: { type: "string", ayuda: "Descripci\xF3n (del producto o del webhook)" },
1860
+ eventos: {
1861
+ type: "string",
1862
+ ayuda: "Eventos del webhook separados por coma (por defecto todos): dte.procesado, dte.rechazado, dte.contingencia, dte.invalidado, retorno.procesado, operaciones_especiales.procesado"
1863
+ },
1864
+ precio: { type: "string", ayuda: "Precio unitario SIN IVA" },
1865
+ "tipo-item": {
1866
+ type: "string",
1867
+ ayuda: "1 bien | 2 servicio | 3 ambos | 4 otros tributos (por defecto 1)"
1868
+ },
1869
+ "uni-medida": { type: "string", ayuda: "Unidad de medida, CAT-014 (por defecto 59 = unidad)" },
1870
+ "tipo-venta": { type: "string", ayuda: "gravada | exenta | noSujeta (por defecto gravada)" }
1871
+ };
1872
+ var GLOBALES = ["help", "json", "url", "profile"];
1873
+ var bandera = (nombre) => {
1874
+ const o = OPCIONES[nombre];
1875
+ return `${o.short ? `-${o.short}, ` : ""}--${nombre}${o.type === "string" ? " <valor>" : ""}`;
1876
+ };
1877
+ var listaOpciones = (nombres) => {
1878
+ const ancho = Math.max(...nombres.map((n) => bandera(n).length));
1879
+ return nombres.map((n) => ` ${bandera(n).padEnd(ancho)} ${OPCIONES[n].ayuda}`).join("\n");
1880
+ };
1881
+ function ayudaGeneral(grupo) {
1882
+ const comandos = grupo ? COMANDOS.filter((c) => c.nombre.startsWith(`${grupo} `)) : COMANDOS;
1883
+ const ancho = Math.max(...comandos.map((c) => c.nombre.length));
1884
+ return [
1885
+ grupo ? `openfactura ${grupo} \u2014 ${GRUPOS[grupo]}` : `openfactura ${VERSION} \u2014 facturaci\xF3n electr\xF3nica de El Salvador (DTE) desde la terminal`,
1886
+ "",
1887
+ `Uso: openfactura ${grupo ? `${grupo} <subcomando>` : "<comando>"} [opciones]`,
1888
+ "",
1889
+ "Comandos:",
1890
+ ...comandos.map((c) => ` ${c.nombre.padEnd(ancho)} ${c.resumen}`),
1891
+ "",
1892
+ "Opciones globales:",
1893
+ listaOpciones(grupo ? GLOBALES : [...GLOBALES, "version"]),
1894
+ "",
1895
+ "Use `openfactura <comando> --help` para ver las opciones de un comando.",
1896
+ ...grupo ? [] : [
1897
+ "",
1898
+ "Entorno: OPENFACTURA_API_KEY, OPENFACTURA_URL, OPENFACTURA_PROFILE.",
1899
+ "Salida: 0 bien \xB7 1 error de la API o de validaci\xF3n \xB7 2 uso incorrecto \xB7 3 sin autenticar \xB7 4 l\xEDmite de peticiones."
1900
+ ]
1901
+ ].join("\n");
1902
+ }
1903
+ function ayudaComando(comando) {
1904
+ return [
1905
+ comando.resumen,
1906
+ "",
1907
+ `Uso: openfactura ${comando.uso}`,
1908
+ ...comando.opciones.length ? ["", "Opciones:", listaOpciones(comando.opciones)] : [],
1909
+ "",
1910
+ "Opciones globales:",
1911
+ listaOpciones(GLOBALES)
1912
+ ].join("\n");
1913
+ }
1914
+ function analizar(argv) {
1915
+ try {
1916
+ const opciones = Object.fromEntries(
1917
+ Object.entries(OPCIONES).map(([nombre, { type, short }]) => [
1918
+ nombre,
1919
+ short ? { type, short } : { type }
1920
+ ])
1921
+ );
1922
+ return parseArgs({ args: argv, options: opciones, allowPositionals: true, strict: true });
1923
+ } catch (error) {
1924
+ const { code, message } = error;
1925
+ const opcion = /'(-[^' ]*)/.exec(message)?.[1];
1926
+ if (code === "ERR_PARSE_ARGS_UNKNOWN_OPTION")
1927
+ throw new ErrorUso(`Opci\xF3n desconocida: ${opcion ?? message}`);
1928
+ if (code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE") {
1929
+ throw new ErrorUso(
1930
+ opcion ? `La opci\xF3n ${opcion} est\xE1 mal usada: revise si lleva o no un valor.` : message
1931
+ );
1932
+ }
1933
+ throw new ErrorUso(message);
1934
+ }
1935
+ }
1936
+ function informarError(error, ent, json) {
1937
+ if (error instanceof ErrorUso) {
1938
+ ent.stderr(`Error: ${error.message}`);
1939
+ ent.stderr("Use `openfactura --help` para ver los comandos.");
1940
+ return SALIDA.uso;
1941
+ }
1942
+ if (error instanceof ErrorApi) {
1943
+ if (json) {
1944
+ ent.stderr(
1945
+ JSON.stringify(
1946
+ error.retryAfter === null ? error.cuerpo : { ...error.cuerpo, retryAfter: error.retryAfter },
1947
+ null,
1948
+ 2
1949
+ )
1950
+ );
1951
+ return error.salida;
1952
+ }
1953
+ ent.stderr(`Error [${error.code}]: ${error.message}`);
1954
+ for (const linea of detalles(error)) ent.stderr(` ${linea}`);
1955
+ if (error.status === 401)
1956
+ ent.stderr(" Revise OPENFACTURA_API_KEY o ejecute `openfactura login`.");
1957
+ if (error.status === 429) {
1958
+ ent.stderr(
1959
+ ` Retry-After: ${error.retryAfter ?? "?"} (segundos que debe esperar antes de reintentar)`
1960
+ );
1961
+ }
1962
+ return error.salida;
1963
+ }
1964
+ if (error instanceof ErrorCli) {
1965
+ ent.stderr(
1966
+ json ? JSON.stringify(error.cuerpo, null, 2) : `Error [${error.code}]: ${error.message}`
1967
+ );
1968
+ return error.salida;
1969
+ }
1970
+ ent.stderr(
1971
+ `Error inesperado: ${error instanceof Error ? error.stack ?? error.message : String(error)}`
1972
+ );
1973
+ return SALIDA.error;
1974
+ }
1975
+ function detalles(error) {
1976
+ const d = error.details;
1977
+ if (d === void 0 || d === null) return [];
1978
+ const problema = (p) => `${p.ruta || "(ra\xEDz)"}: ${p.mensaje}`;
1979
+ if (Array.isArray(d)) return d.map((p) => problema(p));
1980
+ if (Array.isArray(d.problemas)) return d.problemas.map((p) => problema(p));
1981
+ if (d.mh && typeof d.mh === "object") {
1982
+ const mh = d.mh;
1983
+ const observaciones = Array.isArray(mh.observaciones) ? mh.observaciones : [];
1984
+ return [
1985
+ `MH ${mh.codigo ?? ""}: ${mh.mensaje ?? ""}`.trim(),
1986
+ ...observaciones.map((o) => `- ${o}`)
1987
+ ];
1988
+ }
1989
+ if (typeof d.permiso === "string") return [`Permiso que falta en la clave: ${d.permiso}`];
1990
+ return [JSON.stringify(d)];
1991
+ }
1992
+ async function ejecutar(argv, ent) {
1993
+ let json = argv.includes("--json");
1994
+ try {
1995
+ const { values, positionals } = analizar(argv);
1996
+ const valores = values;
1997
+ json = valores.json === true;
1998
+ const [primero, segundo] = positionals;
1999
+ if (!primero) {
2000
+ if (valores.version) {
2001
+ ent.stdout(VERSION);
2002
+ return SALIDA.ok;
2003
+ }
2004
+ if (valores.help) {
2005
+ ent.stdout(ayudaGeneral());
2006
+ return SALIDA.ok;
2007
+ }
2008
+ throw new ErrorUso("Falta el comando.");
2009
+ }
2010
+ const esGrupo = Object.hasOwn(GRUPOS, primero);
2011
+ const nombre = esGrupo ? `${primero} ${segundo ?? ""}`.trim() : primero;
2012
+ const comando = COMANDOS.find((c) => c.nombre === nombre);
2013
+ if (!comando) {
2014
+ if (esGrupo && !segundo && valores.help) {
2015
+ ent.stdout(ayudaGeneral(primero));
2016
+ return SALIDA.ok;
2017
+ }
2018
+ const disponibles = COMANDOS.filter((c) => c.nombre.startsWith(`${primero} `)).map(
2019
+ (c) => c.nombre.split(" ")[1]
2020
+ );
2021
+ throw new ErrorUso(
2022
+ esGrupo ? `${segundo ? `Subcomando desconocido: ${primero} ${segundo}.` : `Falta el subcomando de \xAB${primero}\xBB.`} Disponibles: ${disponibles.join(", ")}` : `Comando desconocido: ${primero}`
2023
+ );
2024
+ }
2025
+ if (valores.help) {
2026
+ ent.stdout(ayudaComando(comando));
2027
+ return SALIDA.ok;
2028
+ }
2029
+ const permitidas = /* @__PURE__ */ new Set([...GLOBALES, ...comando.opciones]);
2030
+ const sobrante = Object.keys(valores).find((opcion) => !permitidas.has(opcion));
2031
+ if (sobrante)
2032
+ throw new ErrorUso(
2033
+ `La opci\xF3n --${sobrante} no aplica a \xAB${comando.nombre}\xBB.
2034
+ Uso: openfactura ${comando.uso}`
2035
+ );
2036
+ const args = positionals.slice(esGrupo ? 2 : 1);
2037
+ const [minimo, maximo] = comando.args;
2038
+ if (args.length < minimo || args.length > maximo) {
2039
+ throw new ErrorUso(
2040
+ `${args.length < minimo ? "Faltan argumentos" : `Sobran argumentos (${args.slice(maximo).join(" ")})`}.
2041
+ Uso: openfactura ${comando.uso}`
2042
+ );
2043
+ }
2044
+ const cfg = resolver(ent.env, {
2045
+ url: valores.url,
2046
+ perfil: valores.profile
2047
+ });
2048
+ let cliente2;
2049
+ const ctx = {
2050
+ ent,
2051
+ version: VERSION,
2052
+ args,
2053
+ json,
2054
+ si: valores.yes === true,
2055
+ cfg,
2056
+ op: (opcion) => typeof valores[opcion] === "string" ? valores[opcion] : void 0,
2057
+ api: () => {
2058
+ if (!cfg.apiKey) {
2059
+ throw new ErrorCli(
2060
+ "No hay una clave de API. Ejecute `openfactura login` o defina OPENFACTURA_API_KEY.",
2061
+ "no_autenticado",
2062
+ SALIDA.noAutenticado
2063
+ );
2064
+ }
2065
+ if (!cliente2) {
2066
+ const { protocol, hostname } = new URL(cfg.url);
2067
+ if (protocol === "http:" && !["localhost", "127.0.0.1", "[::1]"].includes(hostname)) {
2068
+ ent.stderr(`Aviso: ${cfg.url} no usa HTTPS; la clave de API viaja sin cifrar.`);
2069
+ }
2070
+ cliente2 = new ClienteApi({
2071
+ url: cfg.url,
2072
+ apiKey: cfg.apiKey,
2073
+ version: VERSION,
2074
+ fetch: ent.fetch
2075
+ });
2076
+ }
2077
+ return cliente2;
2078
+ }
2079
+ };
2080
+ return await comando.accion(ctx) ?? SALIDA.ok;
2081
+ } catch (error) {
2082
+ return informarError(error, ent, json);
2083
+ }
2084
+ }
2085
+
2086
+ // src/index.ts
2087
+ async function leerEntrada() {
2088
+ const partes = [];
2089
+ for await (const parte of process.stdin) partes.push(Buffer.from(parte));
2090
+ return Buffer.concat(partes).toString("utf8");
2091
+ }
2092
+ function preguntar(pregunta, oculto = false) {
2093
+ const { stdin, stderr } = process;
2094
+ return new Promise((resolve2, reject) => {
2095
+ let texto2 = "";
2096
+ stderr.write(pregunta);
2097
+ if (oculto) stdin.setRawMode(true);
2098
+ stdin.setEncoding("utf8");
2099
+ stdin.resume();
2100
+ const terminar = (accion) => {
2101
+ stdin.off("data", alRecibir);
2102
+ if (oculto) {
2103
+ stdin.setRawMode(false);
2104
+ stderr.write("\n");
2105
+ }
2106
+ stdin.pause();
2107
+ accion();
2108
+ };
2109
+ const alRecibir = (dato) => {
2110
+ for (const letra of dato) {
2111
+ if (letra === "\n" || letra === "\r" || letra === "")
2112
+ return terminar(() => resolve2(texto2));
2113
+ if (letra === "")
2114
+ return terminar(() => reject(new ErrorCli("Operaci\xF3n cancelada.", "cancelado")));
2115
+ if (letra === "\x7F" || letra === "\b") texto2 = texto2.slice(0, -1);
2116
+ else if (letra >= " ") texto2 += letra;
2117
+ }
2118
+ };
2119
+ stdin.on("data", alRecibir);
2120
+ });
2121
+ }
2122
+ var entorno = {
2123
+ env: process.env,
2124
+ fetch: (...args) => fetch(...args),
2125
+ stdout: (linea) => void process.stdout.write(`${linea}
2126
+ `),
2127
+ stderr: (linea) => void process.stderr.write(`${linea}
2128
+ `),
2129
+ leerEntrada,
2130
+ esTTY: Boolean(process.stdin.isTTY),
2131
+ preguntar,
2132
+ cwd: process.cwd()
2133
+ };
2134
+ process.stdout.on("error", (error) => {
2135
+ if (error.code === "EPIPE") process.exit(0);
2136
+ });
2137
+ process.exitCode = await ejecutar(process.argv.slice(2), entorno);