entexto-cli 1.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 ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { program } = require('commander');
5
+ const pkg = require('../package.json');
6
+
7
+ program
8
+ .name('entexto')
9
+ .description('CLI oficial de Entexto — gestiona y deploya tus proyectos')
10
+ .version(pkg.version);
11
+
12
+ // ─── entexto login ────────────────────────────────────────────
13
+ program
14
+ .command('login')
15
+ .description('Iniciar sesión en entexto.com')
16
+ .action(require('../lib/commands/login'));
17
+
18
+ // ─── entexto projects ─────────────────────────────────────────
19
+ program
20
+ .command('projects')
21
+ .description('Listar tus proyectos')
22
+ .action(require('../lib/commands/projects'));
23
+
24
+ // ─── entexto deploy ───────────────────────────────────────────
25
+ program
26
+ .command('deploy')
27
+ .description('Subir archivos a un proyecto')
28
+ .option('-i, --id <uuid>', 'ID del proyecto destino (project_uuid)')
29
+ .option('-d, --dir <path>', 'Carpeta a subir (default: carpeta actual)', '.')
30
+ .option('-w, --watch', 'Modo live: detecta cambios y re-sube automáticamente')
31
+ .option('-p, --publish', 'Publicar el proyecto después del deploy')
32
+ .action(require('../lib/commands/deploy'));
33
+
34
+ // ─── entexto whoami ──────────────────────────────────────────
35
+ program
36
+ .command('whoami')
37
+ .description('Mostrar usuario actual')
38
+ .action(() => {
39
+ const { getConfig } = require('../lib/utils/config');
40
+ const cfg = getConfig();
41
+ if (!cfg.token) {
42
+ console.log('No has iniciado sesión. Ejecuta: entexto login');
43
+ } else {
44
+ const u = cfg.user || {};
45
+ console.log(`Conectado como: ${u.display_name || u.username || '?'} (${u.email || ''})`);
46
+ }
47
+ });
48
+
49
+ // ─── entexto logout ──────────────────────────────────────────
50
+ program
51
+ .command('logout')
52
+ .description('Cerrar sesión')
53
+ .action(() => {
54
+ const { saveConfig } = require('../lib/utils/config');
55
+ saveConfig({ token: null, user: null });
56
+ console.log('Sesión cerrada.');
57
+ });
58
+
59
+ program.parse(process.argv);
60
+
61
+ if (!process.argv.slice(2).length) {
62
+ program.outputHelp();
63
+ }
@@ -0,0 +1,203 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const inquirer = require('inquirer');
6
+ const chalk = require('chalk');
7
+ const ora = require('ora');
8
+ const FormData = require('form-data');
9
+ const chokidar = require('chokidar');
10
+ const { getToken } = require('../utils/config');
11
+ const { getProjects, deployWithPublish, deployFiles, publishProject } = require('../utils/api');
12
+
13
+ // Archivos/carpetas que nunca se suben
14
+ const IGNORE_NAMES = new Set([
15
+ 'node_modules', '.git', '.env', '.env.local', '.DS_Store',
16
+ 'dist', '.next', 'build', 'coverage', '.cache', '.turbo',
17
+ 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
18
+ '.entexto', 'Thumbs.db',
19
+ ]);
20
+
21
+ // Extensiones que se excluyen por seguridad
22
+ const IGNORE_EXT = new Set([
23
+ '.key', '.pem', '.cert', '.p12', '.pfx', '.sqlite', '.sqlite3',
24
+ '.db', '.log', '.bak', '.backup',
25
+ ]);
26
+
27
+ function debeIgnorar(nombre) {
28
+ if (IGNORE_NAMES.has(nombre)) return true;
29
+ const ext = path.extname(nombre).toLowerCase();
30
+ if (IGNORE_EXT.has(ext)) return true;
31
+ if (nombre.startsWith('.') && nombre !== '.htaccess') return true;
32
+ return false;
33
+ }
34
+
35
+ function recolectarArchivos(dir, base) {
36
+ base = base || dir;
37
+ const resultado = [];
38
+ let entries;
39
+ try {
40
+ entries = fs.readdirSync(dir, { withFileTypes: true });
41
+ } catch {
42
+ return resultado;
43
+ }
44
+ for (const entry of entries) {
45
+ if (debeIgnorar(entry.name)) continue;
46
+ const fullPath = path.join(dir, entry.name);
47
+ if (entry.isDirectory()) {
48
+ resultado.push(...recolectarArchivos(fullPath, base));
49
+ } else if (entry.isFile()) {
50
+ resultado.push(fullPath);
51
+ }
52
+ }
53
+ return resultado;
54
+ }
55
+
56
+ async function ejecutarDeploy(uuid, dir, publish) {
57
+ const absDir = path.resolve(dir);
58
+ const archivos = recolectarArchivos(absDir);
59
+
60
+ if (!archivos.length) {
61
+ console.log(chalk.yellow(' No se encontraron archivos para subir.'));
62
+ return false;
63
+ }
64
+
65
+ const spinner = ora(`Subiendo ${archivos.length} archivo(s)...`).start();
66
+
67
+ try {
68
+ const form = new FormData();
69
+
70
+ for (const filePath of archivos) {
71
+ const ruta = path.relative(absDir, filePath).replace(/\\/g, '/');
72
+ form.append(ruta, fs.createReadStream(filePath), { filename: ruta });
73
+ }
74
+
75
+ let result;
76
+ if (publish) {
77
+ result = await deployWithPublish(uuid, form);
78
+ } else {
79
+ result = await deployFiles(uuid, form, false);
80
+ }
81
+
82
+ spinner.stop();
83
+ (result.archivos || []).forEach(r => {
84
+ console.log(' ' + chalk.green('✓') + ' ' + chalk.white(r));
85
+ });
86
+ console.log(
87
+ chalk.bold.green(`\n 🚀 Deploy completado — ${result.total} archivo(s)\n`)
88
+ );
89
+ if (result.publicado && result.publicado.url) {
90
+ console.log(' ' + chalk.bold.cyan('🌐 ' + result.publicado.url) + '\n');
91
+ }
92
+ return true;
93
+ } catch (err) {
94
+ const msg = err.response?.data?.error || err.message;
95
+ spinner.fail(chalk.red('Error en deploy: ' + msg));
96
+ return false;
97
+ }
98
+ }
99
+
100
+ module.exports = async function (options) {
101
+ if (!getToken()) {
102
+ console.log(chalk.red('\n No has iniciado sesión. Ejecuta: entexto login\n'));
103
+ process.exit(1);
104
+ }
105
+
106
+ // ── Resolver proyecto destino ─────────────────────────────
107
+ let uuid = options.id;
108
+
109
+ if (!uuid) {
110
+ const spinner = ora('Cargando proyectos...').start();
111
+ let projects;
112
+ try {
113
+ projects = await getProjects();
114
+ spinner.stop();
115
+ } catch (err) {
116
+ const msg = err.response?.data?.error || err.message;
117
+ spinner.fail(chalk.red('Error cargando proyectos: ' + msg));
118
+ process.exit(1);
119
+ }
120
+
121
+ if (!projects.length) {
122
+ console.log(chalk.yellow('\n No tienes proyectos. Crea uno primero en entexto.com\n'));
123
+ process.exit(0);
124
+ }
125
+
126
+ const { selectedUuid } = await inquirer.prompt([{
127
+ type: 'list',
128
+ name: 'selectedUuid',
129
+ message: '¿A qué proyecto deseas subir los archivos?',
130
+ pageSize: 10,
131
+ choices: projects.map(p => ({
132
+ name: [
133
+ (p.project_uuid || String(p.id)).padEnd(20),
134
+ (p.name || 'Sin nombre').slice(0, 28).padEnd(30),
135
+ p.is_published ? chalk.green('📡') : chalk.yellow('🔒'),
136
+ ].join(' '),
137
+ value: p.project_uuid || String(p.id),
138
+ })),
139
+ }]);
140
+
141
+ uuid = selectedUuid;
142
+ }
143
+
144
+ // ── Carpeta a subir ──────────────────────────────────────
145
+ const dir = options.dir || '.';
146
+ if (!fs.existsSync(path.resolve(dir))) {
147
+ console.log(chalk.red(` La carpeta "${dir}" no existe.\n`));
148
+ process.exit(1);
149
+ }
150
+
151
+ // ── Preguntar publicar (solo si no está en modo watch ni --publish) ──
152
+ let publish = !!options.publish;
153
+ if (!publish && !options.watch) {
154
+ const { shouldPublish } = await inquirer.prompt([{
155
+ type: 'confirm',
156
+ name: 'shouldPublish',
157
+ message: '¿Publicar el proyecto después del deploy?',
158
+ default: true,
159
+ }]);
160
+ publish = shouldPublish;
161
+ }
162
+
163
+ // ── Modo normal ─────────────────────────────────────────
164
+ if (!options.watch) {
165
+ await ejecutarDeploy(uuid, dir, publish);
166
+ return;
167
+ }
168
+
169
+ // ── Modo --watch ─────────────────────────────────────────
170
+ console.log(chalk.bold.cyan('\n 👀 Modo watch activo. Ctrl+C para detener.\n'));
171
+ await ejecutarDeploy(uuid, dir, publish);
172
+
173
+ let debounce = null;
174
+ const changed = new Set();
175
+
176
+ const watcher = chokidar.watch(path.resolve(dir), {
177
+ ignored: (filePath) => {
178
+ const name = path.basename(filePath);
179
+ return debeIgnorar(name);
180
+ },
181
+ ignoreInitial: true,
182
+ usePolling: false,
183
+ awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
184
+ });
185
+
186
+ watcher.on('all', (event, filePath) => {
187
+ changed.add(filePath);
188
+ clearTimeout(debounce);
189
+ debounce = setTimeout(async () => {
190
+ const files = [...changed];
191
+ changed.clear();
192
+ const nombres = files
193
+ .map(f => path.relative(path.resolve(dir), f))
194
+ .join(', ');
195
+ console.log(chalk.gray(` [watch] ${event}: ${nombres}`));
196
+ await ejecutarDeploy(uuid, dir, false);
197
+ }, 300);
198
+ });
199
+
200
+ watcher.on('error', err => {
201
+ console.error(chalk.red(' [watch] Error: ' + err.message));
202
+ });
203
+ };
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const inquirer = require('inquirer');
4
+ const chalk = require('chalk');
5
+ const ora = require('ora');
6
+ const { saveConfig } = require('../utils/config');
7
+ const { login } = require('../utils/api');
8
+
9
+ module.exports = async function () {
10
+ console.log(chalk.bold.cyan('\n🔗 Entexto — Inicio de sesión\n'));
11
+
12
+ const { email, password } = await inquirer.prompt([
13
+ {
14
+ type: 'input',
15
+ name: 'email',
16
+ message: 'Email o usuario:',
17
+ validate: v => v.trim() ? true : 'Campo requerido',
18
+ },
19
+ {
20
+ type: 'password',
21
+ name: 'password',
22
+ message: 'Contraseña:',
23
+ mask: '*',
24
+ validate: v => v ? true : 'Campo requerido',
25
+ },
26
+ ]);
27
+
28
+ const spinner = ora('Autenticando...').start();
29
+
30
+ try {
31
+ const data = await login(email.trim(), password);
32
+ saveConfig({ token: data.token, user: data.user });
33
+ spinner.succeed(
34
+ chalk.green(`✓ Bienvenido, ${data.user.display_name || data.user.username}!`)
35
+ );
36
+ console.log(chalk.gray(' Sesión guardada en ~/.entexto/config.json\n'));
37
+ } catch (err) {
38
+ const msg = err.response?.data?.error || err.message;
39
+ spinner.fail(chalk.red('Error: ' + msg));
40
+ process.exit(1);
41
+ }
42
+ };
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const { getToken } = require('../utils/config');
6
+ const { getProjects } = require('../utils/api');
7
+
8
+ module.exports = async function () {
9
+ if (!getToken()) {
10
+ console.log(chalk.red('\nNo has iniciado sesión. Ejecuta: entexto login\n'));
11
+ process.exit(1);
12
+ }
13
+
14
+ const spinner = ora('Cargando proyectos...').start();
15
+
16
+ try {
17
+ const projects = await getProjects();
18
+ spinner.stop();
19
+
20
+ if (!projects.length) {
21
+ console.log(chalk.yellow('\nNo tienes proyectos todavía. Crea uno en entexto.com\n'));
22
+ return;
23
+ }
24
+
25
+ console.log(chalk.bold.cyan('\n📁 Tus proyectos:\n'));
26
+ console.log(
27
+ chalk.gray(' ' + 'ID'.padEnd(20) + 'Nombre'.padEnd(32) + 'Estado')
28
+ );
29
+ console.log(chalk.gray(' ' + '─'.repeat(72)));
30
+
31
+ projects.forEach(p => {
32
+ const id = chalk.cyan((p.project_uuid || String(p.id)).padEnd(20));
33
+ const nombre = (p.name || 'Sin nombre').slice(0, 30).padEnd(32);
34
+ const estado = p.is_published
35
+ ? chalk.green('📡 Publicado') + chalk.gray(` /p/${p.publish_slug}`)
36
+ : chalk.yellow('🔒 Privado');
37
+ console.log(' ' + id + nombre + estado);
38
+ });
39
+
40
+ console.log('');
41
+ } catch (err) {
42
+ const msg = err.response?.data?.error || err.message;
43
+ spinner.fail(chalk.red('Error: ' + msg));
44
+ process.exit(1);
45
+ }
46
+ };
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const axios = require('axios');
4
+ const { getToken, getBaseUrl } = require('./config');
5
+
6
+ function client() {
7
+ const token = getToken();
8
+ const baseURL = getBaseUrl();
9
+ return axios.create({
10
+ baseURL,
11
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
12
+ timeout: 60000,
13
+ });
14
+ }
15
+
16
+ async function login(email, password) {
17
+ const baseURL = getBaseUrl();
18
+ const res = await axios.post(`${baseURL}/api/auth/login`, { email, password });
19
+ return res.data;
20
+ }
21
+
22
+ async function getProjects() {
23
+ const res = await client().get('/api/cli/projects');
24
+ return res.data.projects;
25
+ }
26
+
27
+ async function deployFiles(uuid, formData, publish) {
28
+ const res = await client().post(
29
+ `/api/cli/deploy/${uuid}`,
30
+ formData,
31
+ {
32
+ headers: {
33
+ ...formData.getHeaders(),
34
+ ...(publish ? { 'x-entexto-publish': 'true' } : {}),
35
+ },
36
+ maxBodyLength: Infinity,
37
+ maxContentLength: Infinity,
38
+ }
39
+ );
40
+ return res.data;
41
+ }
42
+
43
+ async function deployWithPublish(uuid, formData) {
44
+ const res = await client().post(
45
+ `/api/cli/deploy/${uuid}`,
46
+ // agregar campo publish al form
47
+ (() => { formData.append('publish', 'true'); return formData; })(),
48
+ {
49
+ headers: formData.getHeaders(),
50
+ maxBodyLength: Infinity,
51
+ maxContentLength: Infinity,
52
+ }
53
+ );
54
+ return res.data;
55
+ }
56
+
57
+ async function publishProject(uuid) {
58
+ const res = await client().post(`/api/cli/publish/${uuid}`);
59
+ return res.data;
60
+ }
61
+
62
+ module.exports = { login, getProjects, deployFiles, deployWithPublish, publishProject };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const CONFIG_DIR = path.join(os.homedir(), '.entexto');
8
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
+
10
+ function getConfig() {
11
+ try {
12
+ if (!fs.existsSync(CONFIG_FILE)) return {};
13
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
14
+ } catch {
15
+ return {};
16
+ }
17
+ }
18
+
19
+ function saveConfig(data) {
20
+ if (!fs.existsSync(CONFIG_DIR)) {
21
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
22
+ }
23
+ const current = getConfig();
24
+ const merged = { ...current, ...data };
25
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2));
26
+ }
27
+
28
+ function getToken() {
29
+ return getConfig().token || null;
30
+ }
31
+
32
+ function getBaseUrl() {
33
+ return getConfig().baseUrl || 'https://entexto.com';
34
+ }
35
+
36
+ module.exports = { getConfig, saveConfig, getToken, getBaseUrl };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "entexto-cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI oficial de Entexto — Deploy y gestión de proyectos desde tu terminal",
5
+ "main": "lib/index.js",
6
+ "bin": {
7
+ "entexto": "bin/entexto.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "lib/"
12
+ ],
13
+ "scripts": {
14
+ "test": "node bin/entexto.js --version"
15
+ },
16
+ "keywords": [
17
+ "entexto",
18
+ "cli",
19
+ "deploy",
20
+ "hosting",
21
+ "vfs"
22
+ ],
23
+ "author": "reyjosias",
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "axios": "^1.6.0",
27
+ "chalk": "^4.1.2",
28
+ "chokidar": "^3.5.3",
29
+ "commander": "^11.0.0",
30
+ "form-data": "^4.0.0",
31
+ "inquirer": "^8.2.6",
32
+ "ora": "^5.4.1"
33
+ }
34
+ }