@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.
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.js +938 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +669 -0
- package/dist/src/index.js.map +1 -0
- package/package.json +60 -0
- package/templates/app/auth/login/page.tsx +153 -0
- package/templates/app/dashboard/page.tsx +102 -0
- package/templates/app/globals.css +68 -0
- package/templates/app/layout.tsx +40 -0
- package/templates/app/page.tsx +5 -0
- package/templates/components/dashboard/widget.tsx +113 -0
- package/templates/components/layout/admin-midday-sidebar.tsx +247 -0
- package/templates/components/layout/admin-sidebar.tsx +146 -0
- package/templates/components/layout/header.tsx +71 -0
- package/templates/components/layout/impersonation-banner.tsx +36 -0
- package/templates/components/layout/main-content.tsx +28 -0
- package/templates/components/layout/midday-sidebar.tsx +381 -0
- package/templates/components/layout/nav-user.tsx +108 -0
- package/templates/components/layout/page-header.tsx +95 -0
- package/templates/components/layout/sidebar-context.tsx +33 -0
- package/templates/components/layout/sidebar.tsx +194 -0
- package/templates/components/layout/suspension-banner.tsx +21 -0
- package/templates/components/ui/accordion.tsx +58 -0
- package/templates/components/ui/alert-dialog.tsx +165 -0
- package/templates/components/ui/alert.tsx +66 -0
- package/templates/components/ui/avatar.tsx +55 -0
- package/templates/components/ui/badge.tsx +50 -0
- package/templates/components/ui/button.tsx +89 -0
- package/templates/components/ui/calendar.tsx +220 -0
- package/templates/components/ui/card.tsx +89 -0
- package/templates/components/ui/checkbox.tsx +38 -0
- package/templates/components/ui/collapsible.tsx +33 -0
- package/templates/components/ui/command.tsx +196 -0
- package/templates/components/ui/dialog.tsx +153 -0
- package/templates/components/ui/dropdown-menu.tsx +280 -0
- package/templates/components/ui/form.tsx +171 -0
- package/templates/components/ui/icons.tsx +167 -0
- package/templates/components/ui/input.tsx +28 -0
- package/templates/components/ui/label.tsx +25 -0
- package/templates/components/ui/popover.tsx +59 -0
- package/templates/components/ui/progress.tsx +32 -0
- package/templates/components/ui/radio-group.tsx +45 -0
- package/templates/components/ui/scroll-area.tsx +63 -0
- package/templates/components/ui/select.tsx +208 -0
- package/templates/components/ui/separator.tsx +28 -0
- package/templates/components/ui/sheet.tsx +146 -0
- package/templates/components/ui/sidebar.tsx +726 -0
- package/templates/components/ui/skeleton.tsx +15 -0
- package/templates/components/ui/slider.tsx +58 -0
- package/templates/components/ui/sonner.tsx +47 -0
- package/templates/components/ui/spinner.tsx +27 -0
- package/templates/components/ui/submit-button.tsx +47 -0
- package/templates/components/ui/switch.tsx +31 -0
- package/templates/components/ui/table.tsx +120 -0
- package/templates/components/ui/tabs.tsx +75 -0
- package/templates/components/ui/textarea.tsx +26 -0
- package/templates/components/ui/tooltip.tsx +70 -0
- package/templates/hooks/use-mobile.ts +21 -0
- package/templates/lib/utils.ts +6 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import path5 from "path";
|
|
5
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
6
|
+
|
|
7
|
+
// src/config/index.ts
|
|
8
|
+
import fs from "fs/promises";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import { createCipheriv, createDecipheriv, randomBytes, createHash } from "crypto";
|
|
12
|
+
import { machineIdSync } from "node-machine-id";
|
|
13
|
+
import { input, password, select, confirm } from "@inquirer/prompts";
|
|
14
|
+
|
|
15
|
+
// src/ui/logger.ts
|
|
16
|
+
import chalk from "chalk";
|
|
17
|
+
var logger = {
|
|
18
|
+
info: (message) => {
|
|
19
|
+
console.log(chalk.blue("\u2139"), message);
|
|
20
|
+
},
|
|
21
|
+
success: (message) => {
|
|
22
|
+
console.log(chalk.green("\u2714"), message);
|
|
23
|
+
},
|
|
24
|
+
warning: (message) => {
|
|
25
|
+
console.log(chalk.yellow("\u26A0"), message);
|
|
26
|
+
},
|
|
27
|
+
error: (message) => {
|
|
28
|
+
console.log(chalk.red("\u2716"), message);
|
|
29
|
+
},
|
|
30
|
+
step: (step, total, message) => {
|
|
31
|
+
console.log(chalk.cyan(`[${step}/${total}]`), message);
|
|
32
|
+
},
|
|
33
|
+
newLine: () => {
|
|
34
|
+
console.log();
|
|
35
|
+
},
|
|
36
|
+
divider: () => {
|
|
37
|
+
console.log(chalk.gray("\u2500".repeat(50)));
|
|
38
|
+
},
|
|
39
|
+
title: (message) => {
|
|
40
|
+
console.log(chalk.bold.white(message));
|
|
41
|
+
},
|
|
42
|
+
subtitle: (message) => {
|
|
43
|
+
console.log(chalk.gray(message));
|
|
44
|
+
},
|
|
45
|
+
link: (label, url) => {
|
|
46
|
+
console.log(` ${chalk.gray(label + ":")} ${chalk.cyan.underline(url)}`);
|
|
47
|
+
},
|
|
48
|
+
list: (items) => {
|
|
49
|
+
items.forEach((item) => {
|
|
50
|
+
console.log(chalk.gray(" \u2022"), item);
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
table: (rows) => {
|
|
54
|
+
const maxLabelLength = Math.max(...rows.map((r) => r.label.length));
|
|
55
|
+
rows.forEach(({ label, value }) => {
|
|
56
|
+
const paddedLabel = label.padEnd(maxLabelLength);
|
|
57
|
+
console.log(` ${chalk.gray(paddedLabel)} ${value}`);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/ui/spinner.ts
|
|
63
|
+
import ora from "ora";
|
|
64
|
+
function createSpinner(text) {
|
|
65
|
+
return ora({
|
|
66
|
+
text,
|
|
67
|
+
spinner: "dots"
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async function withSpinner(text, fn, successText) {
|
|
71
|
+
const spinner = createSpinner(text).start();
|
|
72
|
+
try {
|
|
73
|
+
const result = await fn();
|
|
74
|
+
spinner.succeed(successText || text);
|
|
75
|
+
return result;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
spinner.fail();
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/config/index.ts
|
|
83
|
+
var CONFIG_FILE = path.join(os.homedir(), ".lftrc");
|
|
84
|
+
var ALGORITHM = "aes-256-gcm";
|
|
85
|
+
function getEncryptionKey() {
|
|
86
|
+
const machineId = machineIdSync();
|
|
87
|
+
return createHash("sha256").update(machineId + "lft-secret").digest();
|
|
88
|
+
}
|
|
89
|
+
function decryptConfig(encrypted) {
|
|
90
|
+
const { iv, tag, data } = JSON.parse(encrypted);
|
|
91
|
+
const key = getEncryptionKey();
|
|
92
|
+
const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(iv, "hex"));
|
|
93
|
+
decipher.setAuthTag(Buffer.from(tag, "hex"));
|
|
94
|
+
let decrypted = decipher.update(data, "hex", "utf8");
|
|
95
|
+
decrypted += decipher.final("utf8");
|
|
96
|
+
return JSON.parse(decrypted);
|
|
97
|
+
}
|
|
98
|
+
async function hasConfig() {
|
|
99
|
+
try {
|
|
100
|
+
await fs.access(CONFIG_FILE);
|
|
101
|
+
return true;
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function loadConfig() {
|
|
107
|
+
const encrypted = await fs.readFile(CONFIG_FILE, "utf8");
|
|
108
|
+
return decryptConfig(encrypted);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/services/github.ts
|
|
112
|
+
import { Octokit } from "octokit";
|
|
113
|
+
async function createGitHubRepo(projectName, config) {
|
|
114
|
+
const octokit = new Octokit({ auth: config.credentials.github.token });
|
|
115
|
+
const org = config.defaults.githubOrg;
|
|
116
|
+
return withSpinner(
|
|
117
|
+
"Creando repositorio en GitHub...",
|
|
118
|
+
async () => {
|
|
119
|
+
let repo;
|
|
120
|
+
if (org) {
|
|
121
|
+
repo = await octokit.rest.repos.createInOrg({
|
|
122
|
+
org,
|
|
123
|
+
name: projectName,
|
|
124
|
+
private: true,
|
|
125
|
+
auto_init: false,
|
|
126
|
+
description: `Proyecto ${projectName} creado con create-lft-app`
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
repo = await octokit.rest.repos.createForAuthenticatedUser({
|
|
130
|
+
name: projectName,
|
|
131
|
+
private: true,
|
|
132
|
+
auto_init: false,
|
|
133
|
+
description: `Proyecto ${projectName} creado con create-lft-app`
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return repo.data.html_url;
|
|
137
|
+
},
|
|
138
|
+
`Repositorio creado: ${org || config.credentials.github.username}/${projectName}`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/services/supabase.ts
|
|
143
|
+
var SUPABASE_API_URL = "https://api.supabase.com/v1";
|
|
144
|
+
async function waitForProjectReady(projectId, token, maxAttempts = 60) {
|
|
145
|
+
const spinner = createSpinner("Provisionando base de datos (esto puede tomar ~2 minutos)...").start();
|
|
146
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
147
|
+
const response = await fetch(`${SUPABASE_API_URL}/projects/${projectId}`, {
|
|
148
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
149
|
+
});
|
|
150
|
+
if (response.ok) {
|
|
151
|
+
const project = await response.json();
|
|
152
|
+
if (project.status === "ACTIVE_HEALTHY") {
|
|
153
|
+
spinner.succeed("Base de datos provisionada");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
await new Promise((resolve) => setTimeout(resolve, 5e3));
|
|
158
|
+
spinner.text = `Provisionando base de datos... (${Math.floor((i + 1) * 5 / 60)}min ${(i + 1) * 5 % 60}s)`;
|
|
159
|
+
}
|
|
160
|
+
spinner.fail("Timeout esperando a que el proyecto est\xE9 listo");
|
|
161
|
+
throw new Error("Timeout: el proyecto de Supabase no se activ\xF3 a tiempo");
|
|
162
|
+
}
|
|
163
|
+
async function getProjectApiKeys(projectId, token) {
|
|
164
|
+
const response = await fetch(`${SUPABASE_API_URL}/projects/${projectId}/api-keys`, {
|
|
165
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
166
|
+
});
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
throw new Error("No se pudieron obtener las API keys de Supabase");
|
|
169
|
+
}
|
|
170
|
+
const keys = await response.json();
|
|
171
|
+
const anonKey = keys.find((k) => k.name === "anon")?.api_key;
|
|
172
|
+
const serviceKey = keys.find((k) => k.name === "service_role")?.api_key;
|
|
173
|
+
if (!anonKey || !serviceKey) {
|
|
174
|
+
throw new Error("No se encontraron las API keys necesarias");
|
|
175
|
+
}
|
|
176
|
+
return { anonKey, serviceKey };
|
|
177
|
+
}
|
|
178
|
+
async function createSupabaseProject(projectName, config) {
|
|
179
|
+
const token = config.credentials.supabase.accessToken;
|
|
180
|
+
const orgId = config.credentials.supabase.organizationId;
|
|
181
|
+
const region = config.defaults.supabaseRegion;
|
|
182
|
+
const dbPassword = generateSecurePassword();
|
|
183
|
+
const project = await withSpinner(
|
|
184
|
+
"Creando proyecto en Supabase...",
|
|
185
|
+
async () => {
|
|
186
|
+
const response = await fetch(`${SUPABASE_API_URL}/projects`, {
|
|
187
|
+
method: "POST",
|
|
188
|
+
headers: {
|
|
189
|
+
Authorization: `Bearer ${token}`,
|
|
190
|
+
"Content-Type": "application/json"
|
|
191
|
+
},
|
|
192
|
+
body: JSON.stringify({
|
|
193
|
+
name: projectName,
|
|
194
|
+
organization_id: orgId,
|
|
195
|
+
region,
|
|
196
|
+
plan: "free",
|
|
197
|
+
db_pass: dbPassword
|
|
198
|
+
})
|
|
199
|
+
});
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
const error = await response.text();
|
|
202
|
+
throw new Error(`Error creando proyecto Supabase: ${error}`);
|
|
203
|
+
}
|
|
204
|
+
return response.json();
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
await waitForProjectReady(project.id, token);
|
|
208
|
+
const { anonKey, serviceKey } = await getProjectApiKeys(project.id, token);
|
|
209
|
+
const projectUrl = `https://${project.id}.supabase.co`;
|
|
210
|
+
return {
|
|
211
|
+
url: projectUrl,
|
|
212
|
+
anonKey,
|
|
213
|
+
serviceKey
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function generateSecurePassword() {
|
|
217
|
+
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
|
|
218
|
+
let password2 = "";
|
|
219
|
+
for (let i = 0; i < 32; i++) {
|
|
220
|
+
password2 += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
221
|
+
}
|
|
222
|
+
return password2;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/utils/validation.ts
|
|
226
|
+
import fs2 from "fs";
|
|
227
|
+
import path2 from "path";
|
|
228
|
+
function validateProjectName(name) {
|
|
229
|
+
if (!name || name.trim() === "") {
|
|
230
|
+
return { valid: false, error: "El nombre del proyecto no puede estar vac\xEDo" };
|
|
231
|
+
}
|
|
232
|
+
const validPattern = /^[a-zA-Z0-9_-]+$/;
|
|
233
|
+
if (!validPattern.test(name)) {
|
|
234
|
+
return {
|
|
235
|
+
valid: false,
|
|
236
|
+
error: "El nombre solo puede contener letras, n\xFAmeros, guiones (-) y guiones bajos (_)"
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (/^[-_0-9]/.test(name)) {
|
|
240
|
+
return {
|
|
241
|
+
valid: false,
|
|
242
|
+
error: "El nombre debe empezar con una letra"
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (name.length < 2) {
|
|
246
|
+
return { valid: false, error: "El nombre debe tener al menos 2 caracteres" };
|
|
247
|
+
}
|
|
248
|
+
if (name.length > 50) {
|
|
249
|
+
return { valid: false, error: "El nombre no puede tener m\xE1s de 50 caracteres" };
|
|
250
|
+
}
|
|
251
|
+
const projectPath = path2.resolve(process.cwd(), name);
|
|
252
|
+
if (fs2.existsSync(projectPath)) {
|
|
253
|
+
return { valid: false, error: `El directorio "${name}" ya existe` };
|
|
254
|
+
}
|
|
255
|
+
return { valid: true };
|
|
256
|
+
}
|
|
257
|
+
function generateJiraKey(projectName) {
|
|
258
|
+
const cleaned = projectName.replace(/[^a-zA-Z0-9]/g, "").toUpperCase().slice(0, 10);
|
|
259
|
+
return cleaned || "PROJ";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/services/jira.ts
|
|
263
|
+
async function createJiraProject(projectName, config) {
|
|
264
|
+
const { email, apiToken, domain } = config.credentials.jira;
|
|
265
|
+
const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
|
|
266
|
+
const projectKey = generateJiraKey(projectName);
|
|
267
|
+
return withSpinner(
|
|
268
|
+
"Creando proyecto en Jira...",
|
|
269
|
+
async () => {
|
|
270
|
+
const meResponse = await fetch(`https://${domain}/rest/api/3/myself`, {
|
|
271
|
+
headers: {
|
|
272
|
+
Authorization: `Basic ${auth}`,
|
|
273
|
+
"Content-Type": "application/json"
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
if (!meResponse.ok) {
|
|
277
|
+
throw new Error("No se pudo obtener informaci\xF3n del usuario de Jira");
|
|
278
|
+
}
|
|
279
|
+
const me = await meResponse.json();
|
|
280
|
+
const response = await fetch(`https://${domain}/rest/api/3/project`, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: {
|
|
283
|
+
Authorization: `Basic ${auth}`,
|
|
284
|
+
"Content-Type": "application/json"
|
|
285
|
+
},
|
|
286
|
+
body: JSON.stringify({
|
|
287
|
+
key: projectKey,
|
|
288
|
+
name: projectName,
|
|
289
|
+
projectTypeKey: "software",
|
|
290
|
+
projectTemplateKey: "com.pyxis.greenhopper.jira:gh-simplified-agility-scrum",
|
|
291
|
+
leadAccountId: me.accountId
|
|
292
|
+
})
|
|
293
|
+
});
|
|
294
|
+
if (!response.ok) {
|
|
295
|
+
const error = await response.text();
|
|
296
|
+
if (error.includes("project key")) {
|
|
297
|
+
const newKey = `${projectKey}${Date.now().toString().slice(-4)}`;
|
|
298
|
+
const retryResponse = await fetch(`https://${domain}/rest/api/3/project`, {
|
|
299
|
+
method: "POST",
|
|
300
|
+
headers: {
|
|
301
|
+
Authorization: `Basic ${auth}`,
|
|
302
|
+
"Content-Type": "application/json"
|
|
303
|
+
},
|
|
304
|
+
body: JSON.stringify({
|
|
305
|
+
key: newKey,
|
|
306
|
+
name: projectName,
|
|
307
|
+
projectTypeKey: "software",
|
|
308
|
+
projectTemplateKey: "com.pyxis.greenhopper.jira:gh-simplified-agility-scrum",
|
|
309
|
+
leadAccountId: me.accountId
|
|
310
|
+
})
|
|
311
|
+
});
|
|
312
|
+
if (!retryResponse.ok) {
|
|
313
|
+
throw new Error(`Error creando proyecto Jira: ${await retryResponse.text()}`);
|
|
314
|
+
}
|
|
315
|
+
const project2 = await retryResponse.json();
|
|
316
|
+
return `https://${domain}/browse/${project2.key}`;
|
|
317
|
+
}
|
|
318
|
+
throw new Error(`Error creando proyecto Jira: ${error}`);
|
|
319
|
+
}
|
|
320
|
+
const project = await response.json();
|
|
321
|
+
return `https://${domain}/browse/${project.key}`;
|
|
322
|
+
},
|
|
323
|
+
`Proyecto Jira creado: ${projectKey}`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/steps/scaffold-nextjs.ts
|
|
328
|
+
import { execa } from "execa";
|
|
329
|
+
async function scaffoldNextJs(projectName, projectPath) {
|
|
330
|
+
await withSpinner(
|
|
331
|
+
"Inicializando proyecto Next.js...",
|
|
332
|
+
async () => {
|
|
333
|
+
await execa("npx", [
|
|
334
|
+
"create-next-app@latest",
|
|
335
|
+
projectName,
|
|
336
|
+
"--typescript",
|
|
337
|
+
"--tailwind",
|
|
338
|
+
"--eslint",
|
|
339
|
+
"--app",
|
|
340
|
+
"--turbopack",
|
|
341
|
+
"--src-dir",
|
|
342
|
+
"--import-alias",
|
|
343
|
+
"@/*",
|
|
344
|
+
"--use-npm"
|
|
345
|
+
], {
|
|
346
|
+
cwd: process.cwd(),
|
|
347
|
+
stdio: "pipe"
|
|
348
|
+
});
|
|
349
|
+
},
|
|
350
|
+
"Proyecto Next.js inicializado"
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/steps/copy-template.ts
|
|
355
|
+
import { cp, mkdir, readFile, writeFile } from "fs/promises";
|
|
356
|
+
import path3 from "path";
|
|
357
|
+
import { fileURLToPath } from "url";
|
|
358
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
359
|
+
var __dirname2 = path3.dirname(__filename2);
|
|
360
|
+
async function copyTemplate(projectPath) {
|
|
361
|
+
await withSpinner(
|
|
362
|
+
"Copiando template LFT...",
|
|
363
|
+
async () => {
|
|
364
|
+
const templatesDir = path3.join(__dirname2, "..", "..", "templates");
|
|
365
|
+
const srcDir = path3.join(projectPath, "src");
|
|
366
|
+
await cp(
|
|
367
|
+
path3.join(templatesDir, "components", "ui"),
|
|
368
|
+
path3.join(srcDir, "components", "ui"),
|
|
369
|
+
{ recursive: true }
|
|
370
|
+
);
|
|
371
|
+
await cp(
|
|
372
|
+
path3.join(templatesDir, "components", "layout"),
|
|
373
|
+
path3.join(srcDir, "components", "layout"),
|
|
374
|
+
{ recursive: true }
|
|
375
|
+
);
|
|
376
|
+
await cp(
|
|
377
|
+
path3.join(templatesDir, "components", "dashboard"),
|
|
378
|
+
path3.join(srcDir, "components", "dashboard"),
|
|
379
|
+
{ recursive: true }
|
|
380
|
+
);
|
|
381
|
+
await mkdir(path3.join(srcDir, "lib"), { recursive: true });
|
|
382
|
+
await cp(
|
|
383
|
+
path3.join(templatesDir, "lib", "utils.ts"),
|
|
384
|
+
path3.join(srcDir, "lib", "utils.ts")
|
|
385
|
+
);
|
|
386
|
+
await mkdir(path3.join(srcDir, "hooks"), { recursive: true });
|
|
387
|
+
await cp(
|
|
388
|
+
path3.join(templatesDir, "hooks"),
|
|
389
|
+
path3.join(srcDir, "hooks"),
|
|
390
|
+
{ recursive: true }
|
|
391
|
+
);
|
|
392
|
+
await cp(
|
|
393
|
+
path3.join(templatesDir, "app", "layout.tsx"),
|
|
394
|
+
path3.join(srcDir, "app", "layout.tsx")
|
|
395
|
+
);
|
|
396
|
+
await cp(
|
|
397
|
+
path3.join(templatesDir, "app", "page.tsx"),
|
|
398
|
+
path3.join(srcDir, "app", "page.tsx")
|
|
399
|
+
);
|
|
400
|
+
await mkdir(path3.join(srcDir, "app", "dashboard"), { recursive: true });
|
|
401
|
+
await cp(
|
|
402
|
+
path3.join(templatesDir, "app", "dashboard", "page.tsx"),
|
|
403
|
+
path3.join(srcDir, "app", "dashboard", "page.tsx")
|
|
404
|
+
);
|
|
405
|
+
await mkdir(path3.join(srcDir, "app", "auth", "login"), { recursive: true });
|
|
406
|
+
await cp(
|
|
407
|
+
path3.join(templatesDir, "app", "auth", "login", "page.tsx"),
|
|
408
|
+
path3.join(srcDir, "app", "auth", "login", "page.tsx")
|
|
409
|
+
);
|
|
410
|
+
await mergeGlobalStyles(projectPath, templatesDir);
|
|
411
|
+
},
|
|
412
|
+
"Template LFT copiado (47 componentes + p\xE1ginas)"
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
async function mergeGlobalStyles(projectPath, templatesDir) {
|
|
416
|
+
const templateCssPath = path3.join(templatesDir, "app", "globals.css");
|
|
417
|
+
const projectCssPath = path3.join(projectPath, "src", "app", "globals.css");
|
|
418
|
+
try {
|
|
419
|
+
const templateCss = await readFile(templateCssPath, "utf-8");
|
|
420
|
+
const existingCss = await readFile(projectCssPath, "utf-8");
|
|
421
|
+
const merged = existingCss + "\n\n/* LFT Custom Styles */\n" + templateCss;
|
|
422
|
+
await writeFile(projectCssPath, merged);
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/steps/install-deps.ts
|
|
428
|
+
import { execa as execa2 } from "execa";
|
|
429
|
+
var TEMPLATE_DEPENDENCIES = [
|
|
430
|
+
// Radix UI primitives
|
|
431
|
+
"@radix-ui/react-accordion",
|
|
432
|
+
"@radix-ui/react-alert-dialog",
|
|
433
|
+
"@radix-ui/react-avatar",
|
|
434
|
+
"@radix-ui/react-checkbox",
|
|
435
|
+
"@radix-ui/react-collapsible",
|
|
436
|
+
"@radix-ui/react-dialog",
|
|
437
|
+
"@radix-ui/react-dropdown-menu",
|
|
438
|
+
"@radix-ui/react-label",
|
|
439
|
+
"@radix-ui/react-popover",
|
|
440
|
+
"@radix-ui/react-progress",
|
|
441
|
+
"@radix-ui/react-radio-group",
|
|
442
|
+
"@radix-ui/react-scroll-area",
|
|
443
|
+
"@radix-ui/react-select",
|
|
444
|
+
"@radix-ui/react-separator",
|
|
445
|
+
"@radix-ui/react-slider",
|
|
446
|
+
"@radix-ui/react-slot",
|
|
447
|
+
"@radix-ui/react-switch",
|
|
448
|
+
"@radix-ui/react-tabs",
|
|
449
|
+
"@radix-ui/react-tooltip",
|
|
450
|
+
// UI Utilities
|
|
451
|
+
"class-variance-authority",
|
|
452
|
+
"clsx",
|
|
453
|
+
"tailwind-merge",
|
|
454
|
+
// Icons
|
|
455
|
+
"lucide-react",
|
|
456
|
+
// Form handling
|
|
457
|
+
"react-hook-form",
|
|
458
|
+
"@hookform/resolvers",
|
|
459
|
+
// Command menu
|
|
460
|
+
"cmdk",
|
|
461
|
+
// Date picker
|
|
462
|
+
"react-day-picker",
|
|
463
|
+
"date-fns",
|
|
464
|
+
// Toast notifications
|
|
465
|
+
"sonner",
|
|
466
|
+
// Validation
|
|
467
|
+
"zod",
|
|
468
|
+
// Supabase client
|
|
469
|
+
"@supabase/supabase-js",
|
|
470
|
+
"@supabase/ssr"
|
|
471
|
+
];
|
|
472
|
+
var TEMPLATE_DEV_DEPENDENCIES = [
|
|
473
|
+
"tailwindcss-animate"
|
|
474
|
+
];
|
|
475
|
+
async function installDependencies(projectPath) {
|
|
476
|
+
await withSpinner(
|
|
477
|
+
`Instalando dependencias (${TEMPLATE_DEPENDENCIES.length} paquetes)...`,
|
|
478
|
+
async () => {
|
|
479
|
+
await execa2("npm", ["install", ...TEMPLATE_DEPENDENCIES], {
|
|
480
|
+
cwd: projectPath,
|
|
481
|
+
stdio: "pipe"
|
|
482
|
+
});
|
|
483
|
+
await execa2("npm", ["install", "-D", ...TEMPLATE_DEV_DEPENDENCIES], {
|
|
484
|
+
cwd: projectPath,
|
|
485
|
+
stdio: "pipe"
|
|
486
|
+
});
|
|
487
|
+
},
|
|
488
|
+
"Dependencias instaladas"
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/steps/create-env.ts
|
|
493
|
+
import { writeFile as writeFile2, readFile as readFile2, appendFile } from "fs/promises";
|
|
494
|
+
import path4 from "path";
|
|
495
|
+
async function createEnvFile(projectPath, supabaseKeys) {
|
|
496
|
+
await withSpinner(
|
|
497
|
+
"Creando archivo .env.local...",
|
|
498
|
+
async () => {
|
|
499
|
+
const envContent = `# Supabase
|
|
500
|
+
NEXT_PUBLIC_SUPABASE_URL=${supabaseKeys.url}
|
|
501
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabaseKeys.anonKey}
|
|
502
|
+
SUPABASE_SERVICE_ROLE_KEY=${supabaseKeys.serviceKey}
|
|
503
|
+
`;
|
|
504
|
+
await writeFile2(
|
|
505
|
+
path4.join(projectPath, ".env.local"),
|
|
506
|
+
envContent
|
|
507
|
+
);
|
|
508
|
+
const gitignorePath = path4.join(projectPath, ".gitignore");
|
|
509
|
+
try {
|
|
510
|
+
const gitignore = await readFile2(gitignorePath, "utf-8");
|
|
511
|
+
if (!gitignore.includes(".env.local")) {
|
|
512
|
+
await appendFile(gitignorePath, "\n# Environment variables\n.env.local\n.env*.local\n");
|
|
513
|
+
}
|
|
514
|
+
} catch {
|
|
515
|
+
await writeFile2(gitignorePath, "# Environment variables\n.env.local\n.env*.local\n");
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
"Archivo .env.local creado con credenciales de Supabase"
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/steps/setup-git.ts
|
|
523
|
+
import { simpleGit } from "simple-git";
|
|
524
|
+
async function setupGit(projectPath, remoteUrl) {
|
|
525
|
+
const git = simpleGit(projectPath);
|
|
526
|
+
await withSpinner(
|
|
527
|
+
"Configurando Git...",
|
|
528
|
+
async () => {
|
|
529
|
+
const isRepo = await git.checkIsRepo();
|
|
530
|
+
if (!isRepo) {
|
|
531
|
+
await git.init();
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
await git.addRemote("origin", remoteUrl);
|
|
535
|
+
} catch {
|
|
536
|
+
await git.remote(["set-url", "origin", remoteUrl]);
|
|
537
|
+
}
|
|
538
|
+
await git.add(".");
|
|
539
|
+
await git.commit("Initial commit - created with create-lft-app", {
|
|
540
|
+
"--author": "create-lft-app <noreply@lft.dev>"
|
|
541
|
+
});
|
|
542
|
+
try {
|
|
543
|
+
await git.branch(["-M", "main"]);
|
|
544
|
+
} catch {
|
|
545
|
+
}
|
|
546
|
+
await git.push(["--set-upstream", "origin", "main"]);
|
|
547
|
+
},
|
|
548
|
+
"Git configurado y c\xF3digo pusheado"
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/ui/banner.ts
|
|
553
|
+
import boxen from "boxen";
|
|
554
|
+
import chalk2 from "chalk";
|
|
555
|
+
function showSuccessBanner(projectName, urls) {
|
|
556
|
+
const lines = [
|
|
557
|
+
chalk2.green.bold(`Proyecto "${projectName}" creado exitosamente!`),
|
|
558
|
+
"",
|
|
559
|
+
chalk2.white("Directorio:") + ` ./${projectName}`,
|
|
560
|
+
""
|
|
561
|
+
];
|
|
562
|
+
if (urls.github || urls.supabase || urls.jira) {
|
|
563
|
+
lines.push(chalk2.white("Enlaces:"));
|
|
564
|
+
if (urls.github) {
|
|
565
|
+
lines.push(` ${chalk2.gray("GitHub:")} ${chalk2.cyan(urls.github)}`);
|
|
566
|
+
}
|
|
567
|
+
if (urls.supabase) {
|
|
568
|
+
lines.push(` ${chalk2.gray("Supabase:")} ${chalk2.cyan(urls.supabase)}`);
|
|
569
|
+
}
|
|
570
|
+
if (urls.jira) {
|
|
571
|
+
lines.push(` ${chalk2.gray("Jira:")} ${chalk2.cyan(urls.jira)}`);
|
|
572
|
+
}
|
|
573
|
+
lines.push("");
|
|
574
|
+
}
|
|
575
|
+
lines.push(chalk2.white("Siguiente pasos:"));
|
|
576
|
+
lines.push(` ${chalk2.cyan("cd")} ${projectName}`);
|
|
577
|
+
lines.push(` ${chalk2.cyan("npm run dev")}`);
|
|
578
|
+
const banner = boxen(lines.join("\n"), {
|
|
579
|
+
padding: 1,
|
|
580
|
+
margin: 1,
|
|
581
|
+
borderStyle: "round",
|
|
582
|
+
borderColor: "green"
|
|
583
|
+
});
|
|
584
|
+
console.log(banner);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/index.ts
|
|
588
|
+
async function createProject(projectName, options = {}) {
|
|
589
|
+
const validation = validateProjectName(projectName);
|
|
590
|
+
if (!validation.valid) {
|
|
591
|
+
throw new Error(validation.error);
|
|
592
|
+
}
|
|
593
|
+
const projectPath = path5.resolve(process.cwd(), projectName);
|
|
594
|
+
if (!await hasConfig()) {
|
|
595
|
+
logger.warning('No se encontr\xF3 configuraci\xF3n. Ejecuta "create-lft-app config" primero.');
|
|
596
|
+
throw new Error("Configuraci\xF3n no encontrada");
|
|
597
|
+
}
|
|
598
|
+
const config = await loadConfig();
|
|
599
|
+
logger.newLine();
|
|
600
|
+
logger.title("Resumen de recursos a crear:");
|
|
601
|
+
logger.newLine();
|
|
602
|
+
const resources = [];
|
|
603
|
+
if (!options.skipGithub) {
|
|
604
|
+
resources.push({ label: "GitHub", value: `${config.defaults.githubOrg || config.credentials.github.username}/${projectName} (privado)` });
|
|
605
|
+
}
|
|
606
|
+
if (!options.skipSupabase) {
|
|
607
|
+
resources.push({ label: "Supabase", value: `${projectName} en ${config.defaults.supabaseRegion}` });
|
|
608
|
+
}
|
|
609
|
+
if (!options.skipJira) {
|
|
610
|
+
resources.push({ label: "Jira", value: `Proyecto "${projectName}" en ${config.credentials.jira.domain}` });
|
|
611
|
+
}
|
|
612
|
+
resources.push({ label: "Next.js", value: "App Router + TypeScript + Tailwind + Dashboard" });
|
|
613
|
+
logger.table(resources);
|
|
614
|
+
logger.newLine();
|
|
615
|
+
if (!options.autoConfirm) {
|
|
616
|
+
const shouldContinue = await confirm2({
|
|
617
|
+
message: "\xBFContinuar con la creaci\xF3n?",
|
|
618
|
+
default: true
|
|
619
|
+
});
|
|
620
|
+
if (!shouldContinue) {
|
|
621
|
+
logger.info("Operaci\xF3n cancelada");
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
logger.newLine();
|
|
626
|
+
logger.divider();
|
|
627
|
+
logger.newLine();
|
|
628
|
+
const urls = {};
|
|
629
|
+
let supabaseKeys;
|
|
630
|
+
const externalTasks = [];
|
|
631
|
+
if (!options.skipGithub) {
|
|
632
|
+
externalTasks.push(
|
|
633
|
+
createGitHubRepo(projectName, config).then((url) => {
|
|
634
|
+
urls.github = url;
|
|
635
|
+
})
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
if (!options.skipSupabase) {
|
|
639
|
+
externalTasks.push(
|
|
640
|
+
createSupabaseProject(projectName, config).then((result) => {
|
|
641
|
+
urls.supabase = result.url;
|
|
642
|
+
supabaseKeys = result;
|
|
643
|
+
})
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
if (!options.skipJira) {
|
|
647
|
+
externalTasks.push(
|
|
648
|
+
createJiraProject(projectName, config).then((url) => {
|
|
649
|
+
urls.jira = url;
|
|
650
|
+
})
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
await Promise.all(externalTasks);
|
|
654
|
+
await scaffoldNextJs(projectName, projectPath);
|
|
655
|
+
await copyTemplate(projectPath);
|
|
656
|
+
await installDependencies(projectPath);
|
|
657
|
+
if (supabaseKeys) {
|
|
658
|
+
await createEnvFile(projectPath, supabaseKeys);
|
|
659
|
+
}
|
|
660
|
+
if (!options.skipGit && urls.github) {
|
|
661
|
+
await setupGit(projectPath, urls.github);
|
|
662
|
+
}
|
|
663
|
+
logger.newLine();
|
|
664
|
+
showSuccessBanner(projectName, urls);
|
|
665
|
+
}
|
|
666
|
+
export {
|
|
667
|
+
createProject
|
|
668
|
+
};
|
|
669
|
+
//# sourceMappingURL=index.js.map
|