@openfactura/cli 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +333 -2
- package/dist/index.js +1642 -0
- package/package.json +51 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1642 @@
|
|
|
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.1.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
|
+
|
|
261
|
+
// src/http.ts
|
|
262
|
+
var TIEMPO_LIMITE_MS = 6e4;
|
|
263
|
+
var ClienteApi = class {
|
|
264
|
+
url;
|
|
265
|
+
opciones;
|
|
266
|
+
constructor(opciones) {
|
|
267
|
+
this.opciones = opciones;
|
|
268
|
+
this.url = opciones.url;
|
|
269
|
+
}
|
|
270
|
+
async pedir(metodo, ruta2, peticion = {}) {
|
|
271
|
+
const respuesta = await this.enviar(metodo, ruta2, peticion);
|
|
272
|
+
const texto2 = await respuesta.text();
|
|
273
|
+
let datos = null;
|
|
274
|
+
if (texto2) {
|
|
275
|
+
try {
|
|
276
|
+
datos = JSON.parse(texto2);
|
|
277
|
+
} catch {
|
|
278
|
+
throw new ErrorCli(
|
|
279
|
+
`El servidor respondi\xF3 algo que no es JSON (HTTP ${respuesta.status}). Revise la URL: ${this.url}`,
|
|
280
|
+
"respuesta_invalida"
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return { status: respuesta.status, headers: respuesta.headers, datos };
|
|
285
|
+
}
|
|
286
|
+
/** Descarga binaria (PDF). */
|
|
287
|
+
async descargar(ruta2) {
|
|
288
|
+
const respuesta = await this.enviar("GET", ruta2, {});
|
|
289
|
+
return new Uint8Array(await respuesta.arrayBuffer());
|
|
290
|
+
}
|
|
291
|
+
async enviar(metodo, ruta2, { query, cuerpo, headers }) {
|
|
292
|
+
const destino = new URL(`${this.url}/api/v1${ruta2}`);
|
|
293
|
+
for (const [clave, valor] of Object.entries(query ?? {})) {
|
|
294
|
+
if (valor !== void 0 && valor !== null && valor !== "")
|
|
295
|
+
destino.searchParams.set(clave, String(valor));
|
|
296
|
+
}
|
|
297
|
+
const { apiKey, version, timeoutMs = TIEMPO_LIMITE_MS } = this.opciones;
|
|
298
|
+
const hacerFetch = this.opciones.fetch ?? fetch;
|
|
299
|
+
let respuesta;
|
|
300
|
+
try {
|
|
301
|
+
respuesta = await hacerFetch(destino, {
|
|
302
|
+
method: metodo,
|
|
303
|
+
headers: {
|
|
304
|
+
Authorization: `Bearer ${apiKey}`,
|
|
305
|
+
"User-Agent": `openfactura-cli/${version}`,
|
|
306
|
+
Accept: "application/json",
|
|
307
|
+
...cuerpo === void 0 ? {} : { "Content-Type": "application/json" },
|
|
308
|
+
...headers
|
|
309
|
+
},
|
|
310
|
+
body: cuerpo === void 0 ? void 0 : JSON.stringify(cuerpo),
|
|
311
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
312
|
+
});
|
|
313
|
+
} catch (error) {
|
|
314
|
+
const nombre = error?.name;
|
|
315
|
+
if (nombre === "TimeoutError" || nombre === "AbortError") {
|
|
316
|
+
throw new ErrorCli(
|
|
317
|
+
`El servidor no respondi\xF3 en ${Math.round(timeoutMs / 1e3)} s (${this.url})`,
|
|
318
|
+
"tiempo_agotado"
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
const causa = error?.cause;
|
|
322
|
+
const detalle = causa?.code || causa?.message || error?.message || "error de red";
|
|
323
|
+
throw new ErrorCli(`No se pudo conectar con ${this.url} (${detalle})`, "sin_conexion");
|
|
324
|
+
}
|
|
325
|
+
if (!respuesta.ok) throw await leerError(respuesta);
|
|
326
|
+
return respuesta;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
async function leerError(respuesta) {
|
|
330
|
+
const texto2 = await respuesta.text().catch(() => "");
|
|
331
|
+
let cuerpo = null;
|
|
332
|
+
try {
|
|
333
|
+
const json = JSON.parse(texto2);
|
|
334
|
+
const e = json?.error;
|
|
335
|
+
if (e && typeof e.code === "string" && typeof e.message === "string") {
|
|
336
|
+
cuerpo = {
|
|
337
|
+
error: {
|
|
338
|
+
code: e.code,
|
|
339
|
+
message: e.message,
|
|
340
|
+
...e.details === void 0 ? {} : { details: e.details }
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
} catch {
|
|
345
|
+
}
|
|
346
|
+
cuerpo ??= {
|
|
347
|
+
error: {
|
|
348
|
+
code: `http_${respuesta.status}`,
|
|
349
|
+
message: `El servidor respondi\xF3 HTTP ${respuesta.status}${respuesta.statusText ? ` ${respuesta.statusText}` : ""}`
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
const espera = Number.parseInt(respuesta.headers.get("Retry-After") ?? "", 10);
|
|
353
|
+
return new ErrorApi(respuesta.status, cuerpo, Number.isFinite(espera) ? espera : null);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/mcp.ts
|
|
357
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
358
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
359
|
+
import { z as z2 } from "zod";
|
|
360
|
+
var listaTipos = Object.entries(TIPOS_DTE).map(([codigo, nombre]) => `${codigo} ${nombre}`).join(", ");
|
|
361
|
+
var DATOS_DTE = `Datos del documento. Campos habituales:
|
|
362
|
+
- 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.
|
|
363
|
+
- 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.
|
|
364
|
+
- condicionOperacion?: 1 contado | 2 cr\xE9dito | 3 otro; pagos?; credito?: { plazo, periodo }.
|
|
365
|
+
- documentosRelacionados (notas de cr\xE9dito/d\xE9bito): [{ tipoDocumento, tipoGeneracion: 2, numeroDocumento: <c\xF3digo de generaci\xF3n>, fechaEmision }].
|
|
366
|
+
Los c\xF3digos de cat\xE1logo se consultan con catalogo_buscar.`;
|
|
367
|
+
var entradaEmision = {
|
|
368
|
+
tipoDte: tipoDte.describe(`Tipo de documento: ${listaTipos}`),
|
|
369
|
+
datos: z2.record(z2.string(), z2.unknown()).describe(DATOS_DTE),
|
|
370
|
+
clienteId: z2.string().optional().describe("Id de un cliente guardado para usarlo como receptor"),
|
|
371
|
+
puntoVentaId: z2.string().optional().describe("Punto de venta emisor; por defecto el primero")
|
|
372
|
+
};
|
|
373
|
+
async function responder(accion) {
|
|
374
|
+
try {
|
|
375
|
+
const { datos } = await accion();
|
|
376
|
+
return {
|
|
377
|
+
content: [{ type: "text", text: JSON.stringify(datos, null, 2) }],
|
|
378
|
+
structuredContent: { resultado: datos }
|
|
379
|
+
};
|
|
380
|
+
} catch (error) {
|
|
381
|
+
const cuerpo = error instanceof ErrorApi || error instanceof ErrorCli ? error.cuerpo : { error: { code: "error_interno", message: "Ocurri\xF3 un error inesperado" } };
|
|
382
|
+
return { content: [{ type: "text", text: JSON.stringify(cuerpo, null, 2) }], isError: true };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
var lectura = { readOnlyHint: true, openWorldHint: false };
|
|
386
|
+
var ruta = (segmento2) => encodeURIComponent(segmento2);
|
|
387
|
+
function crearServidorMcp(api, version) {
|
|
388
|
+
const servidor = new McpServer({ name: "openfactura", version });
|
|
389
|
+
servidor.registerTool(
|
|
390
|
+
"emisor_obtener",
|
|
391
|
+
{
|
|
392
|
+
title: "Ver emisor",
|
|
393
|
+
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.",
|
|
394
|
+
inputSchema: z2.object({}),
|
|
395
|
+
annotations: lectura
|
|
396
|
+
},
|
|
397
|
+
() => responder(() => api.pedir("GET", "/emisor"))
|
|
398
|
+
);
|
|
399
|
+
servidor.registerTool(
|
|
400
|
+
"catalogo_buscar",
|
|
401
|
+
{
|
|
402
|
+
title: "Buscar en cat\xE1logos del MH",
|
|
403
|
+
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.",
|
|
404
|
+
inputSchema: z2.object({
|
|
405
|
+
id: z2.string().optional().describe("Por ejemplo CAT-019"),
|
|
406
|
+
q: z2.string().optional().describe("Texto a buscar en el c\xF3digo o la descripci\xF3n"),
|
|
407
|
+
departamento: z2.string().optional().describe("Filtra municipios o distritos por departamento")
|
|
408
|
+
}),
|
|
409
|
+
annotations: lectura
|
|
410
|
+
},
|
|
411
|
+
({ id, q, departamento }) => responder(
|
|
412
|
+
() => id ? api.pedir("GET", `/catalogos/${ruta(id.toUpperCase())}`, {
|
|
413
|
+
query: { q, departamento, limite: 50 }
|
|
414
|
+
}) : api.pedir("GET", "/catalogos")
|
|
415
|
+
)
|
|
416
|
+
);
|
|
417
|
+
servidor.registerTool(
|
|
418
|
+
"clientes_buscar",
|
|
419
|
+
{
|
|
420
|
+
title: "Buscar clientes",
|
|
421
|
+
description: "Busca clientes guardados por nombre o n\xFAmero de documento.",
|
|
422
|
+
inputSchema: z2.object({ q: z2.string().optional() }),
|
|
423
|
+
annotations: lectura
|
|
424
|
+
},
|
|
425
|
+
({ q }) => responder(() => api.pedir("GET", "/clientes", { query: { q, limite: 25 } }))
|
|
426
|
+
);
|
|
427
|
+
servidor.registerTool(
|
|
428
|
+
"cliente_crear",
|
|
429
|
+
{
|
|
430
|
+
title: "Crear cliente",
|
|
431
|
+
description: "Guarda un cliente para reutilizarlo como receptor. tipoDocumento: 36 NIT, 13 DUI, 03 pasaporte, 02 carn\xE9 de residente, 37 otro.",
|
|
432
|
+
inputSchema: cliente,
|
|
433
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
434
|
+
},
|
|
435
|
+
(datos) => responder(() => api.pedir("POST", "/clientes", { cuerpo: datos }))
|
|
436
|
+
);
|
|
437
|
+
servidor.registerTool(
|
|
438
|
+
"productos_buscar",
|
|
439
|
+
{
|
|
440
|
+
title: "Buscar productos",
|
|
441
|
+
description: "Busca productos y servicios guardados por c\xF3digo o descripci\xF3n. El precio guardado es SIN IVA.",
|
|
442
|
+
inputSchema: z2.object({ q: z2.string().optional() }),
|
|
443
|
+
annotations: lectura
|
|
444
|
+
},
|
|
445
|
+
({ q }) => responder(() => api.pedir("GET", "/productos", { query: { q, limite: 25 } }))
|
|
446
|
+
);
|
|
447
|
+
servidor.registerTool(
|
|
448
|
+
"producto_crear",
|
|
449
|
+
{
|
|
450
|
+
title: "Crear producto",
|
|
451
|
+
description: "Guarda un producto o servicio. precioUnitario va SIN IVA.",
|
|
452
|
+
inputSchema: producto,
|
|
453
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
454
|
+
},
|
|
455
|
+
(datos) => responder(() => api.pedir("POST", "/productos", { cuerpo: datos }))
|
|
456
|
+
);
|
|
457
|
+
servidor.registerTool(
|
|
458
|
+
"dte_validar",
|
|
459
|
+
{
|
|
460
|
+
title: "Validar un DTE sin emitirlo",
|
|
461
|
+
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.",
|
|
462
|
+
inputSchema: z2.object(entradaEmision),
|
|
463
|
+
annotations: lectura
|
|
464
|
+
},
|
|
465
|
+
(entrada) => responder(() => api.pedir("POST", "/dte/validar", { cuerpo: entrada }))
|
|
466
|
+
);
|
|
467
|
+
servidor.registerTool(
|
|
468
|
+
"dte_emitir",
|
|
469
|
+
{
|
|
470
|
+
title: "Emitir un DTE",
|
|
471
|
+
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.',
|
|
472
|
+
inputSchema: z2.object({
|
|
473
|
+
...entradaEmision,
|
|
474
|
+
idempotencyKey: z2.string().min(8).max(200).describe(
|
|
475
|
+
"Identificador \xFAnico de este intento. Repetirlo devuelve el mismo resultado sin emitir dos veces."
|
|
476
|
+
)
|
|
477
|
+
}),
|
|
478
|
+
annotations: {
|
|
479
|
+
readOnlyHint: false,
|
|
480
|
+
destructiveHint: false,
|
|
481
|
+
idempotentHint: true,
|
|
482
|
+
openWorldHint: true
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
({ idempotencyKey, ...entrada }) => responder(
|
|
486
|
+
() => api.pedir("POST", "/dte", {
|
|
487
|
+
cuerpo: entrada,
|
|
488
|
+
headers: { "Idempotency-Key": idempotencyKey }
|
|
489
|
+
})
|
|
490
|
+
)
|
|
491
|
+
);
|
|
492
|
+
servidor.registerTool(
|
|
493
|
+
"dte_listar",
|
|
494
|
+
{
|
|
495
|
+
title: "Listar documentos",
|
|
496
|
+
description: "Lista los DTE del ambiente activo, del m\xE1s reciente al m\xE1s antiguo.",
|
|
497
|
+
inputSchema: z2.object({
|
|
498
|
+
tipo: tipoDte.optional(),
|
|
499
|
+
estado: z2.enum(ESTADOS_DTE).optional(),
|
|
500
|
+
q: z2.string().optional().describe("Receptor, n\xFAmero de control o c\xF3digo de generaci\xF3n"),
|
|
501
|
+
desde: z2.string().optional().describe("AAAA-MM-DD"),
|
|
502
|
+
hasta: z2.string().optional().describe("AAAA-MM-DD"),
|
|
503
|
+
pagina: z2.number().int().min(1).optional()
|
|
504
|
+
}),
|
|
505
|
+
annotations: lectura
|
|
506
|
+
},
|
|
507
|
+
(filtros) => responder(
|
|
508
|
+
() => api.pedir("GET", "/dte", { query: { pagina: 1, porPagina: 25, ...filtros } })
|
|
509
|
+
)
|
|
510
|
+
);
|
|
511
|
+
servidor.registerTool(
|
|
512
|
+
"dte_obtener",
|
|
513
|
+
{
|
|
514
|
+
title: "Ver un documento",
|
|
515
|
+
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.",
|
|
516
|
+
inputSchema: z2.object({ codigoGeneracion: z2.string() }),
|
|
517
|
+
annotations: lectura
|
|
518
|
+
},
|
|
519
|
+
({ codigoGeneracion }) => responder(() => api.pedir("GET", `/dte/${ruta(codigoGeneracion)}`))
|
|
520
|
+
);
|
|
521
|
+
servidor.registerTool(
|
|
522
|
+
"dte_invalidar",
|
|
523
|
+
{
|
|
524
|
+
title: "Invalidar un DTE",
|
|
525
|
+
description: "Transmite el evento de invalidaci\xF3n de un DTE procesado. 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).",
|
|
526
|
+
inputSchema: z2.object({ codigoGeneracion: z2.string(), ...invalidacion.shape }),
|
|
527
|
+
annotations: {
|
|
528
|
+
readOnlyHint: false,
|
|
529
|
+
destructiveHint: true,
|
|
530
|
+
idempotentHint: false,
|
|
531
|
+
openWorldHint: true
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
({ codigoGeneracion, ...entrada }) => responder(
|
|
535
|
+
() => api.pedir("POST", `/dte/${ruta(codigoGeneracion)}/invalidar`, { cuerpo: entrada })
|
|
536
|
+
)
|
|
537
|
+
);
|
|
538
|
+
return servidor;
|
|
539
|
+
}
|
|
540
|
+
function servirMcp(api, version) {
|
|
541
|
+
return new Promise((resolve2) => {
|
|
542
|
+
const conexion = serveStdio(() => crearServidorMcp(api, version), {
|
|
543
|
+
onerror: (error) => process.stderr.write(`openfactura mcp: ${error.message}
|
|
544
|
+
`)
|
|
545
|
+
});
|
|
546
|
+
const cerrar = () => void conexion.close().finally(resolve2);
|
|
547
|
+
process.stdin.once("end", cerrar);
|
|
548
|
+
process.stdin.once("close", cerrar);
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/salida.ts
|
|
553
|
+
var ANCHO_MAXIMO = 44;
|
|
554
|
+
var celda = (valor) => {
|
|
555
|
+
if (valor === null || valor === void 0 || valor === "") return "\u2014";
|
|
556
|
+
const texto2 = String(valor).replace(/\s+/g, " ");
|
|
557
|
+
return texto2.length > ANCHO_MAXIMO ? `${texto2.slice(0, ANCHO_MAXIMO - 1)}\u2026` : texto2;
|
|
558
|
+
};
|
|
559
|
+
function tabla(encabezados, filas, derecha = []) {
|
|
560
|
+
const celdas = filas.map((fila) => fila.map(celda));
|
|
561
|
+
const anchos = encabezados.map(
|
|
562
|
+
(titulo, i) => Math.max(titulo.length, ...celdas.map((fila) => fila[i]?.length ?? 0))
|
|
563
|
+
);
|
|
564
|
+
const linea = (fila) => fila.map(
|
|
565
|
+
(texto2, i) => derecha.includes(i) ? texto2.padStart(anchos[i] ?? 0) : texto2.padEnd(anchos[i] ?? 0)
|
|
566
|
+
).join(" ").trimEnd();
|
|
567
|
+
return [linea(encabezados.map((t) => t.toUpperCase())), ...celdas.map(linea)].join("\n");
|
|
568
|
+
}
|
|
569
|
+
function fichas(pares) {
|
|
570
|
+
const visibles = pares.filter(
|
|
571
|
+
([, valor]) => valor !== null && valor !== void 0 && valor !== ""
|
|
572
|
+
);
|
|
573
|
+
const ancho = Math.max(0, ...visibles.map(([clave]) => clave.length));
|
|
574
|
+
return visibles.map(([clave, valor]) => `${`${clave}:`.padEnd(ancho + 2)}${String(valor)}`).join("\n");
|
|
575
|
+
}
|
|
576
|
+
var dinero = (valor) => typeof valor === "number" ? `$${valor.toFixed(2)}` : "\u2014";
|
|
577
|
+
var siNo = (valor) => valor ? "s\xED" : "no";
|
|
578
|
+
|
|
579
|
+
// src/comandos.ts
|
|
580
|
+
var nombreTipo = (codigo) => `${codigo} ${TIPOS_DTE[String(codigo)] ?? ""}`.trim();
|
|
581
|
+
var nombreAmbiente = (codigo) => `${codigo} ${NOMBRE_AMBIENTE[String(codigo)] ?? ""}`.trim();
|
|
582
|
+
var fecha = (ms) => typeof ms === "number" ? new Date(ms).toISOString().replace("T", " ").slice(0, 19) : ms;
|
|
583
|
+
var segmento = (texto2) => encodeURIComponent(texto2);
|
|
584
|
+
function mostrar(ctx, datos, legible) {
|
|
585
|
+
ctx.ent.stdout(ctx.json ? JSON.stringify(datos, null, 2) : legible());
|
|
586
|
+
}
|
|
587
|
+
async function leerJson(ctx, origen) {
|
|
588
|
+
let texto2;
|
|
589
|
+
if (origen === "-") {
|
|
590
|
+
texto2 = await ctx.ent.leerEntrada();
|
|
591
|
+
} else {
|
|
592
|
+
try {
|
|
593
|
+
texto2 = await readFile(resolve(ctx.ent.cwd, origen), "utf8");
|
|
594
|
+
} catch (error) {
|
|
595
|
+
throw new ErrorUso(`No se pudo leer ${origen}: ${error.message}`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
try {
|
|
599
|
+
return JSON.parse(texto2);
|
|
600
|
+
} catch (error) {
|
|
601
|
+
throw new ErrorUso(
|
|
602
|
+
`${origen === "-" ? "La entrada est\xE1ndar" : origen} no es un JSON v\xE1lido: ${error.message}`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async function leerEmision(ctx, origen) {
|
|
607
|
+
const cuerpo = await leerJson(ctx, origen);
|
|
608
|
+
const valido = cuerpo && typeof cuerpo === "object" && !Array.isArray(cuerpo) && "tipoDte" in cuerpo && "datos" in cuerpo;
|
|
609
|
+
if (!valido) {
|
|
610
|
+
throw new ErrorUso(
|
|
611
|
+
'El archivo debe ser un objeto { "tipoDte": "01", "datos": { \u2026 }, "clienteId"?, "puntoVentaId"? }'
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
return cuerpo;
|
|
615
|
+
}
|
|
616
|
+
async function confirmar(ctx, accion, entradaOcupada = false) {
|
|
617
|
+
if (ctx.si || !ctx.cfg.apiKey || !esClaveDeProduccion(ctx.cfg.apiKey)) return;
|
|
618
|
+
if (!ctx.ent.esTTY || entradaOcupada) {
|
|
619
|
+
throw new ErrorUso(
|
|
620
|
+
`Va a ${accion} en PRODUCCI\xD3N y no hay una terminal para confirmar: agregue --yes.`
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
const respuesta = await ctx.ent.preguntar(
|
|
624
|
+
`Va a ${accion} en PRODUCCI\xD3N (clave of_live_). \xBFContinuar? [s/N] `
|
|
625
|
+
);
|
|
626
|
+
if (!/^(s|si|sí|y|yes)$/i.test(respuesta.trim()))
|
|
627
|
+
throw new ErrorCli("Operaci\xF3n cancelada.", "cancelado");
|
|
628
|
+
}
|
|
629
|
+
function entero(ctx, opcion, minimo = 1) {
|
|
630
|
+
const texto2 = ctx.op(opcion);
|
|
631
|
+
if (texto2 === void 0) return void 0;
|
|
632
|
+
const numero = Number(texto2);
|
|
633
|
+
if (!Number.isInteger(numero) || numero < minimo)
|
|
634
|
+
throw new ErrorUso(`--${opcion} debe ser un n\xFAmero entero (recibido: ${texto2})`);
|
|
635
|
+
return numero;
|
|
636
|
+
}
|
|
637
|
+
function fichaDocumento(doc) {
|
|
638
|
+
return fichas([
|
|
639
|
+
["Estado", doc.estado],
|
|
640
|
+
["Tipo", nombreTipo(doc.tipoDte)],
|
|
641
|
+
["N\xFAmero de control", doc.numeroControl],
|
|
642
|
+
["C\xF3digo de generaci\xF3n", doc.codigoGeneracion],
|
|
643
|
+
["Ambiente", nombreAmbiente(doc.ambiente)],
|
|
644
|
+
["Emisi\xF3n", `${doc.fechaEmision ?? ""} ${doc.horaEmision ?? ""}`.trim()],
|
|
645
|
+
["Receptor", [doc.receptorNombre, doc.receptorDocumento].filter(Boolean).join(" \xB7 ")],
|
|
646
|
+
["Total", dinero(doc.total)],
|
|
647
|
+
["Sello recibido", doc.selloRecibido],
|
|
648
|
+
["Origen", doc.origen],
|
|
649
|
+
["Error", doc.error],
|
|
650
|
+
["Consulta p\xFAblica", doc.urlConsulta]
|
|
651
|
+
]);
|
|
652
|
+
}
|
|
653
|
+
async function cuerpoDeAlta(ctx, campos, numericos = []) {
|
|
654
|
+
const dados = Object.keys(campos).filter((opcion) => ctx.op(opcion) !== void 0);
|
|
655
|
+
const archivo = ctx.op("file");
|
|
656
|
+
if (archivo !== void 0) {
|
|
657
|
+
if (dados.length) throw new ErrorUso("Use --file o las opciones sueltas, no ambas.");
|
|
658
|
+
return leerJson(ctx, archivo);
|
|
659
|
+
}
|
|
660
|
+
if (!dados.length) {
|
|
661
|
+
throw new ErrorUso(
|
|
662
|
+
`Indique --file <archivo.json> o los datos con opciones: ${Object.keys(campos).map((o) => `--${o}`).join(", ")}`
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
const cuerpo = {};
|
|
666
|
+
for (const opcion of dados) {
|
|
667
|
+
const texto2 = ctx.op(opcion);
|
|
668
|
+
const campo = campos[opcion];
|
|
669
|
+
if (numericos.includes(opcion)) {
|
|
670
|
+
const numero = Number(texto2);
|
|
671
|
+
if (texto2.trim() === "" || !Number.isFinite(numero))
|
|
672
|
+
throw new ErrorUso(`--${opcion} debe ser un n\xFAmero (recibido: ${texto2})`);
|
|
673
|
+
cuerpo[campo] = numero;
|
|
674
|
+
} else {
|
|
675
|
+
cuerpo[campo] = texto2;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return cuerpo;
|
|
679
|
+
}
|
|
680
|
+
var CAMPOS_CLIENTE = {
|
|
681
|
+
"tipo-documento": "tipoDocumento",
|
|
682
|
+
"num-documento": "numDocumento",
|
|
683
|
+
nrc: "nrc",
|
|
684
|
+
nombre: "nombre",
|
|
685
|
+
"nombre-comercial": "nombreComercial",
|
|
686
|
+
"cod-actividad": "codActividad",
|
|
687
|
+
"desc-actividad": "descActividad",
|
|
688
|
+
departamento: "departamento",
|
|
689
|
+
municipio: "municipio",
|
|
690
|
+
distrito: "distrito",
|
|
691
|
+
complemento: "complemento",
|
|
692
|
+
telefono: "telefono",
|
|
693
|
+
correo: "correo"
|
|
694
|
+
};
|
|
695
|
+
var CAMPOS_PRODUCTO = {
|
|
696
|
+
codigo: "codigo",
|
|
697
|
+
descripcion: "descripcion",
|
|
698
|
+
precio: "precioUnitario",
|
|
699
|
+
"tipo-item": "tipoItem",
|
|
700
|
+
"uni-medida": "uniMedida",
|
|
701
|
+
"tipo-venta": "tipoVenta"
|
|
702
|
+
};
|
|
703
|
+
async function login(ctx) {
|
|
704
|
+
let clave = ctx.op("key");
|
|
705
|
+
if (clave === void 0) {
|
|
706
|
+
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];
|
|
707
|
+
}
|
|
708
|
+
clave = (clave ?? "").trim();
|
|
709
|
+
if (!esClaveValida(clave))
|
|
710
|
+
throw new ErrorUso("La clave de API debe empezar con of_test_ o of_live_.");
|
|
711
|
+
const api = new ClienteApi({
|
|
712
|
+
url: ctx.cfg.url,
|
|
713
|
+
apiKey: clave,
|
|
714
|
+
version: ctx.version,
|
|
715
|
+
fetch: ctx.ent.fetch
|
|
716
|
+
});
|
|
717
|
+
const { datos } = await api.pedir("GET", "/yo");
|
|
718
|
+
const config = leerConfig(ctx.ent.env);
|
|
719
|
+
config.perfiles[ctx.cfg.perfil] = { url: ctx.cfg.url, apiKey: clave };
|
|
720
|
+
const ruta2 = guardarConfig(ctx.ent.env, config);
|
|
721
|
+
mostrar(
|
|
722
|
+
ctx,
|
|
723
|
+
datos,
|
|
724
|
+
() => [
|
|
725
|
+
`Sesi\xF3n iniciada en ${ctx.cfg.url}`,
|
|
726
|
+
fichas([
|
|
727
|
+
["Clave", enmascarar(clave)],
|
|
728
|
+
["Ambiente", nombreAmbiente(datos.ambiente)],
|
|
729
|
+
["Perfil", ctx.cfg.perfil],
|
|
730
|
+
["Guardado en", ruta2]
|
|
731
|
+
])
|
|
732
|
+
].join("\n")
|
|
733
|
+
);
|
|
734
|
+
if (ctx.ent.env.OPENFACTURA_API_KEY) {
|
|
735
|
+
ctx.ent.stderr(
|
|
736
|
+
"Aviso: OPENFACTURA_API_KEY est\xE1 definida y tiene prioridad sobre la clave guardada."
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
return void 0;
|
|
740
|
+
}
|
|
741
|
+
async function logout(ctx) {
|
|
742
|
+
const config = leerConfig(ctx.ent.env);
|
|
743
|
+
const habia = ctx.cfg.perfil in config.perfiles;
|
|
744
|
+
if (habia) {
|
|
745
|
+
delete config.perfiles[ctx.cfg.perfil];
|
|
746
|
+
guardarConfig(ctx.ent.env, config);
|
|
747
|
+
}
|
|
748
|
+
mostrar(
|
|
749
|
+
ctx,
|
|
750
|
+
{ perfil: ctx.cfg.perfil, eliminado: habia },
|
|
751
|
+
() => 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.`
|
|
752
|
+
);
|
|
753
|
+
if (ctx.ent.env.OPENFACTURA_API_KEY) {
|
|
754
|
+
ctx.ent.stderr("Aviso: OPENFACTURA_API_KEY sigue definida en el entorno.");
|
|
755
|
+
}
|
|
756
|
+
return void 0;
|
|
757
|
+
}
|
|
758
|
+
async function whoami(ctx) {
|
|
759
|
+
const { datos } = await ctx.api().pedir("GET", "/yo");
|
|
760
|
+
const usuario = datos.usuario;
|
|
761
|
+
mostrar(
|
|
762
|
+
ctx,
|
|
763
|
+
datos,
|
|
764
|
+
() => fichas([
|
|
765
|
+
["Actor", datos.actor],
|
|
766
|
+
["Usuario", usuario ? `${usuario.nombre} <${usuario.correo}>` : null],
|
|
767
|
+
["Rol", datos.rol],
|
|
768
|
+
["Organizaci\xF3n", datos.organizationId],
|
|
769
|
+
[
|
|
770
|
+
"Ambiente",
|
|
771
|
+
`${nombreAmbiente(datos.ambiente)}${datos.simulador ? " (simulador del MH)" : ""}`
|
|
772
|
+
],
|
|
773
|
+
["Servidor", `${ctx.cfg.url} (${ctx.cfg.origenUrl})`],
|
|
774
|
+
["Clave", `${enmascarar(ctx.cfg.apiKey)} (${ctx.cfg.origenClave})`],
|
|
775
|
+
["Perfil", ctx.cfg.perfil],
|
|
776
|
+
["Permisos", Array.isArray(datos.permisos) ? datos.permisos.join(", ") : null]
|
|
777
|
+
])
|
|
778
|
+
);
|
|
779
|
+
return void 0;
|
|
780
|
+
}
|
|
781
|
+
async function emisor(ctx) {
|
|
782
|
+
const { datos } = await ctx.api().pedir("GET", "/emisor");
|
|
783
|
+
mostrar(ctx, datos, () => {
|
|
784
|
+
const e = datos.emisor;
|
|
785
|
+
if (!e) return "El emisor a\xFAn no est\xE1 configurado. Complete sus datos en el panel web.";
|
|
786
|
+
const establecimientos = datos.establecimientos ?? [];
|
|
787
|
+
const credenciales = datos.credenciales ?? {};
|
|
788
|
+
return [
|
|
789
|
+
fichas([
|
|
790
|
+
["Nombre", e.nombre],
|
|
791
|
+
["Nombre comercial", e.nombreComercial],
|
|
792
|
+
["NIT", e.nit],
|
|
793
|
+
["NRC", e.nrc],
|
|
794
|
+
["Actividad", `${e.codActividad} ${e.descActividad}`],
|
|
795
|
+
[
|
|
796
|
+
"Direcci\xF3n",
|
|
797
|
+
`${e.complemento} (depto. ${e.departamento}, mun. ${e.municipio}, dist. ${e.distrito})`
|
|
798
|
+
],
|
|
799
|
+
["Tel\xE9fono", e.telefono],
|
|
800
|
+
["Correo", e.correo],
|
|
801
|
+
["Ambiente activo", nombreAmbiente(datos.ambienteActivo)]
|
|
802
|
+
]),
|
|
803
|
+
"",
|
|
804
|
+
"Establecimientos",
|
|
805
|
+
tabla(
|
|
806
|
+
["c\xF3digo", "nombre", "tipo", "puntos de venta"],
|
|
807
|
+
establecimientos.map((est) => [
|
|
808
|
+
est.codMh,
|
|
809
|
+
est.nombre,
|
|
810
|
+
est.tipo,
|
|
811
|
+
(est.puntosVenta ?? []).map((pv) => `${pv.codMh} ${pv.nombre}`).join(", ")
|
|
812
|
+
])
|
|
813
|
+
),
|
|
814
|
+
"",
|
|
815
|
+
"Credenciales del MH",
|
|
816
|
+
tabla(
|
|
817
|
+
["ambiente", "contrase\xF1a api", "certificado", "nit del certificado"],
|
|
818
|
+
Object.entries(credenciales).map(([ambiente, c]) => [
|
|
819
|
+
nombreAmbiente(ambiente),
|
|
820
|
+
siNo(c.apiPassword),
|
|
821
|
+
siNo(c.certificado),
|
|
822
|
+
c.certificadoNit
|
|
823
|
+
])
|
|
824
|
+
)
|
|
825
|
+
].join("\n");
|
|
826
|
+
});
|
|
827
|
+
return void 0;
|
|
828
|
+
}
|
|
829
|
+
async function catalogos(ctx) {
|
|
830
|
+
const id = ctx.args[0];
|
|
831
|
+
if (!id) {
|
|
832
|
+
if (ctx.op("q") !== void 0 || ctx.op("departamento") !== void 0) {
|
|
833
|
+
throw new ErrorUso(
|
|
834
|
+
"--q y --departamento requieren el id del cat\xE1logo, por ejemplo: openfactura catalogos CAT-013 --departamento 06"
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
const { datos: datos2 } = await ctx.api().pedir("GET", "/catalogos");
|
|
838
|
+
mostrar(
|
|
839
|
+
ctx,
|
|
840
|
+
datos2,
|
|
841
|
+
() => tabla(
|
|
842
|
+
["id", "nombre", "valores"],
|
|
843
|
+
datos2.map((c) => [c.id, c.nombre, c.total]),
|
|
844
|
+
[2]
|
|
845
|
+
)
|
|
846
|
+
);
|
|
847
|
+
return void 0;
|
|
848
|
+
}
|
|
849
|
+
const { datos } = await ctx.api().pedir("GET", `/catalogos/${segmento(id.toUpperCase())}`, {
|
|
850
|
+
query: { q: ctx.op("q"), departamento: ctx.op("departamento") }
|
|
851
|
+
});
|
|
852
|
+
mostrar(ctx, datos, () => {
|
|
853
|
+
const conDepartamento = datos.valores.some((v) => v.departamento);
|
|
854
|
+
const filas = datos.valores.map(
|
|
855
|
+
(v) => conDepartamento ? [v.codigo, v.valor, v.departamento] : [v.codigo, v.valor]
|
|
856
|
+
);
|
|
857
|
+
return [
|
|
858
|
+
`${datos.id} ${datos.nombre} \u2014 ${datos.valores.length} de ${datos.total} valores`,
|
|
859
|
+
tabla(conDepartamento ? ["c\xF3digo", "valor", "departamento"] : ["c\xF3digo", "valor"], filas)
|
|
860
|
+
].join("\n");
|
|
861
|
+
});
|
|
862
|
+
return void 0;
|
|
863
|
+
}
|
|
864
|
+
async function clientesList(ctx) {
|
|
865
|
+
const { datos } = await ctx.api().pedir("GET", "/clientes", { query: { q: ctx.op("q") } });
|
|
866
|
+
mostrar(
|
|
867
|
+
ctx,
|
|
868
|
+
datos,
|
|
869
|
+
() => datos.length ? tabla(
|
|
870
|
+
["id", "doc", "n\xFAmero", "nombre", "nrc", "correo"],
|
|
871
|
+
datos.map((c) => [c.id, c.tipoDocumento, c.numDocumento, c.nombre, c.nrc, c.correo])
|
|
872
|
+
) : "No hay clientes."
|
|
873
|
+
);
|
|
874
|
+
return void 0;
|
|
875
|
+
}
|
|
876
|
+
async function clientesAdd(ctx) {
|
|
877
|
+
const cuerpo = await cuerpoDeAlta(ctx, CAMPOS_CLIENTE);
|
|
878
|
+
const { datos } = await ctx.api().pedir("POST", "/clientes", { cuerpo });
|
|
879
|
+
mostrar(
|
|
880
|
+
ctx,
|
|
881
|
+
datos,
|
|
882
|
+
() => [
|
|
883
|
+
"Cliente creado.",
|
|
884
|
+
fichas([
|
|
885
|
+
["Id", datos.id],
|
|
886
|
+
["Documento", `${datos.tipoDocumento} ${datos.numDocumento}`],
|
|
887
|
+
["Nombre", datos.nombre],
|
|
888
|
+
["NRC", datos.nrc],
|
|
889
|
+
["Correo", datos.correo]
|
|
890
|
+
])
|
|
891
|
+
].join("\n")
|
|
892
|
+
);
|
|
893
|
+
return void 0;
|
|
894
|
+
}
|
|
895
|
+
async function productosList(ctx) {
|
|
896
|
+
const { datos } = await ctx.api().pedir("GET", "/productos", { query: { q: ctx.op("q") } });
|
|
897
|
+
mostrar(
|
|
898
|
+
ctx,
|
|
899
|
+
datos,
|
|
900
|
+
() => datos.length ? tabla(
|
|
901
|
+
["id", "c\xF3digo", "descripci\xF3n", "precio sin iva", "\xEDtem", "unidad", "venta"],
|
|
902
|
+
datos.map((p) => [
|
|
903
|
+
p.id,
|
|
904
|
+
p.codigo,
|
|
905
|
+
p.descripcion,
|
|
906
|
+
dinero(p.precioUnitario),
|
|
907
|
+
p.tipoItem,
|
|
908
|
+
p.uniMedida,
|
|
909
|
+
p.tipoVenta
|
|
910
|
+
]),
|
|
911
|
+
[3]
|
|
912
|
+
) : "No hay productos."
|
|
913
|
+
);
|
|
914
|
+
return void 0;
|
|
915
|
+
}
|
|
916
|
+
async function productosAdd(ctx) {
|
|
917
|
+
const cuerpo = await cuerpoDeAlta(ctx, CAMPOS_PRODUCTO, ["precio", "tipo-item", "uni-medida"]);
|
|
918
|
+
const { datos } = await ctx.api().pedir("POST", "/productos", { cuerpo });
|
|
919
|
+
mostrar(
|
|
920
|
+
ctx,
|
|
921
|
+
datos,
|
|
922
|
+
() => [
|
|
923
|
+
"Producto creado.",
|
|
924
|
+
fichas([
|
|
925
|
+
["Id", datos.id],
|
|
926
|
+
["C\xF3digo", datos.codigo],
|
|
927
|
+
["Descripci\xF3n", datos.descripcion],
|
|
928
|
+
["Precio sin IVA", dinero(datos.precioUnitario)],
|
|
929
|
+
["Tipo de venta", datos.tipoVenta]
|
|
930
|
+
])
|
|
931
|
+
].join("\n")
|
|
932
|
+
);
|
|
933
|
+
return void 0;
|
|
934
|
+
}
|
|
935
|
+
async function dteValidar(ctx) {
|
|
936
|
+
const cuerpo = await leerEmision(ctx, ctx.args[0]);
|
|
937
|
+
const { datos } = await ctx.api().pedir("POST", "/dte/validar", { cuerpo });
|
|
938
|
+
const problemas = datos.problemas ?? [];
|
|
939
|
+
mostrar(ctx, datos, () => {
|
|
940
|
+
const resumen = datos.dte?.resumen ?? {};
|
|
941
|
+
const numero = (campo) => typeof resumen[campo] === "number" ? dinero(resumen[campo]) : null;
|
|
942
|
+
return [
|
|
943
|
+
fichas([
|
|
944
|
+
["Tipo", nombreTipo(cuerpo.tipoDte)],
|
|
945
|
+
["Receptor", datos.receptorNombre],
|
|
946
|
+
["Gravado", numero("totalGravada")],
|
|
947
|
+
["Exento", numero("totalExenta")],
|
|
948
|
+
["No sujeto", numero("totalNoSuj")],
|
|
949
|
+
["IVA", numero("totalIva")],
|
|
950
|
+
["Total", dinero(datos.total)],
|
|
951
|
+
["En letras", resumen.totalLetras]
|
|
952
|
+
]),
|
|
953
|
+
"",
|
|
954
|
+
problemas.length ? [
|
|
955
|
+
`${problemas.length} problema(s):`,
|
|
956
|
+
...problemas.map((p) => ` ${p.ruta}: ${p.mensaje}`)
|
|
957
|
+
].join("\n") : "V\xE1lido: cumple el esquema del MH. No se emiti\xF3 nada."
|
|
958
|
+
].join("\n");
|
|
959
|
+
});
|
|
960
|
+
return problemas.length ? SALIDA.error : SALIDA.ok;
|
|
961
|
+
}
|
|
962
|
+
async function dteEmitir(ctx) {
|
|
963
|
+
const origen = ctx.args[0];
|
|
964
|
+
const cuerpo = await leerEmision(ctx, origen);
|
|
965
|
+
const api = ctx.api();
|
|
966
|
+
const clave = ctx.op("idempotency-key") ?? randomUUID();
|
|
967
|
+
if (clave.length < 1 || clave.length > 200)
|
|
968
|
+
throw new ErrorUso("--idempotency-key debe tener entre 1 y 200 caracteres.");
|
|
969
|
+
await confirmar(ctx, `emitir un DTE (${nombreTipo(cuerpo.tipoDte)})`, origen === "-");
|
|
970
|
+
ctx.ent.stderr(`Idempotency-Key: ${clave}`);
|
|
971
|
+
let respuesta;
|
|
972
|
+
try {
|
|
973
|
+
respuesta = await api.pedir("POST", "/dte", {
|
|
974
|
+
cuerpo,
|
|
975
|
+
headers: { "Idempotency-Key": clave }
|
|
976
|
+
});
|
|
977
|
+
} catch (error) {
|
|
978
|
+
if (error instanceof ErrorCli && (error.code === "sin_conexion" || error.code === "tiempo_agotado")) {
|
|
979
|
+
ctx.ent.stderr(
|
|
980
|
+
`No se sabe si el DTE se emiti\xF3. Reintente con la misma clave para no emitir dos veces:
|
|
981
|
+
openfactura dte emitir ${origen} --idempotency-key ${clave}`
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
throw error;
|
|
985
|
+
}
|
|
986
|
+
const doc = respuesta.datos;
|
|
987
|
+
if (respuesta.headers.get("Idempotent-Replayed") === "true") {
|
|
988
|
+
ctx.ent.stderr(
|
|
989
|
+
"Respuesta repetida: esta Idempotency-Key ya se hab\xEDa usado; no se emiti\xF3 de nuevo."
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
if (doc.estado === "contingencia") {
|
|
993
|
+
ctx.ent.stderr(
|
|
994
|
+
"El MH no respondi\xF3: el DTE qued\xF3 firmado en contingencia. Transm\xEDtalo con `openfactura contingencia transmitir`."
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
mostrar(ctx, doc, () => fichaDocumento(doc));
|
|
998
|
+
return void 0;
|
|
999
|
+
}
|
|
1000
|
+
async function dteList(ctx) {
|
|
1001
|
+
const estado = ctx.op("estado");
|
|
1002
|
+
if (estado !== void 0 && !ESTADOS_DTE.includes(estado)) {
|
|
1003
|
+
throw new ErrorUso(`--estado debe ser uno de: ${ESTADOS_DTE.join(", ")}`);
|
|
1004
|
+
}
|
|
1005
|
+
for (const opcion of ["desde", "hasta"]) {
|
|
1006
|
+
const valor = ctx.op(opcion);
|
|
1007
|
+
if (valor !== void 0 && !/^\d{4}-\d{2}-\d{2}$/.test(valor))
|
|
1008
|
+
throw new ErrorUso(`--${opcion} debe tener el formato AAAA-MM-DD`);
|
|
1009
|
+
}
|
|
1010
|
+
const { datos } = await ctx.api().pedir("GET", "/dte", {
|
|
1011
|
+
query: {
|
|
1012
|
+
tipo: ctx.op("tipo"),
|
|
1013
|
+
estado,
|
|
1014
|
+
q: ctx.op("q"),
|
|
1015
|
+
desde: ctx.op("desde"),
|
|
1016
|
+
hasta: ctx.op("hasta"),
|
|
1017
|
+
pagina: entero(ctx, "pagina")
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
mostrar(
|
|
1021
|
+
ctx,
|
|
1022
|
+
datos,
|
|
1023
|
+
() => [
|
|
1024
|
+
datos.items.length ? tabla(
|
|
1025
|
+
[
|
|
1026
|
+
"fecha",
|
|
1027
|
+
"tipo",
|
|
1028
|
+
"n\xFAmero de control",
|
|
1029
|
+
"estado",
|
|
1030
|
+
"receptor",
|
|
1031
|
+
"total",
|
|
1032
|
+
"c\xF3digo de generaci\xF3n"
|
|
1033
|
+
],
|
|
1034
|
+
datos.items.map((d) => [
|
|
1035
|
+
d.fechaEmision,
|
|
1036
|
+
d.tipoDte,
|
|
1037
|
+
d.numeroControl,
|
|
1038
|
+
d.estado,
|
|
1039
|
+
d.receptorNombre,
|
|
1040
|
+
dinero(d.total),
|
|
1041
|
+
d.codigoGeneracion
|
|
1042
|
+
]),
|
|
1043
|
+
[5]
|
|
1044
|
+
) : "No hay documentos.",
|
|
1045
|
+
`P\xE1gina ${datos.pagina} \xB7 ${datos.items.length} de ${datos.total} documento(s) \xB7 ambiente ${nombreAmbiente(datos.ambiente)}`
|
|
1046
|
+
].join("\n")
|
|
1047
|
+
);
|
|
1048
|
+
return void 0;
|
|
1049
|
+
}
|
|
1050
|
+
async function dteGet(ctx) {
|
|
1051
|
+
const { datos } = await ctx.api().pedir("GET", `/dte/${segmento(ctx.args[0])}`);
|
|
1052
|
+
mostrar(ctx, datos, () => {
|
|
1053
|
+
const mh = datos.respuestaMh;
|
|
1054
|
+
const eventos = datos.eventos ?? [];
|
|
1055
|
+
const observaciones = Array.isArray(mh?.observaciones) ? mh.observaciones.join("; ") : null;
|
|
1056
|
+
return [
|
|
1057
|
+
fichaDocumento(datos),
|
|
1058
|
+
...mh ? [
|
|
1059
|
+
"",
|
|
1060
|
+
"Respuesta del MH",
|
|
1061
|
+
fichas([
|
|
1062
|
+
["Estado", mh.estado],
|
|
1063
|
+
["Mensaje", `${mh.codigoMsg ?? ""} ${mh.descripcionMsg ?? ""}`.trim()],
|
|
1064
|
+
["Procesado", mh.fhProcesamiento],
|
|
1065
|
+
["Observaciones", observaciones]
|
|
1066
|
+
])
|
|
1067
|
+
] : [],
|
|
1068
|
+
...eventos.length ? [
|
|
1069
|
+
"",
|
|
1070
|
+
"Eventos",
|
|
1071
|
+
tabla(
|
|
1072
|
+
["tipo", "estado", "fecha (UTC)", "c\xF3digo de generaci\xF3n", "sello"],
|
|
1073
|
+
eventos.map((e) => [
|
|
1074
|
+
e.tipo,
|
|
1075
|
+
e.estado,
|
|
1076
|
+
fecha(e.creado),
|
|
1077
|
+
e.codigoGeneracion,
|
|
1078
|
+
e.selloRecibido
|
|
1079
|
+
])
|
|
1080
|
+
)
|
|
1081
|
+
] : []
|
|
1082
|
+
].join("\n");
|
|
1083
|
+
});
|
|
1084
|
+
return void 0;
|
|
1085
|
+
}
|
|
1086
|
+
async function dteJson(ctx) {
|
|
1087
|
+
const { datos } = await ctx.api().pedir("GET", `/dte/${segmento(ctx.args[0])}/json`);
|
|
1088
|
+
const texto2 = JSON.stringify(datos, null, 2);
|
|
1089
|
+
const salida = ctx.op("output");
|
|
1090
|
+
if (salida === void 0) {
|
|
1091
|
+
ctx.ent.stdout(texto2);
|
|
1092
|
+
return void 0;
|
|
1093
|
+
}
|
|
1094
|
+
const destino = resolve(ctx.ent.cwd, salida);
|
|
1095
|
+
await writeFile(destino, `${texto2}
|
|
1096
|
+
`);
|
|
1097
|
+
mostrar(ctx, { archivo: destino }, () => `JSON guardado en ${destino}`);
|
|
1098
|
+
return void 0;
|
|
1099
|
+
}
|
|
1100
|
+
async function dtePdf(ctx) {
|
|
1101
|
+
const codigo = ctx.args[0];
|
|
1102
|
+
const bytes = await ctx.api().descargar(`/dte/${segmento(codigo)}/pdf`);
|
|
1103
|
+
const destino = resolve(ctx.ent.cwd, ctx.op("output") ?? `${basename(codigo)}.pdf`);
|
|
1104
|
+
await writeFile(destino, bytes);
|
|
1105
|
+
mostrar(
|
|
1106
|
+
ctx,
|
|
1107
|
+
{ archivo: destino, bytes: bytes.length },
|
|
1108
|
+
() => `PDF guardado en ${destino} (${bytes.length} bytes)`
|
|
1109
|
+
);
|
|
1110
|
+
return void 0;
|
|
1111
|
+
}
|
|
1112
|
+
async function dteInvalidar(ctx) {
|
|
1113
|
+
const codigo = ctx.args[0];
|
|
1114
|
+
const tipo = ctx.op("tipo");
|
|
1115
|
+
if (tipo !== "1" && tipo !== "2" && tipo !== "3") {
|
|
1116
|
+
throw new ErrorUso(
|
|
1117
|
+
"Indique --tipo 1|2|3 (1 error en la informaci\xF3n, 2 rescindir la operaci\xF3n, 3 otro)."
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
const cuerpo = { tipoAnulacion: Number(tipo) };
|
|
1121
|
+
if (ctx.op("motivo") !== void 0) cuerpo.motivoAnulacion = ctx.op("motivo");
|
|
1122
|
+
if (ctx.op("reemplazo") !== void 0) cuerpo.codigoGeneracionReemplazo = ctx.op("reemplazo");
|
|
1123
|
+
const api = ctx.api();
|
|
1124
|
+
await confirmar(ctx, `invalidar el DTE ${codigo} (irreversible)`);
|
|
1125
|
+
const { datos } = await api.pedir("POST", `/dte/${segmento(codigo)}/invalidar`, {
|
|
1126
|
+
cuerpo
|
|
1127
|
+
});
|
|
1128
|
+
mostrar(ctx, datos, () => `DTE invalidado.
|
|
1129
|
+
${fichaDocumento(datos)}`);
|
|
1130
|
+
return void 0;
|
|
1131
|
+
}
|
|
1132
|
+
async function contingenciaTransmitir(ctx) {
|
|
1133
|
+
const api = ctx.api();
|
|
1134
|
+
await confirmar(ctx, "transmitir los DTE en contingencia");
|
|
1135
|
+
const { datos } = await api.pedir("POST", "/contingencia/transmitir");
|
|
1136
|
+
mostrar(ctx, datos, () => {
|
|
1137
|
+
const procesados = datos.procesados ?? [];
|
|
1138
|
+
const rechazados = datos.rechazados ?? [];
|
|
1139
|
+
if (!datos.evento && !procesados.length && !rechazados.length)
|
|
1140
|
+
return "No hay documentos en contingencia.";
|
|
1141
|
+
const linea = (d) => ` ${typeof d === "string" ? d : JSON.stringify(d)}`;
|
|
1142
|
+
return [
|
|
1143
|
+
fichas([
|
|
1144
|
+
["Procesados", procesados.length],
|
|
1145
|
+
["Rechazados", rechazados.length],
|
|
1146
|
+
["Restantes", datos.restantes]
|
|
1147
|
+
]),
|
|
1148
|
+
...rechazados.length ? ["", "Rechazados:", ...rechazados.map(linea)] : []
|
|
1149
|
+
].join("\n");
|
|
1150
|
+
});
|
|
1151
|
+
return void 0;
|
|
1152
|
+
}
|
|
1153
|
+
async function mcp(ctx) {
|
|
1154
|
+
const api = ctx.api();
|
|
1155
|
+
ctx.ent.stderr(
|
|
1156
|
+
`openfactura mcp: servidor MCP por stdio \u2192 ${ctx.cfg.url} (clave ${enmascarar(ctx.cfg.apiKey)})`
|
|
1157
|
+
);
|
|
1158
|
+
await servirMcp(api, ctx.version);
|
|
1159
|
+
return void 0;
|
|
1160
|
+
}
|
|
1161
|
+
var GRUPOS = {
|
|
1162
|
+
clientes: "Clientes guardados",
|
|
1163
|
+
productos: "Productos y servicios guardados",
|
|
1164
|
+
dte: "Documentos tributarios electr\xF3nicos",
|
|
1165
|
+
contingencia: "Documentos emitidos en contingencia"
|
|
1166
|
+
};
|
|
1167
|
+
var COMANDOS = [
|
|
1168
|
+
{
|
|
1169
|
+
nombre: "login",
|
|
1170
|
+
uso: "login [--url <url>] [--key <clave>]",
|
|
1171
|
+
resumen: "Verifica una clave de API y la guarda",
|
|
1172
|
+
opciones: ["key"],
|
|
1173
|
+
args: [0, 0],
|
|
1174
|
+
accion: login
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
nombre: "logout",
|
|
1178
|
+
uso: "logout",
|
|
1179
|
+
resumen: "Borra la clave guardada del perfil",
|
|
1180
|
+
opciones: [],
|
|
1181
|
+
args: [0, 0],
|
|
1182
|
+
accion: logout
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
nombre: "whoami",
|
|
1186
|
+
uso: "whoami",
|
|
1187
|
+
resumen: "Muestra el actor, sus permisos, el ambiente y el servidor",
|
|
1188
|
+
opciones: [],
|
|
1189
|
+
args: [0, 0],
|
|
1190
|
+
accion: whoami
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
nombre: "emisor",
|
|
1194
|
+
uso: "emisor",
|
|
1195
|
+
resumen: "Datos del emisor, establecimientos y credenciales cargadas",
|
|
1196
|
+
opciones: [],
|
|
1197
|
+
args: [0, 0],
|
|
1198
|
+
accion: emisor
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
nombre: "clientes list",
|
|
1202
|
+
uso: "clientes list [--q <texto>]",
|
|
1203
|
+
resumen: "Lista o busca clientes",
|
|
1204
|
+
opciones: ["q"],
|
|
1205
|
+
args: [0, 0],
|
|
1206
|
+
accion: clientesList
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
nombre: "clientes add",
|
|
1210
|
+
uso: "clientes add --file <cliente.json> | --tipo-documento <36|13|03|02|37> --num-documento <n> --nombre <nombre> [\u2026]",
|
|
1211
|
+
resumen: "Crea un cliente",
|
|
1212
|
+
opciones: ["file", ...Object.keys(CAMPOS_CLIENTE)],
|
|
1213
|
+
args: [0, 0],
|
|
1214
|
+
accion: clientesAdd
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
nombre: "productos list",
|
|
1218
|
+
uso: "productos list [--q <texto>]",
|
|
1219
|
+
resumen: "Lista o busca productos",
|
|
1220
|
+
opciones: ["q"],
|
|
1221
|
+
args: [0, 0],
|
|
1222
|
+
accion: productosList
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
nombre: "productos add",
|
|
1226
|
+
uso: "productos add --file <producto.json> | --codigo <c> --descripcion <d> --precio <sin IVA> [\u2026]",
|
|
1227
|
+
resumen: "Crea un producto o servicio",
|
|
1228
|
+
opciones: ["file", ...Object.keys(CAMPOS_PRODUCTO)],
|
|
1229
|
+
args: [0, 0],
|
|
1230
|
+
accion: productosAdd
|
|
1231
|
+
},
|
|
1232
|
+
{
|
|
1233
|
+
nombre: "catalogos",
|
|
1234
|
+
uso: "catalogos [<id>] [--q <texto>] [--departamento <06>]",
|
|
1235
|
+
resumen: "Cat\xE1logos oficiales del MH (CAT-012, CAT-019\u2026)",
|
|
1236
|
+
opciones: ["q", "departamento"],
|
|
1237
|
+
args: [0, 1],
|
|
1238
|
+
accion: catalogos
|
|
1239
|
+
},
|
|
1240
|
+
{
|
|
1241
|
+
nombre: "dte validar",
|
|
1242
|
+
uso: "dte validar <archivo.json | ->",
|
|
1243
|
+
resumen: "Valida un DTE sin emitirlo (salida 1 si hay problemas)",
|
|
1244
|
+
opciones: [],
|
|
1245
|
+
args: [1, 1],
|
|
1246
|
+
accion: dteValidar
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
nombre: "dte emitir",
|
|
1250
|
+
uso: "dte emitir <archivo.json | -> [--idempotency-key <k>] [--yes]",
|
|
1251
|
+
resumen: "Emite un DTE: lo firma y lo transmite al MH",
|
|
1252
|
+
opciones: ["idempotency-key", "yes"],
|
|
1253
|
+
args: [1, 1],
|
|
1254
|
+
accion: dteEmitir
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
nombre: "dte list",
|
|
1258
|
+
uso: "dte list [--tipo <01>] [--estado <estado>] [--q <texto>] [--desde <AAAA-MM-DD>] [--hasta <AAAA-MM-DD>] [--pagina <n>]",
|
|
1259
|
+
resumen: "Lista documentos, del m\xE1s reciente al m\xE1s antiguo",
|
|
1260
|
+
opciones: ["tipo", "estado", "q", "desde", "hasta", "pagina"],
|
|
1261
|
+
args: [0, 0],
|
|
1262
|
+
accion: dteList
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
nombre: "dte get",
|
|
1266
|
+
uso: "dte get <codigoGeneracion>",
|
|
1267
|
+
resumen: "Detalle de un documento",
|
|
1268
|
+
opciones: [],
|
|
1269
|
+
args: [1, 1],
|
|
1270
|
+
accion: dteGet
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
nombre: "dte json",
|
|
1274
|
+
uso: "dte json <codigoGeneracion> [-o <archivo.json>]",
|
|
1275
|
+
resumen: "JSON oficial del DTE (firma y sello)",
|
|
1276
|
+
opciones: ["output"],
|
|
1277
|
+
args: [1, 1],
|
|
1278
|
+
accion: dteJson
|
|
1279
|
+
},
|
|
1280
|
+
{
|
|
1281
|
+
nombre: "dte pdf",
|
|
1282
|
+
uso: "dte pdf <codigoGeneracion> [-o <archivo.pdf>]",
|
|
1283
|
+
resumen: "Descarga la representaci\xF3n gr\xE1fica en PDF",
|
|
1284
|
+
opciones: ["output"],
|
|
1285
|
+
args: [1, 1],
|
|
1286
|
+
accion: dtePdf
|
|
1287
|
+
},
|
|
1288
|
+
{
|
|
1289
|
+
nombre: "dte invalidar",
|
|
1290
|
+
uso: "dte invalidar <codigoGeneracion> --tipo <1|2|3> [--motivo <texto>] [--reemplazo <codigo>] [--yes]",
|
|
1291
|
+
resumen: "Invalida un DTE procesado (irreversible)",
|
|
1292
|
+
opciones: ["tipo", "motivo", "reemplazo", "yes"],
|
|
1293
|
+
args: [1, 1],
|
|
1294
|
+
accion: dteInvalidar
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
nombre: "contingencia transmitir",
|
|
1298
|
+
uso: "contingencia transmitir [--yes]",
|
|
1299
|
+
resumen: "Transmite al MH los DTE en contingencia",
|
|
1300
|
+
opciones: ["yes"],
|
|
1301
|
+
args: [0, 0],
|
|
1302
|
+
accion: contingenciaTransmitir
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
nombre: "mcp",
|
|
1306
|
+
uso: "mcp",
|
|
1307
|
+
resumen: "Servidor MCP por stdio para Claude y otros clientes",
|
|
1308
|
+
opciones: [],
|
|
1309
|
+
args: [0, 0],
|
|
1310
|
+
accion: mcp
|
|
1311
|
+
}
|
|
1312
|
+
];
|
|
1313
|
+
|
|
1314
|
+
// src/cli.ts
|
|
1315
|
+
var VERSION = package_default.version;
|
|
1316
|
+
var OPCIONES = {
|
|
1317
|
+
help: { type: "boolean", short: "h", ayuda: "Muestra esta ayuda" },
|
|
1318
|
+
version: { type: "boolean", short: "v", ayuda: "Muestra la versi\xF3n" },
|
|
1319
|
+
json: { type: "boolean", ayuda: "Imprime el JSON de la API (para scripts)" },
|
|
1320
|
+
url: { type: "string", ayuda: "URL del servidor (o OPENFACTURA_URL)" },
|
|
1321
|
+
profile: {
|
|
1322
|
+
type: "string",
|
|
1323
|
+
ayuda: "Perfil de configuraci\xF3n (o OPENFACTURA_PROFILE; por defecto \xABdefault\xBB)"
|
|
1324
|
+
},
|
|
1325
|
+
yes: { type: "boolean", short: "y", ayuda: "No pedir confirmaci\xF3n con claves de producci\xF3n" },
|
|
1326
|
+
key: { type: "string", ayuda: "Clave de API; si se omite, se pide sin mostrarla" },
|
|
1327
|
+
q: { type: "string", ayuda: "Texto a buscar" },
|
|
1328
|
+
file: {
|
|
1329
|
+
type: "string",
|
|
1330
|
+
short: "f",
|
|
1331
|
+
ayuda: "Archivo JSON con los datos (\xAB-\xBB lee la entrada est\xE1ndar)"
|
|
1332
|
+
},
|
|
1333
|
+
output: { type: "string", short: "o", ayuda: "Archivo de salida" },
|
|
1334
|
+
"idempotency-key": {
|
|
1335
|
+
type: "string",
|
|
1336
|
+
ayuda: "Clave de idempotencia; por defecto se genera un UUID"
|
|
1337
|
+
},
|
|
1338
|
+
tipo: {
|
|
1339
|
+
type: "string",
|
|
1340
|
+
ayuda: "Tipo de DTE (01, 03\u2026) o, al invalidar, tipo de anulaci\xF3n (1, 2, 3)"
|
|
1341
|
+
},
|
|
1342
|
+
estado: {
|
|
1343
|
+
type: "string",
|
|
1344
|
+
ayuda: "pendiente | procesado | rechazado | contingencia | invalidado"
|
|
1345
|
+
},
|
|
1346
|
+
desde: { type: "string", ayuda: "Fecha inicial AAAA-MM-DD" },
|
|
1347
|
+
hasta: { type: "string", ayuda: "Fecha final AAAA-MM-DD" },
|
|
1348
|
+
pagina: { type: "string", ayuda: "N\xFAmero de p\xE1gina" },
|
|
1349
|
+
motivo: { type: "string", ayuda: "Motivo de la invalidaci\xF3n" },
|
|
1350
|
+
reemplazo: { type: "string", ayuda: "C\xF3digo de generaci\xF3n del DTE que reemplaza al invalidado" },
|
|
1351
|
+
"tipo-documento": {
|
|
1352
|
+
type: "string",
|
|
1353
|
+
ayuda: "36 NIT | 13 DUI | 03 pasaporte | 02 carn\xE9 de residente | 37 otro"
|
|
1354
|
+
},
|
|
1355
|
+
"num-documento": { type: "string", ayuda: "N\xFAmero del documento" },
|
|
1356
|
+
nrc: { type: "string", ayuda: "NRC del contribuyente" },
|
|
1357
|
+
nombre: { type: "string", ayuda: "Nombre o raz\xF3n social" },
|
|
1358
|
+
"nombre-comercial": { type: "string", ayuda: "Nombre comercial" },
|
|
1359
|
+
"cod-actividad": { type: "string", ayuda: "C\xF3digo de actividad econ\xF3mica (CAT-019)" },
|
|
1360
|
+
"desc-actividad": { type: "string", ayuda: "Descripci\xF3n de la actividad econ\xF3mica" },
|
|
1361
|
+
departamento: { type: "string", ayuda: "C\xF3digo de departamento (CAT-012)" },
|
|
1362
|
+
municipio: { type: "string", ayuda: "C\xF3digo de municipio (CAT-013)" },
|
|
1363
|
+
distrito: { type: "string", ayuda: "C\xF3digo de distrito (CAT-008)" },
|
|
1364
|
+
complemento: { type: "string", ayuda: "Direcci\xF3n" },
|
|
1365
|
+
telefono: { type: "string", ayuda: "Tel\xE9fono" },
|
|
1366
|
+
correo: { type: "string", ayuda: "Correo electr\xF3nico" },
|
|
1367
|
+
codigo: { type: "string", ayuda: "C\xF3digo del producto" },
|
|
1368
|
+
descripcion: { type: "string", ayuda: "Descripci\xF3n del producto" },
|
|
1369
|
+
precio: { type: "string", ayuda: "Precio unitario SIN IVA" },
|
|
1370
|
+
"tipo-item": {
|
|
1371
|
+
type: "string",
|
|
1372
|
+
ayuda: "1 bien | 2 servicio | 3 ambos | 4 otros tributos (por defecto 1)"
|
|
1373
|
+
},
|
|
1374
|
+
"uni-medida": { type: "string", ayuda: "Unidad de medida, CAT-014 (por defecto 59 = unidad)" },
|
|
1375
|
+
"tipo-venta": { type: "string", ayuda: "gravada | exenta | noSujeta (por defecto gravada)" }
|
|
1376
|
+
};
|
|
1377
|
+
var GLOBALES = ["help", "json", "url", "profile"];
|
|
1378
|
+
var bandera = (nombre) => {
|
|
1379
|
+
const o = OPCIONES[nombre];
|
|
1380
|
+
return `${o.short ? `-${o.short}, ` : ""}--${nombre}${o.type === "string" ? " <valor>" : ""}`;
|
|
1381
|
+
};
|
|
1382
|
+
var listaOpciones = (nombres) => {
|
|
1383
|
+
const ancho = Math.max(...nombres.map((n) => bandera(n).length));
|
|
1384
|
+
return nombres.map((n) => ` ${bandera(n).padEnd(ancho)} ${OPCIONES[n].ayuda}`).join("\n");
|
|
1385
|
+
};
|
|
1386
|
+
function ayudaGeneral(grupo) {
|
|
1387
|
+
const comandos = grupo ? COMANDOS.filter((c) => c.nombre.startsWith(`${grupo} `)) : COMANDOS;
|
|
1388
|
+
const ancho = Math.max(...comandos.map((c) => c.nombre.length));
|
|
1389
|
+
return [
|
|
1390
|
+
grupo ? `openfactura ${grupo} \u2014 ${GRUPOS[grupo]}` : `openfactura ${VERSION} \u2014 facturaci\xF3n electr\xF3nica de El Salvador (DTE) desde la terminal`,
|
|
1391
|
+
"",
|
|
1392
|
+
`Uso: openfactura ${grupo ? `${grupo} <subcomando>` : "<comando>"} [opciones]`,
|
|
1393
|
+
"",
|
|
1394
|
+
"Comandos:",
|
|
1395
|
+
...comandos.map((c) => ` ${c.nombre.padEnd(ancho)} ${c.resumen}`),
|
|
1396
|
+
"",
|
|
1397
|
+
"Opciones globales:",
|
|
1398
|
+
listaOpciones(grupo ? GLOBALES : [...GLOBALES, "version"]),
|
|
1399
|
+
"",
|
|
1400
|
+
"Use `openfactura <comando> --help` para ver las opciones de un comando.",
|
|
1401
|
+
...grupo ? [] : [
|
|
1402
|
+
"",
|
|
1403
|
+
"Entorno: OPENFACTURA_API_KEY, OPENFACTURA_URL, OPENFACTURA_PROFILE.",
|
|
1404
|
+
"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."
|
|
1405
|
+
]
|
|
1406
|
+
].join("\n");
|
|
1407
|
+
}
|
|
1408
|
+
function ayudaComando(comando) {
|
|
1409
|
+
return [
|
|
1410
|
+
comando.resumen,
|
|
1411
|
+
"",
|
|
1412
|
+
`Uso: openfactura ${comando.uso}`,
|
|
1413
|
+
...comando.opciones.length ? ["", "Opciones:", listaOpciones(comando.opciones)] : [],
|
|
1414
|
+
"",
|
|
1415
|
+
"Opciones globales:",
|
|
1416
|
+
listaOpciones(GLOBALES)
|
|
1417
|
+
].join("\n");
|
|
1418
|
+
}
|
|
1419
|
+
function analizar(argv) {
|
|
1420
|
+
try {
|
|
1421
|
+
const opciones = Object.fromEntries(
|
|
1422
|
+
Object.entries(OPCIONES).map(([nombre, { type, short }]) => [
|
|
1423
|
+
nombre,
|
|
1424
|
+
short ? { type, short } : { type }
|
|
1425
|
+
])
|
|
1426
|
+
);
|
|
1427
|
+
return parseArgs({ args: argv, options: opciones, allowPositionals: true, strict: true });
|
|
1428
|
+
} catch (error) {
|
|
1429
|
+
const { code, message } = error;
|
|
1430
|
+
const opcion = /'(-[^' ]*)/.exec(message)?.[1];
|
|
1431
|
+
if (code === "ERR_PARSE_ARGS_UNKNOWN_OPTION")
|
|
1432
|
+
throw new ErrorUso(`Opci\xF3n desconocida: ${opcion ?? message}`);
|
|
1433
|
+
if (code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE") {
|
|
1434
|
+
throw new ErrorUso(
|
|
1435
|
+
opcion ? `La opci\xF3n ${opcion} est\xE1 mal usada: revise si lleva o no un valor.` : message
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
throw new ErrorUso(message);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
function informarError(error, ent, json) {
|
|
1442
|
+
if (error instanceof ErrorUso) {
|
|
1443
|
+
ent.stderr(`Error: ${error.message}`);
|
|
1444
|
+
ent.stderr("Use `openfactura --help` para ver los comandos.");
|
|
1445
|
+
return SALIDA.uso;
|
|
1446
|
+
}
|
|
1447
|
+
if (error instanceof ErrorApi) {
|
|
1448
|
+
if (json) {
|
|
1449
|
+
ent.stderr(
|
|
1450
|
+
JSON.stringify(
|
|
1451
|
+
error.retryAfter === null ? error.cuerpo : { ...error.cuerpo, retryAfter: error.retryAfter },
|
|
1452
|
+
null,
|
|
1453
|
+
2
|
|
1454
|
+
)
|
|
1455
|
+
);
|
|
1456
|
+
return error.salida;
|
|
1457
|
+
}
|
|
1458
|
+
ent.stderr(`Error [${error.code}]: ${error.message}`);
|
|
1459
|
+
for (const linea of detalles(error)) ent.stderr(` ${linea}`);
|
|
1460
|
+
if (error.status === 401)
|
|
1461
|
+
ent.stderr(" Revise OPENFACTURA_API_KEY o ejecute `openfactura login`.");
|
|
1462
|
+
if (error.status === 429) {
|
|
1463
|
+
ent.stderr(
|
|
1464
|
+
` Retry-After: ${error.retryAfter ?? "?"} (segundos que debe esperar antes de reintentar)`
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
return error.salida;
|
|
1468
|
+
}
|
|
1469
|
+
if (error instanceof ErrorCli) {
|
|
1470
|
+
ent.stderr(
|
|
1471
|
+
json ? JSON.stringify(error.cuerpo, null, 2) : `Error [${error.code}]: ${error.message}`
|
|
1472
|
+
);
|
|
1473
|
+
return error.salida;
|
|
1474
|
+
}
|
|
1475
|
+
ent.stderr(
|
|
1476
|
+
`Error inesperado: ${error instanceof Error ? error.stack ?? error.message : String(error)}`
|
|
1477
|
+
);
|
|
1478
|
+
return SALIDA.error;
|
|
1479
|
+
}
|
|
1480
|
+
function detalles(error) {
|
|
1481
|
+
const d = error.details;
|
|
1482
|
+
if (d === void 0 || d === null) return [];
|
|
1483
|
+
const problema = (p) => `${p.ruta || "(ra\xEDz)"}: ${p.mensaje}`;
|
|
1484
|
+
if (Array.isArray(d)) return d.map((p) => problema(p));
|
|
1485
|
+
if (Array.isArray(d.problemas)) return d.problemas.map((p) => problema(p));
|
|
1486
|
+
if (d.mh && typeof d.mh === "object") {
|
|
1487
|
+
const mh = d.mh;
|
|
1488
|
+
const observaciones = Array.isArray(mh.observaciones) ? mh.observaciones : [];
|
|
1489
|
+
return [
|
|
1490
|
+
`MH ${mh.codigo ?? ""}: ${mh.mensaje ?? ""}`.trim(),
|
|
1491
|
+
...observaciones.map((o) => `- ${o}`)
|
|
1492
|
+
];
|
|
1493
|
+
}
|
|
1494
|
+
if (typeof d.permiso === "string") return [`Permiso que falta en la clave: ${d.permiso}`];
|
|
1495
|
+
return [JSON.stringify(d)];
|
|
1496
|
+
}
|
|
1497
|
+
async function ejecutar(argv, ent) {
|
|
1498
|
+
let json = argv.includes("--json");
|
|
1499
|
+
try {
|
|
1500
|
+
const { values, positionals } = analizar(argv);
|
|
1501
|
+
const valores = values;
|
|
1502
|
+
json = valores.json === true;
|
|
1503
|
+
const [primero, segundo] = positionals;
|
|
1504
|
+
if (!primero) {
|
|
1505
|
+
if (valores.version) {
|
|
1506
|
+
ent.stdout(VERSION);
|
|
1507
|
+
return SALIDA.ok;
|
|
1508
|
+
}
|
|
1509
|
+
if (valores.help) {
|
|
1510
|
+
ent.stdout(ayudaGeneral());
|
|
1511
|
+
return SALIDA.ok;
|
|
1512
|
+
}
|
|
1513
|
+
throw new ErrorUso("Falta el comando.");
|
|
1514
|
+
}
|
|
1515
|
+
const esGrupo = Object.hasOwn(GRUPOS, primero);
|
|
1516
|
+
const nombre = esGrupo ? `${primero} ${segundo ?? ""}`.trim() : primero;
|
|
1517
|
+
const comando = COMANDOS.find((c) => c.nombre === nombre);
|
|
1518
|
+
if (!comando) {
|
|
1519
|
+
if (esGrupo && !segundo && valores.help) {
|
|
1520
|
+
ent.stdout(ayudaGeneral(primero));
|
|
1521
|
+
return SALIDA.ok;
|
|
1522
|
+
}
|
|
1523
|
+
const disponibles = COMANDOS.filter((c) => c.nombre.startsWith(`${primero} `)).map(
|
|
1524
|
+
(c) => c.nombre.split(" ")[1]
|
|
1525
|
+
);
|
|
1526
|
+
throw new ErrorUso(
|
|
1527
|
+
esGrupo ? `${segundo ? `Subcomando desconocido: ${primero} ${segundo}.` : `Falta el subcomando de \xAB${primero}\xBB.`} Disponibles: ${disponibles.join(", ")}` : `Comando desconocido: ${primero}`
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
if (valores.help) {
|
|
1531
|
+
ent.stdout(ayudaComando(comando));
|
|
1532
|
+
return SALIDA.ok;
|
|
1533
|
+
}
|
|
1534
|
+
const permitidas = /* @__PURE__ */ new Set([...GLOBALES, ...comando.opciones]);
|
|
1535
|
+
const sobrante = Object.keys(valores).find((opcion) => !permitidas.has(opcion));
|
|
1536
|
+
if (sobrante)
|
|
1537
|
+
throw new ErrorUso(
|
|
1538
|
+
`La opci\xF3n --${sobrante} no aplica a \xAB${comando.nombre}\xBB.
|
|
1539
|
+
Uso: openfactura ${comando.uso}`
|
|
1540
|
+
);
|
|
1541
|
+
const args = positionals.slice(esGrupo ? 2 : 1);
|
|
1542
|
+
const [minimo, maximo] = comando.args;
|
|
1543
|
+
if (args.length < minimo || args.length > maximo) {
|
|
1544
|
+
throw new ErrorUso(
|
|
1545
|
+
`${args.length < minimo ? "Faltan argumentos" : `Sobran argumentos (${args.slice(maximo).join(" ")})`}.
|
|
1546
|
+
Uso: openfactura ${comando.uso}`
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
const cfg = resolver(ent.env, {
|
|
1550
|
+
url: valores.url,
|
|
1551
|
+
perfil: valores.profile
|
|
1552
|
+
});
|
|
1553
|
+
let cliente2;
|
|
1554
|
+
const ctx = {
|
|
1555
|
+
ent,
|
|
1556
|
+
version: VERSION,
|
|
1557
|
+
args,
|
|
1558
|
+
json,
|
|
1559
|
+
si: valores.yes === true,
|
|
1560
|
+
cfg,
|
|
1561
|
+
op: (opcion) => typeof valores[opcion] === "string" ? valores[opcion] : void 0,
|
|
1562
|
+
api: () => {
|
|
1563
|
+
if (!cfg.apiKey) {
|
|
1564
|
+
throw new ErrorCli(
|
|
1565
|
+
"No hay una clave de API. Ejecute `openfactura login` o defina OPENFACTURA_API_KEY.",
|
|
1566
|
+
"no_autenticado",
|
|
1567
|
+
SALIDA.noAutenticado
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
if (!cliente2) {
|
|
1571
|
+
const { protocol, hostname } = new URL(cfg.url);
|
|
1572
|
+
if (protocol === "http:" && !["localhost", "127.0.0.1", "[::1]"].includes(hostname)) {
|
|
1573
|
+
ent.stderr(`Aviso: ${cfg.url} no usa HTTPS; la clave de API viaja sin cifrar.`);
|
|
1574
|
+
}
|
|
1575
|
+
cliente2 = new ClienteApi({
|
|
1576
|
+
url: cfg.url,
|
|
1577
|
+
apiKey: cfg.apiKey,
|
|
1578
|
+
version: VERSION,
|
|
1579
|
+
fetch: ent.fetch
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
return cliente2;
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
return await comando.accion(ctx) ?? SALIDA.ok;
|
|
1586
|
+
} catch (error) {
|
|
1587
|
+
return informarError(error, ent, json);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// src/index.ts
|
|
1592
|
+
async function leerEntrada() {
|
|
1593
|
+
const partes = [];
|
|
1594
|
+
for await (const parte of process.stdin) partes.push(Buffer.from(parte));
|
|
1595
|
+
return Buffer.concat(partes).toString("utf8");
|
|
1596
|
+
}
|
|
1597
|
+
function preguntar(pregunta, oculto = false) {
|
|
1598
|
+
const { stdin, stderr } = process;
|
|
1599
|
+
return new Promise((resolve2, reject) => {
|
|
1600
|
+
let texto2 = "";
|
|
1601
|
+
stderr.write(pregunta);
|
|
1602
|
+
if (oculto) stdin.setRawMode(true);
|
|
1603
|
+
stdin.setEncoding("utf8");
|
|
1604
|
+
stdin.resume();
|
|
1605
|
+
const terminar = (accion) => {
|
|
1606
|
+
stdin.off("data", alRecibir);
|
|
1607
|
+
if (oculto) {
|
|
1608
|
+
stdin.setRawMode(false);
|
|
1609
|
+
stderr.write("\n");
|
|
1610
|
+
}
|
|
1611
|
+
stdin.pause();
|
|
1612
|
+
accion();
|
|
1613
|
+
};
|
|
1614
|
+
const alRecibir = (dato) => {
|
|
1615
|
+
for (const letra of dato) {
|
|
1616
|
+
if (letra === "\n" || letra === "\r" || letra === "")
|
|
1617
|
+
return terminar(() => resolve2(texto2));
|
|
1618
|
+
if (letra === "")
|
|
1619
|
+
return terminar(() => reject(new ErrorCli("Operaci\xF3n cancelada.", "cancelado")));
|
|
1620
|
+
if (letra === "\x7F" || letra === "\b") texto2 = texto2.slice(0, -1);
|
|
1621
|
+
else if (letra >= " ") texto2 += letra;
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
stdin.on("data", alRecibir);
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
var entorno = {
|
|
1628
|
+
env: process.env,
|
|
1629
|
+
fetch: (...args) => fetch(...args),
|
|
1630
|
+
stdout: (linea) => void process.stdout.write(`${linea}
|
|
1631
|
+
`),
|
|
1632
|
+
stderr: (linea) => void process.stderr.write(`${linea}
|
|
1633
|
+
`),
|
|
1634
|
+
leerEntrada,
|
|
1635
|
+
esTTY: Boolean(process.stdin.isTTY),
|
|
1636
|
+
preguntar,
|
|
1637
|
+
cwd: process.cwd()
|
|
1638
|
+
};
|
|
1639
|
+
process.stdout.on("error", (error) => {
|
|
1640
|
+
if (error.code === "EPIPE") process.exit(0);
|
|
1641
|
+
});
|
|
1642
|
+
process.exitCode = await ejecutar(process.argv.slice(2), entorno);
|