@create-lft-app/cli 1.0.2 → 1.0.4
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.js +195 -402
- package/dist/bin/cli.js.map +1 -1
- package/dist/src/index.js +190 -176
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -2
package/dist/bin/cli.js
CHANGED
|
@@ -1,20 +1,89 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// bin/cli.ts
|
|
4
|
-
import "dotenv/config";
|
|
5
4
|
import { program } from "commander";
|
|
6
5
|
|
|
7
6
|
// src/index.ts
|
|
8
|
-
import
|
|
9
|
-
import { confirm
|
|
7
|
+
import path4 from "path";
|
|
8
|
+
import { confirm } from "@inquirer/prompts";
|
|
9
|
+
|
|
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 });
|
|
17
|
+
var STATIC_CONFIG = {
|
|
18
|
+
github: {
|
|
19
|
+
token: process.env.LFT_GITHUB_TOKEN || "",
|
|
20
|
+
username: process.env.LFT_GITHUB_USERNAME || "",
|
|
21
|
+
org: process.env.LFT_GITHUB_ORG || ""
|
|
22
|
+
},
|
|
23
|
+
supabase: {
|
|
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"
|
|
27
|
+
},
|
|
28
|
+
jira: {
|
|
29
|
+
email: process.env.LFT_JIRA_EMAIL || "",
|
|
30
|
+
token: process.env.LFT_JIRA_TOKEN || "",
|
|
31
|
+
domain: process.env.LFT_JIRA_DOMAIN || ""
|
|
32
|
+
}
|
|
33
|
+
};
|
|
10
34
|
|
|
11
35
|
// src/config/index.ts
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
36
|
+
async function loadConfig() {
|
|
37
|
+
return {
|
|
38
|
+
version: "1.0.0",
|
|
39
|
+
credentials: {
|
|
40
|
+
github: {
|
|
41
|
+
token: STATIC_CONFIG.github.token,
|
|
42
|
+
username: STATIC_CONFIG.github.username
|
|
43
|
+
},
|
|
44
|
+
supabase: {
|
|
45
|
+
accessToken: STATIC_CONFIG.supabase.token,
|
|
46
|
+
organizationId: STATIC_CONFIG.supabase.orgId
|
|
47
|
+
},
|
|
48
|
+
jira: {
|
|
49
|
+
email: STATIC_CONFIG.jira.email,
|
|
50
|
+
apiToken: STATIC_CONFIG.jira.token,
|
|
51
|
+
domain: STATIC_CONFIG.jira.domain
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
defaults: {
|
|
55
|
+
githubOrg: STATIC_CONFIG.github.org || void 0,
|
|
56
|
+
supabaseRegion: STATIC_CONFIG.supabase.region,
|
|
57
|
+
jiraProjectType: "software"
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async function hasConfig() {
|
|
62
|
+
return STATIC_CONFIG.github.token !== "" && STATIC_CONFIG.supabase.token !== "" && STATIC_CONFIG.jira.token !== "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/services/github.ts
|
|
66
|
+
import { Octokit } from "octokit";
|
|
67
|
+
|
|
68
|
+
// src/ui/spinner.ts
|
|
69
|
+
import ora from "ora";
|
|
70
|
+
function createSpinner(text) {
|
|
71
|
+
return ora({
|
|
72
|
+
text,
|
|
73
|
+
spinner: "dots"
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async function withSpinner(text, fn, successText) {
|
|
77
|
+
const spinner = createSpinner(text).start();
|
|
78
|
+
try {
|
|
79
|
+
const result = await fn();
|
|
80
|
+
spinner.succeed(successText || text);
|
|
81
|
+
return result;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
spinner.fail();
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
18
87
|
|
|
19
88
|
// src/ui/logger.ts
|
|
20
89
|
import chalk from "chalk";
|
|
@@ -63,324 +132,20 @@ var logger = {
|
|
|
63
132
|
}
|
|
64
133
|
};
|
|
65
134
|
|
|
66
|
-
// src/
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
spinner: "dots"
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
async function withSpinner(text, fn, successText) {
|
|
75
|
-
const spinner = createSpinner(text).start();
|
|
76
|
-
try {
|
|
77
|
-
const result = await fn();
|
|
78
|
-
spinner.succeed(successText || text);
|
|
79
|
-
return result;
|
|
80
|
-
} catch (error) {
|
|
81
|
-
spinner.fail();
|
|
82
|
-
throw error;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// src/config/index.ts
|
|
87
|
-
var { machineIdSync } = nodeMachineId;
|
|
88
|
-
var CONFIG_FILE = path.join(os.homedir(), ".lftrc");
|
|
89
|
-
var ALGORITHM = "aes-256-gcm";
|
|
90
|
-
function getEncryptionKey() {
|
|
91
|
-
const machineId = machineIdSync();
|
|
92
|
-
return createHash("sha256").update(machineId + "lft-secret").digest();
|
|
93
|
-
}
|
|
94
|
-
function encryptConfig(config) {
|
|
95
|
-
const key = getEncryptionKey();
|
|
96
|
-
const iv = randomBytes(16);
|
|
97
|
-
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
98
|
-
let encrypted = cipher.update(JSON.stringify(config), "utf8", "hex");
|
|
99
|
-
encrypted += cipher.final("hex");
|
|
100
|
-
const authTag = cipher.getAuthTag();
|
|
101
|
-
return JSON.stringify({
|
|
102
|
-
iv: iv.toString("hex"),
|
|
103
|
-
tag: authTag.toString("hex"),
|
|
104
|
-
data: encrypted
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
function decryptConfig(encrypted) {
|
|
108
|
-
const { iv, tag, data } = JSON.parse(encrypted);
|
|
109
|
-
const key = getEncryptionKey();
|
|
110
|
-
const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(iv, "hex"));
|
|
111
|
-
decipher.setAuthTag(Buffer.from(tag, "hex"));
|
|
112
|
-
let decrypted = decipher.update(data, "hex", "utf8");
|
|
113
|
-
decrypted += decipher.final("utf8");
|
|
114
|
-
return JSON.parse(decrypted);
|
|
115
|
-
}
|
|
116
|
-
var ENV_VARS = {
|
|
117
|
-
GITHUB_TOKEN: "LFT_GITHUB_TOKEN",
|
|
118
|
-
GITHUB_USERNAME: "LFT_GITHUB_USERNAME",
|
|
119
|
-
GITHUB_ORG: "LFT_GITHUB_ORG",
|
|
120
|
-
SUPABASE_TOKEN: "LFT_SUPABASE_TOKEN",
|
|
121
|
-
SUPABASE_ORG_ID: "LFT_SUPABASE_ORG_ID",
|
|
122
|
-
SUPABASE_REGION: "LFT_SUPABASE_REGION",
|
|
123
|
-
JIRA_EMAIL: "LFT_JIRA_EMAIL",
|
|
124
|
-
JIRA_TOKEN: "LFT_JIRA_TOKEN",
|
|
125
|
-
JIRA_DOMAIN: "LFT_JIRA_DOMAIN"
|
|
126
|
-
};
|
|
127
|
-
function loadConfigFromEnv() {
|
|
128
|
-
const githubToken = process.env[ENV_VARS.GITHUB_TOKEN];
|
|
129
|
-
const githubUsername = process.env[ENV_VARS.GITHUB_USERNAME];
|
|
130
|
-
const supabaseToken = process.env[ENV_VARS.SUPABASE_TOKEN];
|
|
131
|
-
const supabaseOrgId = process.env[ENV_VARS.SUPABASE_ORG_ID];
|
|
132
|
-
const jiraEmail = process.env[ENV_VARS.JIRA_EMAIL];
|
|
133
|
-
const jiraToken = process.env[ENV_VARS.JIRA_TOKEN];
|
|
134
|
-
const jiraDomain = process.env[ENV_VARS.JIRA_DOMAIN];
|
|
135
|
-
if (!githubToken || !supabaseToken || !jiraToken) {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
return {
|
|
139
|
-
version: "1.0.0",
|
|
140
|
-
credentials: {
|
|
141
|
-
github: {
|
|
142
|
-
token: githubToken,
|
|
143
|
-
username: githubUsername || ""
|
|
144
|
-
},
|
|
145
|
-
supabase: {
|
|
146
|
-
accessToken: supabaseToken,
|
|
147
|
-
organizationId: supabaseOrgId || ""
|
|
148
|
-
},
|
|
149
|
-
jira: {
|
|
150
|
-
email: jiraEmail || "",
|
|
151
|
-
apiToken: jiraToken,
|
|
152
|
-
domain: jiraDomain || ""
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
|
-
defaults: {
|
|
156
|
-
githubOrg: process.env[ENV_VARS.GITHUB_ORG],
|
|
157
|
-
supabaseRegion: process.env[ENV_VARS.SUPABASE_REGION] || "us-east-1",
|
|
158
|
-
jiraProjectType: "software"
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
function hasEnvConfig() {
|
|
163
|
-
return !!(process.env[ENV_VARS.GITHUB_TOKEN] && process.env[ENV_VARS.SUPABASE_TOKEN] && process.env[ENV_VARS.JIRA_TOKEN]);
|
|
164
|
-
}
|
|
165
|
-
async function hasConfig() {
|
|
166
|
-
if (hasEnvConfig()) return true;
|
|
135
|
+
// src/services/github.ts
|
|
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;
|
|
167
140
|
try {
|
|
168
|
-
await
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
async function loadConfig() {
|
|
175
|
-
const envConfig = loadConfigFromEnv();
|
|
176
|
-
if (envConfig) {
|
|
177
|
-
return envConfig;
|
|
178
|
-
}
|
|
179
|
-
const encrypted = await fs.readFile(CONFIG_FILE, "utf8");
|
|
180
|
-
return decryptConfig(encrypted);
|
|
181
|
-
}
|
|
182
|
-
async function saveConfig(config) {
|
|
183
|
-
const encrypted = encryptConfig(config);
|
|
184
|
-
await fs.writeFile(CONFIG_FILE, encrypted, "utf8");
|
|
185
|
-
}
|
|
186
|
-
async function showConfig() {
|
|
187
|
-
if (!await hasConfig()) {
|
|
188
|
-
logger.warning("No hay configuraci\xF3n guardada");
|
|
189
|
-
logger.info('Ejecuta "create-lft-app config" para configurar');
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const config = await loadConfig();
|
|
193
|
-
logger.newLine();
|
|
194
|
-
logger.title("Configuraci\xF3n actual:");
|
|
195
|
-
logger.newLine();
|
|
196
|
-
logger.subtitle("GitHub:");
|
|
197
|
-
logger.table([
|
|
198
|
-
{ label: "Usuario", value: config.credentials.github.username },
|
|
199
|
-
{ label: "Token", value: "***" + config.credentials.github.token.slice(-4) },
|
|
200
|
-
{ label: "Org por defecto", value: config.defaults.githubOrg || "(ninguna)" }
|
|
201
|
-
]);
|
|
202
|
-
logger.newLine();
|
|
203
|
-
logger.subtitle("Supabase:");
|
|
204
|
-
logger.table([
|
|
205
|
-
{ label: "Org ID", value: config.credentials.supabase.organizationId },
|
|
206
|
-
{ label: "Token", value: "***" + config.credentials.supabase.accessToken.slice(-4) },
|
|
207
|
-
{ label: "Regi\xF3n", value: config.defaults.supabaseRegion }
|
|
208
|
-
]);
|
|
209
|
-
logger.newLine();
|
|
210
|
-
logger.subtitle("Jira:");
|
|
211
|
-
logger.table([
|
|
212
|
-
{ label: "Email", value: config.credentials.jira.email },
|
|
213
|
-
{ label: "Dominio", value: config.credentials.jira.domain },
|
|
214
|
-
{ label: "Token", value: "***" + config.credentials.jira.apiToken.slice(-4) }
|
|
215
|
-
]);
|
|
216
|
-
}
|
|
217
|
-
async function resetConfig() {
|
|
218
|
-
if (!await hasConfig()) {
|
|
219
|
-
logger.info("No hay configuraci\xF3n para resetear");
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
const shouldReset = await confirm({
|
|
223
|
-
message: "\xBFEst\xE1s seguro de que quieres eliminar la configuraci\xF3n?",
|
|
224
|
-
default: false
|
|
225
|
-
});
|
|
226
|
-
if (shouldReset) {
|
|
227
|
-
await fs.unlink(CONFIG_FILE);
|
|
228
|
-
logger.success("Configuraci\xF3n eliminada");
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
async function configureCredentials(options = {}) {
|
|
232
|
-
let config;
|
|
233
|
-
if (await hasConfig()) {
|
|
234
|
-
config = await loadConfig();
|
|
235
|
-
logger.info("Configuraci\xF3n existente encontrada. Actualizando...");
|
|
236
|
-
} else {
|
|
237
|
-
config = {
|
|
238
|
-
version: "1.0.0",
|
|
239
|
-
credentials: {
|
|
240
|
-
github: { token: "", username: "" },
|
|
241
|
-
supabase: { accessToken: "", organizationId: "" },
|
|
242
|
-
jira: { email: "", apiToken: "", domain: "" }
|
|
243
|
-
},
|
|
244
|
-
defaults: {
|
|
245
|
-
supabaseRegion: "us-east-1",
|
|
246
|
-
jiraProjectType: "software"
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
const configureAll = !options.onlyGithub && !options.onlySupabase && !options.onlyJira;
|
|
251
|
-
if (configureAll || options.onlyGithub) {
|
|
252
|
-
logger.newLine();
|
|
253
|
-
logger.title("Configuraci\xF3n de GitHub");
|
|
254
|
-
logger.subtitle("Necesitas un Personal Access Token con permisos: repo, read:org");
|
|
255
|
-
logger.link("Crear token", "https://github.com/settings/tokens/new");
|
|
256
|
-
logger.newLine();
|
|
257
|
-
const githubToken = await password({
|
|
258
|
-
message: "GitHub Personal Access Token:",
|
|
259
|
-
mask: "*"
|
|
260
|
-
});
|
|
261
|
-
const isValid = await withSpinner(
|
|
262
|
-
"Validando token de GitHub...",
|
|
263
|
-
async () => {
|
|
264
|
-
const response = await fetch("https://api.github.com/user", {
|
|
265
|
-
headers: { Authorization: `Bearer ${githubToken}` }
|
|
266
|
-
});
|
|
267
|
-
if (!response.ok) return null;
|
|
268
|
-
return response.json();
|
|
269
|
-
}
|
|
270
|
-
);
|
|
271
|
-
if (!isValid) {
|
|
272
|
-
throw new Error("Token de GitHub inv\xE1lido");
|
|
273
|
-
}
|
|
274
|
-
config.credentials.github.token = githubToken;
|
|
275
|
-
config.credentials.github.username = isValid.login;
|
|
276
|
-
logger.success(`Conectado como: ${isValid.login}`);
|
|
277
|
-
const useOrg = await confirm({
|
|
278
|
-
message: "\xBFQuieres usar una organizaci\xF3n por defecto?",
|
|
279
|
-
default: false
|
|
280
|
-
});
|
|
281
|
-
if (useOrg) {
|
|
282
|
-
config.defaults.githubOrg = await input({
|
|
283
|
-
message: "Nombre de la organizaci\xF3n:"
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
if (configureAll || options.onlySupabase) {
|
|
288
|
-
logger.newLine();
|
|
289
|
-
logger.title("Configuraci\xF3n de Supabase");
|
|
290
|
-
logger.subtitle("Necesitas un Access Token de la Management API");
|
|
291
|
-
logger.link("Crear token", "https://supabase.com/dashboard/account/tokens");
|
|
292
|
-
logger.newLine();
|
|
293
|
-
const supabaseToken = await password({
|
|
294
|
-
message: "Supabase Access Token:",
|
|
295
|
-
mask: "*"
|
|
296
|
-
});
|
|
297
|
-
const orgs = await withSpinner(
|
|
298
|
-
"Obteniendo organizaciones de Supabase...",
|
|
299
|
-
async () => {
|
|
300
|
-
const response = await fetch("https://api.supabase.com/v1/organizations", {
|
|
301
|
-
headers: { Authorization: `Bearer ${supabaseToken}` }
|
|
302
|
-
});
|
|
303
|
-
if (!response.ok) return null;
|
|
304
|
-
return response.json();
|
|
305
|
-
}
|
|
306
|
-
);
|
|
307
|
-
if (!orgs || orgs.length === 0) {
|
|
308
|
-
throw new Error("Token de Supabase inv\xE1lido o no tienes organizaciones");
|
|
309
|
-
}
|
|
310
|
-
config.credentials.supabase.accessToken = supabaseToken;
|
|
311
|
-
if (orgs.length === 1) {
|
|
312
|
-
config.credentials.supabase.organizationId = orgs[0].id;
|
|
313
|
-
logger.success(`Usando organizaci\xF3n: ${orgs[0].name}`);
|
|
314
|
-
} else {
|
|
315
|
-
const selectedOrg = await select({
|
|
316
|
-
message: "Selecciona la organizaci\xF3n:",
|
|
317
|
-
choices: orgs.map((org) => ({ name: org.name, value: org.id }))
|
|
318
|
-
});
|
|
319
|
-
config.credentials.supabase.organizationId = selectedOrg;
|
|
320
|
-
}
|
|
321
|
-
config.defaults.supabaseRegion = await select({
|
|
322
|
-
message: "Regi\xF3n por defecto:",
|
|
323
|
-
choices: [
|
|
324
|
-
{ name: "US East (Virginia)", value: "us-east-1" },
|
|
325
|
-
{ name: "US West (Oregon)", value: "us-west-1" },
|
|
326
|
-
{ name: "EU West (Ireland)", value: "eu-west-1" },
|
|
327
|
-
{ name: "AP Southeast (Singapore)", value: "ap-southeast-1" },
|
|
328
|
-
{ name: "AP Northeast (Tokyo)", value: "ap-northeast-1" },
|
|
329
|
-
{ name: "SA East (S\xE3o Paulo)", value: "sa-east-1" }
|
|
330
|
-
],
|
|
331
|
-
default: "us-east-1"
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
if (configureAll || options.onlyJira) {
|
|
335
|
-
logger.newLine();
|
|
336
|
-
logger.title("Configuraci\xF3n de Jira");
|
|
337
|
-
logger.subtitle("Necesitas un API Token de Atlassian");
|
|
338
|
-
logger.link("Crear token", "https://id.atlassian.com/manage-profile/security/api-tokens");
|
|
339
|
-
logger.newLine();
|
|
340
|
-
const jiraEmail = await input({
|
|
341
|
-
message: "Email de Atlassian:"
|
|
141
|
+
const existing = await octokit.rest.repos.get({
|
|
142
|
+
owner,
|
|
143
|
+
repo: projectName
|
|
342
144
|
});
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
});
|
|
347
|
-
const jiraDomain = await input({
|
|
348
|
-
message: "Dominio de Jira (ej: empresa.atlassian.net):",
|
|
349
|
-
validate: (value) => {
|
|
350
|
-
if (!value.includes(".atlassian.net")) {
|
|
351
|
-
return "El dominio debe terminar en .atlassian.net";
|
|
352
|
-
}
|
|
353
|
-
return true;
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
const isValid = await withSpinner(
|
|
357
|
-
"Validando credenciales de Jira...",
|
|
358
|
-
async () => {
|
|
359
|
-
const auth = Buffer.from(`${jiraEmail}:${jiraToken}`).toString("base64");
|
|
360
|
-
const response = await fetch(`https://${jiraDomain}/rest/api/3/myself`, {
|
|
361
|
-
headers: { Authorization: `Basic ${auth}` }
|
|
362
|
-
});
|
|
363
|
-
return response.ok;
|
|
364
|
-
}
|
|
365
|
-
);
|
|
366
|
-
if (!isValid) {
|
|
367
|
-
throw new Error("Credenciales de Jira inv\xE1lidas");
|
|
368
|
-
}
|
|
369
|
-
config.credentials.jira.email = jiraEmail;
|
|
370
|
-
config.credentials.jira.apiToken = jiraToken;
|
|
371
|
-
config.credentials.jira.domain = jiraDomain;
|
|
372
|
-
logger.success("Credenciales de Jira v\xE1lidas");
|
|
145
|
+
logger.success(`GitHub: ${owner}/${projectName} (ya existe)`);
|
|
146
|
+
return existing.data.html_url;
|
|
147
|
+
} catch {
|
|
373
148
|
}
|
|
374
|
-
await saveConfig(config);
|
|
375
|
-
logger.newLine();
|
|
376
|
-
logger.success("Configuraci\xF3n guardada en ~/.lftrc");
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// src/services/github.ts
|
|
380
|
-
import { Octokit } from "octokit";
|
|
381
|
-
async function createGitHubRepo(projectName, config) {
|
|
382
|
-
const octokit = new Octokit({ auth: config.credentials.github.token });
|
|
383
|
-
const org = config.defaults.githubOrg;
|
|
384
149
|
return withSpinner(
|
|
385
150
|
"Creando repositorio en GitHub...",
|
|
386
151
|
async () => {
|
|
@@ -403,7 +168,7 @@ async function createGitHubRepo(projectName, config) {
|
|
|
403
168
|
}
|
|
404
169
|
return repo.data.html_url;
|
|
405
170
|
},
|
|
406
|
-
`
|
|
171
|
+
`GitHub: ${owner}/${projectName}`
|
|
407
172
|
);
|
|
408
173
|
}
|
|
409
174
|
|
|
@@ -422,7 +187,7 @@ async function waitForProjectReady(projectId, token, maxAttempts = 60) {
|
|
|
422
187
|
return;
|
|
423
188
|
}
|
|
424
189
|
}
|
|
425
|
-
await new Promise((
|
|
190
|
+
await new Promise((resolve2) => setTimeout(resolve2, 5e3));
|
|
426
191
|
spinner.text = `Provisionando base de datos... (${Math.floor((i + 1) * 5 / 60)}min ${(i + 1) * 5 % 60}s)`;
|
|
427
192
|
}
|
|
428
193
|
spinner.fail("Timeout esperando a que el proyecto est\xE9 listo");
|
|
@@ -443,10 +208,28 @@ async function getProjectApiKeys(projectId, token) {
|
|
|
443
208
|
}
|
|
444
209
|
return { anonKey, serviceKey };
|
|
445
210
|
}
|
|
446
|
-
async function
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
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
|
+
}
|
|
450
233
|
const dbPassword = generateSecurePassword();
|
|
451
234
|
const project = await withSpinner(
|
|
452
235
|
"Creando proyecto en Supabase...",
|
|
@@ -483,16 +266,14 @@ async function createSupabaseProject(projectName, config) {
|
|
|
483
266
|
}
|
|
484
267
|
function generateSecurePassword() {
|
|
485
268
|
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
|
|
486
|
-
let
|
|
269
|
+
let password = "";
|
|
487
270
|
for (let i = 0; i < 32; i++) {
|
|
488
|
-
|
|
271
|
+
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
489
272
|
}
|
|
490
|
-
return
|
|
273
|
+
return password;
|
|
491
274
|
}
|
|
492
275
|
|
|
493
276
|
// src/utils/validation.ts
|
|
494
|
-
import fs2 from "fs";
|
|
495
|
-
import path2 from "path";
|
|
496
277
|
function validateProjectName(name) {
|
|
497
278
|
if (!name || name.trim() === "") {
|
|
498
279
|
return { valid: false, error: "El nombre del proyecto no puede estar vac\xEDo" };
|
|
@@ -516,10 +297,6 @@ function validateProjectName(name) {
|
|
|
516
297
|
if (name.length > 50) {
|
|
517
298
|
return { valid: false, error: "El nombre no puede tener m\xE1s de 50 caracteres" };
|
|
518
299
|
}
|
|
519
|
-
const projectPath = path2.resolve(process.cwd(), name);
|
|
520
|
-
if (fs2.existsSync(projectPath)) {
|
|
521
|
-
return { valid: false, error: `El directorio "${name}" ya existe` };
|
|
522
|
-
}
|
|
523
300
|
return { valid: true };
|
|
524
301
|
}
|
|
525
302
|
function generateJiraKey(projectName) {
|
|
@@ -528,10 +305,29 @@ function generateJiraKey(projectName) {
|
|
|
528
305
|
}
|
|
529
306
|
|
|
530
307
|
// src/services/jira.ts
|
|
531
|
-
async function
|
|
532
|
-
const
|
|
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;
|
|
533
324
|
const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
|
|
534
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
|
+
}
|
|
535
331
|
return withSpinner(
|
|
536
332
|
"Creando proyecto en Jira...",
|
|
537
333
|
async () => {
|
|
@@ -588,13 +384,20 @@ async function createJiraProject(projectName, config) {
|
|
|
588
384
|
const project = await response.json();
|
|
589
385
|
return `https://${domain}/browse/${project.key}`;
|
|
590
386
|
},
|
|
591
|
-
`Proyecto Jira
|
|
387
|
+
`Proyecto Jira: ${projectKey}`
|
|
592
388
|
);
|
|
593
389
|
}
|
|
594
390
|
|
|
595
391
|
// src/steps/scaffold-nextjs.ts
|
|
596
392
|
import { execa } from "execa";
|
|
393
|
+
import { existsSync } from "fs";
|
|
394
|
+
import path from "path";
|
|
597
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
|
+
}
|
|
598
401
|
await withSpinner(
|
|
599
402
|
"Inicializando proyecto Next.js...",
|
|
600
403
|
async () => {
|
|
@@ -609,71 +412,72 @@ async function scaffoldNextJs(projectName, projectPath) {
|
|
|
609
412
|
"--src-dir",
|
|
610
413
|
"--import-alias",
|
|
611
414
|
"@/*",
|
|
612
|
-
"--use-npm"
|
|
415
|
+
"--use-npm",
|
|
416
|
+
"--yes"
|
|
613
417
|
], {
|
|
614
418
|
cwd: process.cwd(),
|
|
615
419
|
stdio: "pipe"
|
|
616
420
|
});
|
|
617
421
|
},
|
|
618
|
-
|
|
422
|
+
`Next.js: ${projectName}`
|
|
619
423
|
);
|
|
620
424
|
}
|
|
621
425
|
|
|
622
426
|
// src/steps/copy-template.ts
|
|
623
427
|
import { cp, mkdir, readFile, writeFile } from "fs/promises";
|
|
624
|
-
import
|
|
625
|
-
import { fileURLToPath } from "url";
|
|
626
|
-
var
|
|
627
|
-
var
|
|
428
|
+
import path2 from "path";
|
|
429
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
430
|
+
var __filename3 = fileURLToPath2(import.meta.url);
|
|
431
|
+
var __dirname3 = path2.dirname(__filename3);
|
|
628
432
|
async function copyTemplate(projectPath) {
|
|
629
433
|
await withSpinner(
|
|
630
434
|
"Copiando template LFT...",
|
|
631
435
|
async () => {
|
|
632
|
-
const templatesDir =
|
|
633
|
-
const srcDir =
|
|
436
|
+
const templatesDir = path2.join(__dirname3, "..", "..", "templates");
|
|
437
|
+
const srcDir = path2.join(projectPath, "src");
|
|
634
438
|
await cp(
|
|
635
|
-
|
|
636
|
-
|
|
439
|
+
path2.join(templatesDir, "components", "ui"),
|
|
440
|
+
path2.join(srcDir, "components", "ui"),
|
|
637
441
|
{ recursive: true }
|
|
638
442
|
);
|
|
639
443
|
await cp(
|
|
640
|
-
|
|
641
|
-
|
|
444
|
+
path2.join(templatesDir, "components", "layout"),
|
|
445
|
+
path2.join(srcDir, "components", "layout"),
|
|
642
446
|
{ recursive: true }
|
|
643
447
|
);
|
|
644
448
|
await cp(
|
|
645
|
-
|
|
646
|
-
|
|
449
|
+
path2.join(templatesDir, "components", "dashboard"),
|
|
450
|
+
path2.join(srcDir, "components", "dashboard"),
|
|
647
451
|
{ recursive: true }
|
|
648
452
|
);
|
|
649
|
-
await mkdir(
|
|
453
|
+
await mkdir(path2.join(srcDir, "lib"), { recursive: true });
|
|
650
454
|
await cp(
|
|
651
|
-
|
|
652
|
-
|
|
455
|
+
path2.join(templatesDir, "lib", "utils.ts"),
|
|
456
|
+
path2.join(srcDir, "lib", "utils.ts")
|
|
653
457
|
);
|
|
654
|
-
await mkdir(
|
|
458
|
+
await mkdir(path2.join(srcDir, "hooks"), { recursive: true });
|
|
655
459
|
await cp(
|
|
656
|
-
|
|
657
|
-
|
|
460
|
+
path2.join(templatesDir, "hooks"),
|
|
461
|
+
path2.join(srcDir, "hooks"),
|
|
658
462
|
{ recursive: true }
|
|
659
463
|
);
|
|
660
464
|
await cp(
|
|
661
|
-
|
|
662
|
-
|
|
465
|
+
path2.join(templatesDir, "app", "layout.tsx"),
|
|
466
|
+
path2.join(srcDir, "app", "layout.tsx")
|
|
663
467
|
);
|
|
664
468
|
await cp(
|
|
665
|
-
|
|
666
|
-
|
|
469
|
+
path2.join(templatesDir, "app", "page.tsx"),
|
|
470
|
+
path2.join(srcDir, "app", "page.tsx")
|
|
667
471
|
);
|
|
668
|
-
await mkdir(
|
|
472
|
+
await mkdir(path2.join(srcDir, "app", "dashboard"), { recursive: true });
|
|
669
473
|
await cp(
|
|
670
|
-
|
|
671
|
-
|
|
474
|
+
path2.join(templatesDir, "app", "dashboard", "page.tsx"),
|
|
475
|
+
path2.join(srcDir, "app", "dashboard", "page.tsx")
|
|
672
476
|
);
|
|
673
|
-
await mkdir(
|
|
477
|
+
await mkdir(path2.join(srcDir, "app", "auth", "login"), { recursive: true });
|
|
674
478
|
await cp(
|
|
675
|
-
|
|
676
|
-
|
|
479
|
+
path2.join(templatesDir, "app", "auth", "login", "page.tsx"),
|
|
480
|
+
path2.join(srcDir, "app", "auth", "login", "page.tsx")
|
|
677
481
|
);
|
|
678
482
|
await mergeGlobalStyles(projectPath, templatesDir);
|
|
679
483
|
},
|
|
@@ -681,8 +485,8 @@ async function copyTemplate(projectPath) {
|
|
|
681
485
|
);
|
|
682
486
|
}
|
|
683
487
|
async function mergeGlobalStyles(projectPath, templatesDir) {
|
|
684
|
-
const templateCssPath =
|
|
685
|
-
const projectCssPath =
|
|
488
|
+
const templateCssPath = path2.join(templatesDir, "app", "globals.css");
|
|
489
|
+
const projectCssPath = path2.join(projectPath, "src", "app", "globals.css");
|
|
686
490
|
try {
|
|
687
491
|
const templateCss = await readFile(templateCssPath, "utf-8");
|
|
688
492
|
const existingCss = await readFile(projectCssPath, "utf-8");
|
|
@@ -759,7 +563,7 @@ async function installDependencies(projectPath) {
|
|
|
759
563
|
|
|
760
564
|
// src/steps/create-env.ts
|
|
761
565
|
import { writeFile as writeFile2, readFile as readFile2, appendFile } from "fs/promises";
|
|
762
|
-
import
|
|
566
|
+
import path3 from "path";
|
|
763
567
|
async function createEnvFile(projectPath, supabaseKeys) {
|
|
764
568
|
await withSpinner(
|
|
765
569
|
"Creando archivo .env.local...",
|
|
@@ -770,10 +574,10 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabaseKeys.anonKey}
|
|
|
770
574
|
SUPABASE_SERVICE_ROLE_KEY=${supabaseKeys.serviceKey}
|
|
771
575
|
`;
|
|
772
576
|
await writeFile2(
|
|
773
|
-
|
|
577
|
+
path3.join(projectPath, ".env.local"),
|
|
774
578
|
envContent
|
|
775
579
|
);
|
|
776
|
-
const gitignorePath =
|
|
580
|
+
const gitignorePath = path3.join(projectPath, ".gitignore");
|
|
777
581
|
try {
|
|
778
582
|
const gitignore = await readFile2(gitignorePath, "utf-8");
|
|
779
583
|
if (!gitignore.includes(".env.local")) {
|
|
@@ -871,30 +675,30 @@ async function createProject(projectName, options = {}) {
|
|
|
871
675
|
if (!validation.valid) {
|
|
872
676
|
throw new Error(validation.error);
|
|
873
677
|
}
|
|
874
|
-
const projectPath =
|
|
678
|
+
const projectPath = path4.resolve(process.cwd(), projectName);
|
|
875
679
|
if (!await hasConfig()) {
|
|
876
680
|
logger.warning('No se encontr\xF3 configuraci\xF3n. Ejecuta "create-lft-app config" primero.');
|
|
877
681
|
throw new Error("Configuraci\xF3n no encontrada");
|
|
878
682
|
}
|
|
879
|
-
const
|
|
683
|
+
const config2 = await loadConfig();
|
|
880
684
|
logger.newLine();
|
|
881
685
|
logger.title("Resumen de recursos a crear:");
|
|
882
686
|
logger.newLine();
|
|
883
687
|
const resources = [];
|
|
884
688
|
if (!options.skipGithub) {
|
|
885
|
-
resources.push({ label: "GitHub", value: `${
|
|
689
|
+
resources.push({ label: "GitHub", value: `${config2.defaults.githubOrg || config2.credentials.github.username}/${projectName} (privado)` });
|
|
886
690
|
}
|
|
887
691
|
if (!options.skipSupabase) {
|
|
888
|
-
resources.push({ label: "Supabase", value: `${projectName} en ${
|
|
692
|
+
resources.push({ label: "Supabase", value: `${projectName} en ${config2.defaults.supabaseRegion}` });
|
|
889
693
|
}
|
|
890
694
|
if (!options.skipJira) {
|
|
891
|
-
resources.push({ label: "Jira", value: `Proyecto "${projectName}" en ${
|
|
695
|
+
resources.push({ label: "Jira", value: `Proyecto "${projectName}" en ${config2.credentials.jira.domain}` });
|
|
892
696
|
}
|
|
893
697
|
resources.push({ label: "Next.js", value: "App Router + TypeScript + Tailwind + Dashboard" });
|
|
894
698
|
logger.table(resources);
|
|
895
699
|
logger.newLine();
|
|
896
700
|
if (!options.autoConfirm) {
|
|
897
|
-
const shouldContinue = await
|
|
701
|
+
const shouldContinue = await confirm({
|
|
898
702
|
message: "\xBFContinuar con la creaci\xF3n?",
|
|
899
703
|
default: true
|
|
900
704
|
});
|
|
@@ -911,14 +715,14 @@ async function createProject(projectName, options = {}) {
|
|
|
911
715
|
const externalTasks = [];
|
|
912
716
|
if (!options.skipGithub) {
|
|
913
717
|
externalTasks.push(
|
|
914
|
-
createGitHubRepo(projectName,
|
|
718
|
+
createGitHubRepo(projectName, config2).then((url) => {
|
|
915
719
|
urls.github = url;
|
|
916
720
|
})
|
|
917
721
|
);
|
|
918
722
|
}
|
|
919
723
|
if (!options.skipSupabase) {
|
|
920
724
|
externalTasks.push(
|
|
921
|
-
createSupabaseProject(projectName,
|
|
725
|
+
createSupabaseProject(projectName, config2).then((result) => {
|
|
922
726
|
urls.supabase = result.url;
|
|
923
727
|
supabaseKeys = result;
|
|
924
728
|
})
|
|
@@ -926,7 +730,7 @@ async function createProject(projectName, options = {}) {
|
|
|
926
730
|
}
|
|
927
731
|
if (!options.skipJira) {
|
|
928
732
|
externalTasks.push(
|
|
929
|
-
createJiraProject(projectName,
|
|
733
|
+
createJiraProject(projectName, config2).then((url) => {
|
|
930
734
|
urls.jira = url;
|
|
931
735
|
})
|
|
932
736
|
);
|
|
@@ -959,6 +763,11 @@ program.argument("[project-name]", "Nombre del proyecto a crear").option("--skip
|
|
|
959
763
|
logger.info("Uso: create-lft-app <nombre-proyecto>");
|
|
960
764
|
process.exit(1);
|
|
961
765
|
}
|
|
766
|
+
if (!await hasConfig()) {
|
|
767
|
+
logger.error("Las credenciales no est\xE1n configuradas en el paquete");
|
|
768
|
+
logger.info("Contacta al administrador del CLI");
|
|
769
|
+
process.exit(1);
|
|
770
|
+
}
|
|
962
771
|
try {
|
|
963
772
|
await createProject(projectName, {
|
|
964
773
|
skipGithub: options.skipGithub,
|
|
@@ -974,21 +783,5 @@ program.argument("[project-name]", "Nombre del proyecto a crear").option("--skip
|
|
|
974
783
|
process.exit(1);
|
|
975
784
|
}
|
|
976
785
|
});
|
|
977
|
-
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) => {
|
|
978
|
-
showBanner();
|
|
979
|
-
if (options.show) {
|
|
980
|
-
await showConfig();
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
983
|
-
if (options.reset) {
|
|
984
|
-
await resetConfig();
|
|
985
|
-
return;
|
|
986
|
-
}
|
|
987
|
-
await configureCredentials({
|
|
988
|
-
onlyGithub: options.github,
|
|
989
|
-
onlySupabase: options.supabase,
|
|
990
|
-
onlyJira: options.jira
|
|
991
|
-
});
|
|
992
|
-
});
|
|
993
786
|
program.parse();
|
|
994
787
|
//# sourceMappingURL=cli.js.map
|