mgpanel-cli 1.0.4 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/mgpanel.js +647 -9
  2. package/package.json +1 -1
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,8 @@ Uso:
18
20
 
19
21
  mgpanel dev [puerto]
20
22
 
23
+ mgpanel publish --account <cuenta> [--token <token>] [--api-url <url>]
24
+
21
25
  Ejemplos:
22
26
  mgpanel init miweb
23
27
  cd miweb
@@ -28,6 +32,9 @@ Ejemplos:
28
32
 
29
33
  mgpanel dev
30
34
  mgpanel dev 8080
35
+
36
+ mgpanel publish --account zzz-eloymanuel --token mi-token
37
+ mgpanel publish --account zzz-eloymanuel --token mi-token --api-url https://api.mgpanel.com
31
38
  `);
32
39
  }
33
40
 
@@ -106,6 +113,28 @@ console.log("[MGPanel] Global JS loaded");
106
113
  `;
107
114
  fs.writeFileSync(path.join(projectPath, "index.html"), indexHTML, "utf8");
108
115
 
116
+ // .env: Variables de entorno para publish
117
+ const envContent = `# MGPanel CLI - Variables de entorno
118
+ # Descomenta y configura estas variables para usar 'mgpanel publish'
119
+
120
+ # Token de autenticación para publicar (obtén uno desde el panel de MGPanel)
121
+ # MGPANEL_TOKEN=tu-token-aqui
122
+
123
+ # URL de la API (opcional, por defecto usa https://dev.mgpanel.co)
124
+ # MGPANEL_API_URL=https://dev.mgpanel.co
125
+ `;
126
+ fs.writeFileSync(path.join(projectPath, ".env"), envContent, "utf8");
127
+ // Agregar .env al .gitignore si existe
128
+ const gitignorePath = path.join(projectPath, ".gitignore");
129
+ if (fs.existsSync(gitignorePath)) {
130
+ const gitignore = fs.readFileSync(gitignorePath, "utf8");
131
+ if (!gitignore.includes(".env")) {
132
+ fs.appendFileSync(gitignorePath, "\n# MGPanel CLI\n.env\n");
133
+ }
134
+ } else {
135
+ fs.writeFileSync(gitignorePath, "# MGPanel CLI\n.env\n", "utf8");
136
+ }
137
+
109
138
  // dev/preview.js: carga una page leyendo page.json y componiendo módulos
110
139
  const previewJS = `async function loadModule(name) {
111
140
  const base = \`modules/\${name}/\${name}\`;
@@ -185,6 +214,7 @@ renderPage(pageFromPath()).catch(err => {
185
214
  route: "/",
186
215
  title: "Home",
187
216
  description: "Página principal creada con MGPanel.",
217
+ directory: "",
188
218
  modules: []
189
219
  };
190
220
  fs.writeFileSync(
@@ -248,10 +278,23 @@ Esto crea:
248
278
  "route": "/about",
249
279
  "title": "About",
250
280
  "description": "Descripción de la página about.",
251
- "modules": []
281
+ "directory": "",
282
+ "modules": [
283
+ { "name": "header", "import": false }
284
+ ]
252
285
  }
253
286
  \`\`\`
254
287
 
288
+ **Campo \`directory\`:**
289
+ - Por defecto viene vacío (\`""\`)
290
+ - Puede editarse manualmente para agregar valores como \`"app/"\` cuando se requiera
291
+ - Se usa en la nube para organizar las páginas en directorios
292
+
293
+ **Campo \`import\` en módulos:**
294
+ - \`import: false\` - Módulo propio de esta página (se crea la sección completa)
295
+ - \`import: true\` - Módulo importado de otra página (se referencia la sección existente)
296
+ - Se asigna automáticamente al agregar módulos con \`mgpanel add module\`
297
+
255
298
  ### Crear un Módulo
256
299
 
257
300
  \`\`\`bash
@@ -290,15 +333,20 @@ Esto actualiza el archivo \`pages/<nombre-pagina>/page.json\` añadiendo el mód
290
333
  "route": "/",
291
334
  "title": "Home",
292
335
  "description": "Página principal.",
336
+ "directory": "",
293
337
  "modules": [
294
- { "name": "header" },
295
- { "name": "hero-banner" },
296
- { "name": "footer" }
338
+ { "name": "header", "import": false },
339
+ { "name": "hero-banner", "import": false },
340
+ { "name": "footer", "import": true }
297
341
  ]
298
342
  }
299
343
  \`\`\`
300
344
 
301
- **Importante:** Los módulos se renderizan en el orden en que aparecen en el array \`modules\`.
345
+ **Importante:**
346
+ - Los módulos se renderizan en el orden en que aparecen en el array \`modules\`.
347
+ - El campo \`import\` se agrega automáticamente:
348
+ - \`import: false\` si el módulo es propio de esta página (primera vez que se agrega)
349
+ - \`import: true\` si el módulo pertenece a otra página y se está reutilizando
302
350
 
303
351
  ### Servidor de Desarrollo
304
352
 
@@ -400,6 +448,96 @@ Cuando despliegues el proyecto en MGPanel:
400
448
  3. El contenido puede ser editado desde el panel de administración
401
449
  4. La estructura de archivos se mantiene compatible
402
450
 
451
+ ### Importante: compatibilidad Local Preview vs MGPanel (Nube)
452
+
453
+ En *preview local* (⁠ dev/preview.js ⁠) cada módulo se inserta envuelto en un contenedor como:
454
+
455
+ \`\`\`html
456
+ <div data-mgpanel-module="header">...contenido del módulo...</div>
457
+ \`\`\`
458
+
459
+ En *MGPanel (nube)* el CMS puede renderizar el HTML del módulo *sin ese wrapper, y además el HTML puede inyectarse **después** de que el archivo JS del módulo ya fue cargado.
460
+
461
+ Por eso, en los módulos con JS, sigue estas reglas para que funcionen *tanto local como en nube*:
462
+
463
+ •⁠ ⁠*Seleccionar root con fallback*:
464
+ - Primero intenta ⁠ \`document.querySelector('[data-mgpanel-module="<modulo>"]')\` ⁠ (preview).
465
+ - Si no existe, busca el nodo real del módulo, por ejemplo ⁠ \`.mg-<modulo>\` ⁠ (nube).
466
+ •⁠ ⁠*Inicialización tardía (cloud-safe)*:
467
+ - No asumas que el HTML existe cuando corre el JS.
468
+ - Implementa retry + ⁠ \`MutationObserver\` ⁠, y escucha ⁠ \`DOMContentLoaded\` ⁠ / ⁠ \`load\` ⁠.
469
+ •⁠ ⁠*Evitar doble init*:
470
+ - Marca el nodo root con ⁠ \`dataset\` ⁠ (por ejemplo ⁠ \`data-mg-init="1"\` ⁠) para no registrar listeners dos veces.
471
+ •⁠ ⁠*Rutas de assets*:
472
+ - En SPAs y en nube, usa rutas absolutas: ⁠ \`src="/assets/Logo.png"\` ⁠ en vez de ⁠ \`assets/Logo.png\` ⁠.
473
+ •⁠ ⁠*No ocultar contenido sin JS*:
474
+ - Si usas animaciones por clase (ej. ⁠ \`data-mg-animate\` ⁠), asegúrate que el contenido no quede invisible si el JS no corre.
475
+
476
+ ### Campo \`import\` en módulos
477
+
478
+ 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:
479
+
480
+ - **\`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.
481
+ - **\`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.
482
+
483
+ **Ejemplo:**
484
+ \`\`\`json
485
+ {
486
+ "modules": [
487
+ { "name": "header", "import": false }, // Propio de esta página
488
+ { "name": "banner", "import": false }, // Propio de esta página
489
+ { "name": "footer", "import": true } // Importado de otra página
490
+ ]
491
+ }
492
+ \`\`\`
493
+
494
+ 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\`.
495
+
496
+ #### Plantilla recomendada para JS de módulos (cloud-safe)
497
+
498
+ \`\`\`js
499
+ (() => {
500
+ const MODULE = "header"; // cambia por el nombre del módulo
501
+
502
+ const findRoot = () => {
503
+ const wrapped = document.querySelector(\`[data-mgpanel-module="\${MODULE}"]\`);
504
+ if (wrapped) return wrapped;
505
+ return document.querySelector(\`.mg-\${MODULE}\`);
506
+ };
507
+
508
+ const init = (rootOrEl) => {
509
+ if (!rootOrEl) return false;
510
+ const el = rootOrEl.classList?.contains(\`mg-\${MODULE}\`)
511
+ ? rootOrEl
512
+ : rootOrEl.querySelector?.(\`.mg-\${MODULE}\`);
513
+ if (!el) return false;
514
+
515
+ if (el.dataset.mgInit === "1") return true;
516
+ el.dataset.mgInit = "1";
517
+
518
+ // ... tu lógica aquí ...
519
+ return true;
520
+ };
521
+
522
+ const tryInit = () => init(findRoot());
523
+ if (tryInit()) return;
524
+
525
+ let tries = 0;
526
+ const t = window.setInterval(() => {
527
+ tries += 1;
528
+ if (tryInit() || tries > 200) window.clearInterval(t); // ~10s
529
+ }, 50);
530
+
531
+ const mo = new MutationObserver(() => {
532
+ if (tryInit()) mo.disconnect();
533
+ });
534
+ mo.observe(document.documentElement, { childList: true, subtree: true });
535
+
536
+ window.addEventListener("DOMContentLoaded", tryInit, { once: true });
537
+ window.addEventListener("load", tryInit, { once: true });
538
+ })();
539
+ \`\`\`
540
+
403
541
  ## Consejos para IDEs
404
542
 
405
543
  - Los archivos \`page.json\` definen la configuración de cada página
@@ -471,6 +609,17 @@ Este archivo contiene información detallada sobre:
471
609
  └── index.html # Preview local
472
610
  \`\`\`
473
611
 
612
+ ## ✅ Nota importante (Local vs Nube)
613
+
614
+ En local, \`dev/preview.js\` envuelve módulos en un contenedor \`data-mgpanel-module="..."\`, pero en MGPanel (nube) ese wrapper puede **no existir** y los módulos pueden inyectarse **después** de que el JS ya cargó.
615
+
616
+ Por eso, al crear nuevos módulos con JavaScript:
617
+ - No asumas que el HTML existe en el momento de ejecución.
618
+ - Usa un patrón "cloud-safe" (retry + MutationObserver) y un selector con fallback (\`[data-mgpanel-module="x"]\` o \`.mg-x\`).
619
+ - Usa rutas absolutas para assets (\`/assets/...\`) para evitar problemas de routing.
620
+
621
+ Para más detalles, consulta la sección de compatibilidad en \`dev/config.md\`.
622
+
474
623
  ## 🔗 Más Información
475
624
 
476
625
  - **Documentación de desarrollo:** Ver \`dev/config.md\`
@@ -509,6 +658,7 @@ function cmdMakePage(pageName) {
509
658
  route: `/${pageName === "home" ? "" : pageName}`.replace(/\/$/, "/"),
510
659
  title: pageName.charAt(0).toUpperCase() + pageName.slice(1),
511
660
  description: `Descripción de la página ${pageName}.`,
661
+ directory: "",
512
662
  modules: []
513
663
  },
514
664
  null,
@@ -546,7 +696,7 @@ function cmdMakeModule(moduleName) {
546
696
  const jsPath = path.join(moduleDir, `${moduleName}.js`);
547
697
 
548
698
  const htmlContent = `<!-- Module: ${moduleName} -->
549
- <section class="${moduleName}">
699
+ <section class="${moduleName} mg-${moduleName}">
550
700
  <h2>${moduleName}</h2>
551
701
  <p>Módulo creado con MGPanel ✅</p>
552
702
  </section>
@@ -559,7 +709,57 @@ function cmdMakeModule(moduleName) {
559
709
  `;
560
710
 
561
711
  const jsContent = `// Module: ${moduleName}
562
- console.log("[MGPanel] Module loaded:", "${moduleName}");
712
+ // Patrón cloud-safe: funciona tanto en preview local como en MGPanel (nube)
713
+
714
+ (() => {
715
+ const MODULE = "${moduleName}";
716
+
717
+ // Busca el root del módulo con fallback (local vs nube)
718
+ const findRoot = () => {
719
+ const wrapped = document.querySelector(\`[data-mgpanel-module="\${MODULE}"]\`);
720
+ if (wrapped) return wrapped;
721
+ return document.querySelector(\`.mg-\${MODULE}\`);
722
+ };
723
+
724
+ // Inicializa el módulo
725
+ const init = (rootOrEl) => {
726
+ if (!rootOrEl) return false;
727
+ const el = rootOrEl.classList?.contains(\`mg-\${MODULE}\`)
728
+ ? rootOrEl
729
+ : rootOrEl.querySelector?.(\`.mg-\${MODULE}\`);
730
+ if (!el) return false;
731
+
732
+ // Evitar doble inicialización
733
+ if (el.dataset.mgInit === "1") return true;
734
+ el.dataset.mgInit = "1";
735
+
736
+ // ... tu lógica aquí ...
737
+ console.log("[MGPanel] Module initialized:", MODULE);
738
+
739
+ return true;
740
+ };
741
+
742
+ // Intenta inicializar inmediatamente
743
+ const tryInit = () => init(findRoot());
744
+ if (tryInit()) return;
745
+
746
+ // Retry con intervalo (hasta ~10 segundos)
747
+ let tries = 0;
748
+ const intervalId = window.setInterval(() => {
749
+ tries += 1;
750
+ if (tryInit() || tries > 200) window.clearInterval(intervalId);
751
+ }, 50);
752
+
753
+ // Observar cambios en el DOM (para inyección tardía)
754
+ const mo = new MutationObserver(() => {
755
+ if (tryInit()) mo.disconnect();
756
+ });
757
+ mo.observe(document.documentElement, { childList: true, subtree: true });
758
+
759
+ // Escuchar eventos de carga
760
+ window.addEventListener("DOMContentLoaded", tryInit, { once: true });
761
+ window.addEventListener("load", tryInit, { once: true });
762
+ })();
563
763
  `;
564
764
 
565
765
  const created = [];
@@ -574,6 +774,35 @@ console.log("[MGPanel] Module loaded:", "${moduleName}");
574
774
  if (skipped.length) console.log("↩️ Ya existían:", skipped.join(", "));
575
775
  }
576
776
 
777
+ /**
778
+ * Verifica si un módulo existe en otras páginas (diferentes a la actual)
779
+ */
780
+ function moduleExistsInOtherPages(moduleName, currentPageName) {
781
+ const pagesDir = path.join(process.cwd(), "pages");
782
+ if (!fs.existsSync(pagesDir)) return false;
783
+
784
+ const pageDirs = fs.readdirSync(pagesDir, { withFileTypes: true })
785
+ .filter(dirent => dirent.isDirectory() && dirent.name !== currentPageName)
786
+ .map(dirent => dirent.name);
787
+
788
+ for (const pageName of pageDirs) {
789
+ const pageJsonPath = path.join(pagesDir, pageName, "page.json");
790
+ if (fs.existsSync(pageJsonPath)) {
791
+ try {
792
+ const page = readJSON(pageJsonPath);
793
+ if (Array.isArray(page.modules)) {
794
+ const exists = page.modules.some(m => m && m.name === moduleName);
795
+ if (exists) return true;
796
+ }
797
+ } catch (e) {
798
+ // Si hay error leyendo el archivo, continuar con la siguiente página
799
+ continue;
800
+ }
801
+ }
802
+ }
803
+ return false;
804
+ }
805
+
577
806
  /**
578
807
  * mgpanel add module <moduleName> --to <pageName>
579
808
  */
@@ -607,10 +836,402 @@ function cmdAddModule(moduleName, pageName) {
607
836
  return;
608
837
  }
609
838
 
610
- page.modules.push({ name: moduleName });
839
+ // Determinar si el módulo es importado (existe en otras páginas)
840
+ const isImported = moduleExistsInOtherPages(moduleName, pageName);
841
+ page.modules.push({
842
+ name: moduleName,
843
+ import: isImported
844
+ });
611
845
  writeJSON(pageJsonPath, page);
612
846
 
613
- console.log(`✅ Módulo "${moduleName}" agregado a pages/${pageName}/page.json`);
847
+ const importStatus = isImported ? "importado" : "propio";
848
+ console.log(`✅ Módulo "${moduleName}" agregado a pages/${pageName}/page.json (${importStatus})`);
849
+ }
850
+
851
+ /**
852
+ * mgpanel publish --account <cuenta> [--token <token>] [--api-url <url>]
853
+ */
854
+ async function cmdPublish(args) {
855
+ // Parsear argumentos
856
+ let account = null;
857
+ let token = null;
858
+ let apiUrl = "https://dev.mgpanel.co"; // default
859
+
860
+ for (let i = 0; i < args.length; i++) {
861
+ if (args[i] === "--account" && args[i + 1]) {
862
+ account = args[i + 1];
863
+ i++;
864
+ } else if (args[i] === "--token" && args[i + 1]) {
865
+ token = args[i + 1];
866
+ i++;
867
+ } else if (args[i] === "--api-url" && args[i + 1]) {
868
+ apiUrl = args[i + 1];
869
+ i++;
870
+ }
871
+ }
872
+
873
+ // Cargar variables de entorno desde .env si existe
874
+ const envPath = path.join(process.cwd(), ".env");
875
+ if (fs.existsSync(envPath)) {
876
+ const envContent = fs.readFileSync(envPath, "utf8");
877
+ const envLines = envContent.split("\n");
878
+ for (const line of envLines) {
879
+ const trimmed = line.trim();
880
+ // Ignorar comentarios y líneas vacías
881
+ if (trimmed && !trimmed.startsWith("#")) {
882
+ const match = trimmed.match(/^([^=]+)=(.*)$/);
883
+ if (match) {
884
+ const key = match[1].trim();
885
+ const value = match[2].trim().replace(/^["']|["']$/g, ""); // Remover comillas
886
+ if (key === "MGPANEL_TOKEN") {
887
+ process.env.MGPANEL_TOKEN = value;
888
+ } else if (key === "MGPANEL_API_URL") {
889
+ process.env.MGPANEL_API_URL = value;
890
+ }
891
+ }
892
+ }
893
+ }
894
+ }
895
+
896
+ // Token puede venir de variable de entorno
897
+ if (!token) {
898
+ token = process.env.MGPANEL_TOKEN;
899
+ }
900
+
901
+ // API URL puede venir de variable de entorno (tiene prioridad sobre default)
902
+ if (process.env.MGPANEL_API_URL) {
903
+ apiUrl = process.env.MGPANEL_API_URL;
904
+ }
905
+
906
+ // Validaciones
907
+ if (!account) {
908
+ console.log("❌ Falta el parámetro --account");
909
+ console.log("👉 Usa: mgpanel publish --account <nombre-cuenta> [--token <token>] [--api-url <url>]");
910
+ process.exit(1);
911
+ }
912
+
913
+ if (!token) {
914
+ console.log("❌ Falta el token de autenticación");
915
+ console.log("👉 Usa: mgpanel publish --account <cuenta> --token <token>");
916
+ console.log(" O configura la variable de entorno: MGPANEL_TOKEN");
917
+ console.log(" O agrega MGPANEL_TOKEN=tu-token en el archivo .env");
918
+ process.exit(1);
919
+ }
920
+
921
+ // Mostrar información del token (solo primeros y últimos caracteres por seguridad)
922
+ const tokenPreview = token.length > 10
923
+ ? `${token.substring(0, 6)}...${token.substring(token.length - 4)}`
924
+ : "***";
925
+ console.log(`🔑 Token: ${tokenPreview}`);
926
+
927
+ const projectRoot = process.cwd();
928
+ const pagesDir = path.join(projectRoot, "pages");
929
+ const modulesDir = path.join(projectRoot, "modules");
930
+ const assetsDir = path.join(projectRoot, "assets");
931
+
932
+ // Validar que estamos en un proyecto MGPanel
933
+ if (!fs.existsSync(pagesDir) || !fs.existsSync(modulesDir)) {
934
+ console.log("❌ No se encontró un proyecto MGPanel válido");
935
+ console.log("👉 Asegúrate de estar en un directorio con carpetas 'pages' y 'modules'");
936
+ console.log(" O ejecuta: mgpanel init <nombre-proyecto>");
937
+ process.exit(1);
938
+ }
939
+
940
+ console.log("📦 Leyendo estructura del proyecto...");
941
+
942
+ try {
943
+ // Leer assets globales
944
+ const globalCSS = fs.existsSync(path.join(assetsDir, "css", "style.css"))
945
+ ? fs.readFileSync(path.join(assetsDir, "css", "style.css"), "utf8")
946
+ : "";
947
+ const globalJS = fs.existsSync(path.join(assetsDir, "js", "app.js"))
948
+ ? fs.readFileSync(path.join(assetsDir, "js", "app.js"), "utf8")
949
+ : "";
950
+
951
+ // Leer todas las páginas
952
+ const pages = [];
953
+ const pageDirs = fs.readdirSync(pagesDir, { withFileTypes: true })
954
+ .filter(dirent => dirent.isDirectory())
955
+ .map(dirent => dirent.name);
956
+
957
+ console.log(`📂 Páginas encontradas en directorio: ${pageDirs.length} (${pageDirs.join(", ")})`);
958
+
959
+ for (const pageName of pageDirs) {
960
+ console.log(`📄 Procesando página: ${pageName}`);
961
+ // Compatibilidad: algunas personas lo renombraron a "pago.json" por error.
962
+ // Preferimos "page.json", pero aceptamos "pago.json" como fallback.
963
+ const pageJsonPath = path.join(pagesDir, pageName, "page.json");
964
+ const pagoJsonPath = path.join(pagesDir, pageName, "pago.json");
965
+ let pageConfigPath = null;
966
+
967
+ if (fs.existsSync(pageJsonPath)) {
968
+ pageConfigPath = pageJsonPath;
969
+ } else if (fs.existsSync(pagoJsonPath)) {
970
+ pageConfigPath = pagoJsonPath;
971
+ console.log(`⚠️ Aviso: usando "${pageName}/pago.json". Recomendado: renómbralo a "page.json".`);
972
+ } else {
973
+ console.log(`⚠️ Saltando página "${pageName}": no se encontró page.json (ni pago.json)`);
974
+ continue;
975
+ }
976
+
977
+ let pageConfig;
978
+ try {
979
+ pageConfig = readJSON(pageConfigPath);
980
+ } catch (error) {
981
+ console.log(`❌ Error al leer ${pageConfigPath}: ${error.message}`);
982
+ console.log(`⚠️ Saltando página "${pageName}"`);
983
+ continue;
984
+ }
985
+
986
+ const sections = [];
987
+
988
+ // Procesar módulos de la página
989
+ if (Array.isArray(pageConfig.modules)) {
990
+ for (let i = 0; i < pageConfig.modules.length; i++) {
991
+ const moduleName = pageConfig.modules[i].name;
992
+ if (!moduleName) continue;
993
+
994
+ const moduleDir = path.join(modulesDir, moduleName);
995
+ if (!fs.existsSync(moduleDir)) {
996
+ console.log(`⚠️ Módulo "${moduleName}" no encontrado, saltando...`);
997
+ continue;
998
+ }
999
+
1000
+ const moduleHTML = fs.existsSync(path.join(moduleDir, `${moduleName}.html`))
1001
+ ? fs.readFileSync(path.join(moduleDir, `${moduleName}.html`), "utf8")
1002
+ : "";
1003
+ const moduleCSS = fs.existsSync(path.join(moduleDir, `${moduleName}.css`))
1004
+ ? fs.readFileSync(path.join(moduleDir, `${moduleName}.css`), "utf8")
1005
+ : "";
1006
+ const moduleJS = fs.existsSync(path.join(moduleDir, `${moduleName}.js`))
1007
+ ? fs.readFileSync(path.join(moduleDir, `${moduleName}.js`), "utf8")
1008
+ : "";
1009
+
1010
+ // Leer el campo import del page.json (por defecto false si no existe)
1011
+ const moduleConfig = pageConfig.modules[i];
1012
+ const isImported = moduleConfig.import === true || moduleConfig.import === "true";
1013
+
1014
+ sections.push({
1015
+ code: moduleName,
1016
+ name: moduleName,
1017
+ html: {
1018
+ type: "text/html",
1019
+ code: moduleHTML
1020
+ },
1021
+ css: {
1022
+ type: "text/css",
1023
+ code: moduleCSS
1024
+ },
1025
+ javascript: {
1026
+ type: "application/javascript",
1027
+ code: moduleJS
1028
+ },
1029
+ module: [], // Array de referencias a módulos (el backend lo manejará)
1030
+ order: i + 1,
1031
+ status: 1,
1032
+ import: isImported
1033
+ });
1034
+ }
1035
+ }
1036
+
1037
+ // Validar que la página tenga al menos un campo requerido
1038
+ if (!pageConfig.title && !pageName) {
1039
+ console.log(`⚠️ Saltando página "${pageName}": falta información básica`);
1040
+ continue;
1041
+ }
1042
+
1043
+ pages.push({
1044
+ // page: ObjectId - El backend debe generar o usar el existente
1045
+ name: pageName,
1046
+ title: pageConfig.title || pageName,
1047
+ description: pageConfig.description || "",
1048
+ // IMPORTANTE: en MGPanel el `directory` por defecto debe ser "".
1049
+ // No usar `|| pageName` porque "" es válido y si cae al fallback rompe el render (termina guardando "home", "about", etc.).
1050
+ directory: (typeof pageConfig.directory === "string") ? pageConfig.directory : "",
1051
+ url: pageConfig.route || `/${pageName === "home" ? "" : pageName}`,
1052
+ sections: sections,
1053
+ status: 1,
1054
+ soon: false,
1055
+ index: pageName === "home"
1056
+ });
1057
+
1058
+ console.log(`✅ Página "${pageName}" procesada (${sections.length} secciones)`);
1059
+ }
1060
+
1061
+ console.log(`📊 Total de páginas procesadas: ${pages.length}`);
1062
+
1063
+ // Seguridad: nunca publicar si no se encontró ninguna página.
1064
+ if (pages.length === 0) {
1065
+ console.log("❌ No se encontró ninguna página para publicar.");
1066
+ console.log("👉 Verifica que existan carpetas en ./pages/<pagina>/ y que el archivo sea page.json (o pago.json).");
1067
+ console.log(`💡 Directorios encontrados: ${pageDirs.length > 0 ? pageDirs.join(", ") : "ninguno"}`);
1068
+ process.exit(1);
1069
+ }
1070
+
1071
+ // Construir payload según modelo MongoDB
1072
+ const payload = {
1073
+ type: "published",
1074
+ css: {
1075
+ type: "text/css",
1076
+ code: globalCSS
1077
+ },
1078
+ javascript: {
1079
+ type: "application/javascript",
1080
+ code: globalJS
1081
+ },
1082
+ pages: pages,
1083
+ modified_date: new Date(),
1084
+ has_changes: true,
1085
+ version_number: 1
1086
+ };
1087
+
1088
+ console.log(`📤 Publicando a cuenta: ${account}`);
1089
+ console.log(`📄 Páginas encontradas: ${pages.length}`);
1090
+ console.log(`🧩 Módulos totales: ${pages.reduce((sum, p) => sum + p.sections.length, 0)}`);
1091
+ console.log(`🌐 URL de la API: ${apiUrl}`);
1092
+
1093
+ // Enviar POST request
1094
+ const url = new URL(`${apiUrl}/api/cli/publish`);
1095
+ const postData = JSON.stringify(payload);
1096
+ const urlOptions = {
1097
+ hostname: url.hostname,
1098
+ port: url.port || (url.protocol === "https:" ? 443 : 80),
1099
+ path: url.pathname + url.search,
1100
+ method: "POST",
1101
+ headers: {
1102
+ "Content-Type": "application/json",
1103
+ "Content-Length": Buffer.byteLength(postData),
1104
+ "Authorization": `Bearer ${token}`,
1105
+ "X-Account-Nick": account
1106
+ },
1107
+ timeout: 60000, // 60 segundos de timeout
1108
+ rejectUnauthorized: true, // Aceptar certificados SSL válidos
1109
+ agent: false // No usar agent pool, crear nueva conexión
1110
+ };
1111
+
1112
+ const client = url.protocol === "https:" ? https : http;
1113
+
1114
+ console.log(`🔄 Enviando petición a ${url.hostname}${url.pathname}...`);
1115
+ console.log(`📤 Tamaño del payload: ${Buffer.byteLength(postData)} bytes`);
1116
+
1117
+ // Usar promesa para asegurar que esperamos la respuesta
1118
+ await new Promise((resolve, reject) => {
1119
+ console.log(`🔧 Creando request HTTP...`);
1120
+ const req = client.request(urlOptions, (res) => {
1121
+ console.log(`✅ Callback del request ejecutado`);
1122
+ let responseData = "";
1123
+
1124
+ console.log(`📡 Respuesta recibida: ${res.statusCode} ${res.statusMessage || ""}`);
1125
+
1126
+ // Manejar errores de respuesta
1127
+ res.on("error", (err) => {
1128
+ console.log(`❌ Error al leer la respuesta: ${err.message}`);
1129
+ reject(err);
1130
+ });
1131
+
1132
+ res.on("data", (chunk) => {
1133
+ responseData += chunk;
1134
+ console.log(`📥 Recibiendo datos... (${responseData.length} bytes acumulados)`);
1135
+ });
1136
+
1137
+ res.on("end", () => {
1138
+ console.log(`✅ Evento 'end' disparado`);
1139
+ console.log(`📦 Tamaño de respuesta: ${responseData.length} bytes`);
1140
+
1141
+ if (res.statusCode >= 200 && res.statusCode < 300) {
1142
+ console.log("✅ Publicación exitosa!");
1143
+ try {
1144
+ const response = JSON.parse(responseData);
1145
+ if (response.message) {
1146
+ console.log(`📝 ${response.message}`);
1147
+ }
1148
+ if (response.data) {
1149
+ console.log(`📊 Versión: ${response.data.version_number || "N/A"}`);
1150
+ }
1151
+ if (response.success === false) {
1152
+ console.log(`⚠️ El servidor reportó: success=false`);
1153
+ console.log(`💬 ${JSON.stringify(response, null, 2)}`);
1154
+ }
1155
+ } catch (e) {
1156
+ // No es JSON, mostrar respuesta tal cual
1157
+ if (responseData) {
1158
+ console.log(`📝 Respuesta (texto): ${responseData.substring(0, 500)}`);
1159
+ } else {
1160
+ console.log(`📝 Respuesta vacía`);
1161
+ }
1162
+ }
1163
+ resolve();
1164
+ } else {
1165
+ console.log(`❌ Error en la publicación (${res.statusCode})`);
1166
+ try {
1167
+ const error = JSON.parse(responseData);
1168
+ console.log(`💬 ${error.message || error.error || "Error desconocido"}`);
1169
+ if (error.details) {
1170
+ console.log(`🔍 Detalles:`, JSON.stringify(error.details, null, 2));
1171
+ }
1172
+ } catch (e) {
1173
+ if (responseData) {
1174
+ console.log(`💬 Respuesta del servidor: ${responseData.substring(0, 500)}`);
1175
+ } else {
1176
+ console.log(`💬 Sin respuesta del servidor`);
1177
+ }
1178
+ }
1179
+ reject(new Error(`HTTP ${res.statusCode}: ${responseData}`));
1180
+ }
1181
+ });
1182
+ });
1183
+
1184
+ req.on("error", (error) => {
1185
+ console.log(`❌ Error de conexión: ${error.message}`);
1186
+ console.log(`💡 Código de error: ${error.code || "N/A"}`);
1187
+ console.log(`💡 Stack: ${error.stack || "N/A"}`);
1188
+ console.log(`💡 Verifica que la URL de la API sea correcta: ${apiUrl}`);
1189
+ if (error.code === "ENOTFOUND") {
1190
+ console.log(`💡 No se pudo resolver el hostname: ${url.hostname}`);
1191
+ } else if (error.code === "ECONNREFUSED") {
1192
+ console.log(`💡 Conexión rechazada. ¿El servidor está corriendo?`);
1193
+ } else if (error.code === "ETIMEDOUT") {
1194
+ console.log(`💡 Timeout de conexión. El servidor no respondió a tiempo.`);
1195
+ } else if (error.code === "CERT_HAS_EXPIRED" || error.code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") {
1196
+ console.log(`💡 Problema con el certificado SSL. Intenta con rejectUnauthorized: false (solo para desarrollo)`);
1197
+ }
1198
+ reject(error);
1199
+ });
1200
+
1201
+ req.setTimeout(60000, () => {
1202
+ console.log(`❌ Timeout: La petición tardó más de 60 segundos sin respuesta`);
1203
+ console.log(`💡 El servidor puede estar sobrecargado o hay un problema de red`);
1204
+ req.destroy();
1205
+ reject(new Error("Timeout"));
1206
+ });
1207
+
1208
+ console.log(`📤 Escribiendo datos en el request...`);
1209
+ req.write(postData);
1210
+ console.log(`✅ Datos escritos, cerrando request...`);
1211
+ req.end();
1212
+
1213
+ console.log(`📨 Petición enviada, esperando respuesta...`);
1214
+
1215
+ // Agregar listener para verificar si el request se está enviando
1216
+ req.on("finish", () => {
1217
+ console.log(`✅ Request finalizado (datos enviados)`);
1218
+ });
1219
+
1220
+ req.on("close", () => {
1221
+ console.log(`🔒 Conexión cerrada`);
1222
+ });
1223
+ }).catch((error) => {
1224
+ console.log(`❌ Error en la promesa: ${error.message}`);
1225
+ throw error;
1226
+ });
1227
+
1228
+ } catch (error) {
1229
+ console.log(`❌ Error al procesar el proyecto: ${error.message}`);
1230
+ if (error.stack) {
1231
+ console.log(error.stack);
1232
+ }
1233
+ process.exit(1);
1234
+ }
614
1235
  }
615
1236
 
616
1237
  function cmdDev(port = 3000) {
@@ -720,6 +1341,23 @@ if (cmd1 === "dev") {
720
1341
  return;
721
1342
  }
722
1343
 
1344
+ if (cmd1 === "publish") {
1345
+ (async () => {
1346
+ try {
1347
+ await cmdPublish(args);
1348
+ process.exit(0);
1349
+ } catch (error) {
1350
+ console.log(`❌ Error: ${error.message}`);
1351
+ if (error.stack) {
1352
+ console.log(error.stack);
1353
+ }
1354
+ process.exit(1);
1355
+ }
1356
+ })();
1357
+ // NO hacer process.exit aquí, dejar que la función async maneje la salida
1358
+ return; // Evitar que el código continúe
1359
+ }
1360
+
723
1361
  console.log("❌ Comando no reconocido.");
724
1362
  printHelp();
725
1363
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mgpanel-cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.7",
4
4
  "description": "MGPanel CLI",
5
5
  "bin": {
6
6
  "mgpanel": "./bin/mgpanel.js"