da-proj 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/README.md +410 -0
- package/package.json +51 -0
- package/src/README.md +190 -0
- package/src/commands/init.ts +236 -0
- package/src/commands/pull.ts +119 -0
- package/src/commands/push.ts +68 -0
- package/src/commands/secrets.ts +251 -0
- package/src/commands/setup-github-sync.ts +194 -0
- package/src/commands/sync-status.ts +131 -0
- package/src/commands/sync.ts +159 -0
- package/src/generators/mdx.ts +46 -0
- package/src/generators/readme.ts +45 -0
- package/src/generators/schema.ts +69 -0
- package/src/generators/workflow.ts +69 -0
- package/src/index.ts +98 -0
- package/src/types/index.ts +47 -0
- package/src/utils/config.ts +52 -0
- package/src/utils/github-config.ts +67 -0
- package/src/utils/github.ts +297 -0
- package/src/utils/logger.ts +20 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { mkdir, writeFile, readFile } from "fs/promises";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { input, select } from "@inquirer/prompts";
|
|
4
|
+
import type { ProjectMetadata } from "../types/index.js";
|
|
5
|
+
import { log, colors } from "../utils/logger.js";
|
|
6
|
+
import { generateMDX } from "../generators/mdx.js";
|
|
7
|
+
import { generateWorkflow } from "../generators/workflow.js";
|
|
8
|
+
import { generateReadme } from "../generators/readme.js";
|
|
9
|
+
import { generateSchema } from "../generators/schema.js";
|
|
10
|
+
|
|
11
|
+
export async function initCommand() {
|
|
12
|
+
log.title("🚀 Project Metadata Setup");
|
|
13
|
+
|
|
14
|
+
// Verificar que estamos en un repositorio git
|
|
15
|
+
if (!existsSync(".git")) {
|
|
16
|
+
log.error("Not a git repository. Please run this command in a git repository.");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Recopilar información del proyecto
|
|
21
|
+
log.info("Let's set up your project metadata...\n");
|
|
22
|
+
|
|
23
|
+
const title = await input({
|
|
24
|
+
message: "Project title",
|
|
25
|
+
default: "My Awesome Project"
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const category = await input({
|
|
29
|
+
message: "Category",
|
|
30
|
+
default: "Web Development"
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const type = await select({
|
|
34
|
+
message: "Project type",
|
|
35
|
+
choices: [
|
|
36
|
+
{ value: "small", name: "Small project" },
|
|
37
|
+
{ value: "featured", name: "Featured project" }
|
|
38
|
+
]
|
|
39
|
+
}) as "featured" | "small";
|
|
40
|
+
|
|
41
|
+
const status = await select({
|
|
42
|
+
message: "Project status",
|
|
43
|
+
choices: [
|
|
44
|
+
{ value: "in-progress", name: "In Progress" },
|
|
45
|
+
{ value: "active", name: "Active" },
|
|
46
|
+
{ value: "archived", name: "Archived" }
|
|
47
|
+
]
|
|
48
|
+
}) as "active" | "archived" | "in-progress";
|
|
49
|
+
|
|
50
|
+
const age = await input({
|
|
51
|
+
message: "Project age (optional)",
|
|
52
|
+
default: "+2 months"
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const demo = await input({
|
|
56
|
+
message: "Demo URL (optional)",
|
|
57
|
+
default: ""
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Tecnologías - input múltiple
|
|
61
|
+
const techInput = await input({
|
|
62
|
+
message: "Technologies (comma separated)",
|
|
63
|
+
default: "React, TypeScript, Node.js"
|
|
64
|
+
});
|
|
65
|
+
const technologies = techInput.split(",").map(t => t.trim()).filter(Boolean);
|
|
66
|
+
|
|
67
|
+
const coverImage = await input({
|
|
68
|
+
message: "Cover image path",
|
|
69
|
+
default: "/proj-images/cover.png"
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const galleryInput = await input({
|
|
73
|
+
message: "Gallery images (comma separated, optional)",
|
|
74
|
+
default: ""
|
|
75
|
+
});
|
|
76
|
+
const galleryImages = galleryInput ? galleryInput.split(",").map(t => t.trim()).filter(Boolean) : [];
|
|
77
|
+
|
|
78
|
+
let industry, timeline, details;
|
|
79
|
+
if (type === "featured") {
|
|
80
|
+
industry = await input({
|
|
81
|
+
message: "Industry",
|
|
82
|
+
default: "Technology"
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
timeline = await input({
|
|
86
|
+
message: "Timeline",
|
|
87
|
+
default: "Still Working"
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const detailsInput = await input({
|
|
91
|
+
message: "Project details (comma separated)",
|
|
92
|
+
default: "Built with modern stack, Scalable architecture, Production ready"
|
|
93
|
+
});
|
|
94
|
+
details = detailsInput.split(",").map(t => t.trim()).filter(Boolean);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const metadata: ProjectMetadata = {
|
|
98
|
+
title,
|
|
99
|
+
category,
|
|
100
|
+
type,
|
|
101
|
+
status,
|
|
102
|
+
age: age || undefined,
|
|
103
|
+
demo: demo || undefined,
|
|
104
|
+
technologies,
|
|
105
|
+
images: {
|
|
106
|
+
cover: coverImage,
|
|
107
|
+
gallery: galleryImages,
|
|
108
|
+
},
|
|
109
|
+
industry,
|
|
110
|
+
timeline,
|
|
111
|
+
details,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Contenido inicial del MDX
|
|
115
|
+
const mdxContent = `# ${title}
|
|
116
|
+
|
|
117
|
+
## Description
|
|
118
|
+
|
|
119
|
+
[Add your project description here]
|
|
120
|
+
|
|
121
|
+
## Key Features
|
|
122
|
+
|
|
123
|
+
- Feature 1
|
|
124
|
+
- Feature 2
|
|
125
|
+
- Feature 3
|
|
126
|
+
|
|
127
|
+
## Challenges
|
|
128
|
+
|
|
129
|
+
[Describe the technical challenges you faced]
|
|
130
|
+
|
|
131
|
+
## Outcomes
|
|
132
|
+
|
|
133
|
+
[What did you learn and achieve?]
|
|
134
|
+
|
|
135
|
+
## Future Improvements
|
|
136
|
+
|
|
137
|
+
- [ ] Improvement 1
|
|
138
|
+
- [ ] Improvement 2
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
// Crear archivos
|
|
142
|
+
log.info("\nCreating files...");
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
// .project-metadata.mdx
|
|
146
|
+
await writeFile(".project-metadata.mdx", generateMDX(metadata, mdxContent));
|
|
147
|
+
log.success("Created .project-metadata.mdx");
|
|
148
|
+
|
|
149
|
+
// .github/workflows/sync-portfolio.yml
|
|
150
|
+
await mkdir(".github/workflows", { recursive: true });
|
|
151
|
+
await writeFile(".github/workflows/sync-portfolio.yml", generateWorkflow());
|
|
152
|
+
log.success("Created .github/workflows/sync-portfolio.yml");
|
|
153
|
+
|
|
154
|
+
// README.md (solo si no existe)
|
|
155
|
+
if (!existsSync("README.md")) {
|
|
156
|
+
await writeFile("README.md", generateReadme(metadata));
|
|
157
|
+
log.success("Created README.md");
|
|
158
|
+
} else {
|
|
159
|
+
log.warn("README.md already exists, skipping");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// .project-schema.json
|
|
163
|
+
await writeFile(".project-schema.json", generateSchema());
|
|
164
|
+
log.success("Created .project-schema.json");
|
|
165
|
+
|
|
166
|
+
// Crear carpeta de imágenes con README
|
|
167
|
+
await mkdir("proj-images", { recursive: true });
|
|
168
|
+
const imagesReadme = `# Project Images Folder
|
|
169
|
+
|
|
170
|
+
Place your project images here:
|
|
171
|
+
|
|
172
|
+
- **cover.png/jpg**: Main cover image for your project
|
|
173
|
+
- **screenshot1.png/jpg**: Gallery image 1
|
|
174
|
+
- **screenshot2.png/jpg**: Gallery image 2
|
|
175
|
+
- **screenshot3.png/jpg**: Gallery image 3
|
|
176
|
+
|
|
177
|
+
## Recommended sizes:
|
|
178
|
+
|
|
179
|
+
- Cover image: 1200x630px (or 16:9 ratio)
|
|
180
|
+
- Screenshots: 1920x1080px or similar
|
|
181
|
+
|
|
182
|
+
## Tips:
|
|
183
|
+
|
|
184
|
+
- Use descriptive filenames
|
|
185
|
+
- Optimize images before uploading (use tools like TinyPNG)
|
|
186
|
+
- Supported formats: PNG, JPG, WebP
|
|
187
|
+
- Keep file sizes under 500KB for better performance
|
|
188
|
+
|
|
189
|
+
## Current images needed:
|
|
190
|
+
|
|
191
|
+
${coverImage ? `- [ ] ${coverImage}` : ''}
|
|
192
|
+
${galleryImages.map(img => `- [ ] ${img}`).join('\n')}
|
|
193
|
+
`;
|
|
194
|
+
await writeFile("proj-images/README.md", imagesReadme);
|
|
195
|
+
await writeFile("proj-images/.gitkeep", "");
|
|
196
|
+
log.success("Created proj-images/ folder");
|
|
197
|
+
|
|
198
|
+
// .gitignore update
|
|
199
|
+
let gitignore = "";
|
|
200
|
+
if (existsSync(".gitignore")) {
|
|
201
|
+
gitignore = await readFile(".gitignore", "utf-8");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!gitignore.includes("node_modules")) {
|
|
205
|
+
gitignore += "\n# Dependencies\nnode_modules/\n";
|
|
206
|
+
await writeFile(".gitignore", gitignore);
|
|
207
|
+
log.success("Updated .gitignore");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
log.title("✨ Setup Complete!");
|
|
211
|
+
|
|
212
|
+
console.log(`
|
|
213
|
+
${colors.bright}Next steps:${colors.reset}
|
|
214
|
+
|
|
215
|
+
1. ${colors.cyan}Add images${colors.reset} to the proj-images/ folder:
|
|
216
|
+
- Cover image: ${coverImage}
|
|
217
|
+
${galleryImages.length > 0 ? galleryImages.map(img => `- Gallery: ${img}`).join('\n ') : ''}
|
|
218
|
+
|
|
219
|
+
2. ${colors.cyan}Review and edit${colors.reset} .project-metadata.mdx
|
|
220
|
+
|
|
221
|
+
3. ${colors.cyan}Set up secrets${colors.reset} in GitHub:
|
|
222
|
+
Run: ${colors.yellow}da-proj --secrets${colors.reset}
|
|
223
|
+
|
|
224
|
+
4. ${colors.cyan}Commit and push${colors.reset}:
|
|
225
|
+
${colors.yellow}git add .
|
|
226
|
+
git commit -m "Add portfolio metadata"
|
|
227
|
+
git push${colors.reset}
|
|
228
|
+
|
|
229
|
+
${colors.bright}Portfolio will auto-sync on push!${colors.reset} 🎉
|
|
230
|
+
`);
|
|
231
|
+
|
|
232
|
+
} catch (error) {
|
|
233
|
+
log.error("Failed to create files: " + error);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { select } from "@inquirer/prompts";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { readFile, writeFile } from "fs/promises";
|
|
4
|
+
import type { Config } from "../types/index.js";
|
|
5
|
+
import { getConfigPath } from "../utils/config.js";
|
|
6
|
+
import { readGitHubSyncConfig } from "../utils/github-config.js";
|
|
7
|
+
import { downloadFile } from "../utils/github.js";
|
|
8
|
+
import { colors, log } from "../utils/logger.js";
|
|
9
|
+
|
|
10
|
+
export async function pullCommand() {
|
|
11
|
+
log.title("📥 Pull from GitHub");
|
|
12
|
+
|
|
13
|
+
// 1. Verificar que GitHub sync esté configurado
|
|
14
|
+
const syncConfig = await readGitHubSyncConfig();
|
|
15
|
+
if (!syncConfig) {
|
|
16
|
+
log.error("GitHub sync not configured.");
|
|
17
|
+
console.log(
|
|
18
|
+
`\nRun: ${colors.cyan}da-proj --setup-github-sync${colors.reset}\n`
|
|
19
|
+
);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 2. Descargar desde GitHub
|
|
24
|
+
log.info("Downloading configuration from GitHub...");
|
|
25
|
+
|
|
26
|
+
let remoteConfig: string;
|
|
27
|
+
try {
|
|
28
|
+
remoteConfig = await downloadFile(
|
|
29
|
+
syncConfig.repoUrl,
|
|
30
|
+
"da-proj-config.json"
|
|
31
|
+
);
|
|
32
|
+
} catch (error: any) {
|
|
33
|
+
if (error.message.includes("not found")) {
|
|
34
|
+
log.error("No configuration found in GitHub.");
|
|
35
|
+
console.log(
|
|
36
|
+
`\nUpload your configuration first with: ${colors.cyan}da-proj --push${colors.reset}\n`
|
|
37
|
+
);
|
|
38
|
+
} else {
|
|
39
|
+
log.error(`Failed to download: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Validar que sea JSON válido
|
|
45
|
+
let remoteParsed: Config;
|
|
46
|
+
try {
|
|
47
|
+
remoteParsed = JSON.parse(remoteConfig);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
log.error("Downloaded configuration is not valid JSON.");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. Verificar si existe config local
|
|
54
|
+
const localConfigPath = getConfigPath();
|
|
55
|
+
|
|
56
|
+
if (existsSync(localConfigPath)) {
|
|
57
|
+
const localConfig = await readFile(localConfigPath, "utf-8");
|
|
58
|
+
|
|
59
|
+
// Comparar
|
|
60
|
+
if (localConfig === remoteConfig) {
|
|
61
|
+
log.success("Already up to date!");
|
|
62
|
+
console.log("\nLocal and GitHub configurations are identical.\n");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Preguntar qué hacer
|
|
67
|
+
const action = await select({
|
|
68
|
+
message: "Local configuration exists. What do you want to do?",
|
|
69
|
+
choices: [
|
|
70
|
+
{ value: "replace", name: "Replace local with GitHub version" },
|
|
71
|
+
{ value: "merge", name: "Merge (combine profiles from both)" },
|
|
72
|
+
{ value: "cancel", name: "Cancel" },
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (action === "cancel") {
|
|
77
|
+
log.info("Pull cancelled.");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (action === "merge") {
|
|
82
|
+
// Merge profiles
|
|
83
|
+
const local: Config = JSON.parse(localConfig);
|
|
84
|
+
const remote: Config = remoteParsed;
|
|
85
|
+
|
|
86
|
+
const localProfiles = local.profiles || [];
|
|
87
|
+
const remoteProfiles = remote.profiles || [];
|
|
88
|
+
|
|
89
|
+
// Combinar: primero los de GitHub, luego los locales que no existan en GitHub
|
|
90
|
+
const merged: Config = {
|
|
91
|
+
profiles: [
|
|
92
|
+
...remoteProfiles,
|
|
93
|
+
...localProfiles.filter(
|
|
94
|
+
(lp) => !remoteProfiles.some((rp) => rp.name === lp.name)
|
|
95
|
+
),
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
remoteConfig = JSON.stringify(merged, null, 2);
|
|
100
|
+
log.info(`Merged: ${merged.profiles?.length || 0} total profiles`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 4. Guardar localmente
|
|
105
|
+
await writeFile(localConfigPath, remoteConfig);
|
|
106
|
+
log.success("Configuration pulled from GitHub!");
|
|
107
|
+
|
|
108
|
+
// Mostrar perfiles
|
|
109
|
+
const config: Config = JSON.parse(remoteConfig);
|
|
110
|
+
if (config.profiles && config.profiles.length > 0) {
|
|
111
|
+
console.log(
|
|
112
|
+
`\n${colors.bright}Profiles (${config.profiles.length}):${colors.reset}`
|
|
113
|
+
);
|
|
114
|
+
config.profiles.forEach((p) => {
|
|
115
|
+
console.log(` • ${p.name} (${p.portfolioUrl})`);
|
|
116
|
+
});
|
|
117
|
+
console.log("");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { getConfigPath } from "../utils/config.js";
|
|
4
|
+
import { readGitHubSyncConfig } from "../utils/github-config.js";
|
|
5
|
+
import { uploadFile } from "../utils/github.js";
|
|
6
|
+
import { colors, log } from "../utils/logger.js";
|
|
7
|
+
|
|
8
|
+
export async function pushCommand() {
|
|
9
|
+
log.title("📤 Push to GitHub");
|
|
10
|
+
|
|
11
|
+
// 1. Verificar que GitHub sync esté configurado
|
|
12
|
+
const syncConfig = await readGitHubSyncConfig();
|
|
13
|
+
if (!syncConfig) {
|
|
14
|
+
log.error("GitHub sync not configured.");
|
|
15
|
+
console.log(
|
|
16
|
+
`\nRun: ${colors.cyan}da-proj --setup-github-sync${colors.reset}\n`
|
|
17
|
+
);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 2. Leer configuración local
|
|
22
|
+
const localConfigPath = getConfigPath();
|
|
23
|
+
if (!existsSync(localConfigPath)) {
|
|
24
|
+
log.error("No local configuration found.");
|
|
25
|
+
console.log(
|
|
26
|
+
`\nCreate profiles first with: ${colors.cyan}da-proj --secrets${colors.reset}\n`
|
|
27
|
+
);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const localConfig = await readFile(localConfigPath, "utf-8");
|
|
32
|
+
|
|
33
|
+
// Validar que sea JSON válido
|
|
34
|
+
try {
|
|
35
|
+
JSON.parse(localConfig);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
log.error("Local configuration is not valid JSON.");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 3. Subir a GitHub
|
|
42
|
+
log.info("Uploading configuration to GitHub...");
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await uploadFile(syncConfig.repoUrl, "da-proj-config.json", localConfig);
|
|
46
|
+
|
|
47
|
+
log.success("Configuration pushed to GitHub!");
|
|
48
|
+
console.log(
|
|
49
|
+
`\n${colors.bright}Repository:${colors.reset} ${syncConfig.repoUrl}`
|
|
50
|
+
);
|
|
51
|
+
console.log(`${colors.bright}File:${colors.reset} da-proj-config.json\n`);
|
|
52
|
+
|
|
53
|
+
// Mostrar resumen
|
|
54
|
+
const config = JSON.parse(localConfig);
|
|
55
|
+
if (config.profiles && config.profiles.length > 0) {
|
|
56
|
+
console.log(
|
|
57
|
+
`${colors.bright}Uploaded ${config.profiles.length} profile(s):${colors.reset}`
|
|
58
|
+
);
|
|
59
|
+
config.profiles.forEach((p: any) => {
|
|
60
|
+
console.log(` • ${p.name} (${p.portfolioUrl})`);
|
|
61
|
+
});
|
|
62
|
+
console.log("");
|
|
63
|
+
}
|
|
64
|
+
} catch (error: any) {
|
|
65
|
+
log.error(`Failed to push: ${error.message}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { input, select } from "@inquirer/prompts";
|
|
2
|
+
import { log, colors } from "../utils/logger.js";
|
|
3
|
+
import {
|
|
4
|
+
checkGitHubCLI,
|
|
5
|
+
getExistingSecrets,
|
|
6
|
+
setGitHubSecret
|
|
7
|
+
} from "../utils/github.js";
|
|
8
|
+
import {
|
|
9
|
+
readGlobalConfig,
|
|
10
|
+
saveGlobalConfig,
|
|
11
|
+
getConfigPath
|
|
12
|
+
} from "../utils/config.js";
|
|
13
|
+
import { exec } from "child_process";
|
|
14
|
+
import { promisify } from "util";
|
|
15
|
+
|
|
16
|
+
const execAsync = promisify(exec);
|
|
17
|
+
|
|
18
|
+
export async function secretsCommand() {
|
|
19
|
+
log.title("🔐 GitHub Secrets Setup");
|
|
20
|
+
|
|
21
|
+
// Verificar GitHub CLI
|
|
22
|
+
const hasGH = await checkGitHubCLI();
|
|
23
|
+
if (!hasGH) {
|
|
24
|
+
log.error("GitHub CLI (gh) is not installed.");
|
|
25
|
+
console.log(`
|
|
26
|
+
${colors.yellow}To install GitHub CLI:${colors.reset}
|
|
27
|
+
|
|
28
|
+
${colors.bright}Windows:${colors.reset}
|
|
29
|
+
winget install --id GitHub.cli
|
|
30
|
+
|
|
31
|
+
${colors.bright}Or download from:${colors.reset}
|
|
32
|
+
https://cli.github.com/
|
|
33
|
+
|
|
34
|
+
${colors.bright}After installing, authenticate:${colors.reset}
|
|
35
|
+
gh auth login
|
|
36
|
+
`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Verificar autenticación
|
|
41
|
+
try {
|
|
42
|
+
await execAsync('gh auth status');
|
|
43
|
+
} catch {
|
|
44
|
+
log.error("Not authenticated with GitHub CLI.");
|
|
45
|
+
console.log(`
|
|
46
|
+
${colors.yellow}Please authenticate first:${colors.reset}
|
|
47
|
+
gh auth login
|
|
48
|
+
`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
log.info("GitHub CLI is ready!\n");
|
|
53
|
+
|
|
54
|
+
// Verificar si ya existen secrets en este repo
|
|
55
|
+
const existingSecrets = await getExistingSecrets();
|
|
56
|
+
|
|
57
|
+
if (existingSecrets.url || existingSecrets.key) {
|
|
58
|
+
log.warn("Found existing secrets in this repository:");
|
|
59
|
+
if (existingSecrets.url) console.log(` ${colors.yellow}✓${colors.reset} PORTFOLIO_API_URL`);
|
|
60
|
+
if (existingSecrets.key) console.log(` ${colors.yellow}✓${colors.reset} PORTFOLIO_API_KEY`);
|
|
61
|
+
console.log('');
|
|
62
|
+
|
|
63
|
+
const overwrite = await select({
|
|
64
|
+
message: "Do you want to overwrite them?",
|
|
65
|
+
choices: [
|
|
66
|
+
{ value: "yes", name: "Yes, overwrite with new configuration" },
|
|
67
|
+
{ value: "no", name: "No, keep existing secrets" }
|
|
68
|
+
]
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (overwrite === "no") {
|
|
72
|
+
log.info("Keeping existing secrets. No changes made.");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log('');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Leer configuración existente
|
|
80
|
+
const config = await readGlobalConfig();
|
|
81
|
+
const profiles = config.profiles || [];
|
|
82
|
+
|
|
83
|
+
let selectedProfile: { name: string; portfolioUrl: string; apiKey: string } | null = null;
|
|
84
|
+
|
|
85
|
+
if (profiles.length > 0) {
|
|
86
|
+
log.info(`Found ${profiles.length} saved profile(s):\n`);
|
|
87
|
+
|
|
88
|
+
const choices = [
|
|
89
|
+
...profiles.map((p, i) => ({
|
|
90
|
+
value: `profile-${i}`,
|
|
91
|
+
name: `${p.name} (${p.portfolioUrl})`
|
|
92
|
+
})),
|
|
93
|
+
{ value: "new", name: "➕ Create new profile" },
|
|
94
|
+
{ value: "list", name: "📋 List all profiles" }
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const selection = await select({
|
|
98
|
+
message: "Select a profile or create new:",
|
|
99
|
+
choices
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (selection === "list") {
|
|
103
|
+
// Mostrar todas las configuraciones
|
|
104
|
+
log.title("📋 Saved Profiles");
|
|
105
|
+
profiles.forEach((p, i) => {
|
|
106
|
+
console.log(`\n${colors.bright}${i + 1}. ${p.name}${colors.reset}`);
|
|
107
|
+
console.log(` URL: ${p.portfolioUrl}`);
|
|
108
|
+
console.log(` Key: ${p.apiKey.substring(0, 16)}...${p.apiKey.substring(p.apiKey.length - 4)}`);
|
|
109
|
+
});
|
|
110
|
+
console.log('');
|
|
111
|
+
|
|
112
|
+
const profileIndex = await input({
|
|
113
|
+
message: "Enter profile number to use (or 0 to create new):",
|
|
114
|
+
default: "1"
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const idx = parseInt(profileIndex) - 1;
|
|
118
|
+
if (idx >= 0 && idx < profiles.length) {
|
|
119
|
+
selectedProfile = profiles[idx] || null;
|
|
120
|
+
}
|
|
121
|
+
} else if (selection.startsWith("profile-")) {
|
|
122
|
+
const idx = parseInt(selection.replace("profile-", ""));
|
|
123
|
+
selectedProfile = profiles[idx] || null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (selectedProfile) {
|
|
127
|
+
log.info(`Using profile: ${selectedProfile.name}`);
|
|
128
|
+
|
|
129
|
+
// Usar configuración existente
|
|
130
|
+
try {
|
|
131
|
+
if (existingSecrets.url) {
|
|
132
|
+
log.info("Overwriting PORTFOLIO_API_URL...");
|
|
133
|
+
}
|
|
134
|
+
await setGitHubSecret('PORTFOLIO_API_URL', selectedProfile.portfolioUrl);
|
|
135
|
+
log.success(existingSecrets.url ? "Updated PORTFOLIO_API_URL" : "Set PORTFOLIO_API_URL");
|
|
136
|
+
|
|
137
|
+
if (existingSecrets.key) {
|
|
138
|
+
log.info("Overwriting PORTFOLIO_API_KEY...");
|
|
139
|
+
}
|
|
140
|
+
await setGitHubSecret('PORTFOLIO_API_KEY', selectedProfile.apiKey);
|
|
141
|
+
log.success(existingSecrets.key ? "Updated PORTFOLIO_API_KEY" : "Set PORTFOLIO_API_KEY");
|
|
142
|
+
|
|
143
|
+
log.title("✨ Secrets configured successfully!");
|
|
144
|
+
console.log(`\n${colors.bright}Using profile: ${selectedProfile.name}${colors.reset} 🎉\n`);
|
|
145
|
+
return;
|
|
146
|
+
} catch (error: any) {
|
|
147
|
+
log.error("Failed to set secrets: " + error.message);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Crear nuevo perfil
|
|
154
|
+
const profileName = await input({
|
|
155
|
+
message: "Profile name (e.g., 'main', 'personal', 'work')",
|
|
156
|
+
default: profiles.length === 0 ? "main" : "profile-" + (profiles.length + 1)
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const portfolioUrl = await input({
|
|
160
|
+
message: "Portfolio API URL",
|
|
161
|
+
default: "https://your-portfolio.com"
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const portfolioKey = await input({
|
|
165
|
+
message: "Portfolio API Key (leave empty to generate one)",
|
|
166
|
+
default: ""
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
let apiKey = portfolioKey;
|
|
170
|
+
|
|
171
|
+
// Generar API key si está vacía
|
|
172
|
+
if (!apiKey) {
|
|
173
|
+
log.info("Generating API key...");
|
|
174
|
+
const randomBytes = new Uint8Array(32);
|
|
175
|
+
crypto.getRandomValues(randomBytes);
|
|
176
|
+
apiKey = Array.from(randomBytes)
|
|
177
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
178
|
+
.join('');
|
|
179
|
+
|
|
180
|
+
console.log(`\n${colors.bright}Generated API Key:${colors.reset}`);
|
|
181
|
+
console.log('━'.repeat(70));
|
|
182
|
+
console.log(apiKey);
|
|
183
|
+
console.log('━'.repeat(70));
|
|
184
|
+
console.log(`\n${colors.yellow}⚠️ IMPORTANT: Save this key!${colors.reset}`);
|
|
185
|
+
console.log(`\n${colors.bright}1. Add it to your portfolio as environment variable:${colors.reset}`);
|
|
186
|
+
console.log(` PORTFOLIO_API_KEY=${apiKey}`);
|
|
187
|
+
console.log(`\n${colors.bright}2. This key will be saved locally and reused for all your projects${colors.reset}\n`);
|
|
188
|
+
|
|
189
|
+
await input({
|
|
190
|
+
message: "Press Enter to continue and set the secrets in GitHub...",
|
|
191
|
+
default: ""
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Guardar configuración globalmente
|
|
196
|
+
await saveGlobalConfig({ name: profileName, portfolioUrl, apiKey });
|
|
197
|
+
|
|
198
|
+
// Configurar secrets en GitHub
|
|
199
|
+
log.info("Setting GitHub secrets...");
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
if (existingSecrets.url) {
|
|
203
|
+
log.info("Overwriting PORTFOLIO_API_URL...");
|
|
204
|
+
}
|
|
205
|
+
await setGitHubSecret('PORTFOLIO_API_URL', portfolioUrl);
|
|
206
|
+
log.success(existingSecrets.url ? "Updated PORTFOLIO_API_URL" : "Set PORTFOLIO_API_URL");
|
|
207
|
+
|
|
208
|
+
if (existingSecrets.key) {
|
|
209
|
+
log.info("Overwriting PORTFOLIO_API_KEY...");
|
|
210
|
+
}
|
|
211
|
+
await setGitHubSecret('PORTFOLIO_API_KEY', apiKey);
|
|
212
|
+
log.success(existingSecrets.key ? "Updated PORTFOLIO_API_KEY" : "Set PORTFOLIO_API_KEY");
|
|
213
|
+
|
|
214
|
+
log.title("✨ Secrets configured successfully!");
|
|
215
|
+
|
|
216
|
+
console.log(`
|
|
217
|
+
${colors.bright}Configuration saved!${colors.reset}
|
|
218
|
+
|
|
219
|
+
${colors.green}✓${colors.reset} Profile "${profileName}" saved in: ${getConfigPath()}
|
|
220
|
+
${colors.green}✓${colors.reset} You can now use this profile in other repos!
|
|
221
|
+
|
|
222
|
+
${colors.bright}Next steps:${colors.reset}
|
|
223
|
+
|
|
224
|
+
1. ${colors.cyan}Make sure your portfolio has the API key${colors.reset}:
|
|
225
|
+
PORTFOLIO_API_KEY=${apiKey}
|
|
226
|
+
|
|
227
|
+
2. ${colors.cyan}Test the workflow${colors.reset}:
|
|
228
|
+
git add .
|
|
229
|
+
git commit -m "Test workflow"
|
|
230
|
+
git push
|
|
231
|
+
|
|
232
|
+
${colors.bright}All your projects using this profile will sync to the same portfolio!${colors.reset} 🎉
|
|
233
|
+
`);
|
|
234
|
+
|
|
235
|
+
} catch (error: any) {
|
|
236
|
+
log.error("Failed to set secrets: " + error.message);
|
|
237
|
+
console.log(`
|
|
238
|
+
${colors.yellow}You can set them manually:${colors.reset}
|
|
239
|
+
|
|
240
|
+
1. Go to your repo on GitHub
|
|
241
|
+
2. Settings > Secrets and variables > Actions
|
|
242
|
+
3. New repository secret:
|
|
243
|
+
- Name: PORTFOLIO_API_URL
|
|
244
|
+
- Value: ${portfolioUrl}
|
|
245
|
+
4. New repository secret:
|
|
246
|
+
- Name: PORTFOLIO_API_KEY
|
|
247
|
+
- Value: ${apiKey}
|
|
248
|
+
`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
}
|