mgpanel-cli 1.0.5 → 1.0.8

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/README-API.md ADDED
@@ -0,0 +1,27 @@
1
+ # Contrato CLI ↔ API (publish)
2
+
3
+ Este documento describe el comportamiento esperado entre **mgpanel-cli** y la API de MGPanel para el comando `mgpanel publish` (`POST /api/cli/publish`).
4
+
5
+ ## Páginas que envía el CLI
6
+
7
+ El CLI **solo envía las páginas que existen en el proyecto local**, es decir, las que están definidas en `pages/<nombre>/page.json`. No envía páginas que no existan en el repositorio.
8
+
9
+ ## Páginas por defecto del panel
10
+
11
+ El panel de MGPanel incluye **páginas del sistema** que no forman parte del proyecto del CLI: **Forms**, **Docs**, **Blog**, **Checkout**, **Facturas**, **Presupuestos**, **Soon**. Estas páginas se gestionan desde el panel y no tienen equivalente en la carpeta `pages/` del proyecto.
12
+
13
+ ## Comportamiento esperado de la API
14
+
15
+ La API **debe preservar** las páginas por defecto del panel cuando no vienen en el payload del CLI. En concreto:
16
+
17
+ - **No** debe reemplazar el array `pages` del draft por el array enviado por el CLI.
18
+ - **Sí** debe hacer **merge**: aplicar o actualizar cada página que **sí** viene en el payload y **conservar** las páginas del sistema (Forms, Docs, Blog, Checkout, Facturas, Presupuestos, Soon) que ya existían en el draft y no vienen en el payload.
19
+
20
+ Así, al hacer `mgpanel publish`, las páginas locales se actualizan en el draft y las páginas del sistema se mantienen.
21
+
22
+ ## Resumen
23
+
24
+ | Responsable | Comportamiento |
25
+ |------------|----------------|
26
+ | **CLI** | Envía solo las páginas que existen en `pages/` del proyecto. |
27
+ | **API** | Hace merge de `pages`: actualiza las que vienen en el payload y preserva las páginas del sistema que no vienen. |
package/README.md CHANGED
@@ -47,10 +47,23 @@ Crear un paginas:
47
47
  mgpanel make page nombre-pagina
48
48
  ```
49
49
 
50
- Crear un secciones y asignarlas a páginas:
50
+ Crear secciones (módulos) y asignarlas a páginas:
51
51
  ```bash
52
52
  mgpanel make module nombre-modulo
53
53
  mgpanel add module nombre-modulo --to nombre-pagina
54
+ ```
55
+ Puedes crear diferentes secciones (módulos) y asignarlas a tu preferencia a cualquier página.
56
+
57
+ Descargar el sitio desde MGPanel (draft) al directorio actual:
58
+ ```bash
59
+ mgpanel pull --account <cuenta> [--token <token>] [--api-url <url>]
60
+ ```
61
+ Si el directorio no tiene estructura MGPanel (`pages/`, `modules/`), se crea automáticamente y se rellena con los datos del servidor. Luego puedes editar localmente y publicar con `mgpanel publish`.
62
+ *Requisito backend:* la API debe exponer `GET /api/cli/draft` con la misma autenticación que `publish` (Bearer token, header `X-Account-Nick`).
54
63
 
64
+ Publicar el proyecto local a MGPanel:
65
+ ```bash
66
+ mgpanel publish --account <cuenta> [--token <token>] [--api-url <url>]
55
67
  ```
56
- Puedes crear diferentes secciones (módulos) y asginarlas a tu preferencia a cualquier página.
68
+
69
+ **Flujo típico:** `mgpanel pull` → editar en local (`mgpanel dev` para previsualizar) → `mgpanel publish`.
package/bin/mgpanel.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const http = require("http");
4
+ const https = require("https");
4
5
  const fs = require("fs");
5
6
  const path = require("path");
7
+ const { URL } = require("url");
6
8
 
7
9
  function printHelp() {
8
10
  console.log(`
@@ -18,6 +20,9 @@ Uso:
18
20
 
19
21
  mgpanel dev [puerto]
20
22
 
23
+ mgpanel publish --account <cuenta> [--token <token>] [--api-url <url>]
24
+ mgpanel pull --account <cuenta> [--token <token>] [--api-url <url>]
25
+
21
26
  Ejemplos:
22
27
  mgpanel init miweb
23
28
  cd miweb
@@ -28,6 +33,9 @@ Ejemplos:
28
33
 
29
34
  mgpanel dev
30
35
  mgpanel dev 8080
36
+
37
+ mgpanel publish --account zzz-eloymanuel --token mi-token
38
+ mgpanel pull --account zzz-eloymanuel
31
39
  `);
32
40
  }
33
41
 
@@ -54,6 +62,164 @@ function isValidSlug(name) {
54
62
  return /^[a-z0-9-]+$/i.test(name);
55
63
  }
56
64
 
65
+ /**
66
+ * Carga .env desde process.cwd() y parsea args para account, token, apiUrl.
67
+ * Usado por cmdPublish y cmdPull.
68
+ * @param {string[]} args - process.argv.slice(2)
69
+ * @returns {{ account: string|null, token: string|null, apiUrl: string }}
70
+ */
71
+ function parseCliAuthArgs(args) {
72
+ let account = null;
73
+ let token = null;
74
+ let apiUrl = "https://dev.mgpanel.co";
75
+
76
+ for (let i = 0; i < args.length; i++) {
77
+ if (args[i] === "--account" && args[i + 1]) {
78
+ account = args[i + 1];
79
+ i++;
80
+ } else if (args[i] === "--token" && args[i + 1]) {
81
+ token = args[i + 1];
82
+ i++;
83
+ } else if (args[i] === "--api-url" && args[i + 1]) {
84
+ apiUrl = args[i + 1];
85
+ i++;
86
+ }
87
+ }
88
+
89
+ const envPath = path.join(process.cwd(), ".env");
90
+ if (fs.existsSync(envPath)) {
91
+ const envContent = fs.readFileSync(envPath, "utf8");
92
+ const envLines = envContent.split("\n");
93
+ for (const line of envLines) {
94
+ const trimmed = line.trim();
95
+ if (trimmed && !trimmed.startsWith("#")) {
96
+ const match = trimmed.match(/^([^=]+)=(.*)$/);
97
+ if (match) {
98
+ const key = match[1].trim();
99
+ const value = match[2].trim().replace(/^["']|["']$/g, "");
100
+ if (key === "MGPANEL_TOKEN") process.env.MGPANEL_TOKEN = value;
101
+ else if (key === "MGPANEL_API_URL") process.env.MGPANEL_API_URL = value;
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ if (!token) token = process.env.MGPANEL_TOKEN;
108
+ if (process.env.MGPANEL_API_URL) apiUrl = process.env.MGPANEL_API_URL;
109
+
110
+ return { account, token, apiUrl };
111
+ }
112
+
113
+ /**
114
+ * Crea la estructura mínima de proyecto MGPanel en projectRoot (pages/, modules/, assets/, dev/, index.html, .env).
115
+ * No crea páginas por defecto. Usado por mgpanel pull cuando el directorio no tiene estructura.
116
+ * @param {string} projectRoot - Ruta absoluta del directorio del proyecto (ej. process.cwd())
117
+ */
118
+ function ensureProjectStructure(projectRoot) {
119
+ ensureDir(path.join(projectRoot, "pages"));
120
+ ensureDir(path.join(projectRoot, "modules"));
121
+ ensureDir(path.join(projectRoot, "dev"));
122
+ ensureDir(path.join(projectRoot, "assets", "css"));
123
+ ensureDir(path.join(projectRoot, "assets", "js"));
124
+
125
+ const defaultCSS = "/* MGPanel Global Styles */\n:root { color-scheme: light; }\nbody { margin: 0; }\n";
126
+ const defaultJS = "// MGPanel Global JS\nconsole.log(\"[MGPanel] Global JS loaded\");\n";
127
+ fs.writeFileSync(path.join(projectRoot, "assets", "css", "style.css"), defaultCSS, "utf8");
128
+ fs.writeFileSync(path.join(projectRoot, "assets", "js", "app.js"), defaultJS, "utf8");
129
+
130
+ const indexHTML = `<!doctype html>
131
+ <html lang="es">
132
+ <head>
133
+ <meta charset="utf-8" />
134
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
135
+ <title>MGPanel (Local Preview)</title>
136
+
137
+ <link rel="stylesheet" href="assets/css/style.css" />
138
+ <script src="assets/js/app.js" defer></script>
139
+ </head>
140
+ <body>
141
+ <div id="app"></div>
142
+ <script src="dev/preview.js"></script>
143
+ </body>
144
+ </html>
145
+ `;
146
+ fs.writeFileSync(path.join(projectRoot, "index.html"), indexHTML, "utf8");
147
+
148
+ const envContent = `# MGPanel CLI - Variables de entorno
149
+ # Descomenta y configura estas variables para usar 'mgpanel publish' y 'mgpanel pull'
150
+
151
+ # MGPANEL_TOKEN=tu-token-aqui
152
+ # MGPANEL_API_URL=https://dev.mgpanel.co
153
+ `;
154
+ fs.writeFileSync(path.join(projectRoot, ".env"), envContent, "utf8");
155
+
156
+ const gitignorePath = path.join(projectRoot, ".gitignore");
157
+ if (fs.existsSync(gitignorePath)) {
158
+ const gitignore = fs.readFileSync(gitignorePath, "utf8");
159
+ if (!gitignore.includes(".env")) {
160
+ fs.appendFileSync(gitignorePath, "\n# MGPanel CLI\n.env\n");
161
+ }
162
+ } else {
163
+ fs.writeFileSync(gitignorePath, "# MGPanel CLI\n.env\n", "utf8");
164
+ }
165
+
166
+ const previewJS = `async function loadModule(name) {
167
+ const base = \`modules/\${name}/\${name}\`;
168
+ const html = await fetch(\`\${base}.html\`).then(r => r.text());
169
+ const cssHref = \`\${base}.css\`;
170
+ if (!document.querySelector(\`link[data-mgpanel-css="\${cssHref}"]\`)) {
171
+ const link = document.createElement("link");
172
+ link.rel = "stylesheet";
173
+ link.href = cssHref;
174
+ link.setAttribute("data-mgpanel-css", cssHref);
175
+ document.head.appendChild(link);
176
+ }
177
+ const jsSrc = \`\${base}.js\`;
178
+ if (!document.querySelector(\`script[data-mgpanel-js="\${jsSrc}"]\`)) {
179
+ const script = document.createElement("script");
180
+ script.src = jsSrc;
181
+ script.defer = true;
182
+ script.setAttribute("data-mgpanel-js", jsSrc);
183
+ document.body.appendChild(script);
184
+ }
185
+ return html;
186
+ }
187
+ async function renderPage(pageName) {
188
+ const configPath = \`pages/\${pageName}/page.json\`;
189
+ const page = await fetch(configPath).then(r => r.json());
190
+ if (page.title) document.title = page.title;
191
+ if (page.description) {
192
+ let meta = document.querySelector('meta[name="description"]');
193
+ if (!meta) {
194
+ meta = document.createElement("meta");
195
+ meta.setAttribute("name", "description");
196
+ document.head.appendChild(meta);
197
+ }
198
+ meta.setAttribute("content", page.description);
199
+ }
200
+ const app = document.getElementById("app");
201
+ app.innerHTML = "";
202
+ for (const item of (page.modules || [])) {
203
+ const html = await loadModule(item.name);
204
+ const wrapper = document.createElement("div");
205
+ wrapper.setAttribute("data-mgpanel-module", item.name);
206
+ wrapper.innerHTML = html;
207
+ app.appendChild(wrapper);
208
+ }
209
+ }
210
+ const pageFromPath = () => {
211
+ const p = window.location.pathname.replace(/^\\/+/g, "").replace(/\\/+$/g, "");
212
+ return p === "" ? "home" : p;
213
+ };
214
+ renderPage(pageFromPath()).catch(err => {
215
+ console.error("[MGPanel Preview] Error:", err);
216
+ document.getElementById("app").innerHTML =
217
+ "<h2>Error cargando la página. ¿Existe pages/&lt;page&gt;/page.json?</h2>";
218
+ });
219
+ `;
220
+ fs.writeFileSync(path.join(projectRoot, "dev", "preview.js"), previewJS, "utf8");
221
+ }
222
+
57
223
  /**
58
224
  * mgpanel init <projectName>
59
225
  */
@@ -106,6 +272,28 @@ console.log("[MGPanel] Global JS loaded");
106
272
  `;
107
273
  fs.writeFileSync(path.join(projectPath, "index.html"), indexHTML, "utf8");
108
274
 
275
+ // .env: Variables de entorno para publish
276
+ const envContent = `# MGPanel CLI - Variables de entorno
277
+ # Descomenta y configura estas variables para usar 'mgpanel publish'
278
+
279
+ # Token de autenticación para publicar (obtén uno desde el panel de MGPanel)
280
+ # MGPANEL_TOKEN=tu-token-aqui
281
+
282
+ # URL de la API (opcional, por defecto usa https://dev.mgpanel.co)
283
+ # MGPANEL_API_URL=https://dev.mgpanel.co
284
+ `;
285
+ fs.writeFileSync(path.join(projectPath, ".env"), envContent, "utf8");
286
+ // Agregar .env al .gitignore si existe
287
+ const gitignorePath = path.join(projectPath, ".gitignore");
288
+ if (fs.existsSync(gitignorePath)) {
289
+ const gitignore = fs.readFileSync(gitignorePath, "utf8");
290
+ if (!gitignore.includes(".env")) {
291
+ fs.appendFileSync(gitignorePath, "\n# MGPanel CLI\n.env\n");
292
+ }
293
+ } else {
294
+ fs.writeFileSync(gitignorePath, "# MGPanel CLI\n.env\n", "utf8");
295
+ }
296
+
109
297
  // dev/preview.js: carga una page leyendo page.json y componiendo módulos
110
298
  const previewJS = `async function loadModule(name) {
111
299
  const base = \`modules/\${name}/\${name}\`;
@@ -185,6 +373,7 @@ renderPage(pageFromPath()).catch(err => {
185
373
  route: "/",
186
374
  title: "Home",
187
375
  description: "Página principal creada con MGPanel.",
376
+ directory: "",
188
377
  modules: []
189
378
  };
190
379
  fs.writeFileSync(
@@ -248,10 +437,23 @@ Esto crea:
248
437
  "route": "/about",
249
438
  "title": "About",
250
439
  "description": "Descripción de la página about.",
251
- "modules": []
440
+ "directory": "",
441
+ "modules": [
442
+ { "name": "header", "import": false }
443
+ ]
252
444
  }
253
445
  \`\`\`
254
446
 
447
+ **Campo \`directory\`:**
448
+ - Por defecto viene vacío (\`""\`)
449
+ - Puede editarse manualmente para agregar valores como \`"app/"\` cuando se requiera
450
+ - Se usa en la nube para organizar las páginas en directorios
451
+
452
+ **Campo \`import\` en módulos:**
453
+ - \`import: false\` - Módulo propio de esta página (se crea la sección completa)
454
+ - \`import: true\` - Módulo importado de otra página (se referencia la sección existente)
455
+ - Se asigna automáticamente al agregar módulos con \`mgpanel add module\`
456
+
255
457
  ### Crear un Módulo
256
458
 
257
459
  \`\`\`bash
@@ -290,15 +492,20 @@ Esto actualiza el archivo \`pages/<nombre-pagina>/page.json\` añadiendo el mód
290
492
  "route": "/",
291
493
  "title": "Home",
292
494
  "description": "Página principal.",
495
+ "directory": "",
293
496
  "modules": [
294
- { "name": "header" },
295
- { "name": "hero-banner" },
296
- { "name": "footer" }
497
+ { "name": "header", "import": false },
498
+ { "name": "hero-banner", "import": false },
499
+ { "name": "footer", "import": true }
297
500
  ]
298
501
  }
299
502
  \`\`\`
300
503
 
301
- **Importante:** Los módulos se renderizan en el orden en que aparecen en el array \`modules\`.
504
+ **Importante:**
505
+ - Los módulos se renderizan en el orden en que aparecen en el array \`modules\`.
506
+ - El campo \`import\` se agrega automáticamente:
507
+ - \`import: false\` si el módulo es propio de esta página (primera vez que se agrega)
508
+ - \`import: true\` si el módulo pertenece a otra página y se está reutilizando
302
509
 
303
510
  ### Servidor de Desarrollo
304
511
 
@@ -425,6 +632,26 @@ Por eso, en los módulos con JS, sigue estas reglas para que funcionen *tanto lo
425
632
  •⁠ ⁠*No ocultar contenido sin JS*:
426
633
  - Si usas animaciones por clase (ej. ⁠ \`data-mg-animate\` ⁠), asegúrate que el contenido no quede invisible si el JS no corre.
427
634
 
635
+ ### Campo \`import\` en módulos
636
+
637
+ Cada módulo en \`page.json\` puede tener un campo \`import\` que indica si el módulo es propio de la página o se está importando de otra:
638
+
639
+ - **\`import: false\`** - Módulo propio: La primera página donde se agrega un módulo tiene \`import: false\`. En la nube, esto crea una nueva sección completa.
640
+ - **\`import: true\`** - Módulo importado: Si un módulo ya existe en otra página y lo agregas a una nueva página, tendrá \`import: true\`. En la nube, esto referencia la sección existente en lugar de duplicarla.
641
+
642
+ **Ejemplo:**
643
+ \`\`\`json
644
+ {
645
+ "modules": [
646
+ { "name": "header", "import": false }, // Propio de esta página
647
+ { "name": "banner", "import": false }, // Propio de esta página
648
+ { "name": "footer", "import": true } // Importado de otra página
649
+ ]
650
+ }
651
+ \`\`\`
652
+
653
+ El campo \`import\` se asigna automáticamente cuando usas \`mgpanel add module\`. La primera página donde agregas un módulo será la "dueña" (\`import: false\`), y las páginas subsecuentes que usen ese módulo tendrán \`import: true\`.
654
+
428
655
  #### Plantilla recomendada para JS de módulos (cloud-safe)
429
656
 
430
657
  \`\`\`js
@@ -590,6 +817,7 @@ function cmdMakePage(pageName) {
590
817
  route: `/${pageName === "home" ? "" : pageName}`.replace(/\/$/, "/"),
591
818
  title: pageName.charAt(0).toUpperCase() + pageName.slice(1),
592
819
  description: `Descripción de la página ${pageName}.`,
820
+ directory: "",
593
821
  modules: []
594
822
  },
595
823
  null,
@@ -705,6 +933,35 @@ function cmdMakeModule(moduleName) {
705
933
  if (skipped.length) console.log("↩️ Ya existían:", skipped.join(", "));
706
934
  }
707
935
 
936
+ /**
937
+ * Verifica si un módulo existe en otras páginas (diferentes a la actual)
938
+ */
939
+ function moduleExistsInOtherPages(moduleName, currentPageName) {
940
+ const pagesDir = path.join(process.cwd(), "pages");
941
+ if (!fs.existsSync(pagesDir)) return false;
942
+
943
+ const pageDirs = fs.readdirSync(pagesDir, { withFileTypes: true })
944
+ .filter(dirent => dirent.isDirectory() && dirent.name !== currentPageName)
945
+ .map(dirent => dirent.name);
946
+
947
+ for (const pageName of pageDirs) {
948
+ const pageJsonPath = path.join(pagesDir, pageName, "page.json");
949
+ if (fs.existsSync(pageJsonPath)) {
950
+ try {
951
+ const page = readJSON(pageJsonPath);
952
+ if (Array.isArray(page.modules)) {
953
+ const exists = page.modules.some(m => m && m.name === moduleName);
954
+ if (exists) return true;
955
+ }
956
+ } catch (e) {
957
+ // Si hay error leyendo el archivo, continuar con la siguiente página
958
+ continue;
959
+ }
960
+ }
961
+ }
962
+ return false;
963
+ }
964
+
708
965
  /**
709
966
  * mgpanel add module <moduleName> --to <pageName>
710
967
  */
@@ -738,10 +995,530 @@ function cmdAddModule(moduleName, pageName) {
738
995
  return;
739
996
  }
740
997
 
741
- page.modules.push({ name: moduleName });
998
+ // Determinar si el módulo es importado (existe en otras páginas)
999
+ const isImported = moduleExistsInOtherPages(moduleName, pageName);
1000
+ page.modules.push({
1001
+ name: moduleName,
1002
+ import: isImported
1003
+ });
742
1004
  writeJSON(pageJsonPath, page);
743
1005
 
744
- console.log(`✅ Módulo "${moduleName}" agregado a pages/${pageName}/page.json`);
1006
+ const importStatus = isImported ? "importado" : "propio";
1007
+ console.log(`✅ Módulo "${moduleName}" agregado a pages/${pageName}/page.json (${importStatus})`);
1008
+ }
1009
+
1010
+ /**
1011
+ * mgpanel publish --account <cuenta> [--token <token>] [--api-url <url>]
1012
+ */
1013
+ async function cmdPublish(args) {
1014
+ const { account, token, apiUrl } = parseCliAuthArgs(args);
1015
+
1016
+ if (!account) {
1017
+ console.log("❌ Falta el parámetro --account");
1018
+ console.log("👉 Usa: mgpanel publish --account <nombre-cuenta> [--token <token>] [--api-url <url>]");
1019
+ process.exit(1);
1020
+ }
1021
+
1022
+ if (!token) {
1023
+ console.log("❌ Falta el token de autenticación");
1024
+ console.log("👉 Usa: mgpanel publish --account <cuenta> --token <token>");
1025
+ console.log(" O configura la variable de entorno: MGPANEL_TOKEN");
1026
+ console.log(" O agrega MGPANEL_TOKEN=tu-token en el archivo .env");
1027
+ process.exit(1);
1028
+ }
1029
+
1030
+ // Mostrar información del token (solo primeros y últimos caracteres por seguridad)
1031
+ const tokenPreview = token.length > 10
1032
+ ? `${token.substring(0, 6)}...${token.substring(token.length - 4)}`
1033
+ : "***";
1034
+ console.log(`🔑 Token: ${tokenPreview}`);
1035
+
1036
+ const projectRoot = process.cwd();
1037
+ const pagesDir = path.join(projectRoot, "pages");
1038
+ const modulesDir = path.join(projectRoot, "modules");
1039
+ const assetsDir = path.join(projectRoot, "assets");
1040
+
1041
+ // Validar que estamos en un proyecto MGPanel
1042
+ if (!fs.existsSync(pagesDir) || !fs.existsSync(modulesDir)) {
1043
+ console.log("❌ No se encontró un proyecto MGPanel válido");
1044
+ console.log("👉 Asegúrate de estar en un directorio con carpetas 'pages' y 'modules'");
1045
+ console.log(" O ejecuta: mgpanel init <nombre-proyecto>");
1046
+ process.exit(1);
1047
+ }
1048
+
1049
+ console.log("📦 Leyendo estructura del proyecto...");
1050
+
1051
+ try {
1052
+ // Leer assets globales
1053
+ const globalCSS = fs.existsSync(path.join(assetsDir, "css", "style.css"))
1054
+ ? fs.readFileSync(path.join(assetsDir, "css", "style.css"), "utf8")
1055
+ : "";
1056
+ const globalJS = fs.existsSync(path.join(assetsDir, "js", "app.js"))
1057
+ ? fs.readFileSync(path.join(assetsDir, "js", "app.js"), "utf8")
1058
+ : "";
1059
+
1060
+ // Páginas del sistema que no deben enviarse al publish (el backend las preserva del draft).
1061
+ const PAGINAS_SISTEMA = new Set([
1062
+ "forms", "form", "docs", "blog", "checkout", "facturas", "invoice",
1063
+ "presupuestos", "budget", "soon"
1064
+ ]);
1065
+
1066
+ // Leer todas las páginas
1067
+ const pages = [];
1068
+ const pageDirs = fs.readdirSync(pagesDir, { withFileTypes: true })
1069
+ .filter(dirent => dirent.isDirectory())
1070
+ .map(dirent => dirent.name);
1071
+
1072
+ console.log(`📂 Páginas encontradas en directorio: ${pageDirs.length} (${pageDirs.join(", ")})`);
1073
+
1074
+ for (const pageName of pageDirs) {
1075
+ const pageNameNorm = pageName.toLowerCase().trim();
1076
+ if (PAGINAS_SISTEMA.has(pageNameNorm)) {
1077
+ console.log(`⏭️ Saltando página del sistema "${pageName}" (no se envía en publish)`);
1078
+ continue;
1079
+ }
1080
+ console.log(`📄 Procesando página: ${pageName}`);
1081
+ // Compatibilidad: algunas personas lo renombraron a "pago.json" por error.
1082
+ // Preferimos "page.json", pero aceptamos "pago.json" como fallback.
1083
+ const pageJsonPath = path.join(pagesDir, pageName, "page.json");
1084
+ const pagoJsonPath = path.join(pagesDir, pageName, "pago.json");
1085
+ let pageConfigPath = null;
1086
+
1087
+ if (fs.existsSync(pageJsonPath)) {
1088
+ pageConfigPath = pageJsonPath;
1089
+ } else if (fs.existsSync(pagoJsonPath)) {
1090
+ pageConfigPath = pagoJsonPath;
1091
+ console.log(`⚠️ Aviso: usando "${pageName}/pago.json". Recomendado: renómbralo a "page.json".`);
1092
+ } else {
1093
+ console.log(`⚠️ Saltando página "${pageName}": no se encontró page.json (ni pago.json)`);
1094
+ continue;
1095
+ }
1096
+
1097
+ let pageConfig;
1098
+ try {
1099
+ pageConfig = readJSON(pageConfigPath);
1100
+ } catch (error) {
1101
+ console.log(`❌ Error al leer ${pageConfigPath}: ${error.message}`);
1102
+ console.log(`⚠️ Saltando página "${pageName}"`);
1103
+ continue;
1104
+ }
1105
+
1106
+ const sections = [];
1107
+
1108
+ // Procesar módulos de la página
1109
+ if (Array.isArray(pageConfig.modules)) {
1110
+ for (let i = 0; i < pageConfig.modules.length; i++) {
1111
+ const moduleName = pageConfig.modules[i].name;
1112
+ if (!moduleName) continue;
1113
+
1114
+ const moduleDir = path.join(modulesDir, moduleName);
1115
+ if (!fs.existsSync(moduleDir)) {
1116
+ console.log(`⚠️ Módulo "${moduleName}" no encontrado, saltando...`);
1117
+ continue;
1118
+ }
1119
+
1120
+ const moduleHTML = fs.existsSync(path.join(moduleDir, `${moduleName}.html`))
1121
+ ? fs.readFileSync(path.join(moduleDir, `${moduleName}.html`), "utf8")
1122
+ : "";
1123
+ const moduleCSS = fs.existsSync(path.join(moduleDir, `${moduleName}.css`))
1124
+ ? fs.readFileSync(path.join(moduleDir, `${moduleName}.css`), "utf8")
1125
+ : "";
1126
+ const moduleJS = fs.existsSync(path.join(moduleDir, `${moduleName}.js`))
1127
+ ? fs.readFileSync(path.join(moduleDir, `${moduleName}.js`), "utf8")
1128
+ : "";
1129
+
1130
+ // Usar el valor de `import` tal cual viene en page.json (no recalcular).
1131
+ // La regla "primera página = dueña" solo aplica al agregar con `mgpanel add module`.
1132
+ const moduleEntry = pageConfig.modules[i];
1133
+ const importValue = moduleEntry && typeof moduleEntry.import === "boolean"
1134
+ ? moduleEntry.import
1135
+ : (moduleEntry && String(moduleEntry.import).toLowerCase() === "true");
1136
+
1137
+ sections.push({
1138
+ code: moduleName,
1139
+ name: moduleName,
1140
+ html: {
1141
+ type: "text/html",
1142
+ code: moduleHTML
1143
+ },
1144
+ css: {
1145
+ type: "text/css",
1146
+ code: moduleCSS
1147
+ },
1148
+ javascript: {
1149
+ type: "application/javascript",
1150
+ code: moduleJS
1151
+ },
1152
+ module: [], // Array de referencias a módulos (el backend lo manejará)
1153
+ order: i + 1,
1154
+ status: 1,
1155
+ import: !!importValue
1156
+ });
1157
+ }
1158
+ }
1159
+
1160
+ // Validar que la página tenga al menos un campo requerido
1161
+ if (!pageConfig.title && !pageName) {
1162
+ console.log(`⚠️ Saltando página "${pageName}": falta información básica`);
1163
+ continue;
1164
+ }
1165
+
1166
+ pages.push({
1167
+ // page: ObjectId - El backend debe generar o usar el existente
1168
+ name: pageName,
1169
+ title: pageConfig.title || pageName,
1170
+ description: pageConfig.description || "",
1171
+ // IMPORTANTE: en MGPanel el `directory` por defecto debe ser "".
1172
+ // No usar `|| pageName` porque "" es válido y si cae al fallback rompe el render (termina guardando "home", "about", etc.).
1173
+ directory: (typeof pageConfig.directory === "string") ? pageConfig.directory : "",
1174
+ url: pageConfig.route || `/${pageName === "home" ? "" : pageName}`,
1175
+ sections: sections,
1176
+ status: 1,
1177
+ soon: false,
1178
+ index: pageName === "home"
1179
+ });
1180
+
1181
+ console.log(`✅ Página "${pageName}" procesada (${sections.length} secciones)`);
1182
+ }
1183
+
1184
+ console.log(`📊 Total de páginas procesadas: ${pages.length}`);
1185
+
1186
+ // Seguridad: nunca publicar si no se encontró ninguna página.
1187
+ if (pages.length === 0) {
1188
+ console.log("❌ No se encontró ninguna página para publicar.");
1189
+ console.log("👉 Verifica que existan carpetas en ./pages/<pagina>/ y que el archivo sea page.json (o pago.json).");
1190
+ console.log(`💡 Directorios encontrados: ${pageDirs.length > 0 ? pageDirs.join(", ") : "ninguno"}`);
1191
+ process.exit(1);
1192
+ }
1193
+
1194
+ // Construir payload según modelo MongoDB
1195
+ const payload = {
1196
+ type: "published",
1197
+ css: {
1198
+ type: "text/css",
1199
+ code: globalCSS
1200
+ },
1201
+ javascript: {
1202
+ type: "application/javascript",
1203
+ code: globalJS
1204
+ },
1205
+ pages: pages,
1206
+ modified_date: new Date(),
1207
+ has_changes: true,
1208
+ version_number: 1
1209
+ };
1210
+
1211
+ console.log(`📤 Publicando a cuenta: ${account}`);
1212
+ console.log(`📄 Páginas encontradas: ${pages.length}`);
1213
+ console.log(`🧩 Módulos totales: ${pages.reduce((sum, p) => sum + p.sections.length, 0)}`);
1214
+ console.log(`🌐 URL de la API: ${apiUrl}`);
1215
+
1216
+ // Enviar POST request
1217
+ const url = new URL(`${apiUrl}/api/cli/publish`);
1218
+ const postData = JSON.stringify(payload);
1219
+ const urlOptions = {
1220
+ hostname: url.hostname,
1221
+ port: url.port || (url.protocol === "https:" ? 443 : 80),
1222
+ path: url.pathname + url.search,
1223
+ method: "POST",
1224
+ headers: {
1225
+ "Content-Type": "application/json",
1226
+ "Content-Length": Buffer.byteLength(postData),
1227
+ "Authorization": `Bearer ${token}`,
1228
+ "X-Account-Nick": account
1229
+ },
1230
+ timeout: 60000, // 60 segundos de timeout
1231
+ rejectUnauthorized: true, // Aceptar certificados SSL válidos
1232
+ agent: false // No usar agent pool, crear nueva conexión
1233
+ };
1234
+
1235
+ const client = url.protocol === "https:" ? https : http;
1236
+
1237
+ console.log(`🔄 Enviando petición a ${url.hostname}${url.pathname}...`);
1238
+ console.log(`📤 Tamaño del payload: ${Buffer.byteLength(postData)} bytes`);
1239
+
1240
+ // Usar promesa para asegurar que esperamos la respuesta
1241
+ await new Promise((resolve, reject) => {
1242
+ console.log(`🔧 Creando request HTTP...`);
1243
+ const req = client.request(urlOptions, (res) => {
1244
+ console.log(`✅ Callback del request ejecutado`);
1245
+ let responseData = "";
1246
+
1247
+ console.log(`📡 Respuesta recibida: ${res.statusCode} ${res.statusMessage || ""}`);
1248
+
1249
+ // Manejar errores de respuesta
1250
+ res.on("error", (err) => {
1251
+ console.log(`❌ Error al leer la respuesta: ${err.message}`);
1252
+ reject(err);
1253
+ });
1254
+
1255
+ res.on("data", (chunk) => {
1256
+ responseData += chunk;
1257
+ console.log(`📥 Recibiendo datos... (${responseData.length} bytes acumulados)`);
1258
+ });
1259
+
1260
+ res.on("end", () => {
1261
+ console.log(`✅ Evento 'end' disparado`);
1262
+ console.log(`📦 Tamaño de respuesta: ${responseData.length} bytes`);
1263
+
1264
+ if (res.statusCode >= 200 && res.statusCode < 300) {
1265
+ console.log("✅ Publicación exitosa!");
1266
+ try {
1267
+ const response = JSON.parse(responseData);
1268
+ if (response.message) {
1269
+ console.log(`📝 ${response.message}`);
1270
+ }
1271
+ if (response.data) {
1272
+ console.log(`📊 Versión: ${response.data.version_number || "N/A"}`);
1273
+ }
1274
+ if (response.success === false) {
1275
+ console.log(`⚠️ El servidor reportó: success=false`);
1276
+ console.log(`💬 ${JSON.stringify(response, null, 2)}`);
1277
+ }
1278
+ } catch (e) {
1279
+ // No es JSON, mostrar respuesta tal cual
1280
+ if (responseData) {
1281
+ console.log(`📝 Respuesta (texto): ${responseData.substring(0, 500)}`);
1282
+ } else {
1283
+ console.log(`📝 Respuesta vacía`);
1284
+ }
1285
+ }
1286
+ resolve();
1287
+ } else {
1288
+ console.log(`❌ Error en la publicación (${res.statusCode})`);
1289
+ try {
1290
+ const error = JSON.parse(responseData);
1291
+ console.log(`💬 ${error.message || error.error || "Error desconocido"}`);
1292
+ if (error.details) {
1293
+ console.log(`🔍 Detalles:`, JSON.stringify(error.details, null, 2));
1294
+ }
1295
+ } catch (e) {
1296
+ if (responseData) {
1297
+ console.log(`💬 Respuesta del servidor: ${responseData.substring(0, 500)}`);
1298
+ } else {
1299
+ console.log(`💬 Sin respuesta del servidor`);
1300
+ }
1301
+ }
1302
+ reject(new Error(`HTTP ${res.statusCode}: ${responseData}`));
1303
+ }
1304
+ });
1305
+ });
1306
+
1307
+ req.on("error", (error) => {
1308
+ console.log(`❌ Error de conexión: ${error.message}`);
1309
+ console.log(`💡 Código de error: ${error.code || "N/A"}`);
1310
+ console.log(`💡 Stack: ${error.stack || "N/A"}`);
1311
+ console.log(`💡 Verifica que la URL de la API sea correcta: ${apiUrl}`);
1312
+ if (error.code === "ENOTFOUND") {
1313
+ console.log(`💡 No se pudo resolver el hostname: ${url.hostname}`);
1314
+ } else if (error.code === "ECONNREFUSED") {
1315
+ console.log(`💡 Conexión rechazada. ¿El servidor está corriendo?`);
1316
+ } else if (error.code === "ETIMEDOUT") {
1317
+ console.log(`💡 Timeout de conexión. El servidor no respondió a tiempo.`);
1318
+ } else if (error.code === "CERT_HAS_EXPIRED" || error.code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") {
1319
+ console.log(`💡 Problema con el certificado SSL. Intenta con rejectUnauthorized: false (solo para desarrollo)`);
1320
+ }
1321
+ reject(error);
1322
+ });
1323
+
1324
+ req.setTimeout(60000, () => {
1325
+ console.log(`❌ Timeout: La petición tardó más de 60 segundos sin respuesta`);
1326
+ console.log(`💡 El servidor puede estar sobrecargado o hay un problema de red`);
1327
+ req.destroy();
1328
+ reject(new Error("Timeout"));
1329
+ });
1330
+
1331
+ console.log(`📤 Escribiendo datos en el request...`);
1332
+ req.write(postData);
1333
+ console.log(`✅ Datos escritos, cerrando request...`);
1334
+ req.end();
1335
+
1336
+ console.log(`📨 Petición enviada, esperando respuesta...`);
1337
+
1338
+ // Agregar listener para verificar si el request se está enviando
1339
+ req.on("finish", () => {
1340
+ console.log(`✅ Request finalizado (datos enviados)`);
1341
+ });
1342
+
1343
+ req.on("close", () => {
1344
+ console.log(`🔒 Conexión cerrada`);
1345
+ });
1346
+ }).catch((error) => {
1347
+ console.log(`❌ Error en la promesa: ${error.message}`);
1348
+ throw error;
1349
+ });
1350
+
1351
+ } catch (error) {
1352
+ console.log(`❌ Error al procesar el proyecto: ${error.message}`);
1353
+ if (error.stack) {
1354
+ console.log(error.stack);
1355
+ }
1356
+ process.exit(1);
1357
+ }
1358
+ }
1359
+
1360
+ /**
1361
+ * Deriva un slug seguro para el nombre de carpeta de una página desde url o name/title.
1362
+ */
1363
+ function pageNameFromApiPage(page) {
1364
+ const name = (page.name || "").trim();
1365
+ if (name && isValidSlug(name)) return name;
1366
+ const url = String(page.url ?? page.route ?? "").trim().replace(/^\/+|\/+$/g, "");
1367
+ if (url === "" || url === "/") return "home";
1368
+ const slug = url.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1369
+ return slug || "page";
1370
+ }
1371
+
1372
+ /**
1373
+ * mgpanel pull --account <cuenta> [--token <token>] [--api-url <url>]
1374
+ * Descarga el draft del sitio desde MGPanel al directorio actual.
1375
+ */
1376
+ async function cmdPull(args) {
1377
+ const { account, token, apiUrl } = parseCliAuthArgs(args);
1378
+
1379
+ if (!account) {
1380
+ console.log("❌ Falta el parámetro --account");
1381
+ console.log("👉 Usa: mgpanel pull --account <nombre-cuenta> [--token <token>] [--api-url <url>]");
1382
+ process.exit(1);
1383
+ }
1384
+
1385
+ if (!token) {
1386
+ console.log("❌ Falta el token de autenticación");
1387
+ console.log("👉 Usa: mgpanel pull --account <cuenta> --token <token>");
1388
+ console.log(" O configura MGPANEL_TOKEN en .env o en el entorno");
1389
+ process.exit(1);
1390
+ }
1391
+
1392
+ const tokenPreview = token.length > 10 ? `${token.substring(0, 6)}...${token.substring(token.length - 4)}` : "***";
1393
+ console.log(`🔑 Token: ${tokenPreview}`);
1394
+ console.log(`📥 Descargando draft de la cuenta: ${account}`);
1395
+ console.log(`🌐 API: ${apiUrl}`);
1396
+
1397
+ const projectRoot = process.cwd();
1398
+ const pagesDir = path.join(projectRoot, "pages");
1399
+ const modulesDir = path.join(projectRoot, "modules");
1400
+ const hasStructure = fs.existsSync(pagesDir) && fs.existsSync(modulesDir);
1401
+
1402
+ if (!hasStructure) {
1403
+ console.log("📂 No hay estructura MGPanel; creando páginas, módulos y assets...");
1404
+ ensureProjectStructure(projectRoot);
1405
+ }
1406
+
1407
+ const draftUrl = new URL(`${apiUrl}/api/cli/draft`);
1408
+ const client = draftUrl.protocol === "https:" ? https : http;
1409
+ const urlOptions = {
1410
+ hostname: draftUrl.hostname,
1411
+ port: draftUrl.port || (draftUrl.protocol === "https:" ? 443 : 80),
1412
+ path: draftUrl.pathname + draftUrl.search,
1413
+ method: "GET",
1414
+ headers: {
1415
+ "Authorization": `Bearer ${token}`,
1416
+ "X-Account-Nick": account
1417
+ },
1418
+ timeout: 60000,
1419
+ rejectUnauthorized: true,
1420
+ agent: false
1421
+ };
1422
+
1423
+ const responseBody = await new Promise((resolve, reject) => {
1424
+ const req = client.request(urlOptions, (res) => {
1425
+ let data = "";
1426
+ res.on("data", (chunk) => { data += chunk; });
1427
+ res.on("end", () => {
1428
+ if (res.statusCode >= 200 && res.statusCode < 300) {
1429
+ resolve(data);
1430
+ } else {
1431
+ let msg = `HTTP ${res.statusCode}`;
1432
+ try {
1433
+ const err = JSON.parse(data);
1434
+ msg = err.message || err.error || msg;
1435
+ } catch (_) {
1436
+ if (data) msg = data.substring(0, 300);
1437
+ }
1438
+ reject(new Error(msg));
1439
+ }
1440
+ });
1441
+ });
1442
+ req.on("error", (err) => reject(err));
1443
+ req.setTimeout(60000, () => {
1444
+ req.destroy();
1445
+ reject(new Error("Timeout"));
1446
+ });
1447
+ req.end();
1448
+ });
1449
+
1450
+ let payload;
1451
+ try {
1452
+ payload = JSON.parse(responseBody);
1453
+ } catch (e) {
1454
+ console.log("❌ La API no devolvió JSON válido");
1455
+ process.exit(1);
1456
+ }
1457
+
1458
+ // Aceptar { data: draft } o el draft directamente
1459
+ const draft = payload.data && typeof payload.data === "object" ? payload.data : payload;
1460
+ const pages = Array.isArray(draft.pages) ? draft.pages : [];
1461
+ const globalCSS = (draft.css && typeof draft.css.code === "string") ? draft.css.code : "";
1462
+ const globalJS = (draft.javascript && typeof draft.javascript.code === "string") ? draft.javascript.code : "";
1463
+
1464
+ ensureDir(path.join(projectRoot, "assets", "css"));
1465
+ ensureDir(path.join(projectRoot, "assets", "js"));
1466
+ fs.writeFileSync(path.join(projectRoot, "assets", "css", "style.css"), globalCSS, "utf8");
1467
+ fs.writeFileSync(path.join(projectRoot, "assets", "js", "app.js"), globalJS, "utf8");
1468
+
1469
+ // Módulos: clave por name (legible), fallback a code. Contenido de la sección "dueña" (import === false) o la primera.
1470
+ const moduleContentByKey = new Map();
1471
+ for (const page of pages) {
1472
+ const sections = Array.isArray(page.sections) ? page.sections : [];
1473
+ for (const section of sections) {
1474
+ const key = (section.name || section.code || "").trim();
1475
+ if (!key) continue;
1476
+ const isOwner = section.import === false;
1477
+ if (!moduleContentByKey.has(key) || isOwner) {
1478
+ const html = (section.html && typeof section.html === "object") ? (section.html.code ?? "") : (section.html ?? "");
1479
+ const css = (section.css && typeof section.css === "object") ? (section.css.code ?? "") : (section.css ?? "");
1480
+ const js = (section.javascript && typeof section.javascript === "object") ? (section.javascript.code ?? "") : (section.javascript ?? "");
1481
+ moduleContentByKey.set(key, { html: String(html), css: String(css), js: String(js) });
1482
+ }
1483
+ }
1484
+ }
1485
+
1486
+ const safeModuleName = (name) => String(name || "").replace(/[/\\]/g, "-").trim() || "module";
1487
+ for (const [key, content] of moduleContentByKey) {
1488
+ const dirName = safeModuleName(key);
1489
+ const dir = path.join(modulesDir, dirName);
1490
+ ensureDir(dir);
1491
+ fs.writeFileSync(path.join(dir, `${dirName}.html`), content.html, "utf8");
1492
+ fs.writeFileSync(path.join(dir, `${dirName}.css`), content.css, "utf8");
1493
+ fs.writeFileSync(path.join(dir, `${dirName}.js`), content.js, "utf8");
1494
+ }
1495
+
1496
+ for (const page of pages) {
1497
+ const pageName = pageNameFromApiPage(page);
1498
+ const pageDir = path.join(pagesDir, pageName);
1499
+ ensureDir(pageDir);
1500
+
1501
+ const sections = Array.isArray(page.sections) ? page.sections : [];
1502
+ const route = (() => {
1503
+ const u = String(page.url ?? page.route ?? "").trim();
1504
+ if (u === "" || u === "/") return "/";
1505
+ return u.startsWith("/") ? u : `/${u}`;
1506
+ })();
1507
+ const directory = typeof page.directory === "string" ? page.directory : "";
1508
+ const pageJson = {
1509
+ route,
1510
+ title: page.title || pageName,
1511
+ description: page.description || "",
1512
+ directory,
1513
+ modules: sections.map((s) => ({
1514
+ name: (s.name || s.code || "").trim() || "section",
1515
+ import: s.import === true
1516
+ }))
1517
+ };
1518
+ writeJSON(path.join(pageDir, "page.json"), pageJson);
1519
+ }
1520
+
1521
+ console.log(`✅ Pull completado: ${pages.length} página(s), ${moduleContentByKey.size} módulo(s)`);
745
1522
  }
746
1523
 
747
1524
  function cmdDev(port = 3000) {
@@ -851,6 +1628,34 @@ if (cmd1 === "dev") {
851
1628
  return;
852
1629
  }
853
1630
 
1631
+ if (cmd1 === "publish") {
1632
+ (async () => {
1633
+ try {
1634
+ await cmdPublish(args);
1635
+ process.exit(0);
1636
+ } catch (error) {
1637
+ console.log(`❌ Error: ${error.message}`);
1638
+ if (error.stack) console.log(error.stack);
1639
+ process.exit(1);
1640
+ }
1641
+ })();
1642
+ return;
1643
+ }
1644
+
1645
+ if (cmd1 === "pull") {
1646
+ (async () => {
1647
+ try {
1648
+ await cmdPull(args);
1649
+ process.exit(0);
1650
+ } catch (error) {
1651
+ console.log(`❌ Error: ${error.message}`);
1652
+ if (error.stack) console.log(error.stack);
1653
+ process.exit(1);
1654
+ }
1655
+ })();
1656
+ return;
1657
+ }
1658
+
854
1659
  console.log("❌ Comando no reconocido.");
855
1660
  printHelp();
856
1661
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mgpanel-cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.8",
4
4
  "description": "MGPanel CLI",
5
5
  "bin": {
6
6
  "mgpanel": "./bin/mgpanel.js"