entexto-cli 1.4.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/entexto.js +39 -0
- package/docs/fullappdeploy.md +657 -0
- package/lib/commands/api-platform.js +258 -0
- package/lib/commands/create.js +415 -0
- package/lib/commands/domain.js +222 -0
- package/lib/commands/live.js +105 -0
- package/lib/commands/publish.js +51 -0
- package/lib/commands/tunnel.js +22 -1
- package/lib/utils/api.js +81 -1
- package/package.json +10 -4
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const { getToken } = require('../utils/config');
|
|
7
|
+
const {
|
|
8
|
+
apiCreateProject, apiListProjects, apiGetProject,
|
|
9
|
+
apiCreateCollection, apiInsertDoc, apiQueryDocs
|
|
10
|
+
} = require('../utils/api');
|
|
11
|
+
|
|
12
|
+
module.exports = async function apiPlatform(action, slugArg, options) {
|
|
13
|
+
if (!getToken()) {
|
|
14
|
+
console.log(chalk.red('\n No has iniciado sesión. Ejecuta: entexto login\n'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!action) {
|
|
19
|
+
const { choice } = await inquirer.prompt([{
|
|
20
|
+
type: 'list',
|
|
21
|
+
name: 'choice',
|
|
22
|
+
message: 'API Platform — ¿Qué quieres hacer?',
|
|
23
|
+
choices: [
|
|
24
|
+
{ name: 'Listar mis APIs', value: 'list' },
|
|
25
|
+
{ name: 'Crear nueva API', value: 'create' },
|
|
26
|
+
{ name: 'Ver detalle de una API', value: 'info' },
|
|
27
|
+
{ name: 'Crear colección', value: 'collection' },
|
|
28
|
+
{ name: 'Insertar documento', value: 'insert' },
|
|
29
|
+
{ name: 'Consultar documentos', value: 'query' },
|
|
30
|
+
],
|
|
31
|
+
}]);
|
|
32
|
+
action = choice;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
switch (action) {
|
|
36
|
+
case 'list': return await doList();
|
|
37
|
+
case 'ls': return await doList();
|
|
38
|
+
case 'create': return await doCreate(slugArg, options);
|
|
39
|
+
case 'info': return await doInfo(slugArg);
|
|
40
|
+
case 'collection': return await doCollection(slugArg, options);
|
|
41
|
+
case 'insert': return await doInsert(slugArg, options);
|
|
42
|
+
case 'query': return await doQuery(slugArg, options);
|
|
43
|
+
default:
|
|
44
|
+
console.log(chalk.red('Acción no reconocida: ' + action));
|
|
45
|
+
console.log(chalk.gray(' Acciones: list, create, info, collection, insert, query'));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ─── LISTAR APIs ─────────────────────────────────────────
|
|
50
|
+
async function doList() {
|
|
51
|
+
const spinner = ora('Cargando APIs...').start();
|
|
52
|
+
try {
|
|
53
|
+
const projects = await apiListProjects();
|
|
54
|
+
spinner.stop();
|
|
55
|
+
if (!projects || projects.length === 0) {
|
|
56
|
+
console.log(chalk.yellow('\n No tienes APIs creadas.\n'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
console.log(chalk.bold.cyan('\n Tus APIs:\n'));
|
|
60
|
+
console.log(chalk.gray(' ' + 'Slug'.padEnd(28) + 'Nombre'.padEnd(25) + 'Colecciones Docs'));
|
|
61
|
+
console.log(chalk.gray(' ' + '─'.repeat(70)));
|
|
62
|
+
for (const p of projects) {
|
|
63
|
+
console.log(
|
|
64
|
+
' ' + chalk.cyan(p.slug.padEnd(28)) +
|
|
65
|
+
(p.name || '?').slice(0, 23).padEnd(25) +
|
|
66
|
+
chalk.white(String(p.collection_count || 0).padEnd(13)) +
|
|
67
|
+
chalk.gray(String(p.document_count || 0))
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
console.log('');
|
|
71
|
+
} catch (err) {
|
|
72
|
+
spinner.fail(chalk.red(err.response?.data?.error || err.message));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── CREAR API ───────────────────────────────────────────
|
|
77
|
+
async function doCreate(nameArg, options) {
|
|
78
|
+
const answers = await inquirer.prompt([
|
|
79
|
+
{
|
|
80
|
+
type: 'input',
|
|
81
|
+
name: 'name',
|
|
82
|
+
message: 'Nombre de la API:',
|
|
83
|
+
default: nameArg || '',
|
|
84
|
+
validate: v => v.trim() ? true : 'El nombre es requerido',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: 'input',
|
|
88
|
+
name: 'description',
|
|
89
|
+
message: 'Descripción (opcional):',
|
|
90
|
+
default: options?.description || '',
|
|
91
|
+
},
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
const spinner = ora('Creando API...').start();
|
|
95
|
+
try {
|
|
96
|
+
const result = await apiCreateProject({
|
|
97
|
+
name: answers.name.trim(),
|
|
98
|
+
description: answers.description.trim(),
|
|
99
|
+
});
|
|
100
|
+
spinner.succeed(chalk.green('API creada'));
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log(' ' + chalk.gray('Slug: ') + chalk.cyan(result.project.slug));
|
|
103
|
+
console.log(' ' + chalk.gray('API Key: ') + chalk.yellow(result.project.api_key));
|
|
104
|
+
console.log(' ' + chalk.gray('Base: ') + chalk.white('https://api.entexto.com/v1/api/' + result.project.slug));
|
|
105
|
+
console.log('');
|
|
106
|
+
console.log(chalk.gray(' Usa el API Key en el header X-API-Key para autenticarte.'));
|
|
107
|
+
console.log(chalk.gray(' O usa JWT: POST /v1/api/auth/login { email, password }'));
|
|
108
|
+
console.log('');
|
|
109
|
+
} catch (err) {
|
|
110
|
+
spinner.fail(chalk.red(err.response?.data?.error || err.message));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── INFO DE API ─────────────────────────────────────────
|
|
115
|
+
async function doInfo(slugArg) {
|
|
116
|
+
let slug = slugArg;
|
|
117
|
+
if (!slug) {
|
|
118
|
+
const projects = await apiListProjects();
|
|
119
|
+
if (!projects || projects.length === 0) {
|
|
120
|
+
console.log(chalk.yellow(' No tienes APIs.'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const { s } = await inquirer.prompt([{
|
|
124
|
+
type: 'list',
|
|
125
|
+
name: 's',
|
|
126
|
+
message: 'Selecciona API:',
|
|
127
|
+
choices: projects.map(p => ({ name: `${p.name} (${p.slug})`, value: p.slug })),
|
|
128
|
+
}]);
|
|
129
|
+
slug = s;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const spinner = ora('Cargando...').start();
|
|
133
|
+
try {
|
|
134
|
+
const data = await apiGetProject(slug);
|
|
135
|
+
spinner.stop();
|
|
136
|
+
console.log(chalk.bold.cyan('\n API: ' + data.project.name + '\n'));
|
|
137
|
+
console.log(' ' + chalk.gray('Slug: ') + chalk.cyan(data.project.slug));
|
|
138
|
+
console.log(' ' + chalk.gray('API Key: ') + chalk.yellow(data.project.api_key));
|
|
139
|
+
console.log(' ' + chalk.gray('Base: ') + 'https://api.entexto.com/v1/api/' + data.project.slug);
|
|
140
|
+
if (data.collections && data.collections.length > 0) {
|
|
141
|
+
console.log(chalk.bold('\n Colecciones:\n'));
|
|
142
|
+
for (const c of data.collections) {
|
|
143
|
+
console.log(' ' + chalk.white(c.name) + chalk.gray(` (${c.document_count || 0} docs)`));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
console.log('');
|
|
147
|
+
} catch (err) {
|
|
148
|
+
spinner.fail(chalk.red(err.response?.data?.error || err.message));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─── CREAR COLECCIÓN ─────────────────────────────────────
|
|
153
|
+
async function doCollection(slugArg, options) {
|
|
154
|
+
let slug = slugArg;
|
|
155
|
+
if (!slug) {
|
|
156
|
+
const projects = await apiListProjects();
|
|
157
|
+
const { s } = await inquirer.prompt([{
|
|
158
|
+
type: 'list', name: 's', message: 'API:',
|
|
159
|
+
choices: projects.map(p => ({ name: `${p.name} (${p.slug})`, value: p.slug })),
|
|
160
|
+
}]);
|
|
161
|
+
slug = s;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { name } = await inquirer.prompt([{
|
|
165
|
+
type: 'input', name: 'name', message: 'Nombre de la colección:',
|
|
166
|
+
default: options?.name || '',
|
|
167
|
+
validate: v => v.trim() ? true : 'Requerido',
|
|
168
|
+
}]);
|
|
169
|
+
|
|
170
|
+
const spinner = ora('Creando colección...').start();
|
|
171
|
+
try {
|
|
172
|
+
await apiCreateCollection(slug, name.trim());
|
|
173
|
+
spinner.succeed(chalk.green('Colección "' + name.trim() + '" creada'));
|
|
174
|
+
} catch (err) {
|
|
175
|
+
spinner.fail(chalk.red(err.response?.data?.error || err.message));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── INSERTAR DOCUMENTO ──────────────────────────────────
|
|
180
|
+
async function doInsert(slugArg, options) {
|
|
181
|
+
let slug = slugArg;
|
|
182
|
+
if (!slug) {
|
|
183
|
+
const projects = await apiListProjects();
|
|
184
|
+
const { s } = await inquirer.prompt([{
|
|
185
|
+
type: 'list', name: 's', message: 'API:',
|
|
186
|
+
choices: projects.map(p => ({ name: `${p.name} (${p.slug})`, value: p.slug })),
|
|
187
|
+
}]);
|
|
188
|
+
slug = s;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const data = await apiGetProject(slug);
|
|
192
|
+
if (!data.collections || data.collections.length === 0) {
|
|
193
|
+
console.log(chalk.yellow(' No hay colecciones. Crea una primero.'));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const { collection } = await inquirer.prompt([{
|
|
198
|
+
type: 'list', name: 'collection', message: 'Colección:',
|
|
199
|
+
choices: data.collections.map(c => c.name),
|
|
200
|
+
}]);
|
|
201
|
+
|
|
202
|
+
const { json } = await inquirer.prompt([{
|
|
203
|
+
type: 'editor', name: 'json', message: 'Documento (JSON):',
|
|
204
|
+
default: '{\n "key": "value"\n}',
|
|
205
|
+
}]);
|
|
206
|
+
|
|
207
|
+
let doc;
|
|
208
|
+
try { doc = JSON.parse(json); } catch {
|
|
209
|
+
console.log(chalk.red(' JSON inválido.')); return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const spinner = ora('Insertando...').start();
|
|
213
|
+
try {
|
|
214
|
+
const result = await apiInsertDoc(slug, collection, doc);
|
|
215
|
+
spinner.succeed(chalk.green('Documento insertado — ID: ' + (result.document?.id || '?')));
|
|
216
|
+
} catch (err) {
|
|
217
|
+
spinner.fail(chalk.red(err.response?.data?.error || err.message));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ─── CONSULTAR DOCUMENTOS ────────────────────────────────
|
|
222
|
+
async function doQuery(slugArg, options) {
|
|
223
|
+
let slug = slugArg;
|
|
224
|
+
if (!slug) {
|
|
225
|
+
const projects = await apiListProjects();
|
|
226
|
+
const { s } = await inquirer.prompt([{
|
|
227
|
+
type: 'list', name: 's', message: 'API:',
|
|
228
|
+
choices: projects.map(p => ({ name: `${p.name} (${p.slug})`, value: p.slug })),
|
|
229
|
+
}]);
|
|
230
|
+
slug = s;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const data = await apiGetProject(slug);
|
|
234
|
+
if (!data.collections || data.collections.length === 0) {
|
|
235
|
+
console.log(chalk.yellow(' No hay colecciones.'));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const { collection } = await inquirer.prompt([{
|
|
240
|
+
type: 'list', name: 'collection', message: 'Colección:',
|
|
241
|
+
choices: data.collections.map(c => c.name),
|
|
242
|
+
}]);
|
|
243
|
+
|
|
244
|
+
const spinner = ora('Consultando...').start();
|
|
245
|
+
try {
|
|
246
|
+
const result = await apiQueryDocs(slug, collection);
|
|
247
|
+
spinner.stop();
|
|
248
|
+
const docs = result.documents || [];
|
|
249
|
+
console.log(chalk.bold.cyan(`\n ${collection} — ${docs.length} documento(s):\n`));
|
|
250
|
+
for (const doc of docs.slice(0, 20)) {
|
|
251
|
+
console.log(' ' + chalk.gray(doc.id + ':') + ' ' + JSON.stringify(doc.data || doc).substring(0, 100));
|
|
252
|
+
}
|
|
253
|
+
if (docs.length > 20) console.log(chalk.gray(` ... y ${docs.length - 20} más`));
|
|
254
|
+
console.log('');
|
|
255
|
+
} catch (err) {
|
|
256
|
+
spinner.fail(chalk.red(err.response?.data?.error || err.message));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const FormData = require('form-data');
|
|
9
|
+
const { getToken } = require('../utils/config');
|
|
10
|
+
const { createProject, getProjects, deployFiles, publishProject } = require('../utils/api');
|
|
11
|
+
|
|
12
|
+
module.exports = async function create(options) {
|
|
13
|
+
if (!getToken()) {
|
|
14
|
+
console.log(chalk.red('\n No has iniciado sesión. Ejecuta: entexto login\n'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log(chalk.bold.cyan('\n Crear nuevo proyecto EntExto\n'));
|
|
19
|
+
|
|
20
|
+
// ── Preguntar datos del proyecto ──
|
|
21
|
+
const answers = await inquirer.prompt([
|
|
22
|
+
{
|
|
23
|
+
type: 'input',
|
|
24
|
+
name: 'name',
|
|
25
|
+
message: 'Nombre del proyecto:',
|
|
26
|
+
default: options.name || path.basename(process.cwd()),
|
|
27
|
+
validate: v => v.trim().length >= 1 ? true : 'El nombre no puede estar vacío',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
type: 'input',
|
|
31
|
+
name: 'description',
|
|
32
|
+
message: 'Descripción (opcional):',
|
|
33
|
+
default: options.description || '',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'list',
|
|
37
|
+
name: 'template',
|
|
38
|
+
message: 'Plantilla:',
|
|
39
|
+
choices: [
|
|
40
|
+
{ name: 'Vacío (solo index.html)', value: 'blank' },
|
|
41
|
+
{ name: 'HTML + CSS + JS', value: 'basic' },
|
|
42
|
+
{ name: 'React (JSX + CDN)', value: 'react' },
|
|
43
|
+
{ name: 'Fullstack (VFS + Live SDK)', value: 'fullstack' },
|
|
44
|
+
{ name: 'Desde carpeta local', value: 'local' },
|
|
45
|
+
],
|
|
46
|
+
default: options.template || 'blank',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: 'confirm',
|
|
50
|
+
name: 'publish',
|
|
51
|
+
message: '¿Publicar inmediatamente?',
|
|
52
|
+
default: true,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
type: 'confirm',
|
|
56
|
+
name: 'linkDomain',
|
|
57
|
+
message: '¿Vincular un dominio/subdominio?',
|
|
58
|
+
default: false,
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
// ── Crear proyecto en el servidor ──
|
|
63
|
+
const spinner = ora('Creando proyecto...').start();
|
|
64
|
+
let project;
|
|
65
|
+
try {
|
|
66
|
+
project = await createProject({
|
|
67
|
+
name: answers.name.trim(),
|
|
68
|
+
description: answers.description.trim(),
|
|
69
|
+
});
|
|
70
|
+
spinner.succeed(chalk.green(`Proyecto creado — ID: ${project.project_uuid}`));
|
|
71
|
+
} catch (err) {
|
|
72
|
+
spinner.fail(chalk.red('Error: ' + (err.response?.data?.error || err.message)));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const uuid = project.project_uuid;
|
|
77
|
+
|
|
78
|
+
// ── Generar archivos según la plantilla ──
|
|
79
|
+
let files = {};
|
|
80
|
+
if (answers.template === 'blank') {
|
|
81
|
+
files = { 'index.html': TEMPLATES.blank(answers.name) };
|
|
82
|
+
} else if (answers.template === 'basic') {
|
|
83
|
+
files = TEMPLATES.basic(answers.name);
|
|
84
|
+
} else if (answers.template === 'react') {
|
|
85
|
+
files = TEMPLATES.react(answers.name);
|
|
86
|
+
} else if (answers.template === 'fullstack') {
|
|
87
|
+
files = TEMPLATES.fullstack(answers.name);
|
|
88
|
+
} else if (answers.template === 'local') {
|
|
89
|
+
// Subir carpeta actual
|
|
90
|
+
const dir = options.dir || '.';
|
|
91
|
+
const absDir = path.resolve(dir);
|
|
92
|
+
if (!fs.existsSync(absDir)) {
|
|
93
|
+
console.log(chalk.red(` La carpeta "${dir}" no existe.`));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
files = recolectarArchivosComoObj(absDir);
|
|
97
|
+
if (Object.keys(files).length === 0) {
|
|
98
|
+
console.log(chalk.yellow(' No hay archivos para subir.'));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Subir archivos ──
|
|
103
|
+
if (Object.keys(files).length > 0) {
|
|
104
|
+
const spinDeploy = ora(`Subiendo ${Object.keys(files).length} archivo(s)...`).start();
|
|
105
|
+
try {
|
|
106
|
+
const form = new FormData();
|
|
107
|
+
for (const [ruta, contenido] of Object.entries(files)) {
|
|
108
|
+
form.append(ruta, Buffer.from(contenido, 'utf8'), { filename: ruta });
|
|
109
|
+
}
|
|
110
|
+
await deployFiles(uuid, form, false);
|
|
111
|
+
spinDeploy.succeed(`${Object.keys(files).length} archivo(s) subidos`);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
spinDeploy.fail(chalk.red('Error subiendo: ' + (err.response?.data?.error || err.message)));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Publicar ──
|
|
118
|
+
let slug = null;
|
|
119
|
+
if (answers.publish) {
|
|
120
|
+
const spinPub = ora('Publicando...').start();
|
|
121
|
+
try {
|
|
122
|
+
const pubResult = await publishProject(uuid);
|
|
123
|
+
slug = pubResult.url;
|
|
124
|
+
spinPub.succeed('Publicado: ' + chalk.cyan(slug));
|
|
125
|
+
} catch (err) {
|
|
126
|
+
spinPub.fail(chalk.red('Error publicando: ' + (err.response?.data?.error || err.message)));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Vincular dominio ──
|
|
131
|
+
if (answers.linkDomain) {
|
|
132
|
+
await vincularDominio(project);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Resumen ──
|
|
136
|
+
console.log(chalk.bold.green('\n Proyecto listo!\n'));
|
|
137
|
+
console.log(' ' + chalk.gray('ID: ') + chalk.cyan(uuid));
|
|
138
|
+
console.log(' ' + chalk.gray('Nombre: ') + answers.name);
|
|
139
|
+
if (slug) console.log(' ' + chalk.gray('URL: ') + chalk.cyan(slug));
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log(chalk.gray(' Comandos útiles:'));
|
|
142
|
+
console.log(chalk.gray(' entexto deploy -i ' + uuid + ' # Subir archivos'));
|
|
143
|
+
console.log(chalk.gray(' entexto sync -i ' + uuid + ' # Sincronizar en vivo'));
|
|
144
|
+
console.log(chalk.gray(' entexto pull -i ' + uuid + ' # Descargar archivos'));
|
|
145
|
+
console.log(chalk.gray(' entexto domain # Gestionar dominios'));
|
|
146
|
+
console.log('');
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// ─── VINCULAR DOMINIO ──────────────────────────────────────
|
|
150
|
+
async function vincularDominio(project) {
|
|
151
|
+
const { addDomain, addSubdomain, verifyDomain, listDomains } = require('../utils/api');
|
|
152
|
+
|
|
153
|
+
const { tipo } = await inquirer.prompt([{
|
|
154
|
+
type: 'list',
|
|
155
|
+
name: 'tipo',
|
|
156
|
+
message: '¿Qué tipo de dominio?',
|
|
157
|
+
choices: [
|
|
158
|
+
{ name: 'Subdominio de un dominio verificado', value: 'subdomain' },
|
|
159
|
+
{ name: 'Dominio propio (requiere configuración DNS)', value: 'custom' },
|
|
160
|
+
],
|
|
161
|
+
}]);
|
|
162
|
+
|
|
163
|
+
if (tipo === 'subdomain') {
|
|
164
|
+
// Listar dominios verificados
|
|
165
|
+
let domains;
|
|
166
|
+
try { domains = await listDomains(); } catch { domains = []; }
|
|
167
|
+
const verified = domains.filter(d => d.verified);
|
|
168
|
+
|
|
169
|
+
if (verified.length === 0) {
|
|
170
|
+
console.log(chalk.yellow(' No tienes dominios verificados. Primero agrega un dominio.'));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const { parent } = await inquirer.prompt([{
|
|
175
|
+
type: 'list',
|
|
176
|
+
name: 'parent',
|
|
177
|
+
message: 'Dominio padre:',
|
|
178
|
+
choices: verified.map(d => ({ name: d.domain, value: d.domain })),
|
|
179
|
+
}]);
|
|
180
|
+
|
|
181
|
+
const { sub } = await inquirer.prompt([{
|
|
182
|
+
type: 'input',
|
|
183
|
+
name: 'sub',
|
|
184
|
+
message: `Subdominio (se creará: ___.${parent}):`,
|
|
185
|
+
validate: v => /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/i.test(v.trim()) ? true : 'Solo letras, números y guiones',
|
|
186
|
+
}]);
|
|
187
|
+
|
|
188
|
+
const spinner = ora('Creando subdominio...').start();
|
|
189
|
+
try {
|
|
190
|
+
const result = await addSubdomain(parent, sub.trim(), project.id);
|
|
191
|
+
spinner.succeed(chalk.green(`${result.domain} → activo ` + (result.dns_created ? '(DNS auto)' : '')));
|
|
192
|
+
} catch (err) {
|
|
193
|
+
spinner.fail(chalk.red(err.response?.data?.error || err.message));
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
const { domain } = await inquirer.prompt([{
|
|
197
|
+
type: 'input',
|
|
198
|
+
name: 'domain',
|
|
199
|
+
message: 'Tu dominio (ej: miapp.com):',
|
|
200
|
+
validate: v => /^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/.test(v.trim()) ? true : 'Formato inválido',
|
|
201
|
+
}]);
|
|
202
|
+
|
|
203
|
+
const spinner = ora('Registrando dominio...').start();
|
|
204
|
+
try {
|
|
205
|
+
const result = await addDomain(project.id, domain.trim());
|
|
206
|
+
spinner.succeed(chalk.green('Dominio registrado'));
|
|
207
|
+
console.log(chalk.yellow('\n Configura tu DNS:\n'));
|
|
208
|
+
if (result.dns_instructions?.option_a_cname) {
|
|
209
|
+
result.dns_instructions.option_a_cname.steps.forEach(s => console.log(' ' + s));
|
|
210
|
+
}
|
|
211
|
+
console.log(chalk.gray('\n Luego ejecuta: entexto domain verify ' + domain.trim() + '\n'));
|
|
212
|
+
} catch (err) {
|
|
213
|
+
spinner.fail(chalk.red(err.response?.data?.error || err.message));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ─── RECOLECTAR ARCHIVOS LOCALES ──────────────────────────
|
|
219
|
+
const IGNORE_NAMES = new Set([
|
|
220
|
+
'node_modules', '.git', '.env', '.env.local', '.DS_Store',
|
|
221
|
+
'dist', '.next', 'build', 'coverage', '.cache', '.turbo',
|
|
222
|
+
'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
|
|
223
|
+
'.entexto', 'Thumbs.db',
|
|
224
|
+
]);
|
|
225
|
+
const IGNORE_EXT = new Set([
|
|
226
|
+
'.key', '.pem', '.cert', '.p12', '.pfx', '.sqlite', '.sqlite3',
|
|
227
|
+
'.db', '.log', '.bak', '.backup',
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
function recolectarArchivosComoObj(dir, base) {
|
|
231
|
+
base = base || dir;
|
|
232
|
+
const result = {};
|
|
233
|
+
let entries;
|
|
234
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return result; }
|
|
235
|
+
for (const entry of entries) {
|
|
236
|
+
if (IGNORE_NAMES.has(entry.name)) continue;
|
|
237
|
+
if (entry.name.startsWith('.') && entry.name !== '.htaccess') continue;
|
|
238
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
239
|
+
if (IGNORE_EXT.has(ext)) continue;
|
|
240
|
+
const full = path.join(dir, entry.name);
|
|
241
|
+
if (entry.isDirectory()) {
|
|
242
|
+
Object.assign(result, recolectarArchivosComoObj(full, base));
|
|
243
|
+
} else if (entry.isFile()) {
|
|
244
|
+
const ruta = path.relative(base, full).replace(/\\/g, '/');
|
|
245
|
+
try {
|
|
246
|
+
result[ruta] = fs.readFileSync(full, 'utf8');
|
|
247
|
+
} catch {}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ─── PLANTILLAS ──────────────────────────────────────────
|
|
254
|
+
const TEMPLATES = {
|
|
255
|
+
blank: (name) => `<!DOCTYPE html>
|
|
256
|
+
<html lang="es">
|
|
257
|
+
<head>
|
|
258
|
+
<meta charset="UTF-8">
|
|
259
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
260
|
+
<title>${name}</title>
|
|
261
|
+
<style>
|
|
262
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
263
|
+
body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; background: #0f172a; color: #e2e8f0; }
|
|
264
|
+
h1 { font-size: 2.5rem; }
|
|
265
|
+
</style>
|
|
266
|
+
</head>
|
|
267
|
+
<body>
|
|
268
|
+
<h1>${name}</h1>
|
|
269
|
+
</body>
|
|
270
|
+
</html>`,
|
|
271
|
+
|
|
272
|
+
basic: (name) => ({
|
|
273
|
+
'index.html': `<!DOCTYPE html>
|
|
274
|
+
<html lang="es">
|
|
275
|
+
<head>
|
|
276
|
+
<meta charset="UTF-8">
|
|
277
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
278
|
+
<title>${name}</title>
|
|
279
|
+
<link rel="stylesheet" href="style.css">
|
|
280
|
+
</head>
|
|
281
|
+
<body>
|
|
282
|
+
<div id="app">
|
|
283
|
+
<h1>${name}</h1>
|
|
284
|
+
<p>Edita los archivos para empezar.</p>
|
|
285
|
+
</div>
|
|
286
|
+
<script src="app.js"></script>
|
|
287
|
+
</body>
|
|
288
|
+
</html>`,
|
|
289
|
+
'style.css': `* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
290
|
+
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
|
|
291
|
+
#app { text-align: center; }
|
|
292
|
+
h1 { font-size: 2.5rem; margin-bottom: 1rem; }`,
|
|
293
|
+
'app.js': `// ${name} — App JS\nconsole.log('${name} cargado');`,
|
|
294
|
+
}),
|
|
295
|
+
|
|
296
|
+
react: (name) => ({
|
|
297
|
+
'index.html': `<!DOCTYPE html>
|
|
298
|
+
<html lang="es">
|
|
299
|
+
<head>
|
|
300
|
+
<meta charset="UTF-8">
|
|
301
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
302
|
+
<title>${name}</title>
|
|
303
|
+
<script src="/vendor/react/react.production.min.js" crossorigin></script>
|
|
304
|
+
<script src="/vendor/react/react-dom.production.min.js" crossorigin></script>
|
|
305
|
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
306
|
+
<style>
|
|
307
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
308
|
+
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; }
|
|
309
|
+
</style>
|
|
310
|
+
</head>
|
|
311
|
+
<body>
|
|
312
|
+
<div id="root"></div>
|
|
313
|
+
<script type="text/babel">
|
|
314
|
+
const { useState, useEffect } = React;
|
|
315
|
+
|
|
316
|
+
function App() {
|
|
317
|
+
const [count, setCount] = useState(0);
|
|
318
|
+
return (
|
|
319
|
+
<div style={{display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',minHeight:'100vh'}}>
|
|
320
|
+
<h1 style={{fontSize:'2.5rem',marginBottom:'1rem'}}>${name}</h1>
|
|
321
|
+
<p style={{marginBottom:'1rem'}}>Contador: {count}</p>
|
|
322
|
+
<button onClick={() => setCount(c => c + 1)}
|
|
323
|
+
style={{padding:'0.75rem 2rem',fontSize:'1rem',background:'#3b82f6',color:'white',border:'none',borderRadius:'0.5rem',cursor:'pointer'}}>
|
|
324
|
+
+1
|
|
325
|
+
</button>
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
|
331
|
+
</script>
|
|
332
|
+
</body>
|
|
333
|
+
</html>`,
|
|
334
|
+
}),
|
|
335
|
+
|
|
336
|
+
fullstack: (name) => ({
|
|
337
|
+
'index.html': `<!DOCTYPE html>
|
|
338
|
+
<html lang="es">
|
|
339
|
+
<head>
|
|
340
|
+
<meta charset="UTF-8">
|
|
341
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
342
|
+
<title>${name}</title>
|
|
343
|
+
<script src="/vendor/react/react.production.min.js" crossorigin></script>
|
|
344
|
+
<script src="/vendor/react/react-dom.production.min.js" crossorigin></script>
|
|
345
|
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
346
|
+
<style>
|
|
347
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
348
|
+
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; }
|
|
349
|
+
.container { max-width: 600px; margin: auto; padding: 2rem; }
|
|
350
|
+
input, button { padding: 0.5rem 1rem; font-size: 1rem; border-radius: 0.5rem; border: 1px solid #334155; }
|
|
351
|
+
input { background: #1e293b; color: white; flex: 1; }
|
|
352
|
+
button { background: #3b82f6; color: white; border: none; cursor: pointer; }
|
|
353
|
+
button:hover { background: #2563eb; }
|
|
354
|
+
.item { display: flex; justify-content: space-between; padding: 0.75rem; background: #1e293b; border-radius: 0.5rem; margin-top: 0.5rem; }
|
|
355
|
+
</style>
|
|
356
|
+
</head>
|
|
357
|
+
<body>
|
|
358
|
+
<div id="root"></div>
|
|
359
|
+
<script type="text/babel">
|
|
360
|
+
const { useState, useEffect } = React;
|
|
361
|
+
const fs = window.__entexto?.fs;
|
|
362
|
+
|
|
363
|
+
function App() {
|
|
364
|
+
const [items, setItems] = useState([]);
|
|
365
|
+
const [input, setInput] = useState('');
|
|
366
|
+
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
if (!fs) return;
|
|
369
|
+
fs.leer('database.json').then(r => {
|
|
370
|
+
if (r.ok) setItems(JSON.parse(r.contenido || '[]'));
|
|
371
|
+
}).catch(() => {});
|
|
372
|
+
}, []);
|
|
373
|
+
|
|
374
|
+
const save = async (newItems) => {
|
|
375
|
+
setItems(newItems);
|
|
376
|
+
if (fs) await fs.escribir('database.json', JSON.stringify(newItems, null, 2));
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const add = () => {
|
|
380
|
+
if (!input.trim()) return;
|
|
381
|
+
save([...items, { id: Date.now(), text: input.trim() }]);
|
|
382
|
+
setInput('');
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const remove = (id) => save(items.filter(i => i.id !== id));
|
|
386
|
+
|
|
387
|
+
return (
|
|
388
|
+
<div className="container">
|
|
389
|
+
<h1 style={{fontSize:'2rem',marginBottom:'1.5rem'}}>${name}</h1>
|
|
390
|
+
<div style={{display:'flex',gap:'0.5rem',marginBottom:'1rem'}}>
|
|
391
|
+
<input value={input} onChange={e => setInput(e.target.value)}
|
|
392
|
+
onKeyDown={e => e.key === 'Enter' && add()}
|
|
393
|
+
placeholder="Agregar item..." />
|
|
394
|
+
<button onClick={add}>+</button>
|
|
395
|
+
</div>
|
|
396
|
+
{items.map(item => (
|
|
397
|
+
<div key={item.id} className="item">
|
|
398
|
+
<span>{item.text}</span>
|
|
399
|
+
<button onClick={() => remove(item.id)} style={{background:'#ef4444',fontSize:'0.8rem'}}>X</button>
|
|
400
|
+
</div>
|
|
401
|
+
))}
|
|
402
|
+
<p style={{marginTop:'1rem',color:'#64748b',fontSize:'0.8rem'}}>
|
|
403
|
+
{items.length} items — datos guardados con VFS (window.__entexto.fs)
|
|
404
|
+
</p>
|
|
405
|
+
</div>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
|
410
|
+
</script>
|
|
411
|
+
</body>
|
|
412
|
+
</html>`,
|
|
413
|
+
'database.json': '[]',
|
|
414
|
+
}),
|
|
415
|
+
};
|