ecomiq 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ecomiq
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,30 @@
1
- # Temporary Holding Version
1
+ # ecomiq
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ CLI de Ecomiq para bajar, probar y publicar la tienda online como código.
4
+
5
+ ```bash
6
+ npm install -g ecomiq
7
+ ecomiq login
8
+ ecomiq init mi-tienda
9
+ cd mi-tienda && npm install
10
+ ecomiq dev
11
+ ecomiq publish
12
+ ```
13
+
14
+ | Comando | Qué hace |
15
+ | ----------------------- | ------------------------------------------------- |
16
+ | `ecomiq login [tienda]` | Inicia sesión y elige la tienda en el navegador. |
17
+ | `ecomiq logout` | Cierra la sesión en este equipo y en Ecomiq. |
18
+ | `ecomiq init [carpeta]` | Baja el código de la tienda a una carpeta nueva. |
19
+ | `ecomiq dev` | Levanta la tienda en local con sus datos reales. |
20
+ | `ecomiq pack [archivo]` | Construye el tema y lo comprime en un ZIP. |
21
+ | `ecomiq publish` | Construye y sube una vista previa. |
22
+ | `ecomiq publish --live` | Construye, sube y publica en vivo. |
23
+
24
+ `publish` y `pack` no suben `node_modules`, `build`, `.env*`, `.dev.vars` ni
25
+ los archivos ocultos de la raíz, salvo `.gitignore`. Los `.map` del build no se
26
+ suben.
27
+
28
+ Necesita Node.js 22.22 o superior.
29
+
30
+ Guía completa: https://docs.ecomiq.pe/es/docs/guides/storefront/themes
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {}
package/dist/cli.js ADDED
@@ -0,0 +1,589 @@
1
+ #!/usr/bin/env node
2
+ import { platformFiles } from "./platform.js";
3
+ import { spawn } from "node:child_process";
4
+ import { createHash, randomBytes } from "node:crypto";
5
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
6
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
7
+ import { createServer } from "node:http";
8
+ import { homedir } from "node:os";
9
+ import { crc32, deflateRawSync, gzipSync } from "node:zlib";
10
+ // ../ecomiq-core/package.json
11
+ var version$2 = "1.0.0";
12
+ // ../react-router/package.json
13
+ var version$1 = "1.0.0";
14
+ // package.json
15
+ var version = "0.1.0";
16
+ // src/auth.ts
17
+ const CALLBACK = "/ecomiq/cli/callback";
18
+ const SCOPES = "openid profile email offline_access api_admin";
19
+ const TIMEOUT = 3e5;
20
+ const LEEWAY = 6e4;
21
+ const LOCAL_ADMIN_URL = "http://localhost:8004";
22
+ function adminUrl(local = false, environment = process.env) {
23
+ if (local) return LOCAL_ADMIN_URL;
24
+ return url(environment.ECOMIQ_ADMIN_URL?.trim() || "https://api-admin.ecomiq.pe", "ECOMIQ_ADMIN_URL");
25
+ }
26
+ function url(value, name) {
27
+ let parsed;
28
+ try {
29
+ parsed = new URL(value);
30
+ } catch {
31
+ throw new Error(`${name} debe ser una URL absoluta.`);
32
+ }
33
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new Error(`${name} debe usar http o https.`);
34
+ return parsed.href.replace(/\/+$/, "");
35
+ }
36
+ function credentialsPath(environment = process.env) {
37
+ const directory = environment.ECOMIQ_CONFIG_DIR?.trim() || join(environment.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config"), "ecomiq");
38
+ return join(directory, "credentials.json");
39
+ }
40
+ function readCredentials() {
41
+ const path = credentialsPath();
42
+ if (!existsSync(path)) return {
43
+ version: 1,
44
+ profiles: {}
45
+ };
46
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
47
+ if (parsed === null || typeof parsed !== "object" || !("version" in parsed) || parsed.version !== 1 || !("profiles" in parsed)) throw new Error(`${path} no tiene un formato conocido.`);
48
+ return parsed;
49
+ }
50
+ function writeCredentials(credentials) {
51
+ const path = credentialsPath();
52
+ mkdirSync(dirname(path), {
53
+ recursive: true,
54
+ mode: 448
55
+ });
56
+ writeFileSync(path, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 384 });
57
+ chmodSync(path, 384);
58
+ }
59
+ function base64Url(bytes) {
60
+ return bytes.toString("base64url");
61
+ }
62
+ function claims(token) {
63
+ const payload = token.split(".")[1];
64
+ if (!payload) throw new Error("El token de acceso no es un JWT.");
65
+ return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
66
+ }
67
+ function profile(tokens, apiUrl, tokenUrl, clientId, previous = null) {
68
+ if (typeof tokens.access_token !== "string" || tokens.access_token === "") throw new Error("No llegó un token de acceso.");
69
+ const claimSet = claims(tokens.access_token);
70
+ return {
71
+ apiUrl,
72
+ tokenUrl,
73
+ clientId,
74
+ accessToken: tokens.access_token,
75
+ refreshToken: typeof tokens.refresh_token === "string" ? tokens.refresh_token : previous?.refreshToken ?? null,
76
+ expiresAt: new Date(Date.now() + Math.max(0, Number(tokens.expires_in ?? 0)) * 1e3).toISOString(),
77
+ storeId: previous?.storeId ?? null,
78
+ storeSlug: previous?.storeSlug ?? null,
79
+ email: typeof claimSet.email === "string" ? claimSet.email : null
80
+ };
81
+ }
82
+ async function requestTokens(tokenUrl, form) {
83
+ const response = await fetch(tokenUrl, {
84
+ method: "POST",
85
+ headers: {
86
+ "content-type": "application/x-www-form-urlencoded",
87
+ accept: "application/json"
88
+ },
89
+ body: new URLSearchParams(form).toString()
90
+ });
91
+ const text = await response.text();
92
+ let body = {};
93
+ try {
94
+ body = text ? JSON.parse(text) : {};
95
+ } catch {
96
+ body = {};
97
+ }
98
+ if (!response.ok) throw new Error(`La autorización falló: ${String(body.error_description ?? body.error ?? `HTTP ${response.status}`)}`);
99
+ return body;
100
+ }
101
+ async function siteConfig(apiUrl) {
102
+ let response;
103
+ try {
104
+ response = await fetch(`${apiUrl}/v1/site/config`, { headers: { accept: "application/json" } });
105
+ } catch (error) {
106
+ throw new Error(`No se pudo contactar con Ecomiq (${apiUrl}): ${error instanceof Error ? error.message : String(error)}`);
107
+ }
108
+ if (!response.ok) throw new Error(`Ecomiq respondió HTTP ${response.status} al preparar el inicio de sesión.`);
109
+ const config = await response.json();
110
+ if (config === null || typeof config !== "object" || !("issuer" in config) || typeof config.issuer !== "string" || !("tokenUrl" in config) || typeof config.tokenUrl !== "string" || !("revocationUrl" in config) || typeof config.revocationUrl !== "string" || !("clientId" in config) || typeof config.clientId !== "string" || !("storefrontApiUrl" in config) || typeof config.storefrontApiUrl !== "string") throw new Error("Ecomiq no devolvió la configuración esperada.");
111
+ return {
112
+ issuer: url(config.issuer, "issuer"),
113
+ tokenUrl: url(config.tokenUrl, "tokenUrl"),
114
+ revocationUrl: url(config.revocationUrl, "revocationUrl"),
115
+ clientId: config.clientId,
116
+ storefrontApiUrl: url(config.storefrontApiUrl, "storefrontApiUrl")
117
+ };
118
+ }
119
+ function browserCommand(address, platform = process.platform) {
120
+ if (platform === "darwin") return {
121
+ command: "open",
122
+ args: [address]
123
+ };
124
+ if (platform === "win32") return {
125
+ command: "rundll32",
126
+ args: ["url.dll,FileProtocolHandler", address]
127
+ };
128
+ return {
129
+ command: "xdg-open",
130
+ args: [address]
131
+ };
132
+ }
133
+ function openBrowser(address) {
134
+ const { command, args } = browserCommand(address);
135
+ const child = spawn(command, args, {
136
+ detached: true,
137
+ stdio: "ignore"
138
+ });
139
+ child.on("error", () => void 0);
140
+ child.unref();
141
+ }
142
+ function page(title, body) {
143
+ return `<!doctype html><meta charset="utf-8"><title>${title}</title><body style="font-family:system-ui;padding:3rem"><h1 style="font-size:1.25rem">${title}</h1><p>${body}</p></body>`;
144
+ }
145
+ function callback(server, state) {
146
+ return new Promise((resolve, reject) => {
147
+ const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("El inicio de sesión expiró.")), TIMEOUT);
148
+ server.on("request", (request, response) => {
149
+ const requested = new URL(request.url ?? "/", "http://127.0.0.1");
150
+ if (requested.pathname !== CALLBACK) {
151
+ response.writeHead(404).end();
152
+ return;
153
+ }
154
+ if (requested.searchParams.get("state") !== state) {
155
+ response.writeHead(400, { "content-type": "text/plain; charset=utf-8" }).end("Sesión no válida.");
156
+ return;
157
+ }
158
+ clearTimeout(timer);
159
+ const code = requested.searchParams.get("code");
160
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
161
+ if (!code) {
162
+ response.end(page("No se autorizó", "Cierra esta ventana e inténtalo de nuevo."));
163
+ reject(/* @__PURE__ */ new Error(`La autorización fue rechazada: ${requested.searchParams.get("error_description") ?? requested.searchParams.get("error") ?? "sin código"}`));
164
+ return;
165
+ }
166
+ response.end(page("Listo", "Vuelve a la terminal."));
167
+ resolve(code);
168
+ });
169
+ });
170
+ }
171
+ async function login(apiUrl, log) {
172
+ const { issuer, tokenUrl, clientId } = await siteConfig(apiUrl);
173
+ const verifier = base64Url(randomBytes(48));
174
+ const challenge = base64Url(createHash("sha256").update(verifier).digest());
175
+ const state = base64Url(randomBytes(24));
176
+ const server = createServer();
177
+ await new Promise((resolve, reject) => {
178
+ server.once("error", reject);
179
+ server.listen(0, "127.0.0.1", () => resolve());
180
+ });
181
+ try {
182
+ const redirectUri = `http://127.0.0.1:${server.address().port}${CALLBACK}`;
183
+ const authorize = new URL(`${issuer}/connect/authorize`);
184
+ for (const [name, value] of Object.entries({
185
+ client_id: clientId,
186
+ response_type: "code",
187
+ redirect_uri: redirectUri,
188
+ scope: SCOPES,
189
+ code_challenge: challenge,
190
+ code_challenge_method: "S256",
191
+ state
192
+ })) authorize.searchParams.set(name, value);
193
+ const code = callback(server, state);
194
+ log(`Si el navegador no se abre, entra a:\n ${authorize.href}`);
195
+ openBrowser(authorize.href);
196
+ const created = profile(await requestTokens(tokenUrl, {
197
+ grant_type: "authorization_code",
198
+ client_id: clientId,
199
+ code: await code,
200
+ redirect_uri: redirectUri,
201
+ code_verifier: verifier
202
+ }), apiUrl, tokenUrl, clientId);
203
+ const credentials = readCredentials();
204
+ credentials.profiles[apiUrl] = created;
205
+ writeCredentials(credentials);
206
+ return created;
207
+ } finally {
208
+ await new Promise((resolve) => server.close(() => resolve()));
209
+ }
210
+ }
211
+ function chooseStore(apiUrl, storeId, storeSlug) {
212
+ const credentials = readCredentials();
213
+ const current = credentials.profiles[apiUrl];
214
+ if (!current) throw new Error("No has iniciado sesión. Ejecuta `ecomiq login`.");
215
+ credentials.profiles[apiUrl] = {
216
+ ...current,
217
+ storeId,
218
+ storeSlug
219
+ };
220
+ writeCredentials(credentials);
221
+ }
222
+ async function revoke(apiUrl, current) {
223
+ if (!current.refreshToken) return;
224
+ const { revocationUrl } = await siteConfig(apiUrl);
225
+ const response = await fetch(revocationUrl, {
226
+ method: "POST",
227
+ headers: {
228
+ "content-type": "application/x-www-form-urlencoded",
229
+ accept: "application/json"
230
+ },
231
+ body: new URLSearchParams({
232
+ client_id: current.clientId,
233
+ token: current.refreshToken,
234
+ token_type_hint: "refresh_token"
235
+ }).toString()
236
+ });
237
+ if (!response.ok) throw new Error(`Ecomiq no cerró la sesión (HTTP ${response.status}). Inténtalo de nuevo.`);
238
+ }
239
+ async function logout(apiUrl) {
240
+ const credentials = readCredentials();
241
+ const current = credentials.profiles[apiUrl];
242
+ if (!current) return false;
243
+ await revoke(apiUrl, current);
244
+ delete credentials.profiles[apiUrl];
245
+ writeCredentials(credentials);
246
+ return true;
247
+ }
248
+ async function session(apiUrl, force = false) {
249
+ const credentials = readCredentials();
250
+ const current = credentials.profiles[apiUrl];
251
+ if (!current) throw new Error("No has iniciado sesión. Ejecuta `ecomiq login`.");
252
+ if (!force && Date.parse(current.expiresAt) - LEEWAY > Date.now()) return current;
253
+ if (!current.refreshToken || !current.tokenUrl) throw new Error("La sesión expiró. Ejecuta `ecomiq login`.");
254
+ const refreshed = profile(await requestTokens(current.tokenUrl, {
255
+ grant_type: "refresh_token",
256
+ client_id: current.clientId,
257
+ refresh_token: current.refreshToken
258
+ }), apiUrl, current.tokenUrl, current.clientId, current);
259
+ credentials.profiles[apiUrl] = refreshed;
260
+ writeCredentials(credentials);
261
+ return refreshed;
262
+ }
263
+ // src/api.ts
264
+ async function problem(response) {
265
+ const text = await response.text();
266
+ try {
267
+ const body = JSON.parse(text);
268
+ if (body !== null && typeof body === "object" && "detail" in body && typeof body.detail === "string") return body.detail;
269
+ } catch {
270
+ return text || `HTTP ${response.status}`;
271
+ }
272
+ return text || `HTTP ${response.status}`;
273
+ }
274
+ async function call(apiUrl, path, init = () => ({})) {
275
+ const send = async (force) => {
276
+ const current = await session(apiUrl, force);
277
+ const request = init();
278
+ const headers = new Headers(request.headers);
279
+ headers.set("authorization", `Bearer ${current.accessToken}`);
280
+ headers.set("accept", "application/json");
281
+ if (current.storeId) headers.set("x-store-id", current.storeId);
282
+ return fetch(`${apiUrl}${path}`, {
283
+ ...request,
284
+ headers
285
+ });
286
+ };
287
+ let response = await send(false);
288
+ if (response.status === 401) response = await send(true);
289
+ if (!response.ok) throw new Error(await problem(response));
290
+ return await response.json();
291
+ }
292
+ // src/tar.ts
293
+ const BLOCK = 512;
294
+ const SKIP = /* @__PURE__ */ new Set([
295
+ "node_modules",
296
+ "build",
297
+ "dist",
298
+ ".react-router",
299
+ ".wrangler",
300
+ ".git",
301
+ ".dev.vars",
302
+ ".DS_Store"
303
+ ]);
304
+ const ROOT_DOTFILES = /* @__PURE__ */ new Set([".gitignore"]);
305
+ function octal(value, length) {
306
+ return value.toString(8).padStart(length - 1, "0") + "\0";
307
+ }
308
+ function split(path) {
309
+ if (Buffer.byteLength(path) <= 100) return {
310
+ name: path,
311
+ prefix: ""
312
+ };
313
+ const cut = path.lastIndexOf("/", path.length - 1);
314
+ for (let index = cut; index > 0; index = path.lastIndexOf("/", index - 1)) {
315
+ const prefix = path.slice(0, index);
316
+ const name = path.slice(index + 1);
317
+ if (Buffer.byteLength(prefix) <= 155 && Buffer.byteLength(name) <= 100) return {
318
+ name,
319
+ prefix
320
+ };
321
+ }
322
+ throw new Error(`Ruta demasiado larga para empaquetar: ${path}`);
323
+ }
324
+ function header(path, size) {
325
+ const block = Buffer.alloc(BLOCK);
326
+ const { name, prefix } = split(path);
327
+ block.write(name, 0, 100, "utf8");
328
+ block.write(octal(420, 8), 100, 8, "ascii");
329
+ block.write(octal(0, 8), 108, 8, "ascii");
330
+ block.write(octal(0, 8), 116, 8, "ascii");
331
+ block.write(octal(size, 12), 124, 12, "ascii");
332
+ block.write(octal(0, 12), 136, 12, "ascii");
333
+ block.fill(" ", 148, 156);
334
+ block.write("0", 156, 1, "ascii");
335
+ block.write("ustar\0", 257, 6, "ascii");
336
+ block.write("00", 263, 2, "ascii");
337
+ block.write(prefix, 345, 155, "utf8");
338
+ let checksum = 0;
339
+ for (const byte of block) checksum += byte;
340
+ block.write(octal(checksum, 7) + " ", 148, 8, "ascii");
341
+ return block;
342
+ }
343
+ function walk(root, skipped) {
344
+ const found = [];
345
+ const visit = (directory) => {
346
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
347
+ const absolute = join(directory, entry.name);
348
+ const path = relative(root, absolute).split(sep).join("/");
349
+ if (entry.name.startsWith(".env") || skipped(path, entry.name)) continue;
350
+ if (entry.isDirectory()) visit(absolute);
351
+ else if (entry.isFile()) found.push(path);
352
+ }
353
+ };
354
+ visit(root);
355
+ return found.sort();
356
+ }
357
+ function projectFiles(root) {
358
+ return walk(root, (path, name) => SKIP.has(name) || path === name && name.startsWith(".") && !ROOT_DOTFILES.has(name));
359
+ }
360
+ function buildFiles(root) {
361
+ return walk(root, (_path, name) => name === ".dev.vars" || name === ".DS_Store" || name.endsWith(".map"));
362
+ }
363
+ function pack(root, paths) {
364
+ const chunks = [];
365
+ for (const path of paths) {
366
+ const content = readFileSync(join(root, path));
367
+ if (!statSync(join(root, path)).isFile()) continue;
368
+ chunks.push(header(path, content.length), content, Buffer.alloc((BLOCK - content.length % BLOCK) % BLOCK));
369
+ }
370
+ chunks.push(Buffer.alloc(BLOCK * 2));
371
+ return gzipSync(Buffer.concat(chunks), { level: 9 });
372
+ }
373
+ // src/zip.ts
374
+ function dosTime(date) {
375
+ const year = Math.max(1980, date.getFullYear());
376
+ return {
377
+ time: date.getHours() << 11 | date.getMinutes() << 5 | Math.floor(date.getSeconds() / 2),
378
+ day: year - 1980 << 9 | date.getMonth() + 1 << 5 | date.getDate()
379
+ };
380
+ }
381
+ function zip(root, paths, folder) {
382
+ const locals = [];
383
+ const centrals = [];
384
+ let offset = 0;
385
+ for (const path of paths) {
386
+ const content = readFileSync(join(root, path));
387
+ const deflated = deflateRawSync(content, { level: 9 });
388
+ const method = deflated.length < content.length ? 8 : 0;
389
+ const data = method === 8 ? deflated : content;
390
+ const name = Buffer.from(`${folder}/${path}`, "utf8");
391
+ const checksum = crc32(content);
392
+ const { time, day } = dosTime(statSync(join(root, path)).mtime);
393
+ const local = Buffer.alloc(30);
394
+ local.writeUInt32LE(67324752, 0);
395
+ local.writeUInt16LE(20, 4);
396
+ local.writeUInt16LE(2048, 6);
397
+ local.writeUInt16LE(method, 8);
398
+ local.writeUInt16LE(time, 10);
399
+ local.writeUInt16LE(day, 12);
400
+ local.writeUInt32LE(checksum, 14);
401
+ local.writeUInt32LE(data.length, 18);
402
+ local.writeUInt32LE(content.length, 22);
403
+ local.writeUInt16LE(name.length, 26);
404
+ const central = Buffer.alloc(46);
405
+ central.writeUInt32LE(33639248, 0);
406
+ central.writeUInt16LE(798, 4);
407
+ central.writeUInt16LE(20, 6);
408
+ central.writeUInt16LE(2048, 8);
409
+ central.writeUInt16LE(method, 10);
410
+ central.writeUInt16LE(time, 12);
411
+ central.writeUInt16LE(day, 14);
412
+ central.writeUInt32LE(checksum, 16);
413
+ central.writeUInt32LE(data.length, 20);
414
+ central.writeUInt32LE(content.length, 24);
415
+ central.writeUInt16LE(name.length, 28);
416
+ central.writeUInt32LE(33188 << 16 >>> 0, 38);
417
+ central.writeUInt32LE(offset, 42);
418
+ locals.push(local, name, data);
419
+ centrals.push(central, name);
420
+ offset += local.length + name.length + data.length;
421
+ if (offset > 4294967295) throw new Error("El tema es demasiado grande para comprimirlo.");
422
+ }
423
+ const directory = Buffer.concat(centrals);
424
+ const end = Buffer.alloc(22);
425
+ end.writeUInt32LE(101010256, 0);
426
+ end.writeUInt16LE(paths.length, 8);
427
+ end.writeUInt16LE(paths.length, 10);
428
+ end.writeUInt32LE(directory.length, 12);
429
+ end.writeUInt32LE(offset, 16);
430
+ return Buffer.concat([
431
+ ...locals,
432
+ directory,
433
+ end
434
+ ]);
435
+ }
436
+ // src/cli.ts
437
+ const HELP = `Uso: ecomiq <comando>
438
+
439
+ login [tienda] Inicia sesión y elige tu tienda en el navegador
440
+ logout Cierra la sesión
441
+ init [carpeta] Baja el código de tu tienda a una carpeta nueva
442
+ dev Levanta la tienda en local con sus datos reales
443
+ pack [archivo] Construye el tema y lo comprime en un ZIP para importarlo en Ecomiq
444
+ publish Construye y sube una vista previa
445
+ publish --live Construye, sube y publica en vivo
446
+
447
+ --local Usa el backend local de desarrollo (${LOCAL_ADMIN_URL})
448
+ `;
449
+ const RELEASED = {
450
+ "@ecomiq/core": version$2,
451
+ "@ecomiq/react-router": version$1,
452
+ ecomiq: version
453
+ };
454
+ function binary(name) {
455
+ const path = join(process.cwd(), "node_modules", ".bin", process.platform === "win32" ? `${name}.cmd` : name);
456
+ if (!existsSync(path)) throw new Error("Instala las dependencias del proyecto (npm install).");
457
+ return path;
458
+ }
459
+ function run(command, args) {
460
+ return new Promise((resolve, reject) => {
461
+ const child = spawn(command, args, {
462
+ stdio: "inherit",
463
+ shell: process.platform === "win32"
464
+ });
465
+ child.on("error", reject);
466
+ child.on("close", (code) => code === 0 ? resolve() : reject(/* @__PURE__ */ new Error(`${args[0] ?? command} terminó con código ${code}.`)));
467
+ });
468
+ }
469
+ function project() {
470
+ if (!["react-router.config.ts", "react-router.config.js"].some((name) => existsSync(name))) throw new Error("Ejecuta este comando dentro de tu proyecto (falta react-router.config.ts).");
471
+ }
472
+ function readVars(path) {
473
+ const vars = /* @__PURE__ */ new Map();
474
+ if (!existsSync(path)) return vars;
475
+ for (const line of readFileSync(path, "utf8").split("\n")) {
476
+ const match = /^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/.exec(line);
477
+ if (match) vars.set(match[1], match[2]);
478
+ }
479
+ return vars;
480
+ }
481
+ function installableManifest(content) {
482
+ const manifest = JSON.parse(content);
483
+ for (const group of [manifest.dependencies ?? {}, manifest.devDependencies ?? {}]) for (const [name, range] of Object.entries(group)) if (range.startsWith("workspace:") && RELEASED[name] !== void 0) group[name] = `^${RELEASED[name]}`;
484
+ return `${JSON.stringify(manifest, null, 2)}\n`;
485
+ }
486
+ async function init(apiUrl, folder) {
487
+ const site = await call(apiUrl, "/v1/site");
488
+ const target = resolve(folder ?? site.storeSlug);
489
+ if (existsSync(target) && readdirSync(target).length > 0) throw new Error(`La carpeta ${target} no está vacía.`);
490
+ const listed = await call(apiUrl, "/v1/site/files");
491
+ if (listed.files.length === 0) throw new Error(`La tienda ${site.storeSlug} todavía no tiene tema. Elige uno en Ecomiq.`);
492
+ const { files } = await call(apiUrl, "/v1/site/files?content=true");
493
+ const binaries = listed.files.filter((file) => file.encoding === "base64");
494
+ for (let index = 0; index < binaries.length; index += 8) files.push(...await Promise.all(binaries.slice(index, index + 8).map((file) => call(apiUrl, `/v1/site/file?path=${encodeURIComponent(file.path)}`))));
495
+ const platform = platformFiles(site.storeSlug, site.storeSlug, site.storefrontApiUrl).map((file) => ({
496
+ ...file,
497
+ encoding: "utf8"
498
+ }));
499
+ for (const file of [...files, ...platform]) {
500
+ const destination = resolve(target, file.path);
501
+ if (!destination.startsWith(`${target}${sep}`)) throw new Error(`Ecomiq devolvió una ruta no permitida: ${file.path}`);
502
+ mkdirSync(dirname(destination), { recursive: true });
503
+ if (file.path === "package.json") writeFileSync(destination, installableManifest(file.content));
504
+ else writeFileSync(destination, file.encoding === "base64" ? Buffer.from(file.content, "base64") : file.content);
505
+ }
506
+ console.log(`✓ ${target} · ${files.length + platform.length} archivos`);
507
+ console.log(` Sigue con: cd ${folder ?? site.storeSlug} && npm install && npx ecomiq dev`);
508
+ }
509
+ async function dev(apiUrl, args) {
510
+ project();
511
+ const site = await call(apiUrl, "/v1/site");
512
+ const vars = readVars(".dev.vars");
513
+ vars.set("ECOMIQ_STORE", site.storeSlug);
514
+ vars.set("ECOMIQ_API_URL", site.storefrontApiUrl);
515
+ if (!vars.get("SESSION_SECRET")) vars.set("SESSION_SECRET", randomBytes(32).toString("base64url"));
516
+ writeFileSync(".dev.vars", [...vars].map(([name, value]) => `${name}=${value}`).join("\n") + "\n", { mode: 384 });
517
+ console.log(`✓ Tienda ${site.storeSlug}`);
518
+ await run(binary("react-router"), ["dev", ...args]);
519
+ }
520
+ async function wait(apiUrl, id) {
521
+ const started = Date.now();
522
+ while (Date.now() - started < 9e5) {
523
+ const deployment = await call(apiUrl, `/v1/site/deployments/${id}`);
524
+ if (deployment.status === "ready" || deployment.status === "failed") return deployment;
525
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
526
+ }
527
+ throw new Error("El despliegue tardó demasiado. Revisa su estado en Ecomiq.");
528
+ }
529
+ async function publish(apiUrl, live) {
530
+ project();
531
+ await run(binary("react-router"), ["build"]);
532
+ const form = new FormData();
533
+ form.set("build", new Blob([new Uint8Array(pack("build", buildFiles("build")))]), "build.tar.gz");
534
+ form.set("source", new Blob([new Uint8Array(pack(".", projectFiles(".")))]), "source.tar.gz");
535
+ console.log("Subiendo…");
536
+ const created = await call(apiUrl, "/v1/site/deployments", () => ({
537
+ method: "POST",
538
+ body: form
539
+ }));
540
+ console.log("Desplegando…");
541
+ const deployment = await wait(apiUrl, created.id);
542
+ if (deployment.status === "failed") throw new Error(deployment.error ?? "El despliegue falló.");
543
+ if (!live) {
544
+ console.log(`✓ Vista previa lista: ${deployment.previewUrl}`);
545
+ return;
546
+ }
547
+ const site = await call(apiUrl, "/v1/site");
548
+ await call(apiUrl, `/v1/site/deployments/${deployment.id}/live`, () => ({ method: "POST" }));
549
+ console.log(`✓ En vivo en ${site.storeSlug}`);
550
+ }
551
+ async function packTheme(output) {
552
+ project();
553
+ await run(binary("react-router"), ["build"]);
554
+ const folder = basename(process.cwd());
555
+ const target = resolve(output ?? join("..", `${folder}.zip`));
556
+ const paths = projectFiles(".").filter((path) => resolve(path) !== target);
557
+ writeFileSync(target, zip(".", paths, folder));
558
+ console.log(`✓ ${target} · ${paths.length} archivos`);
559
+ console.log(" Impórtalo en Tienda online → Personalizar tienda → Tema → Cambiar → Importar ZIP.");
560
+ }
561
+ async function main(argv) {
562
+ const [command, ...rest] = argv.filter((arg) => arg !== "--local");
563
+ const apiUrl = adminUrl(argv.includes("--local"));
564
+ if (command === "login") {
565
+ await login(apiUrl, (line) => console.log(line));
566
+ const store = await call(apiUrl, "/v1/stores/current/access");
567
+ const wanted = rest[0];
568
+ if (wanted !== void 0 && wanted !== store.storeSlug && wanted !== store.storeId) {
569
+ await logout(apiUrl);
570
+ throw new Error(`Autorizaste la tienda ${store.storeSlug}, no ${wanted}. Vuelve a iniciar sesión y elige ${wanted} en el navegador.`);
571
+ }
572
+ chooseStore(apiUrl, store.storeId, store.storeSlug);
573
+ console.log(`✓ Sesión iniciada · tienda ${store.storeSlug}`);
574
+ } else if (command === "logout") console.log(await logout(apiUrl) ? "✓ Sesión cerrada" : "No había una sesión abierta.");
575
+ else if (command === "init") await init(apiUrl, rest[0]);
576
+ else if (command === "dev") await dev(apiUrl, rest);
577
+ else if (command === "pack") await packTheme(rest[0]);
578
+ else if (command === "publish") await publish(apiUrl, rest.includes("--live"));
579
+ else if (command === "--version" || command === "-v") console.log(version);
580
+ else {
581
+ console.log(HELP);
582
+ if (command !== void 0 && command !== "help" && command !== "--help" && command !== "-h") process.exitCode = 1;
583
+ }
584
+ }
585
+ main(process.argv.slice(2)).catch((error) => {
586
+ console.error(`✗ ${error instanceof Error ? error.message : String(error)}`);
587
+ process.exitCode = 1;
588
+ });
589
+ export {};
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ export declare function platformFiles(name: string, store: string, apiUrl: string): {
3
+ path: string;
4
+ content: string;
5
+ }[];
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ // src/platform.ts
3
+ const VITE_CONFIG = String.raw`import { publicAssets, catalogLinks, paymentLogos, sectionLoaders, themeUi } from "@ecomiq/react-router/vite";
4
+ import { fileURLToPath } from "node:url";
5
+ import { cloudflare } from "@cloudflare/vite-plugin";
6
+ import { reactRouter } from "@react-router/dev/vite";
7
+ import tailwindcss from "@tailwindcss/vite";
8
+ import { defineConfig } from "vite";
9
+
10
+ export default defineConfig({
11
+ plugins: [publicAssets(), catalogLinks(), sectionLoaders(), paymentLogos(), themeUi(), cloudflare({ viteEnvironment: { name: "ssr" } }), tailwindcss(), reactRouter()],
12
+ resolve: { alias: { "~": fileURLToPath(new URL("./app", import.meta.url)) } },
13
+ build: { rolldownOptions: { output: { codeSplitting: { groups: [{ name: "icons", test: /[\\/]node_modules[\\/](?:@phosphor-icons|@heroicons|@mdi|@remixicon|lucide-react|iconoir-react)[\\/]/ }] } } } },
14
+ });
15
+ `;
16
+ const ROUTER_CONFIG = `import type { Config } from "@react-router/dev/config";
17
+
18
+ export default {
19
+ ssr: true,
20
+ } satisfies Config;
21
+ `;
22
+ const WORKER = `import { createRequestHandler } from "@ecomiq/react-router";
23
+
24
+ export default createRequestHandler(() => import("virtual:react-router/server-build"), import.meta.env.MODE);
25
+ `;
26
+ function wrangler(name, store, apiUrl) {
27
+ return `{
28
+ "$schema": "node_modules/wrangler/config-schema.json",
29
+ "name": ${JSON.stringify(name)},
30
+ "main": "./workers/app.ts",
31
+ "compatibility_date": "2026-07-15",
32
+ "compatibility_flags": ["nodejs_compat"],
33
+ "vars": {
34
+ "ECOMIQ_STORE": ${JSON.stringify(store)},
35
+ "ECOMIQ_API_URL": ${JSON.stringify(apiUrl)}
36
+ }
37
+ }
38
+ `;
39
+ }
40
+ function platformFiles(name, store, apiUrl) {
41
+ return [
42
+ {
43
+ path: "vite.config.ts",
44
+ content: VITE_CONFIG
45
+ },
46
+ {
47
+ path: "react-router.config.ts",
48
+ content: ROUTER_CONFIG
49
+ },
50
+ {
51
+ path: "workers/app.ts",
52
+ content: WORKER
53
+ },
54
+ {
55
+ path: "wrangler.jsonc",
56
+ content: wrangler(name, store, apiUrl)
57
+ }
58
+ ];
59
+ }
60
+ export { platformFiles };
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "ecomiq",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "CLI de Ecomiq para bajar, probar y publicar la tienda online",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Ecomiq",
8
+ "homepage": "https://docs.ecomiq.pe/es/docs/guides/storefront/themes",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/ecomiq/ecomiq-storefront.git",
12
+ "directory": "packages/cli"
13
+ },
14
+ "bin": {
15
+ "ecomiq": "./dist/cli.js"
16
+ },
17
+ "exports": {
18
+ "./platform": {
19
+ "types": "./dist/platform.d.ts",
20
+ "import": "./dist/platform.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "engines": {
28
+ "node": ">=22.22.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^26.1.1",
32
+ "tsdown": "^0.23.0",
33
+ "typescript": "^7.0.2"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public",
37
+ "registry": "https://registry.npmjs.org"
38
+ },
39
+ "scripts": {
40
+ "build": "tsdown",
41
+ "typecheck": "tsc --noEmit -p tsconfig.json"
42
+ }
6
43
  }