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 +27 -0
- package/README.md +15 -2
- package/bin/mgpanel.js +812 -7
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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/<page>/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
|
-
"
|
|
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:**
|
|
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
|
-
|
|
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
|
-
|
|
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);
|