create-canaima-app 1.0.0 → 1.0.3

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/index.js +229 -70
  2. package/package.json +6 -3
package/index.js CHANGED
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import prompts from 'prompts';
4
- import { promises as fs } from 'fs';
4
+ import { promises as fs, existsSync } from 'fs';
5
5
  import path from 'path';
6
6
  import { fileURLToPath } from 'url';
7
+ import { exec, spawn } from 'child_process';
8
+ import { promisify } from 'util';
9
+
10
+ const execPromise = promisify(exec);
7
11
 
8
12
  // Resolver __dirname en módulos ES
9
13
  const __filename = fileURLToPath(import.meta.url);
@@ -29,24 +33,143 @@ const TEMPLATES = {
29
33
  folder : 'tauri-materialize',
30
34
  },
31
35
  'tauri-material-tailwind': {
32
- label : 'Material Tailwind (Moderno / Utility-First)',
36
+ label : 'Material Tailwind (Moderno / Solo CSS)',
33
37
  folder : 'tauri-material-tailwind',
34
38
  },
35
39
  };
36
40
 
37
- function printBanner() {
38
- const banner = `
39
- ██████╗ █████╗ ███╗ ██╗ █████╗ ██╗███╗ ███╗ █████╗ ██████╗██╗ ██╗
40
- ██╔════╝██╔══██╗████╗ ██║██╔══██╗██║████╗ ████║██╔══██╗ ██╔════╝██║ ██║
41
- ██║ ███████║██╔██╗ ██║███████║██║██╔████╔██║███████║ ██║ ██║ ██║
42
- ██║ ██╔══██║██║╚██╗██║██╔══██║██║██║╚██╔╝██║██╔══██║ ██║ ██║ ██║
43
- ╚██████╗██║ ██║██║ ╚████║██║ ██║██║██║ ╚═╝ ██║██║ ██║ ╚██████╗███████╗██║
44
- ╚═════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝
45
- `;
46
- console.log(c.cyan(banner));
47
- console.log(c.bold(c.white(' 🚀 Generador de proyectos CanaimaApp (Tauri + Vue + Rust)\n')));
41
+ // ==========================================
42
+ // SISTEMA DE ANIMACIÓN DEL COATÍ (TUI)
43
+ // ==========================================
44
+ class CoatiAnimator {
45
+ constructor(messages) {
46
+ this.messages = messages;
47
+ this.interval = null;
48
+ this.frame = 0;
49
+ this.msgIndex = 0;
50
+ // Ocultar el cursor para que se vea limpio
51
+ process.stdout.write('\x1b[?25l');
52
+ }
53
+
54
+ start() {
55
+ this.render(); // Render inicial
56
+ this.interval = setInterval(() => {
57
+ this.frame++;
58
+ if (this.frame % 8 === 0) {
59
+ // Cambiar mensaje cada ciertos frames
60
+ this.msgIndex = (this.msgIndex + 1) % this.messages.length;
61
+ }
62
+ this.render();
63
+ }, 400); // Velocidad del parpadeo
64
+ }
65
+
66
+ render() {
67
+ // Subir 3 líneas y limpiar para redibujar
68
+ if (this.frame > 0) {
69
+ process.stdout.write('\x1b[3A\x1b[0J');
70
+ }
71
+
72
+ const isBlinking = this.frame % 2 !== 0; // Parpadea intercalado
73
+ const face = isBlinking ? '( -_- )' : '( ._. )';
74
+ const msg = this.messages[this.msgIndex];
75
+
76
+ // Borde de la caja dinámico
77
+ const line = '─'.repeat(msg.length + 2);
78
+
79
+ console.log(c.cyan(` ^---^ `) + c.dim(`╭${line}╮`));
80
+ console.log(c.cyan(` ${face} `) + c.dim(`│ `) + c.bold(c.green(msg)) + c.dim(` │`));
81
+ console.log(` ` + c.dim(`╰${line}╯`));
82
+ }
83
+
84
+ stop(finalMessage = null) {
85
+ if (this.interval) clearInterval(this.interval);
86
+ // Mostrar cursor de nuevo
87
+ process.stdout.write('\x1b[?25h');
88
+
89
+ if (finalMessage) {
90
+ process.stdout.write('\x1b[3A\x1b[0J');
91
+ const line = '─'.repeat(finalMessage.length + 2);
92
+ console.log(c.cyan(` ^---^ `) + c.dim(`╭${line}╮`));
93
+ console.log(c.cyan(` ( ^_^ ) `) + c.dim(`│ `) + c.bold(c.white(finalMessage)) + c.dim(` │`));
94
+ console.log(` ` + c.dim(`╰${line}╯\n`));
95
+ }
96
+ }
48
97
  }
49
98
 
99
+ // ==========================================
100
+ // VERIFICACIÓN DE DEPENDENCIAS (Debian/Rust)
101
+ // ==========================================
102
+ async function checkAndInstallDependencies() {
103
+ const isDebian = existsSync('/etc/debian_version');
104
+ let missingApt = false;
105
+ let missingRust = false;
106
+
107
+ // Verificar Rust
108
+ try {
109
+ await execPromise('command -v cargo');
110
+ } catch {
111
+ missingRust = true;
112
+ }
113
+
114
+ // Verificar librerías solo si es Debian/Canaima
115
+ if (isDebian) {
116
+ try {
117
+ // Usamos pkg-config o dpkg para ver si está la librería principal
118
+ await execPromise('dpkg -l libwebkit2gtk-4.1-dev');
119
+ } catch {
120
+ missingApt = true;
121
+ }
122
+ }
123
+
124
+ if (!missingApt && !missingRust) return; // Todo listo
125
+
126
+ console.log(c.yellow('\n ⚠ Faltan dependencias necesarias para compilar Tauri.'));
127
+
128
+ if (missingApt) {
129
+ console.log(c.dim(' Se solicitará tu contraseña para instalar paquetes del sistema...'));
130
+ // Pedimos sudo por adelantado para que no rompa nuestra interfaz ASCII
131
+ try {
132
+ await execPromise('sudo -v', { stdio: 'inherit' });
133
+ } catch (err) {
134
+ console.log(c.red(' ✖ No se pudo obtener permisos de administrador.'));
135
+ process.exit(1);
136
+ }
137
+ }
138
+
139
+ const anim = new CoatiAnimator([
140
+ "Revisando el sistema...",
141
+ "Canaima GNU/Linux: puro talento local.",
142
+ "Instalando magia negra (y librerías)...",
143
+ "Estabilidad y rendimiento basados en Debian.",
144
+ "Descargando herramientas de Rust...",
145
+ "¡Ya casi termino!"
146
+ ]);
147
+
148
+ anim.start();
149
+
150
+ try {
151
+ if (missingApt) {
152
+ const aptCmd = `sudo apt update && sudo DEBIAN_FRONTEND=noninteractive apt install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev`;
153
+ await execPromise(aptCmd);
154
+ }
155
+
156
+ if (missingRust) {
157
+ await execPromise(`curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y`);
158
+ // Aplicar el env al proceso actual de Node por si acaso
159
+ process.env.PATH = `${process.env.HOME}/.cargo/bin:${process.env.PATH}`;
160
+ }
161
+
162
+ anim.stop("¡Dependencias instaladas con éxito!");
163
+ } catch (err) {
164
+ anim.stop("¡Ups! Algo falló.");
165
+ console.error(c.red(`\n ✖ Error instalando dependencias: ${err.message}\n`));
166
+ process.exit(1);
167
+ }
168
+ }
169
+
170
+ // ==========================================
171
+ // FLUJO PRINCIPAL DE PREGUNTAS
172
+ // ==========================================
50
173
  async function askQuestions() {
51
174
  return await prompts([
52
175
  {
@@ -62,6 +185,14 @@ async function askQuestions() {
62
185
  message: '¿Qué framework de diseño prefieres?',
63
186
  choices: Object.entries(TEMPLATES).map(([value, { label }]) => ({ title: label, value })),
64
187
  },
188
+ {
189
+ type : 'toggle',
190
+ name : 'installNpm',
191
+ message: '¿Deseas instalar las dependencias de NPM ahora mismo?',
192
+ initial: true,
193
+ active : 'Sí',
194
+ inactive: 'No'
195
+ }
65
196
  ], {
66
197
  onCancel: () => {
67
198
  console.log(c.yellow('\n ⚠ Operación cancelada.\n'));
@@ -70,76 +201,93 @@ async function askQuestions() {
70
201
  });
71
202
  }
72
203
 
204
+ // ... [copyTemplate y personalizeProject se mantienen iguales] ...
73
205
  async function copyTemplate(folderName, targetDir) {
74
- console.log(c.cyan(`\n 📦 Generando archivos del proyecto...`));
206
+ const templateDir = path.join(__dirname, folderName);
207
+ await fs.cp(templateDir, targetDir, {
208
+ recursive: true,
209
+ filter: (source) => {
210
+ const basename = path.basename(source);
211
+ return !['node_modules', 'target', '.git', 'package-lock.json', 'dist'].includes(basename);
212
+ }
213
+ });
75
214
 
76
- const templateDir = path.join(__dirname, folderName);
77
-
78
- await fs.cp(templateDir, targetDir, {
79
- recursive: true,
80
- // Ignoramos carpetas pesadas o de git si quedaron en tu entorno local
81
- filter: (source) => {
82
- const basename = path.basename(source);
83
- return !['node_modules', 'target', '.git', 'package-lock.json', 'dist'].includes(basename);
215
+ const npmignorePath = path.join(targetDir, '.npmignore');
216
+ if (true) {
217
+ try { await fs.rename(npmignorePath, path.join(targetDir, '.gitignore')); } catch (e) {}
84
218
  }
85
- });
86
-
87
- // NPM renombra .gitignore a .npmignore a veces. Esto lo soluciona si pasa.
88
- const npmignorePath = path.join(targetDir, '.npmignore');
89
- if (true) {
90
- try {
91
- await fs.rename(npmignorePath, path.join(targetDir, '.gitignore'));
92
- } catch (e) {} // Ignorar si no existe
93
- }
94
-
95
- console.log(c.green(' ✔ Archivos base copiados.'));
96
219
  }
97
-
220
+
98
221
  async function personalizeProject(targetDir, projectName) {
99
- console.log(c.cyan(' 🔧 Configurando dependencias y Rust...'));
222
+ const pkgPath = path.join(targetDir, 'package.json');
223
+ try {
224
+ const pkgRaw = await fs.readFile(pkgPath, 'utf-8');
225
+ const pkgJson = JSON.parse(pkgRaw);
226
+ pkgJson.name = projectName;
227
+ await fs.writeFile(pkgPath, JSON.stringify(pkgJson, null, 2) + '\n');
228
+ } catch (err) {}
229
+
230
+ const cargoPath = path.join(targetDir, 'src-tauri', 'Cargo.toml');
231
+ try {
232
+ let cargo = await fs.readFile(cargoPath, 'utf-8');
233
+ cargo = cargo.replace(/^(name\s*=\s*)"[^"]*"/m, `$1"${projectName}"`);
234
+ await fs.writeFile(cargoPath, cargo);
235
+ } catch (err) {}
236
+
237
+ const tauriConfPath = path.join(targetDir, 'src-tauri', 'tauri.conf.json');
238
+ try {
239
+ const confRaw = await fs.readFile(tauriConfPath, 'utf-8');
240
+ const confJson = JSON.parse(confRaw);
241
+
242
+ if (confJson.productName !== undefined) confJson.productName = projectName;
243
+ if (confJson.identifier !== undefined) confJson.identifier = `com.canaima.${projectName}`;
244
+ if (confJson.bundle?.identifier) confJson.bundle.identifier = `com.canaima.${projectName}`;
245
+
246
+ await writeFile(tauriConfPath, JSON.stringify(confJson, null, 2) + '\n');
247
+ } catch (err) {}
248
+ }
100
249
 
101
- // 1. package.json
102
- const pkgPath = path.join(targetDir, 'package.json');
103
- try {
104
- const pkgRaw = await fs.readFile(pkgPath, 'utf-8');
105
- const pkgJson = JSON.parse(pkgRaw);
106
- pkgJson.name = projectName;
107
- await fs.writeFile(pkgPath, JSON.stringify(pkgJson, null, 2) + '\n');
108
- } catch (err) {}
109
-
110
- // 2. Cargo.toml
111
- const cargoPath = path.join(targetDir, 'src-tauri', 'Cargo.toml');
112
- try {
113
- let cargo = await fs.readFile(cargoPath, 'utf-8');
114
- cargo = cargo.replace(/^(name\s*=\s*)"[^"]*"/m, `$1"${projectName}"`);
115
- await fs.writeFile(cargoPath, cargo);
116
- } catch (err) {}
250
+ async function runNpmInstall(targetDir) {
251
+ const anim = new CoatiAnimator([
252
+ "Configurando los nodos...",
253
+ "Trayendo paquetes del ciberespacio...",
254
+ "¡Preparando el entorno Vue!",
255
+ "Haciendo que todo encaje perfecto..."
256
+ ]);
257
+
258
+ anim.start();
117
259
 
118
- // 3. tauri.conf.json
119
- const tauriConfPath = path.join(targetDir, 'src-tauri', 'tauri.conf.json');
120
- try {
121
- const confRaw = await fs.readFile(tauriConfPath, 'utf-8');
122
- const confJson = JSON.parse(confRaw);
123
-
124
- if (confJson.productName !== undefined) confJson.productName = projectName;
125
- if (confJson.identifier !== undefined) confJson.identifier = `com.canaima.${projectName}`;
126
-
127
- // Soporte Tauri v2
128
- if (confJson.bundle?.identifier) confJson.bundle.identifier = `com.canaima.${projectName}`;
129
-
130
- await fs.writeFile(tauriConfPath, JSON.stringify(confJson, null, 2) + '\n');
131
- } catch (err) {}
260
+ return new Promise((resolve) => {
261
+ // Usamos spawn en background para que no ensucie la TUI
262
+ const child = spawn('npm', ['install'], {
263
+ cwd: targetDir,
264
+ shell: true,
265
+ stdio: 'ignore'
266
+ });
132
267
 
133
- console.log(c.green(' ✔ Proyecto personalizado con éxito.'));
268
+ child.on('close', (code) => {
269
+ if (code !== 0) {
270
+ anim.stop("Mmm, hubo un problema con NPM.");
271
+ resolve(false);
272
+ } else {
273
+ anim.stop("¡NPM Install completado! Listo para la acción.");
274
+ resolve(true);
275
+ }
276
+ });
277
+ });
134
278
  }
135
279
 
136
280
  async function main() {
137
- printBanner();
281
+ console.log(); // Espacio inicial para limpieza
138
282
 
139
- const { projectName, templateType } = await askQuestions();
283
+ // 1. Revisar e instalar dependencias del sistema primero
284
+ await checkAndInstallDependencies();
285
+
286
+ // 2. Preguntas al usuario
287
+ const { projectName, templateType, installNpm } = await askQuestions();
140
288
  if (!projectName || !templateType) process.exit(1);
141
289
 
142
- const targetDir = path.resolve(process.cwd(), projectName);
290
+ const targetDir = resolve(process.cwd(), projectName);
143
291
  const template = TEMPLATES[templateType];
144
292
 
145
293
  try {
@@ -149,13 +297,21 @@ async function main() {
149
297
  } catch {}
150
298
 
151
299
  try {
300
+ console.log(c.cyan(`\n 📦 Generando archivos del proyecto...`));
152
301
  await copyTemplate(template.folder, targetDir);
153
302
  await personalizeProject(targetDir, projectName);
303
+ console.log(c.green(' ✔ Archivos base copiados y configurados.'));
304
+
305
+ // 3. Instalación de NPM si el usuario aceptó
306
+ if (installNpm) {
307
+ console.log();
308
+ await runNpmInstall(targetDir);
309
+ }
154
310
 
155
311
  console.log('\n' + c.bgGreen(c.white(' ✅ ¡Proyecto creado con éxito! ')));
156
312
  console.log(c.bold('\n Siguientes pasos:\n'));
157
313
  console.log(c.white(` ${c.cyan('$')} cd ${projectName}`));
158
- console.log(c.white(` ${c.cyan('$')} npm install`));
314
+ if (!installNpm) console.log(c.white(` ${c.cyan('$')} npm install`));
159
315
  console.log(c.white(` ${c.cyan('$')} npm run tauri dev`));
160
316
  console.log(c.dim('\n ¡A programar! 🦅\n'));
161
317
 
@@ -165,4 +321,7 @@ async function main() {
165
321
  }
166
322
  }
167
323
 
324
+ // Helper para path.resolve
325
+ const { resolve } = path;
326
+
168
327
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-canaima-app",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "CLI para scaffolding de proyectos de escritorio con Tauri, Vue 3 y Rust.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -24,5 +24,8 @@
24
24
  "template"
25
25
  ],
26
26
  "author": "Sam Urbina",
27
- "license": "ISC"
28
- }
27
+ "license": "ISC",
28
+ "dependencies": {
29
+ "prompts": "^2.4.2"
30
+ }
31
+ }