@create-lft-app/cli 1.0.3 → 1.0.5

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.md ADDED
@@ -0,0 +1,120 @@
1
+ # @create-lft-app/cli
2
+
3
+ CLI para crear proyectos LFT con Next.js, GitHub, Supabase y Jira integrados.
4
+
5
+ ## Instalacion
6
+
7
+ ```bash
8
+ npm install -g @create-lft-app/cli
9
+ ```
10
+
11
+ ## Configuracion
12
+
13
+ Antes de usar el CLI, configura las credenciales en un archivo `.env` en el directorio del paquete:
14
+
15
+ ```bash
16
+ # ===========================================
17
+ # GITHUB
18
+ # ===========================================
19
+ # Token: https://github.com/settings/tokens/new
20
+ # Permisos necesarios: repo, read:org, admin:org (para crear repos en org)
21
+ LFT_GITHUB_TOKEN=ghp_xxxxxxxxxxxx
22
+ LFT_GITHUB_USERNAME=tu-usuario
23
+ LFT_GITHUB_ORG=tu-organizacion # Opcional, deja vacio para cuenta personal
24
+
25
+ # ===========================================
26
+ # SUPABASE
27
+ # ===========================================
28
+ # Token: https://supabase.com/dashboard/account/tokens
29
+ LFT_SUPABASE_TOKEN=sbp_xxxxxxxxxxxx
30
+ # Org ID: https://supabase.com/dashboard/org/[ORG_ID]/settings
31
+ LFT_SUPABASE_ORG_ID=xxxxxxxxxxxx
32
+ LFT_SUPABASE_REGION=us-east-1 # Opcional
33
+
34
+ # ===========================================
35
+ # JIRA
36
+ # ===========================================
37
+ # Token: https://id.atlassian.com/manage-profile/security/api-tokens
38
+ LFT_JIRA_TOKEN=ATATT3xxxxxxxxxxx
39
+ LFT_JIRA_EMAIL=tu-email@ejemplo.com
40
+ LFT_JIRA_DOMAIN=tu-dominio.atlassian.net
41
+ ```
42
+
43
+ ## Uso
44
+
45
+ ### Crear un proyecto nuevo
46
+
47
+ ```bash
48
+ create-lft-app mi-proyecto
49
+ ```
50
+
51
+ ### Crear proyecto con confirmacion automatica
52
+
53
+ ```bash
54
+ create-lft-app mi-proyecto -y
55
+ ```
56
+
57
+ ### Opciones disponibles
58
+
59
+ ```bash
60
+ create-lft-app [nombre-proyecto] [opciones]
61
+
62
+ Opciones:
63
+ -V, --version Muestra la version
64
+ --skip-github No crear repositorio en GitHub
65
+ --skip-supabase No crear proyecto en Supabase
66
+ --skip-jira No crear proyecto en Jira
67
+ --skip-git No inicializar git ni hacer push
68
+ -y, --yes Aceptar todas las confirmaciones
69
+ -h, --help Muestra ayuda
70
+ ```
71
+
72
+ ### Ejemplos
73
+
74
+ ```bash
75
+ # Crear proyecto completo
76
+ create-lft-app mi-app
77
+
78
+ # Crear solo con GitHub y Next.js (sin Supabase ni Jira)
79
+ create-lft-app mi-app --skip-supabase --skip-jira
80
+
81
+ # Crear proyecto local sin servicios externos
82
+ create-lft-app mi-app --skip-github --skip-supabase --skip-jira --skip-git
83
+ ```
84
+
85
+ ## Que crea el CLI
86
+
87
+ 1. **GitHub**: Repositorio privado en tu cuenta u organizacion
88
+ 2. **Supabase**: Proyecto con base de datos PostgreSQL
89
+ 3. **Jira**: Proyecto de software con template Scrum
90
+ 4. **Next.js**: Aplicacion con:
91
+ - TypeScript
92
+ - Tailwind CSS
93
+ - App Router
94
+ - ESLint
95
+ - Turbopack
96
+
97
+ ## Recursos existentes
98
+
99
+ Si alguno de los recursos ya existe (repo, proyecto Supabase, proyecto Jira, o directorio), el CLI lo detecta y continua sin error, mostrando un checkmark con "(ya existe)".
100
+
101
+ ## Desarrollo local
102
+
103
+ ```bash
104
+ # Clonar repositorio
105
+ git clone https://github.com/Test-Boiler/create-lft-app.git
106
+ cd create-lft-app
107
+
108
+ # Instalar dependencias
109
+ npm install
110
+
111
+ # Build
112
+ npm run build
113
+
114
+ # Ejecutar localmente
115
+ node dist/bin/cli.js mi-proyecto
116
+ ```
117
+
118
+ ## Licencia
119
+
120
+ MIT
package/dist/bin/cli.js CHANGED
@@ -8,21 +8,27 @@ import path4 from "path";
8
8
  import { confirm } from "@inquirer/prompts";
9
9
 
10
10
  // src/config/static-config.ts
11
+ import { config } from "dotenv";
12
+ import { fileURLToPath } from "url";
13
+ import { dirname, resolve } from "path";
14
+ var __filename = fileURLToPath(import.meta.url);
15
+ var __dirname = dirname(__filename);
16
+ config({ path: resolve(__dirname, "../../.env"), quiet: true });
11
17
  var STATIC_CONFIG = {
12
18
  github: {
13
- token: "__GITHUB_TOKEN__",
14
- username: "__GITHUB_USERNAME__",
15
- org: "__GITHUB_ORG__"
19
+ token: process.env.LFT_GITHUB_TOKEN || "",
20
+ username: process.env.LFT_GITHUB_USERNAME || "",
21
+ org: process.env.LFT_GITHUB_ORG || ""
16
22
  },
17
23
  supabase: {
18
- token: "__SUPABASE_TOKEN__",
19
- orgId: "__SUPABASE_ORG_ID__",
20
- region: "us-east-1"
24
+ token: process.env.LFT_SUPABASE_TOKEN || "",
25
+ orgId: process.env.LFT_SUPABASE_ORG_ID || "",
26
+ region: process.env.LFT_SUPABASE_REGION || "us-east-1"
21
27
  },
22
28
  jira: {
23
- email: "__JIRA_EMAIL__",
24
- token: "__JIRA_TOKEN__",
25
- domain: "__JIRA_DOMAIN__"
29
+ email: process.env.LFT_JIRA_EMAIL || "",
30
+ token: process.env.LFT_JIRA_TOKEN || "",
31
+ domain: process.env.LFT_JIRA_DOMAIN || ""
26
32
  }
27
33
  };
28
34
 
@@ -53,7 +59,7 @@ async function loadConfig() {
53
59
  };
54
60
  }
55
61
  async function hasConfig() {
56
- return STATIC_CONFIG.github.token !== "__GITHUB_TOKEN__" && STATIC_CONFIG.supabase.token !== "__SUPABASE_TOKEN__" && STATIC_CONFIG.jira.token !== "__JIRA_TOKEN__";
62
+ return STATIC_CONFIG.github.token !== "" && STATIC_CONFIG.supabase.token !== "" && STATIC_CONFIG.jira.token !== "";
57
63
  }
58
64
 
59
65
  // src/services/github.ts
@@ -79,10 +85,67 @@ async function withSpinner(text, fn, successText) {
79
85
  }
80
86
  }
81
87
 
88
+ // src/ui/logger.ts
89
+ import chalk from "chalk";
90
+ var logger = {
91
+ info: (message) => {
92
+ console.log(chalk.blue("\u2139"), message);
93
+ },
94
+ success: (message) => {
95
+ console.log(chalk.green("\u2714"), message);
96
+ },
97
+ warning: (message) => {
98
+ console.log(chalk.yellow("\u26A0"), message);
99
+ },
100
+ error: (message) => {
101
+ console.log(chalk.red("\u2716"), message);
102
+ },
103
+ step: (step, total, message) => {
104
+ console.log(chalk.cyan(`[${step}/${total}]`), message);
105
+ },
106
+ newLine: () => {
107
+ console.log();
108
+ },
109
+ divider: () => {
110
+ console.log(chalk.gray("\u2500".repeat(50)));
111
+ },
112
+ title: (message) => {
113
+ console.log(chalk.bold.white(message));
114
+ },
115
+ subtitle: (message) => {
116
+ console.log(chalk.gray(message));
117
+ },
118
+ link: (label, url) => {
119
+ console.log(` ${chalk.gray(label + ":")} ${chalk.cyan.underline(url)}`);
120
+ },
121
+ list: (items) => {
122
+ items.forEach((item) => {
123
+ console.log(chalk.gray(" \u2022"), item);
124
+ });
125
+ },
126
+ table: (rows) => {
127
+ const maxLabelLength = Math.max(...rows.map((r) => r.label.length));
128
+ rows.forEach(({ label, value }) => {
129
+ const paddedLabel = label.padEnd(maxLabelLength);
130
+ console.log(` ${chalk.gray(paddedLabel)} ${value}`);
131
+ });
132
+ }
133
+ };
134
+
82
135
  // src/services/github.ts
83
- async function createGitHubRepo(projectName, config) {
84
- const octokit = new Octokit({ auth: config.credentials.github.token });
85
- const org = config.defaults.githubOrg;
136
+ async function createGitHubRepo(projectName, config2) {
137
+ const octokit = new Octokit({ auth: config2.credentials.github.token });
138
+ const org = config2.defaults.githubOrg;
139
+ const owner = org || config2.credentials.github.username;
140
+ try {
141
+ const existing = await octokit.rest.repos.get({
142
+ owner,
143
+ repo: projectName
144
+ });
145
+ logger.success(`GitHub: ${owner}/${projectName} (ya existe)`);
146
+ return existing.data.html_url;
147
+ } catch {
148
+ }
86
149
  return withSpinner(
87
150
  "Creando repositorio en GitHub...",
88
151
  async () => {
@@ -105,7 +168,7 @@ async function createGitHubRepo(projectName, config) {
105
168
  }
106
169
  return repo.data.html_url;
107
170
  },
108
- `Repositorio creado: ${org || config.credentials.github.username}/${projectName}`
171
+ `GitHub: ${owner}/${projectName}`
109
172
  );
110
173
  }
111
174
 
@@ -124,7 +187,7 @@ async function waitForProjectReady(projectId, token, maxAttempts = 60) {
124
187
  return;
125
188
  }
126
189
  }
127
- await new Promise((resolve) => setTimeout(resolve, 5e3));
190
+ await new Promise((resolve2) => setTimeout(resolve2, 5e3));
128
191
  spinner.text = `Provisionando base de datos... (${Math.floor((i + 1) * 5 / 60)}min ${(i + 1) * 5 % 60}s)`;
129
192
  }
130
193
  spinner.fail("Timeout esperando a que el proyecto est\xE9 listo");
@@ -145,10 +208,28 @@ async function getProjectApiKeys(projectId, token) {
145
208
  }
146
209
  return { anonKey, serviceKey };
147
210
  }
148
- async function createSupabaseProject(projectName, config) {
149
- const token = config.credentials.supabase.accessToken;
150
- const orgId = config.credentials.supabase.organizationId;
151
- const region = config.defaults.supabaseRegion;
211
+ async function findExistingProject(projectName, token) {
212
+ const response = await fetch(`${SUPABASE_API_URL}/projects`, {
213
+ headers: { Authorization: `Bearer ${token}` }
214
+ });
215
+ if (!response.ok) return null;
216
+ const projects = await response.json();
217
+ return projects.find((p) => p.name === projectName) || null;
218
+ }
219
+ async function createSupabaseProject(projectName, config2) {
220
+ const token = config2.credentials.supabase.accessToken;
221
+ const orgId = config2.credentials.supabase.organizationId;
222
+ const region = config2.defaults.supabaseRegion;
223
+ const existing = await findExistingProject(projectName, token);
224
+ if (existing) {
225
+ logger.success(`Supabase: ${projectName} (ya existe)`);
226
+ const { anonKey: anonKey2, serviceKey: serviceKey2 } = await getProjectApiKeys(existing.id, token);
227
+ return {
228
+ url: `https://${existing.id}.supabase.co`,
229
+ anonKey: anonKey2,
230
+ serviceKey: serviceKey2
231
+ };
232
+ }
152
233
  const dbPassword = generateSecurePassword();
153
234
  const project = await withSpinner(
154
235
  "Creando proyecto en Supabase...",
@@ -193,8 +274,6 @@ function generateSecurePassword() {
193
274
  }
194
275
 
195
276
  // src/utils/validation.ts
196
- import fs from "fs";
197
- import path from "path";
198
277
  function validateProjectName(name) {
199
278
  if (!name || name.trim() === "") {
200
279
  return { valid: false, error: "El nombre del proyecto no puede estar vac\xEDo" };
@@ -218,10 +297,6 @@ function validateProjectName(name) {
218
297
  if (name.length > 50) {
219
298
  return { valid: false, error: "El nombre no puede tener m\xE1s de 50 caracteres" };
220
299
  }
221
- const projectPath = path.resolve(process.cwd(), name);
222
- if (fs.existsSync(projectPath)) {
223
- return { valid: false, error: `El directorio "${name}" ya existe` };
224
- }
225
300
  return { valid: true };
226
301
  }
227
302
  function generateJiraKey(projectName) {
@@ -230,10 +305,29 @@ function generateJiraKey(projectName) {
230
305
  }
231
306
 
232
307
  // src/services/jira.ts
233
- async function createJiraProject(projectName, config) {
234
- const { email, apiToken, domain } = config.credentials.jira;
308
+ async function findExistingJiraProject(projectName, auth, domain) {
309
+ const response = await fetch(
310
+ `https://${domain}/rest/api/3/project/search?query=${encodeURIComponent(projectName)}`,
311
+ {
312
+ headers: {
313
+ Authorization: `Basic ${auth}`,
314
+ "Content-Type": "application/json"
315
+ }
316
+ }
317
+ );
318
+ if (!response.ok) return null;
319
+ const data = await response.json();
320
+ return data.values.find((p) => p.name === projectName) || null;
321
+ }
322
+ async function createJiraProject(projectName, config2) {
323
+ const { email, apiToken, domain } = config2.credentials.jira;
235
324
  const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
236
325
  const projectKey = generateJiraKey(projectName);
326
+ const existing = await findExistingJiraProject(projectName, auth, domain);
327
+ if (existing) {
328
+ logger.success(`Jira: ${existing.key} (ya existe)`);
329
+ return `https://${domain}/browse/${existing.key}`;
330
+ }
237
331
  return withSpinner(
238
332
  "Creando proyecto en Jira...",
239
333
  async () => {
@@ -290,13 +384,20 @@ async function createJiraProject(projectName, config) {
290
384
  const project = await response.json();
291
385
  return `https://${domain}/browse/${project.key}`;
292
386
  },
293
- `Proyecto Jira creado: ${projectKey}`
387
+ `Proyecto Jira: ${projectKey}`
294
388
  );
295
389
  }
296
390
 
297
391
  // src/steps/scaffold-nextjs.ts
298
392
  import { execa } from "execa";
393
+ import { existsSync } from "fs";
394
+ import path from "path";
299
395
  async function scaffoldNextJs(projectName, projectPath) {
396
+ const targetDir = path.join(process.cwd(), projectName);
397
+ if (existsSync(targetDir)) {
398
+ logger.success(`Next.js: ${projectName} (ya existe)`);
399
+ return;
400
+ }
300
401
  await withSpinner(
301
402
  "Inicializando proyecto Next.js...",
302
403
  async () => {
@@ -311,27 +412,28 @@ async function scaffoldNextJs(projectName, projectPath) {
311
412
  "--src-dir",
312
413
  "--import-alias",
313
414
  "@/*",
314
- "--use-npm"
415
+ "--use-npm",
416
+ "--yes"
315
417
  ], {
316
418
  cwd: process.cwd(),
317
419
  stdio: "pipe"
318
420
  });
319
421
  },
320
- "Proyecto Next.js inicializado"
422
+ `Next.js: ${projectName}`
321
423
  );
322
424
  }
323
425
 
324
426
  // src/steps/copy-template.ts
325
427
  import { cp, mkdir, readFile, writeFile } from "fs/promises";
326
428
  import path2 from "path";
327
- import { fileURLToPath } from "url";
328
- var __filename2 = fileURLToPath(import.meta.url);
329
- var __dirname2 = path2.dirname(__filename2);
429
+ import { fileURLToPath as fileURLToPath2 } from "url";
430
+ var __filename3 = fileURLToPath2(import.meta.url);
431
+ var __dirname3 = path2.dirname(__filename3);
330
432
  async function copyTemplate(projectPath) {
331
433
  await withSpinner(
332
434
  "Copiando template LFT...",
333
435
  async () => {
334
- const templatesDir = path2.join(__dirname2, "..", "..", "templates");
436
+ const templatesDir = path2.join(__dirname3, "..", "..", "templates");
335
437
  const srcDir = path2.join(projectPath, "src");
336
438
  await cp(
337
439
  path2.join(templatesDir, "components", "ui"),
@@ -519,53 +621,6 @@ async function setupGit(projectPath, remoteUrl) {
519
621
  );
520
622
  }
521
623
 
522
- // src/ui/logger.ts
523
- import chalk from "chalk";
524
- var logger = {
525
- info: (message) => {
526
- console.log(chalk.blue("\u2139"), message);
527
- },
528
- success: (message) => {
529
- console.log(chalk.green("\u2714"), message);
530
- },
531
- warning: (message) => {
532
- console.log(chalk.yellow("\u26A0"), message);
533
- },
534
- error: (message) => {
535
- console.log(chalk.red("\u2716"), message);
536
- },
537
- step: (step, total, message) => {
538
- console.log(chalk.cyan(`[${step}/${total}]`), message);
539
- },
540
- newLine: () => {
541
- console.log();
542
- },
543
- divider: () => {
544
- console.log(chalk.gray("\u2500".repeat(50)));
545
- },
546
- title: (message) => {
547
- console.log(chalk.bold.white(message));
548
- },
549
- subtitle: (message) => {
550
- console.log(chalk.gray(message));
551
- },
552
- link: (label, url) => {
553
- console.log(` ${chalk.gray(label + ":")} ${chalk.cyan.underline(url)}`);
554
- },
555
- list: (items) => {
556
- items.forEach((item) => {
557
- console.log(chalk.gray(" \u2022"), item);
558
- });
559
- },
560
- table: (rows) => {
561
- const maxLabelLength = Math.max(...rows.map((r) => r.label.length));
562
- rows.forEach(({ label, value }) => {
563
- const paddedLabel = label.padEnd(maxLabelLength);
564
- console.log(` ${chalk.gray(paddedLabel)} ${value}`);
565
- });
566
- }
567
- };
568
-
569
624
  // src/ui/banner.ts
570
625
  import boxen from "boxen";
571
626
  import chalk2 from "chalk";
@@ -625,19 +680,19 @@ async function createProject(projectName, options = {}) {
625
680
  logger.warning('No se encontr\xF3 configuraci\xF3n. Ejecuta "create-lft-app config" primero.');
626
681
  throw new Error("Configuraci\xF3n no encontrada");
627
682
  }
628
- const config = await loadConfig();
683
+ const config2 = await loadConfig();
629
684
  logger.newLine();
630
685
  logger.title("Resumen de recursos a crear:");
631
686
  logger.newLine();
632
687
  const resources = [];
633
688
  if (!options.skipGithub) {
634
- resources.push({ label: "GitHub", value: `${config.defaults.githubOrg || config.credentials.github.username}/${projectName} (privado)` });
689
+ resources.push({ label: "GitHub", value: `${config2.defaults.githubOrg || config2.credentials.github.username}/${projectName} (privado)` });
635
690
  }
636
691
  if (!options.skipSupabase) {
637
- resources.push({ label: "Supabase", value: `${projectName} en ${config.defaults.supabaseRegion}` });
692
+ resources.push({ label: "Supabase", value: `${projectName} en ${config2.defaults.supabaseRegion}` });
638
693
  }
639
694
  if (!options.skipJira) {
640
- resources.push({ label: "Jira", value: `Proyecto "${projectName}" en ${config.credentials.jira.domain}` });
695
+ resources.push({ label: "Jira", value: `Proyecto "${projectName}" en ${config2.credentials.jira.domain}` });
641
696
  }
642
697
  resources.push({ label: "Next.js", value: "App Router + TypeScript + Tailwind + Dashboard" });
643
698
  logger.table(resources);
@@ -660,14 +715,14 @@ async function createProject(projectName, options = {}) {
660
715
  const externalTasks = [];
661
716
  if (!options.skipGithub) {
662
717
  externalTasks.push(
663
- createGitHubRepo(projectName, config).then((url) => {
718
+ createGitHubRepo(projectName, config2).then((url) => {
664
719
  urls.github = url;
665
720
  })
666
721
  );
667
722
  }
668
723
  if (!options.skipSupabase) {
669
724
  externalTasks.push(
670
- createSupabaseProject(projectName, config).then((result) => {
725
+ createSupabaseProject(projectName, config2).then((result) => {
671
726
  urls.supabase = result.url;
672
727
  supabaseKeys = result;
673
728
  })
@@ -675,7 +730,7 @@ async function createProject(projectName, options = {}) {
675
730
  }
676
731
  if (!options.skipJira) {
677
732
  externalTasks.push(
678
- createJiraProject(projectName, config).then((url) => {
733
+ createJiraProject(projectName, config2).then((url) => {
679
734
  urls.jira = url;
680
735
  })
681
736
  );