@create-lft-app/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.
Files changed (61) hide show
  1. package/dist/bin/cli.d.ts +2 -0
  2. package/dist/bin/cli.js +938 -0
  3. package/dist/bin/cli.js.map +1 -0
  4. package/dist/src/index.d.ts +10 -0
  5. package/dist/src/index.js +669 -0
  6. package/dist/src/index.js.map +1 -0
  7. package/package.json +60 -0
  8. package/templates/app/auth/login/page.tsx +153 -0
  9. package/templates/app/dashboard/page.tsx +102 -0
  10. package/templates/app/globals.css +68 -0
  11. package/templates/app/layout.tsx +40 -0
  12. package/templates/app/page.tsx +5 -0
  13. package/templates/components/dashboard/widget.tsx +113 -0
  14. package/templates/components/layout/admin-midday-sidebar.tsx +247 -0
  15. package/templates/components/layout/admin-sidebar.tsx +146 -0
  16. package/templates/components/layout/header.tsx +71 -0
  17. package/templates/components/layout/impersonation-banner.tsx +36 -0
  18. package/templates/components/layout/main-content.tsx +28 -0
  19. package/templates/components/layout/midday-sidebar.tsx +381 -0
  20. package/templates/components/layout/nav-user.tsx +108 -0
  21. package/templates/components/layout/page-header.tsx +95 -0
  22. package/templates/components/layout/sidebar-context.tsx +33 -0
  23. package/templates/components/layout/sidebar.tsx +194 -0
  24. package/templates/components/layout/suspension-banner.tsx +21 -0
  25. package/templates/components/ui/accordion.tsx +58 -0
  26. package/templates/components/ui/alert-dialog.tsx +165 -0
  27. package/templates/components/ui/alert.tsx +66 -0
  28. package/templates/components/ui/avatar.tsx +55 -0
  29. package/templates/components/ui/badge.tsx +50 -0
  30. package/templates/components/ui/button.tsx +89 -0
  31. package/templates/components/ui/calendar.tsx +220 -0
  32. package/templates/components/ui/card.tsx +89 -0
  33. package/templates/components/ui/checkbox.tsx +38 -0
  34. package/templates/components/ui/collapsible.tsx +33 -0
  35. package/templates/components/ui/command.tsx +196 -0
  36. package/templates/components/ui/dialog.tsx +153 -0
  37. package/templates/components/ui/dropdown-menu.tsx +280 -0
  38. package/templates/components/ui/form.tsx +171 -0
  39. package/templates/components/ui/icons.tsx +167 -0
  40. package/templates/components/ui/input.tsx +28 -0
  41. package/templates/components/ui/label.tsx +25 -0
  42. package/templates/components/ui/popover.tsx +59 -0
  43. package/templates/components/ui/progress.tsx +32 -0
  44. package/templates/components/ui/radio-group.tsx +45 -0
  45. package/templates/components/ui/scroll-area.tsx +63 -0
  46. package/templates/components/ui/select.tsx +208 -0
  47. package/templates/components/ui/separator.tsx +28 -0
  48. package/templates/components/ui/sheet.tsx +146 -0
  49. package/templates/components/ui/sidebar.tsx +726 -0
  50. package/templates/components/ui/skeleton.tsx +15 -0
  51. package/templates/components/ui/slider.tsx +58 -0
  52. package/templates/components/ui/sonner.tsx +47 -0
  53. package/templates/components/ui/spinner.tsx +27 -0
  54. package/templates/components/ui/submit-button.tsx +47 -0
  55. package/templates/components/ui/switch.tsx +31 -0
  56. package/templates/components/ui/table.tsx +120 -0
  57. package/templates/components/ui/tabs.tsx +75 -0
  58. package/templates/components/ui/textarea.tsx +26 -0
  59. package/templates/components/ui/tooltip.tsx +70 -0
  60. package/templates/hooks/use-mobile.ts +21 -0
  61. package/templates/lib/utils.ts +6 -0
@@ -0,0 +1,938 @@
1
+ #!/usr/bin/env node
2
+
3
+ // bin/cli.ts
4
+ import { program } from "commander";
5
+
6
+ // src/index.ts
7
+ import path5 from "path";
8
+ import { confirm as confirm2 } from "@inquirer/prompts";
9
+
10
+ // src/config/index.ts
11
+ import fs from "fs/promises";
12
+ import path from "path";
13
+ import os from "os";
14
+ import { createCipheriv, createDecipheriv, randomBytes, createHash } from "crypto";
15
+ import { machineIdSync } from "node-machine-id";
16
+ import { input, password, select, confirm } from "@inquirer/prompts";
17
+
18
+ // src/ui/logger.ts
19
+ import chalk from "chalk";
20
+ var logger = {
21
+ info: (message) => {
22
+ console.log(chalk.blue("\u2139"), message);
23
+ },
24
+ success: (message) => {
25
+ console.log(chalk.green("\u2714"), message);
26
+ },
27
+ warning: (message) => {
28
+ console.log(chalk.yellow("\u26A0"), message);
29
+ },
30
+ error: (message) => {
31
+ console.log(chalk.red("\u2716"), message);
32
+ },
33
+ step: (step, total, message) => {
34
+ console.log(chalk.cyan(`[${step}/${total}]`), message);
35
+ },
36
+ newLine: () => {
37
+ console.log();
38
+ },
39
+ divider: () => {
40
+ console.log(chalk.gray("\u2500".repeat(50)));
41
+ },
42
+ title: (message) => {
43
+ console.log(chalk.bold.white(message));
44
+ },
45
+ subtitle: (message) => {
46
+ console.log(chalk.gray(message));
47
+ },
48
+ link: (label, url) => {
49
+ console.log(` ${chalk.gray(label + ":")} ${chalk.cyan.underline(url)}`);
50
+ },
51
+ list: (items) => {
52
+ items.forEach((item) => {
53
+ console.log(chalk.gray(" \u2022"), item);
54
+ });
55
+ },
56
+ table: (rows) => {
57
+ const maxLabelLength = Math.max(...rows.map((r) => r.label.length));
58
+ rows.forEach(({ label, value }) => {
59
+ const paddedLabel = label.padEnd(maxLabelLength);
60
+ console.log(` ${chalk.gray(paddedLabel)} ${value}`);
61
+ });
62
+ }
63
+ };
64
+
65
+ // src/ui/spinner.ts
66
+ import ora from "ora";
67
+ function createSpinner(text) {
68
+ return ora({
69
+ text,
70
+ spinner: "dots"
71
+ });
72
+ }
73
+ async function withSpinner(text, fn, successText) {
74
+ const spinner = createSpinner(text).start();
75
+ try {
76
+ const result = await fn();
77
+ spinner.succeed(successText || text);
78
+ return result;
79
+ } catch (error) {
80
+ spinner.fail();
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ // src/config/index.ts
86
+ var CONFIG_FILE = path.join(os.homedir(), ".lftrc");
87
+ var ALGORITHM = "aes-256-gcm";
88
+ function getEncryptionKey() {
89
+ const machineId = machineIdSync();
90
+ return createHash("sha256").update(machineId + "lft-secret").digest();
91
+ }
92
+ function encryptConfig(config) {
93
+ const key = getEncryptionKey();
94
+ const iv = randomBytes(16);
95
+ const cipher = createCipheriv(ALGORITHM, key, iv);
96
+ let encrypted = cipher.update(JSON.stringify(config), "utf8", "hex");
97
+ encrypted += cipher.final("hex");
98
+ const authTag = cipher.getAuthTag();
99
+ return JSON.stringify({
100
+ iv: iv.toString("hex"),
101
+ tag: authTag.toString("hex"),
102
+ data: encrypted
103
+ });
104
+ }
105
+ function decryptConfig(encrypted) {
106
+ const { iv, tag, data } = JSON.parse(encrypted);
107
+ const key = getEncryptionKey();
108
+ const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(iv, "hex"));
109
+ decipher.setAuthTag(Buffer.from(tag, "hex"));
110
+ let decrypted = decipher.update(data, "hex", "utf8");
111
+ decrypted += decipher.final("utf8");
112
+ return JSON.parse(decrypted);
113
+ }
114
+ async function hasConfig() {
115
+ try {
116
+ await fs.access(CONFIG_FILE);
117
+ return true;
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+ async function loadConfig() {
123
+ const encrypted = await fs.readFile(CONFIG_FILE, "utf8");
124
+ return decryptConfig(encrypted);
125
+ }
126
+ async function saveConfig(config) {
127
+ const encrypted = encryptConfig(config);
128
+ await fs.writeFile(CONFIG_FILE, encrypted, "utf8");
129
+ }
130
+ async function showConfig() {
131
+ if (!await hasConfig()) {
132
+ logger.warning("No hay configuraci\xF3n guardada");
133
+ logger.info('Ejecuta "create-lft-app config" para configurar');
134
+ return;
135
+ }
136
+ const config = await loadConfig();
137
+ logger.newLine();
138
+ logger.title("Configuraci\xF3n actual:");
139
+ logger.newLine();
140
+ logger.subtitle("GitHub:");
141
+ logger.table([
142
+ { label: "Usuario", value: config.credentials.github.username },
143
+ { label: "Token", value: "***" + config.credentials.github.token.slice(-4) },
144
+ { label: "Org por defecto", value: config.defaults.githubOrg || "(ninguna)" }
145
+ ]);
146
+ logger.newLine();
147
+ logger.subtitle("Supabase:");
148
+ logger.table([
149
+ { label: "Org ID", value: config.credentials.supabase.organizationId },
150
+ { label: "Token", value: "***" + config.credentials.supabase.accessToken.slice(-4) },
151
+ { label: "Regi\xF3n", value: config.defaults.supabaseRegion }
152
+ ]);
153
+ logger.newLine();
154
+ logger.subtitle("Jira:");
155
+ logger.table([
156
+ { label: "Email", value: config.credentials.jira.email },
157
+ { label: "Dominio", value: config.credentials.jira.domain },
158
+ { label: "Token", value: "***" + config.credentials.jira.apiToken.slice(-4) }
159
+ ]);
160
+ }
161
+ async function resetConfig() {
162
+ if (!await hasConfig()) {
163
+ logger.info("No hay configuraci\xF3n para resetear");
164
+ return;
165
+ }
166
+ const shouldReset = await confirm({
167
+ message: "\xBFEst\xE1s seguro de que quieres eliminar la configuraci\xF3n?",
168
+ default: false
169
+ });
170
+ if (shouldReset) {
171
+ await fs.unlink(CONFIG_FILE);
172
+ logger.success("Configuraci\xF3n eliminada");
173
+ }
174
+ }
175
+ async function configureCredentials(options = {}) {
176
+ let config;
177
+ if (await hasConfig()) {
178
+ config = await loadConfig();
179
+ logger.info("Configuraci\xF3n existente encontrada. Actualizando...");
180
+ } else {
181
+ config = {
182
+ version: "1.0.0",
183
+ credentials: {
184
+ github: { token: "", username: "" },
185
+ supabase: { accessToken: "", organizationId: "" },
186
+ jira: { email: "", apiToken: "", domain: "" }
187
+ },
188
+ defaults: {
189
+ supabaseRegion: "us-east-1",
190
+ jiraProjectType: "software"
191
+ }
192
+ };
193
+ }
194
+ const configureAll = !options.onlyGithub && !options.onlySupabase && !options.onlyJira;
195
+ if (configureAll || options.onlyGithub) {
196
+ logger.newLine();
197
+ logger.title("Configuraci\xF3n de GitHub");
198
+ logger.subtitle("Necesitas un Personal Access Token con permisos: repo, read:org");
199
+ logger.link("Crear token", "https://github.com/settings/tokens/new");
200
+ logger.newLine();
201
+ const githubToken = await password({
202
+ message: "GitHub Personal Access Token:",
203
+ mask: "*"
204
+ });
205
+ const isValid = await withSpinner(
206
+ "Validando token de GitHub...",
207
+ async () => {
208
+ const response = await fetch("https://api.github.com/user", {
209
+ headers: { Authorization: `Bearer ${githubToken}` }
210
+ });
211
+ if (!response.ok) return null;
212
+ return response.json();
213
+ }
214
+ );
215
+ if (!isValid) {
216
+ throw new Error("Token de GitHub inv\xE1lido");
217
+ }
218
+ config.credentials.github.token = githubToken;
219
+ config.credentials.github.username = isValid.login;
220
+ logger.success(`Conectado como: ${isValid.login}`);
221
+ const useOrg = await confirm({
222
+ message: "\xBFQuieres usar una organizaci\xF3n por defecto?",
223
+ default: false
224
+ });
225
+ if (useOrg) {
226
+ config.defaults.githubOrg = await input({
227
+ message: "Nombre de la organizaci\xF3n:"
228
+ });
229
+ }
230
+ }
231
+ if (configureAll || options.onlySupabase) {
232
+ logger.newLine();
233
+ logger.title("Configuraci\xF3n de Supabase");
234
+ logger.subtitle("Necesitas un Access Token de la Management API");
235
+ logger.link("Crear token", "https://supabase.com/dashboard/account/tokens");
236
+ logger.newLine();
237
+ const supabaseToken = await password({
238
+ message: "Supabase Access Token:",
239
+ mask: "*"
240
+ });
241
+ const orgs = await withSpinner(
242
+ "Obteniendo organizaciones de Supabase...",
243
+ async () => {
244
+ const response = await fetch("https://api.supabase.com/v1/organizations", {
245
+ headers: { Authorization: `Bearer ${supabaseToken}` }
246
+ });
247
+ if (!response.ok) return null;
248
+ return response.json();
249
+ }
250
+ );
251
+ if (!orgs || orgs.length === 0) {
252
+ throw new Error("Token de Supabase inv\xE1lido o no tienes organizaciones");
253
+ }
254
+ config.credentials.supabase.accessToken = supabaseToken;
255
+ if (orgs.length === 1) {
256
+ config.credentials.supabase.organizationId = orgs[0].id;
257
+ logger.success(`Usando organizaci\xF3n: ${orgs[0].name}`);
258
+ } else {
259
+ const selectedOrg = await select({
260
+ message: "Selecciona la organizaci\xF3n:",
261
+ choices: orgs.map((org) => ({ name: org.name, value: org.id }))
262
+ });
263
+ config.credentials.supabase.organizationId = selectedOrg;
264
+ }
265
+ config.defaults.supabaseRegion = await select({
266
+ message: "Regi\xF3n por defecto:",
267
+ choices: [
268
+ { name: "US East (Virginia)", value: "us-east-1" },
269
+ { name: "US West (Oregon)", value: "us-west-1" },
270
+ { name: "EU West (Ireland)", value: "eu-west-1" },
271
+ { name: "AP Southeast (Singapore)", value: "ap-southeast-1" },
272
+ { name: "AP Northeast (Tokyo)", value: "ap-northeast-1" },
273
+ { name: "SA East (S\xE3o Paulo)", value: "sa-east-1" }
274
+ ],
275
+ default: "us-east-1"
276
+ });
277
+ }
278
+ if (configureAll || options.onlyJira) {
279
+ logger.newLine();
280
+ logger.title("Configuraci\xF3n de Jira");
281
+ logger.subtitle("Necesitas un API Token de Atlassian");
282
+ logger.link("Crear token", "https://id.atlassian.com/manage-profile/security/api-tokens");
283
+ logger.newLine();
284
+ const jiraEmail = await input({
285
+ message: "Email de Atlassian:"
286
+ });
287
+ const jiraToken = await password({
288
+ message: "Jira API Token:",
289
+ mask: "*"
290
+ });
291
+ const jiraDomain = await input({
292
+ message: "Dominio de Jira (ej: empresa.atlassian.net):",
293
+ validate: (value) => {
294
+ if (!value.includes(".atlassian.net")) {
295
+ return "El dominio debe terminar en .atlassian.net";
296
+ }
297
+ return true;
298
+ }
299
+ });
300
+ const isValid = await withSpinner(
301
+ "Validando credenciales de Jira...",
302
+ async () => {
303
+ const auth = Buffer.from(`${jiraEmail}:${jiraToken}`).toString("base64");
304
+ const response = await fetch(`https://${jiraDomain}/rest/api/3/myself`, {
305
+ headers: { Authorization: `Basic ${auth}` }
306
+ });
307
+ return response.ok;
308
+ }
309
+ );
310
+ if (!isValid) {
311
+ throw new Error("Credenciales de Jira inv\xE1lidas");
312
+ }
313
+ config.credentials.jira.email = jiraEmail;
314
+ config.credentials.jira.apiToken = jiraToken;
315
+ config.credentials.jira.domain = jiraDomain;
316
+ logger.success("Credenciales de Jira v\xE1lidas");
317
+ }
318
+ await saveConfig(config);
319
+ logger.newLine();
320
+ logger.success("Configuraci\xF3n guardada en ~/.lftrc");
321
+ }
322
+
323
+ // src/services/github.ts
324
+ import { Octokit } from "octokit";
325
+ async function createGitHubRepo(projectName, config) {
326
+ const octokit = new Octokit({ auth: config.credentials.github.token });
327
+ const org = config.defaults.githubOrg;
328
+ return withSpinner(
329
+ "Creando repositorio en GitHub...",
330
+ async () => {
331
+ let repo;
332
+ if (org) {
333
+ repo = await octokit.rest.repos.createInOrg({
334
+ org,
335
+ name: projectName,
336
+ private: true,
337
+ auto_init: false,
338
+ description: `Proyecto ${projectName} creado con create-lft-app`
339
+ });
340
+ } else {
341
+ repo = await octokit.rest.repos.createForAuthenticatedUser({
342
+ name: projectName,
343
+ private: true,
344
+ auto_init: false,
345
+ description: `Proyecto ${projectName} creado con create-lft-app`
346
+ });
347
+ }
348
+ return repo.data.html_url;
349
+ },
350
+ `Repositorio creado: ${org || config.credentials.github.username}/${projectName}`
351
+ );
352
+ }
353
+
354
+ // src/services/supabase.ts
355
+ var SUPABASE_API_URL = "https://api.supabase.com/v1";
356
+ async function waitForProjectReady(projectId, token, maxAttempts = 60) {
357
+ const spinner = createSpinner("Provisionando base de datos (esto puede tomar ~2 minutos)...").start();
358
+ for (let i = 0; i < maxAttempts; i++) {
359
+ const response = await fetch(`${SUPABASE_API_URL}/projects/${projectId}`, {
360
+ headers: { Authorization: `Bearer ${token}` }
361
+ });
362
+ if (response.ok) {
363
+ const project = await response.json();
364
+ if (project.status === "ACTIVE_HEALTHY") {
365
+ spinner.succeed("Base de datos provisionada");
366
+ return;
367
+ }
368
+ }
369
+ await new Promise((resolve) => setTimeout(resolve, 5e3));
370
+ spinner.text = `Provisionando base de datos... (${Math.floor((i + 1) * 5 / 60)}min ${(i + 1) * 5 % 60}s)`;
371
+ }
372
+ spinner.fail("Timeout esperando a que el proyecto est\xE9 listo");
373
+ throw new Error("Timeout: el proyecto de Supabase no se activ\xF3 a tiempo");
374
+ }
375
+ async function getProjectApiKeys(projectId, token) {
376
+ const response = await fetch(`${SUPABASE_API_URL}/projects/${projectId}/api-keys`, {
377
+ headers: { Authorization: `Bearer ${token}` }
378
+ });
379
+ if (!response.ok) {
380
+ throw new Error("No se pudieron obtener las API keys de Supabase");
381
+ }
382
+ const keys = await response.json();
383
+ const anonKey = keys.find((k) => k.name === "anon")?.api_key;
384
+ const serviceKey = keys.find((k) => k.name === "service_role")?.api_key;
385
+ if (!anonKey || !serviceKey) {
386
+ throw new Error("No se encontraron las API keys necesarias");
387
+ }
388
+ return { anonKey, serviceKey };
389
+ }
390
+ async function createSupabaseProject(projectName, config) {
391
+ const token = config.credentials.supabase.accessToken;
392
+ const orgId = config.credentials.supabase.organizationId;
393
+ const region = config.defaults.supabaseRegion;
394
+ const dbPassword = generateSecurePassword();
395
+ const project = await withSpinner(
396
+ "Creando proyecto en Supabase...",
397
+ async () => {
398
+ const response = await fetch(`${SUPABASE_API_URL}/projects`, {
399
+ method: "POST",
400
+ headers: {
401
+ Authorization: `Bearer ${token}`,
402
+ "Content-Type": "application/json"
403
+ },
404
+ body: JSON.stringify({
405
+ name: projectName,
406
+ organization_id: orgId,
407
+ region,
408
+ plan: "free",
409
+ db_pass: dbPassword
410
+ })
411
+ });
412
+ if (!response.ok) {
413
+ const error = await response.text();
414
+ throw new Error(`Error creando proyecto Supabase: ${error}`);
415
+ }
416
+ return response.json();
417
+ }
418
+ );
419
+ await waitForProjectReady(project.id, token);
420
+ const { anonKey, serviceKey } = await getProjectApiKeys(project.id, token);
421
+ const projectUrl = `https://${project.id}.supabase.co`;
422
+ return {
423
+ url: projectUrl,
424
+ anonKey,
425
+ serviceKey
426
+ };
427
+ }
428
+ function generateSecurePassword() {
429
+ const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
430
+ let password2 = "";
431
+ for (let i = 0; i < 32; i++) {
432
+ password2 += chars.charAt(Math.floor(Math.random() * chars.length));
433
+ }
434
+ return password2;
435
+ }
436
+
437
+ // src/utils/validation.ts
438
+ import fs2 from "fs";
439
+ import path2 from "path";
440
+ function validateProjectName(name) {
441
+ if (!name || name.trim() === "") {
442
+ return { valid: false, error: "El nombre del proyecto no puede estar vac\xEDo" };
443
+ }
444
+ const validPattern = /^[a-zA-Z0-9_-]+$/;
445
+ if (!validPattern.test(name)) {
446
+ return {
447
+ valid: false,
448
+ error: "El nombre solo puede contener letras, n\xFAmeros, guiones (-) y guiones bajos (_)"
449
+ };
450
+ }
451
+ if (/^[-_0-9]/.test(name)) {
452
+ return {
453
+ valid: false,
454
+ error: "El nombre debe empezar con una letra"
455
+ };
456
+ }
457
+ if (name.length < 2) {
458
+ return { valid: false, error: "El nombre debe tener al menos 2 caracteres" };
459
+ }
460
+ if (name.length > 50) {
461
+ return { valid: false, error: "El nombre no puede tener m\xE1s de 50 caracteres" };
462
+ }
463
+ const projectPath = path2.resolve(process.cwd(), name);
464
+ if (fs2.existsSync(projectPath)) {
465
+ return { valid: false, error: `El directorio "${name}" ya existe` };
466
+ }
467
+ return { valid: true };
468
+ }
469
+ function generateJiraKey(projectName) {
470
+ const cleaned = projectName.replace(/[^a-zA-Z0-9]/g, "").toUpperCase().slice(0, 10);
471
+ return cleaned || "PROJ";
472
+ }
473
+
474
+ // src/services/jira.ts
475
+ async function createJiraProject(projectName, config) {
476
+ const { email, apiToken, domain } = config.credentials.jira;
477
+ const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
478
+ const projectKey = generateJiraKey(projectName);
479
+ return withSpinner(
480
+ "Creando proyecto en Jira...",
481
+ async () => {
482
+ const meResponse = await fetch(`https://${domain}/rest/api/3/myself`, {
483
+ headers: {
484
+ Authorization: `Basic ${auth}`,
485
+ "Content-Type": "application/json"
486
+ }
487
+ });
488
+ if (!meResponse.ok) {
489
+ throw new Error("No se pudo obtener informaci\xF3n del usuario de Jira");
490
+ }
491
+ const me = await meResponse.json();
492
+ const response = await fetch(`https://${domain}/rest/api/3/project`, {
493
+ method: "POST",
494
+ headers: {
495
+ Authorization: `Basic ${auth}`,
496
+ "Content-Type": "application/json"
497
+ },
498
+ body: JSON.stringify({
499
+ key: projectKey,
500
+ name: projectName,
501
+ projectTypeKey: "software",
502
+ projectTemplateKey: "com.pyxis.greenhopper.jira:gh-simplified-agility-scrum",
503
+ leadAccountId: me.accountId
504
+ })
505
+ });
506
+ if (!response.ok) {
507
+ const error = await response.text();
508
+ if (error.includes("project key")) {
509
+ const newKey = `${projectKey}${Date.now().toString().slice(-4)}`;
510
+ const retryResponse = await fetch(`https://${domain}/rest/api/3/project`, {
511
+ method: "POST",
512
+ headers: {
513
+ Authorization: `Basic ${auth}`,
514
+ "Content-Type": "application/json"
515
+ },
516
+ body: JSON.stringify({
517
+ key: newKey,
518
+ name: projectName,
519
+ projectTypeKey: "software",
520
+ projectTemplateKey: "com.pyxis.greenhopper.jira:gh-simplified-agility-scrum",
521
+ leadAccountId: me.accountId
522
+ })
523
+ });
524
+ if (!retryResponse.ok) {
525
+ throw new Error(`Error creando proyecto Jira: ${await retryResponse.text()}`);
526
+ }
527
+ const project2 = await retryResponse.json();
528
+ return `https://${domain}/browse/${project2.key}`;
529
+ }
530
+ throw new Error(`Error creando proyecto Jira: ${error}`);
531
+ }
532
+ const project = await response.json();
533
+ return `https://${domain}/browse/${project.key}`;
534
+ },
535
+ `Proyecto Jira creado: ${projectKey}`
536
+ );
537
+ }
538
+
539
+ // src/steps/scaffold-nextjs.ts
540
+ import { execa } from "execa";
541
+ async function scaffoldNextJs(projectName, projectPath) {
542
+ await withSpinner(
543
+ "Inicializando proyecto Next.js...",
544
+ async () => {
545
+ await execa("npx", [
546
+ "create-next-app@latest",
547
+ projectName,
548
+ "--typescript",
549
+ "--tailwind",
550
+ "--eslint",
551
+ "--app",
552
+ "--turbopack",
553
+ "--src-dir",
554
+ "--import-alias",
555
+ "@/*",
556
+ "--use-npm"
557
+ ], {
558
+ cwd: process.cwd(),
559
+ stdio: "pipe"
560
+ });
561
+ },
562
+ "Proyecto Next.js inicializado"
563
+ );
564
+ }
565
+
566
+ // src/steps/copy-template.ts
567
+ import { cp, mkdir, readFile, writeFile } from "fs/promises";
568
+ import path3 from "path";
569
+ import { fileURLToPath } from "url";
570
+ var __filename2 = fileURLToPath(import.meta.url);
571
+ var __dirname2 = path3.dirname(__filename2);
572
+ async function copyTemplate(projectPath) {
573
+ await withSpinner(
574
+ "Copiando template LFT...",
575
+ async () => {
576
+ const templatesDir = path3.join(__dirname2, "..", "..", "templates");
577
+ const srcDir = path3.join(projectPath, "src");
578
+ await cp(
579
+ path3.join(templatesDir, "components", "ui"),
580
+ path3.join(srcDir, "components", "ui"),
581
+ { recursive: true }
582
+ );
583
+ await cp(
584
+ path3.join(templatesDir, "components", "layout"),
585
+ path3.join(srcDir, "components", "layout"),
586
+ { recursive: true }
587
+ );
588
+ await cp(
589
+ path3.join(templatesDir, "components", "dashboard"),
590
+ path3.join(srcDir, "components", "dashboard"),
591
+ { recursive: true }
592
+ );
593
+ await mkdir(path3.join(srcDir, "lib"), { recursive: true });
594
+ await cp(
595
+ path3.join(templatesDir, "lib", "utils.ts"),
596
+ path3.join(srcDir, "lib", "utils.ts")
597
+ );
598
+ await mkdir(path3.join(srcDir, "hooks"), { recursive: true });
599
+ await cp(
600
+ path3.join(templatesDir, "hooks"),
601
+ path3.join(srcDir, "hooks"),
602
+ { recursive: true }
603
+ );
604
+ await cp(
605
+ path3.join(templatesDir, "app", "layout.tsx"),
606
+ path3.join(srcDir, "app", "layout.tsx")
607
+ );
608
+ await cp(
609
+ path3.join(templatesDir, "app", "page.tsx"),
610
+ path3.join(srcDir, "app", "page.tsx")
611
+ );
612
+ await mkdir(path3.join(srcDir, "app", "dashboard"), { recursive: true });
613
+ await cp(
614
+ path3.join(templatesDir, "app", "dashboard", "page.tsx"),
615
+ path3.join(srcDir, "app", "dashboard", "page.tsx")
616
+ );
617
+ await mkdir(path3.join(srcDir, "app", "auth", "login"), { recursive: true });
618
+ await cp(
619
+ path3.join(templatesDir, "app", "auth", "login", "page.tsx"),
620
+ path3.join(srcDir, "app", "auth", "login", "page.tsx")
621
+ );
622
+ await mergeGlobalStyles(projectPath, templatesDir);
623
+ },
624
+ "Template LFT copiado (47 componentes + p\xE1ginas)"
625
+ );
626
+ }
627
+ async function mergeGlobalStyles(projectPath, templatesDir) {
628
+ const templateCssPath = path3.join(templatesDir, "app", "globals.css");
629
+ const projectCssPath = path3.join(projectPath, "src", "app", "globals.css");
630
+ try {
631
+ const templateCss = await readFile(templateCssPath, "utf-8");
632
+ const existingCss = await readFile(projectCssPath, "utf-8");
633
+ const merged = existingCss + "\n\n/* LFT Custom Styles */\n" + templateCss;
634
+ await writeFile(projectCssPath, merged);
635
+ } catch {
636
+ }
637
+ }
638
+
639
+ // src/steps/install-deps.ts
640
+ import { execa as execa2 } from "execa";
641
+ var TEMPLATE_DEPENDENCIES = [
642
+ // Radix UI primitives
643
+ "@radix-ui/react-accordion",
644
+ "@radix-ui/react-alert-dialog",
645
+ "@radix-ui/react-avatar",
646
+ "@radix-ui/react-checkbox",
647
+ "@radix-ui/react-collapsible",
648
+ "@radix-ui/react-dialog",
649
+ "@radix-ui/react-dropdown-menu",
650
+ "@radix-ui/react-label",
651
+ "@radix-ui/react-popover",
652
+ "@radix-ui/react-progress",
653
+ "@radix-ui/react-radio-group",
654
+ "@radix-ui/react-scroll-area",
655
+ "@radix-ui/react-select",
656
+ "@radix-ui/react-separator",
657
+ "@radix-ui/react-slider",
658
+ "@radix-ui/react-slot",
659
+ "@radix-ui/react-switch",
660
+ "@radix-ui/react-tabs",
661
+ "@radix-ui/react-tooltip",
662
+ // UI Utilities
663
+ "class-variance-authority",
664
+ "clsx",
665
+ "tailwind-merge",
666
+ // Icons
667
+ "lucide-react",
668
+ // Form handling
669
+ "react-hook-form",
670
+ "@hookform/resolvers",
671
+ // Command menu
672
+ "cmdk",
673
+ // Date picker
674
+ "react-day-picker",
675
+ "date-fns",
676
+ // Toast notifications
677
+ "sonner",
678
+ // Validation
679
+ "zod",
680
+ // Supabase client
681
+ "@supabase/supabase-js",
682
+ "@supabase/ssr"
683
+ ];
684
+ var TEMPLATE_DEV_DEPENDENCIES = [
685
+ "tailwindcss-animate"
686
+ ];
687
+ async function installDependencies(projectPath) {
688
+ await withSpinner(
689
+ `Instalando dependencias (${TEMPLATE_DEPENDENCIES.length} paquetes)...`,
690
+ async () => {
691
+ await execa2("npm", ["install", ...TEMPLATE_DEPENDENCIES], {
692
+ cwd: projectPath,
693
+ stdio: "pipe"
694
+ });
695
+ await execa2("npm", ["install", "-D", ...TEMPLATE_DEV_DEPENDENCIES], {
696
+ cwd: projectPath,
697
+ stdio: "pipe"
698
+ });
699
+ },
700
+ "Dependencias instaladas"
701
+ );
702
+ }
703
+
704
+ // src/steps/create-env.ts
705
+ import { writeFile as writeFile2, readFile as readFile2, appendFile } from "fs/promises";
706
+ import path4 from "path";
707
+ async function createEnvFile(projectPath, supabaseKeys) {
708
+ await withSpinner(
709
+ "Creando archivo .env.local...",
710
+ async () => {
711
+ const envContent = `# Supabase
712
+ NEXT_PUBLIC_SUPABASE_URL=${supabaseKeys.url}
713
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabaseKeys.anonKey}
714
+ SUPABASE_SERVICE_ROLE_KEY=${supabaseKeys.serviceKey}
715
+ `;
716
+ await writeFile2(
717
+ path4.join(projectPath, ".env.local"),
718
+ envContent
719
+ );
720
+ const gitignorePath = path4.join(projectPath, ".gitignore");
721
+ try {
722
+ const gitignore = await readFile2(gitignorePath, "utf-8");
723
+ if (!gitignore.includes(".env.local")) {
724
+ await appendFile(gitignorePath, "\n# Environment variables\n.env.local\n.env*.local\n");
725
+ }
726
+ } catch {
727
+ await writeFile2(gitignorePath, "# Environment variables\n.env.local\n.env*.local\n");
728
+ }
729
+ },
730
+ "Archivo .env.local creado con credenciales de Supabase"
731
+ );
732
+ }
733
+
734
+ // src/steps/setup-git.ts
735
+ import { simpleGit } from "simple-git";
736
+ async function setupGit(projectPath, remoteUrl) {
737
+ const git = simpleGit(projectPath);
738
+ await withSpinner(
739
+ "Configurando Git...",
740
+ async () => {
741
+ const isRepo = await git.checkIsRepo();
742
+ if (!isRepo) {
743
+ await git.init();
744
+ }
745
+ try {
746
+ await git.addRemote("origin", remoteUrl);
747
+ } catch {
748
+ await git.remote(["set-url", "origin", remoteUrl]);
749
+ }
750
+ await git.add(".");
751
+ await git.commit("Initial commit - created with create-lft-app", {
752
+ "--author": "create-lft-app <noreply@lft.dev>"
753
+ });
754
+ try {
755
+ await git.branch(["-M", "main"]);
756
+ } catch {
757
+ }
758
+ await git.push(["--set-upstream", "origin", "main"]);
759
+ },
760
+ "Git configurado y c\xF3digo pusheado"
761
+ );
762
+ }
763
+
764
+ // src/ui/banner.ts
765
+ import boxen from "boxen";
766
+ import chalk2 from "chalk";
767
+ function showBanner() {
768
+ const title = chalk2.bold.cyan("create-lft-app");
769
+ const version = chalk2.gray("v1.0.0");
770
+ const description = chalk2.white("Scaffolding para proyectos LFT");
771
+ const banner = boxen(`${title} ${version}
772
+ ${description}`, {
773
+ padding: 1,
774
+ margin: 1,
775
+ borderStyle: "round",
776
+ borderColor: "cyan"
777
+ });
778
+ console.log(banner);
779
+ }
780
+ function showSuccessBanner(projectName, urls) {
781
+ const lines = [
782
+ chalk2.green.bold(`Proyecto "${projectName}" creado exitosamente!`),
783
+ "",
784
+ chalk2.white("Directorio:") + ` ./${projectName}`,
785
+ ""
786
+ ];
787
+ if (urls.github || urls.supabase || urls.jira) {
788
+ lines.push(chalk2.white("Enlaces:"));
789
+ if (urls.github) {
790
+ lines.push(` ${chalk2.gray("GitHub:")} ${chalk2.cyan(urls.github)}`);
791
+ }
792
+ if (urls.supabase) {
793
+ lines.push(` ${chalk2.gray("Supabase:")} ${chalk2.cyan(urls.supabase)}`);
794
+ }
795
+ if (urls.jira) {
796
+ lines.push(` ${chalk2.gray("Jira:")} ${chalk2.cyan(urls.jira)}`);
797
+ }
798
+ lines.push("");
799
+ }
800
+ lines.push(chalk2.white("Siguiente pasos:"));
801
+ lines.push(` ${chalk2.cyan("cd")} ${projectName}`);
802
+ lines.push(` ${chalk2.cyan("npm run dev")}`);
803
+ const banner = boxen(lines.join("\n"), {
804
+ padding: 1,
805
+ margin: 1,
806
+ borderStyle: "round",
807
+ borderColor: "green"
808
+ });
809
+ console.log(banner);
810
+ }
811
+
812
+ // src/index.ts
813
+ async function createProject(projectName, options = {}) {
814
+ const validation = validateProjectName(projectName);
815
+ if (!validation.valid) {
816
+ throw new Error(validation.error);
817
+ }
818
+ const projectPath = path5.resolve(process.cwd(), projectName);
819
+ if (!await hasConfig()) {
820
+ logger.warning('No se encontr\xF3 configuraci\xF3n. Ejecuta "create-lft-app config" primero.');
821
+ throw new Error("Configuraci\xF3n no encontrada");
822
+ }
823
+ const config = await loadConfig();
824
+ logger.newLine();
825
+ logger.title("Resumen de recursos a crear:");
826
+ logger.newLine();
827
+ const resources = [];
828
+ if (!options.skipGithub) {
829
+ resources.push({ label: "GitHub", value: `${config.defaults.githubOrg || config.credentials.github.username}/${projectName} (privado)` });
830
+ }
831
+ if (!options.skipSupabase) {
832
+ resources.push({ label: "Supabase", value: `${projectName} en ${config.defaults.supabaseRegion}` });
833
+ }
834
+ if (!options.skipJira) {
835
+ resources.push({ label: "Jira", value: `Proyecto "${projectName}" en ${config.credentials.jira.domain}` });
836
+ }
837
+ resources.push({ label: "Next.js", value: "App Router + TypeScript + Tailwind + Dashboard" });
838
+ logger.table(resources);
839
+ logger.newLine();
840
+ if (!options.autoConfirm) {
841
+ const shouldContinue = await confirm2({
842
+ message: "\xBFContinuar con la creaci\xF3n?",
843
+ default: true
844
+ });
845
+ if (!shouldContinue) {
846
+ logger.info("Operaci\xF3n cancelada");
847
+ return;
848
+ }
849
+ }
850
+ logger.newLine();
851
+ logger.divider();
852
+ logger.newLine();
853
+ const urls = {};
854
+ let supabaseKeys;
855
+ const externalTasks = [];
856
+ if (!options.skipGithub) {
857
+ externalTasks.push(
858
+ createGitHubRepo(projectName, config).then((url) => {
859
+ urls.github = url;
860
+ })
861
+ );
862
+ }
863
+ if (!options.skipSupabase) {
864
+ externalTasks.push(
865
+ createSupabaseProject(projectName, config).then((result) => {
866
+ urls.supabase = result.url;
867
+ supabaseKeys = result;
868
+ })
869
+ );
870
+ }
871
+ if (!options.skipJira) {
872
+ externalTasks.push(
873
+ createJiraProject(projectName, config).then((url) => {
874
+ urls.jira = url;
875
+ })
876
+ );
877
+ }
878
+ await Promise.all(externalTasks);
879
+ await scaffoldNextJs(projectName, projectPath);
880
+ await copyTemplate(projectPath);
881
+ await installDependencies(projectPath);
882
+ if (supabaseKeys) {
883
+ await createEnvFile(projectPath, supabaseKeys);
884
+ }
885
+ if (!options.skipGit && urls.github) {
886
+ await setupGit(projectPath, urls.github);
887
+ }
888
+ logger.newLine();
889
+ showSuccessBanner(projectName, urls);
890
+ }
891
+
892
+ // bin/cli.ts
893
+ var packageJson = {
894
+ name: "create-lft-app",
895
+ version: "1.0.0",
896
+ description: "CLI para crear proyectos LFT con Next.js, GitHub, Supabase y Jira"
897
+ };
898
+ program.name("create-lft-app").description(packageJson.description).version(packageJson.version);
899
+ program.argument("[project-name]", "Nombre del proyecto a crear").option("--skip-github", "No crear repositorio en GitHub").option("--skip-supabase", "No crear proyecto en Supabase").option("--skip-jira", "No crear workspace en Jira").option("--skip-git", "No inicializar git ni hacer push").option("-y, --yes", "Aceptar todas las confirmaciones").action(async (projectName, options) => {
900
+ showBanner();
901
+ if (!projectName) {
902
+ logger.error("Debes especificar un nombre de proyecto");
903
+ logger.info("Uso: create-lft-app <nombre-proyecto>");
904
+ process.exit(1);
905
+ }
906
+ try {
907
+ await createProject(projectName, {
908
+ skipGithub: options.skipGithub,
909
+ skipSupabase: options.skipSupabase,
910
+ skipJira: options.skipJira,
911
+ skipGit: options.skipGit,
912
+ autoConfirm: options.yes
913
+ });
914
+ } catch (error) {
915
+ if (error instanceof Error) {
916
+ logger.error(error.message);
917
+ }
918
+ process.exit(1);
919
+ }
920
+ });
921
+ program.command("config").description("Configurar credenciales de APIs").option("--show", "Mostrar configuraci\xF3n actual").option("--reset", "Resetear configuraci\xF3n").option("--github", "Configurar solo GitHub").option("--supabase", "Configurar solo Supabase").option("--jira", "Configurar solo Jira").action(async (options) => {
922
+ showBanner();
923
+ if (options.show) {
924
+ await showConfig();
925
+ return;
926
+ }
927
+ if (options.reset) {
928
+ await resetConfig();
929
+ return;
930
+ }
931
+ await configureCredentials({
932
+ onlyGithub: options.github,
933
+ onlySupabase: options.supabase,
934
+ onlyJira: options.jira
935
+ });
936
+ });
937
+ program.parse();
938
+ //# sourceMappingURL=cli.js.map