shipfolio 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 +330 -0
- package/bin/cli.js +2 -0
- package/dist/cli.js +2027 -0
- package/dist/cli.js.map +1 -0
- package/dist/lib/orchestrator/detect.js +33 -0
- package/dist/lib/orchestrator/detect.js.map +1 -0
- package/dist/lib/orchestrator/prompt-builder.js +59 -0
- package/dist/lib/orchestrator/prompt-builder.js.map +1 -0
- package/dist/lib/orchestrator/validator.js +68 -0
- package/dist/lib/orchestrator/validator.js.map +1 -0
- package/dist/lib/scanner/git.js +86 -0
- package/dist/lib/scanner/git.js.map +1 -0
- package/dist/lib/scanner/index.js +457 -0
- package/dist/lib/scanner/index.js.map +1 -0
- package/dist/lib/spec/builder.js +21 -0
- package/dist/lib/spec/builder.js.map +1 -0
- package/dist/lib/spec/diff.js +42 -0
- package/dist/lib/spec/diff.js.map +1 -0
- package/package.json +49 -0
- package/prompts/fresh-build.md +108 -0
- package/prompts/update.md +46 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2027 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var init_esm_shims = __esm({
|
|
16
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
17
|
+
"use strict";
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// src/scanner/git.ts
|
|
22
|
+
import simpleGit from "simple-git";
|
|
23
|
+
async function getGitMeta(projectPath) {
|
|
24
|
+
const git = simpleGit(projectPath);
|
|
25
|
+
const empty = {
|
|
26
|
+
isRepo: false,
|
|
27
|
+
firstCommitDate: null,
|
|
28
|
+
lastCommitDate: null,
|
|
29
|
+
totalCommits: 0,
|
|
30
|
+
remoteUrl: null,
|
|
31
|
+
lastCommitHash: null
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
const isRepo = await git.checkIsRepo();
|
|
35
|
+
if (!isRepo) return empty;
|
|
36
|
+
} catch {
|
|
37
|
+
return empty;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const log = await git.log({ maxCount: 1 });
|
|
41
|
+
let firstCommitDate = null;
|
|
42
|
+
try {
|
|
43
|
+
const firstResult = await git.raw(["log", "--reverse", "--format=%aI", "--max-count=1"]);
|
|
44
|
+
firstCommitDate = firstResult.trim() || null;
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
let remoteUrl = null;
|
|
48
|
+
try {
|
|
49
|
+
const remotes = await git.getRemotes(true);
|
|
50
|
+
const origin = remotes.find((r) => r.name === "origin");
|
|
51
|
+
if (origin?.refs?.fetch) {
|
|
52
|
+
remoteUrl = normalizeGitUrl(origin.refs.fetch);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
let totalCommits = 0;
|
|
57
|
+
try {
|
|
58
|
+
const result = await git.raw(["rev-list", "--count", "HEAD"]);
|
|
59
|
+
totalCommits = parseInt(result.trim(), 10) || 0;
|
|
60
|
+
} catch {
|
|
61
|
+
totalCommits = 0;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
isRepo: true,
|
|
65
|
+
firstCommitDate,
|
|
66
|
+
lastCommitDate: log.latest?.date || null,
|
|
67
|
+
totalCommits,
|
|
68
|
+
remoteUrl,
|
|
69
|
+
lastCommitHash: log.latest?.hash || null
|
|
70
|
+
};
|
|
71
|
+
} catch {
|
|
72
|
+
return { ...empty, isRepo: true };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function normalizeGitUrl(url) {
|
|
76
|
+
if (url.startsWith("git@")) {
|
|
77
|
+
url = url.replace(":", "/").replace("git@", "https://");
|
|
78
|
+
}
|
|
79
|
+
if (url.endsWith(".git")) {
|
|
80
|
+
url = url.slice(0, -4);
|
|
81
|
+
}
|
|
82
|
+
return url;
|
|
83
|
+
}
|
|
84
|
+
async function findGitRepos(rootPath, maxDepth = 3) {
|
|
85
|
+
const { glob: glob2 } = await import("glob");
|
|
86
|
+
const gitDirs = await glob2("**/.git", {
|
|
87
|
+
cwd: rootPath,
|
|
88
|
+
maxDepth: maxDepth + 1,
|
|
89
|
+
dot: true,
|
|
90
|
+
ignore: [
|
|
91
|
+
"**/node_modules/**/.git",
|
|
92
|
+
"**/vendor/**/.git",
|
|
93
|
+
"**/__pycache__/**/.git"
|
|
94
|
+
]
|
|
95
|
+
});
|
|
96
|
+
return gitDirs.map((gitDir) => {
|
|
97
|
+
const parts = gitDir.split("/");
|
|
98
|
+
parts.pop();
|
|
99
|
+
return parts.length > 0 ? `${rootPath}/${parts.join("/")}` : rootPath;
|
|
100
|
+
}).sort();
|
|
101
|
+
}
|
|
102
|
+
var init_git = __esm({
|
|
103
|
+
"src/scanner/git.ts"() {
|
|
104
|
+
"use strict";
|
|
105
|
+
init_esm_shims();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// src/utils/fs.ts
|
|
110
|
+
import { readFile, writeFile, access, mkdir } from "fs/promises";
|
|
111
|
+
import { join } from "path";
|
|
112
|
+
async function fileExists(path2) {
|
|
113
|
+
try {
|
|
114
|
+
await access(path2);
|
|
115
|
+
return true;
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function readJson(path2) {
|
|
121
|
+
const content = await readFile(path2, "utf-8");
|
|
122
|
+
return JSON.parse(content);
|
|
123
|
+
}
|
|
124
|
+
async function writeJson(path2, data) {
|
|
125
|
+
await writeFile(path2, JSON.stringify(data, null, 2), "utf-8");
|
|
126
|
+
}
|
|
127
|
+
async function readText(path2) {
|
|
128
|
+
return readFile(path2, "utf-8");
|
|
129
|
+
}
|
|
130
|
+
async function writeText(path2, content) {
|
|
131
|
+
await writeFile(path2, content, "utf-8");
|
|
132
|
+
}
|
|
133
|
+
async function ensureDir(path2) {
|
|
134
|
+
await mkdir(path2, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
var init_fs = __esm({
|
|
137
|
+
"src/utils/fs.ts"() {
|
|
138
|
+
"use strict";
|
|
139
|
+
init_esm_shims();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// src/scanner/detectors/node.ts
|
|
144
|
+
async function detectNode(projectPath) {
|
|
145
|
+
const pkgPath = join(projectPath, "package.json");
|
|
146
|
+
if (!await fileExists(pkgPath)) return null;
|
|
147
|
+
const pkg = await readJson(pkgPath);
|
|
148
|
+
const allDeps = {
|
|
149
|
+
...pkg.dependencies,
|
|
150
|
+
...pkg.devDependencies
|
|
151
|
+
};
|
|
152
|
+
const depNames = Object.keys(allDeps);
|
|
153
|
+
const techStack = [];
|
|
154
|
+
if (depNames.includes("next")) techStack.push("Next.js");
|
|
155
|
+
else if (depNames.includes("nuxt")) techStack.push("Nuxt");
|
|
156
|
+
else if (depNames.includes("astro")) techStack.push("Astro");
|
|
157
|
+
else if (depNames.includes("svelte") || depNames.includes("@sveltejs/kit"))
|
|
158
|
+
techStack.push("Svelte");
|
|
159
|
+
if (depNames.includes("react")) techStack.push("React");
|
|
160
|
+
if (depNames.includes("vue")) techStack.push("Vue");
|
|
161
|
+
if (depNames.includes("tailwindcss")) techStack.push("Tailwind CSS");
|
|
162
|
+
if (depNames.includes("express")) techStack.push("Express");
|
|
163
|
+
if (depNames.includes("fastify")) techStack.push("Fastify");
|
|
164
|
+
if (depNames.includes("hono")) techStack.push("Hono");
|
|
165
|
+
if (depNames.includes("prisma") || depNames.includes("@prisma/client"))
|
|
166
|
+
techStack.push("Prisma");
|
|
167
|
+
if (depNames.includes("drizzle-orm")) techStack.push("Drizzle");
|
|
168
|
+
if (depNames.includes("mongoose")) techStack.push("MongoDB");
|
|
169
|
+
if (depNames.includes("openai")) techStack.push("OpenAI");
|
|
170
|
+
if (depNames.includes("@anthropic-ai/sdk")) techStack.push("Claude API");
|
|
171
|
+
if (depNames.includes("langchain")) techStack.push("LangChain");
|
|
172
|
+
if (depNames.includes("typescript")) techStack.push("TypeScript");
|
|
173
|
+
else techStack.push("JavaScript");
|
|
174
|
+
return {
|
|
175
|
+
name: pkg.name || null,
|
|
176
|
+
description: pkg.description || null,
|
|
177
|
+
homepage: pkg.homepage || null,
|
|
178
|
+
techStack
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
var init_node = __esm({
|
|
182
|
+
"src/scanner/detectors/node.ts"() {
|
|
183
|
+
"use strict";
|
|
184
|
+
init_esm_shims();
|
|
185
|
+
init_fs();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// src/scanner/detectors/python.ts
|
|
190
|
+
async function detectPython(projectPath) {
|
|
191
|
+
const techStack = ["Python"];
|
|
192
|
+
const reqPath = join(projectPath, "requirements.txt");
|
|
193
|
+
const pyprojectPath = join(projectPath, "pyproject.toml");
|
|
194
|
+
const setupPath = join(projectPath, "setup.py");
|
|
195
|
+
let deps = "";
|
|
196
|
+
if (await fileExists(reqPath)) {
|
|
197
|
+
deps = await readText(reqPath);
|
|
198
|
+
} else if (await fileExists(pyprojectPath)) {
|
|
199
|
+
deps = await readText(pyprojectPath);
|
|
200
|
+
} else if (await fileExists(setupPath)) {
|
|
201
|
+
deps = await readText(setupPath);
|
|
202
|
+
} else {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const depsLower = deps.toLowerCase();
|
|
206
|
+
if (depsLower.includes("django")) techStack.push("Django");
|
|
207
|
+
if (depsLower.includes("flask")) techStack.push("Flask");
|
|
208
|
+
if (depsLower.includes("fastapi")) techStack.push("FastAPI");
|
|
209
|
+
if (depsLower.includes("pytorch") || depsLower.includes("torch"))
|
|
210
|
+
techStack.push("PyTorch");
|
|
211
|
+
if (depsLower.includes("tensorflow")) techStack.push("TensorFlow");
|
|
212
|
+
if (depsLower.includes("transformers")) techStack.push("Transformers");
|
|
213
|
+
if (depsLower.includes("langchain")) techStack.push("LangChain");
|
|
214
|
+
if (depsLower.includes("openai")) techStack.push("OpenAI");
|
|
215
|
+
if (depsLower.includes("pandas")) techStack.push("Pandas");
|
|
216
|
+
if (depsLower.includes("numpy")) techStack.push("NumPy");
|
|
217
|
+
if (depsLower.includes("scikit")) techStack.push("Scikit-learn");
|
|
218
|
+
return { techStack };
|
|
219
|
+
}
|
|
220
|
+
var init_python = __esm({
|
|
221
|
+
"src/scanner/detectors/python.ts"() {
|
|
222
|
+
"use strict";
|
|
223
|
+
init_esm_shims();
|
|
224
|
+
init_fs();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// src/scanner/detectors/rust.ts
|
|
229
|
+
async function detectRust(projectPath) {
|
|
230
|
+
const cargoPath = join(projectPath, "Cargo.toml");
|
|
231
|
+
if (!await fileExists(cargoPath)) return null;
|
|
232
|
+
const content = await readText(cargoPath);
|
|
233
|
+
const techStack = ["Rust"];
|
|
234
|
+
if (content.includes("actix")) techStack.push("Actix");
|
|
235
|
+
if (content.includes("axum")) techStack.push("Axum");
|
|
236
|
+
if (content.includes("tokio")) techStack.push("Tokio");
|
|
237
|
+
if (content.includes("wasm")) techStack.push("WebAssembly");
|
|
238
|
+
if (content.includes("tauri")) techStack.push("Tauri");
|
|
239
|
+
if (content.includes("diesel")) techStack.push("Diesel");
|
|
240
|
+
if (content.includes("sqlx")) techStack.push("SQLx");
|
|
241
|
+
const nameMatch = content.match(/\[package\][\s\S]*?name\s*=\s*"([^"]+)"/);
|
|
242
|
+
const descMatch = content.match(
|
|
243
|
+
/\[package\][\s\S]*?description\s*=\s*"([^"]+)"/
|
|
244
|
+
);
|
|
245
|
+
return {
|
|
246
|
+
name: nameMatch?.[1] || null,
|
|
247
|
+
description: descMatch?.[1] || null,
|
|
248
|
+
techStack
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
var init_rust = __esm({
|
|
252
|
+
"src/scanner/detectors/rust.ts"() {
|
|
253
|
+
"use strict";
|
|
254
|
+
init_esm_shims();
|
|
255
|
+
init_fs();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// src/scanner/detectors/go.ts
|
|
260
|
+
async function detectGo(projectPath) {
|
|
261
|
+
const goModPath = join(projectPath, "go.mod");
|
|
262
|
+
if (!await fileExists(goModPath)) return null;
|
|
263
|
+
const content = await readText(goModPath);
|
|
264
|
+
const techStack = ["Go"];
|
|
265
|
+
if (content.includes("gin-gonic")) techStack.push("Gin");
|
|
266
|
+
if (content.includes("echo")) techStack.push("Echo");
|
|
267
|
+
if (content.includes("fiber")) techStack.push("Fiber");
|
|
268
|
+
if (content.includes("grpc")) techStack.push("gRPC");
|
|
269
|
+
if (content.includes("gorm")) techStack.push("GORM");
|
|
270
|
+
if (content.includes("cobra")) techStack.push("Cobra");
|
|
271
|
+
if (content.includes("ent")) techStack.push("Ent");
|
|
272
|
+
return { techStack };
|
|
273
|
+
}
|
|
274
|
+
var init_go = __esm({
|
|
275
|
+
"src/scanner/detectors/go.ts"() {
|
|
276
|
+
"use strict";
|
|
277
|
+
init_esm_shims();
|
|
278
|
+
init_fs();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// src/scanner/detectors/generic.ts
|
|
283
|
+
import { glob } from "glob";
|
|
284
|
+
import { basename, extname } from "path";
|
|
285
|
+
async function detectLanguages(projectPath) {
|
|
286
|
+
const ignorePattern = IGNORE_DIRS.map((d) => `**/${d}/**`);
|
|
287
|
+
const files = await glob("**/*.*", {
|
|
288
|
+
cwd: projectPath,
|
|
289
|
+
ignore: ignorePattern,
|
|
290
|
+
nodir: true,
|
|
291
|
+
maxDepth: 5
|
|
292
|
+
});
|
|
293
|
+
const counts = {};
|
|
294
|
+
for (const file of files) {
|
|
295
|
+
const ext = extname(file).toLowerCase();
|
|
296
|
+
const lang = EXTENSION_MAP[ext];
|
|
297
|
+
if (lang) {
|
|
298
|
+
counts[lang] = (counts[lang] || 0) + 1;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return counts;
|
|
302
|
+
}
|
|
303
|
+
function deriveNameFromPath(projectPath) {
|
|
304
|
+
return basename(projectPath);
|
|
305
|
+
}
|
|
306
|
+
var EXTENSION_MAP, IGNORE_DIRS;
|
|
307
|
+
var init_generic = __esm({
|
|
308
|
+
"src/scanner/detectors/generic.ts"() {
|
|
309
|
+
"use strict";
|
|
310
|
+
init_esm_shims();
|
|
311
|
+
EXTENSION_MAP = {
|
|
312
|
+
".ts": "TypeScript",
|
|
313
|
+
".tsx": "TypeScript",
|
|
314
|
+
".js": "JavaScript",
|
|
315
|
+
".jsx": "JavaScript",
|
|
316
|
+
".py": "Python",
|
|
317
|
+
".rs": "Rust",
|
|
318
|
+
".go": "Go",
|
|
319
|
+
".java": "Java",
|
|
320
|
+
".kt": "Kotlin",
|
|
321
|
+
".swift": "Swift",
|
|
322
|
+
".rb": "Ruby",
|
|
323
|
+
".php": "PHP",
|
|
324
|
+
".cs": "C#",
|
|
325
|
+
".cpp": "C++",
|
|
326
|
+
".c": "C",
|
|
327
|
+
".dart": "Dart",
|
|
328
|
+
".lua": "Lua",
|
|
329
|
+
".zig": "Zig",
|
|
330
|
+
".sol": "Solidity",
|
|
331
|
+
".ex": "Elixir",
|
|
332
|
+
".exs": "Elixir"
|
|
333
|
+
};
|
|
334
|
+
IGNORE_DIRS = [
|
|
335
|
+
"node_modules",
|
|
336
|
+
".git",
|
|
337
|
+
"dist",
|
|
338
|
+
"build",
|
|
339
|
+
"out",
|
|
340
|
+
".next",
|
|
341
|
+
"target",
|
|
342
|
+
"vendor",
|
|
343
|
+
"__pycache__",
|
|
344
|
+
".venv",
|
|
345
|
+
"venv"
|
|
346
|
+
];
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// src/scanner/extractors.ts
|
|
351
|
+
async function extractReadme(projectPath) {
|
|
352
|
+
const candidates = [
|
|
353
|
+
"README.md",
|
|
354
|
+
"readme.md",
|
|
355
|
+
"Readme.md",
|
|
356
|
+
"README.txt",
|
|
357
|
+
"README"
|
|
358
|
+
];
|
|
359
|
+
for (const name of candidates) {
|
|
360
|
+
const p6 = join(projectPath, name);
|
|
361
|
+
if (await fileExists(p6)) {
|
|
362
|
+
const content = await readText(p6);
|
|
363
|
+
return content.slice(0, 3e3);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
function extractFirstParagraph(readme) {
|
|
369
|
+
if (!readme) return null;
|
|
370
|
+
const lines = readme.split("\n");
|
|
371
|
+
let collecting = false;
|
|
372
|
+
const paragraphLines = [];
|
|
373
|
+
for (const line of lines) {
|
|
374
|
+
const trimmed = line.trim();
|
|
375
|
+
if (trimmed.startsWith("#")) {
|
|
376
|
+
if (collecting && paragraphLines.length > 0) break;
|
|
377
|
+
collecting = true;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (collecting && trimmed === "" && paragraphLines.length > 0) break;
|
|
381
|
+
if (collecting && trimmed !== "") {
|
|
382
|
+
paragraphLines.push(trimmed);
|
|
383
|
+
}
|
|
384
|
+
if (!collecting && trimmed !== "" && !trimmed.startsWith("[")) {
|
|
385
|
+
paragraphLines.push(trimmed);
|
|
386
|
+
collecting = true;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return paragraphLines.length > 0 ? paragraphLines.join(" ") : null;
|
|
390
|
+
}
|
|
391
|
+
function extractDemoUrl(readme) {
|
|
392
|
+
if (!readme) return null;
|
|
393
|
+
const patterns = [
|
|
394
|
+
/(?:demo|live|website|site|url|link)[\s:]*\[?[^\]]*\]?\(?(https?:\/\/[^\s)]+)/i,
|
|
395
|
+
/\[(?:demo|live|website|try it)\]\((https?:\/\/[^\s)]+)\)/i,
|
|
396
|
+
/(https?:\/\/[^\s)]+\.(?:vercel|netlify|pages\.dev|herokuapp|railway)\.app[^\s)]*)/i
|
|
397
|
+
];
|
|
398
|
+
for (const pattern of patterns) {
|
|
399
|
+
const match = readme.match(pattern);
|
|
400
|
+
if (match?.[1]) return match[1];
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
var init_extractors = __esm({
|
|
405
|
+
"src/scanner/extractors.ts"() {
|
|
406
|
+
"use strict";
|
|
407
|
+
init_esm_shims();
|
|
408
|
+
init_fs();
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// src/utils/logger.ts
|
|
413
|
+
import chalk from "chalk";
|
|
414
|
+
var logger;
|
|
415
|
+
var init_logger = __esm({
|
|
416
|
+
"src/utils/logger.ts"() {
|
|
417
|
+
"use strict";
|
|
418
|
+
init_esm_shims();
|
|
419
|
+
logger = {
|
|
420
|
+
info(msg) {
|
|
421
|
+
console.log(chalk.cyan(`-- ${msg}`));
|
|
422
|
+
},
|
|
423
|
+
success(msg) {
|
|
424
|
+
console.log(chalk.green(`-- ${msg}`));
|
|
425
|
+
},
|
|
426
|
+
warn(msg) {
|
|
427
|
+
console.log(chalk.yellow(`-- ${msg}`));
|
|
428
|
+
},
|
|
429
|
+
error(msg) {
|
|
430
|
+
console.error(chalk.red(`-- ${msg}`));
|
|
431
|
+
},
|
|
432
|
+
plain(msg) {
|
|
433
|
+
console.log(msg);
|
|
434
|
+
},
|
|
435
|
+
blank() {
|
|
436
|
+
console.log();
|
|
437
|
+
},
|
|
438
|
+
header(msg) {
|
|
439
|
+
console.log();
|
|
440
|
+
console.log(chalk.bold(msg));
|
|
441
|
+
console.log();
|
|
442
|
+
},
|
|
443
|
+
table(rows) {
|
|
444
|
+
if (rows.length === 0) return;
|
|
445
|
+
const colWidths = rows[0].map(
|
|
446
|
+
(_, colIdx) => Math.max(...rows.map((row) => (row[colIdx] || "").length))
|
|
447
|
+
);
|
|
448
|
+
for (const row of rows) {
|
|
449
|
+
const line = row.map((cell, i) => (cell || "").padEnd(colWidths[i] + 2)).join("");
|
|
450
|
+
console.log(` ${line}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// src/scanner/index.ts
|
|
458
|
+
import ora from "ora";
|
|
459
|
+
async function scanProjects(directories) {
|
|
460
|
+
const allRepos = [];
|
|
461
|
+
const spinner = ora("Scanning for projects...").start();
|
|
462
|
+
for (const dir of directories) {
|
|
463
|
+
try {
|
|
464
|
+
const repos = await findGitRepos(dir);
|
|
465
|
+
allRepos.push(...repos);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
logger.warn(`Could not scan ${dir}: ${err}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const uniqueRepos = [...new Set(allRepos)];
|
|
471
|
+
spinner.text = `Found ${uniqueRepos.length} repositories. Extracting metadata...`;
|
|
472
|
+
const projects = [];
|
|
473
|
+
for (const repoPath of uniqueRepos) {
|
|
474
|
+
try {
|
|
475
|
+
const project = await extractProjectMeta(repoPath);
|
|
476
|
+
if (project) {
|
|
477
|
+
projects.push(project);
|
|
478
|
+
}
|
|
479
|
+
} catch (err) {
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
spinner.succeed(`Scanned ${projects.length} projects`);
|
|
483
|
+
return projects;
|
|
484
|
+
}
|
|
485
|
+
async function extractProjectMeta(projectPath) {
|
|
486
|
+
const gitMeta = await getGitMeta(projectPath);
|
|
487
|
+
if (!gitMeta.isRepo) return null;
|
|
488
|
+
let techStack = [];
|
|
489
|
+
let name = null;
|
|
490
|
+
let description = null;
|
|
491
|
+
let homepage = null;
|
|
492
|
+
const nodeInfo = await detectNode(projectPath);
|
|
493
|
+
if (nodeInfo) {
|
|
494
|
+
techStack.push(...nodeInfo.techStack);
|
|
495
|
+
name = nodeInfo.name || name;
|
|
496
|
+
description = nodeInfo.description || description;
|
|
497
|
+
homepage = nodeInfo.homepage || homepage;
|
|
498
|
+
}
|
|
499
|
+
const pythonInfo = await detectPython(projectPath);
|
|
500
|
+
if (pythonInfo) {
|
|
501
|
+
techStack.push(...pythonInfo.techStack);
|
|
502
|
+
}
|
|
503
|
+
const rustInfo = await detectRust(projectPath);
|
|
504
|
+
if (rustInfo) {
|
|
505
|
+
techStack.push(...rustInfo.techStack);
|
|
506
|
+
name = rustInfo.name || name;
|
|
507
|
+
description = rustInfo.description || description;
|
|
508
|
+
}
|
|
509
|
+
const goInfo = await detectGo(projectPath);
|
|
510
|
+
if (goInfo) {
|
|
511
|
+
techStack.push(...goInfo.techStack);
|
|
512
|
+
}
|
|
513
|
+
techStack = [...new Set(techStack)];
|
|
514
|
+
const languages = await detectLanguages(projectPath);
|
|
515
|
+
if (techStack.length === 0) {
|
|
516
|
+
const topLangs = Object.entries(languages).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([lang]) => lang);
|
|
517
|
+
techStack = topLangs;
|
|
518
|
+
}
|
|
519
|
+
const readmeContent = await extractReadme(projectPath);
|
|
520
|
+
if (!description) {
|
|
521
|
+
description = extractFirstParagraph(readmeContent) || "";
|
|
522
|
+
}
|
|
523
|
+
const demoUrl = homepage || extractDemoUrl(readmeContent);
|
|
524
|
+
if (!name) {
|
|
525
|
+
name = deriveNameFromPath(projectPath);
|
|
526
|
+
}
|
|
527
|
+
const id = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
528
|
+
return {
|
|
529
|
+
id,
|
|
530
|
+
name,
|
|
531
|
+
localPath: projectPath,
|
|
532
|
+
description: description || "",
|
|
533
|
+
techStack,
|
|
534
|
+
languages,
|
|
535
|
+
firstCommitDate: gitMeta.firstCommitDate || "",
|
|
536
|
+
lastCommitDate: gitMeta.lastCommitDate || "",
|
|
537
|
+
totalCommits: gitMeta.totalCommits,
|
|
538
|
+
remoteUrl: gitMeta.remoteUrl,
|
|
539
|
+
demoUrl,
|
|
540
|
+
readmeContent,
|
|
541
|
+
lastScannedCommit: gitMeta.lastCommitHash || ""
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
var init_scanner = __esm({
|
|
545
|
+
"src/scanner/index.ts"() {
|
|
546
|
+
"use strict";
|
|
547
|
+
init_esm_shims();
|
|
548
|
+
init_git();
|
|
549
|
+
init_node();
|
|
550
|
+
init_python();
|
|
551
|
+
init_rust();
|
|
552
|
+
init_go();
|
|
553
|
+
init_generic();
|
|
554
|
+
init_extractors();
|
|
555
|
+
init_logger();
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// src/interviewer/questions.ts
|
|
560
|
+
var THEME_OPTIONS, FONT_OPTIONS, ANIMATION_OPTIONS, SECTION_OPTIONS, ENGINE_OPTIONS, DEPLOY_OPTIONS, ROLE_OPTIONS, DEFAULT_ACCENT_COLORS;
|
|
561
|
+
var init_questions = __esm({
|
|
562
|
+
"src/interviewer/questions.ts"() {
|
|
563
|
+
"use strict";
|
|
564
|
+
init_esm_shims();
|
|
565
|
+
THEME_OPTIONS = [
|
|
566
|
+
{ value: "dark-minimal", label: "Dark Minimal" },
|
|
567
|
+
{ value: "light-clean", label: "Light Clean" },
|
|
568
|
+
{ value: "monochrome", label: "Monochrome" },
|
|
569
|
+
{ value: "custom", label: "Custom (AI decides)" }
|
|
570
|
+
];
|
|
571
|
+
FONT_OPTIONS = [
|
|
572
|
+
{ value: "Inter", label: "Inter" },
|
|
573
|
+
{ value: "JetBrains Mono", label: "JetBrains Mono" },
|
|
574
|
+
{ value: "system", label: "System Default" }
|
|
575
|
+
];
|
|
576
|
+
ANIMATION_OPTIONS = [
|
|
577
|
+
{ value: "subtle", label: "Subtle" },
|
|
578
|
+
{ value: "moderate", label: "Moderate" },
|
|
579
|
+
{ value: "none", label: "None" }
|
|
580
|
+
];
|
|
581
|
+
SECTION_OPTIONS = [
|
|
582
|
+
{ value: "skills", label: "Skills / Tech Stack" },
|
|
583
|
+
{ value: "about", label: "About Me" },
|
|
584
|
+
{ value: "timeline", label: "Timeline / Changelog" },
|
|
585
|
+
{ value: "blog", label: "Blog" },
|
|
586
|
+
{ value: "metrics", label: "Metrics Dashboard" },
|
|
587
|
+
{ value: "contact", label: "Contact" }
|
|
588
|
+
];
|
|
589
|
+
ENGINE_OPTIONS = [
|
|
590
|
+
{ value: "claude", label: "Claude Code" },
|
|
591
|
+
{ value: "codex", label: "Codex" },
|
|
592
|
+
{ value: "v0", label: "v0 (Vercel)" }
|
|
593
|
+
];
|
|
594
|
+
DEPLOY_OPTIONS = [
|
|
595
|
+
{ value: "cloudflare", label: "Cloudflare Pages" },
|
|
596
|
+
{ value: "vercel", label: "Vercel" },
|
|
597
|
+
{ value: "local", label: "Local only (no deploy)" }
|
|
598
|
+
];
|
|
599
|
+
ROLE_OPTIONS = [
|
|
600
|
+
{ value: "solo", label: "Solo" },
|
|
601
|
+
{ value: "lead", label: "Lead" },
|
|
602
|
+
{ value: "contributor", label: "Contributor" }
|
|
603
|
+
];
|
|
604
|
+
DEFAULT_ACCENT_COLORS = [
|
|
605
|
+
{ value: "#7c3aed", label: "Purple" },
|
|
606
|
+
{ value: "#10b981", label: "Green" },
|
|
607
|
+
{ value: "#f97316", label: "Orange" },
|
|
608
|
+
{ value: "#3b82f6", label: "Blue" },
|
|
609
|
+
{ value: "#ef4444", label: "Red" },
|
|
610
|
+
{ value: "#ec4899", label: "Pink" }
|
|
611
|
+
];
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// src/interviewer/index.ts
|
|
616
|
+
import * as p from "@clack/prompts";
|
|
617
|
+
function handleCancel(value) {
|
|
618
|
+
if (p.isCancel(value)) {
|
|
619
|
+
p.cancel("Cancelled.");
|
|
620
|
+
process.exit(0);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
async function runInterview(scannedProjects, availableEngines) {
|
|
624
|
+
p.intro("shipfolio");
|
|
625
|
+
logger.header("Project Selection");
|
|
626
|
+
const projectOptions = scannedProjects.map((proj) => ({
|
|
627
|
+
value: proj.id,
|
|
628
|
+
label: `${proj.name}`,
|
|
629
|
+
hint: `${proj.techStack.slice(0, 3).join(", ")} | ${proj.totalCommits} commits`
|
|
630
|
+
}));
|
|
631
|
+
const selectedIds = await p.multiselect({
|
|
632
|
+
message: "Select projects to include:",
|
|
633
|
+
options: projectOptions,
|
|
634
|
+
required: true
|
|
635
|
+
});
|
|
636
|
+
handleCancel(selectedIds);
|
|
637
|
+
const projectEntries = [];
|
|
638
|
+
for (const id of selectedIds) {
|
|
639
|
+
const meta = scannedProjects.find((p6) => p6.id === id);
|
|
640
|
+
logger.plain(`
|
|
641
|
+
Configuring: ${meta.name}`);
|
|
642
|
+
const overrideDesc = await p.text({
|
|
643
|
+
message: `Description for ${meta.name} (enter to use auto-generated):`,
|
|
644
|
+
placeholder: meta.description?.slice(0, 80) || "Auto-generate from README",
|
|
645
|
+
defaultValue: ""
|
|
646
|
+
});
|
|
647
|
+
handleCancel(overrideDesc);
|
|
648
|
+
const demoUrl = await p.text({
|
|
649
|
+
message: `Demo URL for ${meta.name}:`,
|
|
650
|
+
placeholder: meta.demoUrl || "none",
|
|
651
|
+
defaultValue: meta.demoUrl || ""
|
|
652
|
+
});
|
|
653
|
+
handleCancel(demoUrl);
|
|
654
|
+
const showSource = await p.confirm({
|
|
655
|
+
message: `Show source code link for ${meta.name}?`,
|
|
656
|
+
initialValue: !!meta.remoteUrl
|
|
657
|
+
});
|
|
658
|
+
handleCancel(showSource);
|
|
659
|
+
const role = await p.select({
|
|
660
|
+
message: `Your role in ${meta.name}:`,
|
|
661
|
+
options: ROLE_OPTIONS
|
|
662
|
+
});
|
|
663
|
+
handleCancel(role);
|
|
664
|
+
const metricsInput = await p.text({
|
|
665
|
+
message: `Key metrics for ${meta.name} (e.g. "1k users, $5k MRR"):`,
|
|
666
|
+
placeholder: "optional",
|
|
667
|
+
defaultValue: ""
|
|
668
|
+
});
|
|
669
|
+
handleCancel(metricsInput);
|
|
670
|
+
const metrics = {};
|
|
671
|
+
if (metricsInput) {
|
|
672
|
+
metrics.custom = { summary: metricsInput };
|
|
673
|
+
}
|
|
674
|
+
projectEntries.push({
|
|
675
|
+
...meta,
|
|
676
|
+
included: true,
|
|
677
|
+
overrideDescription: overrideDesc || null,
|
|
678
|
+
showSourceLink: showSource,
|
|
679
|
+
demoUrl: demoUrl || meta.demoUrl,
|
|
680
|
+
role,
|
|
681
|
+
metrics
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
logger.header("Personal Information");
|
|
685
|
+
const name = await p.text({
|
|
686
|
+
message: "Your full name:",
|
|
687
|
+
validate: (v) => v.length === 0 ? "Name is required" : void 0
|
|
688
|
+
});
|
|
689
|
+
handleCancel(name);
|
|
690
|
+
const tagline = await p.text({
|
|
691
|
+
message: "Professional tagline (one line):",
|
|
692
|
+
placeholder: "Full-stack developer. I ship things."
|
|
693
|
+
});
|
|
694
|
+
handleCancel(tagline);
|
|
695
|
+
const bioChoice = await p.select({
|
|
696
|
+
message: "Bio:",
|
|
697
|
+
options: [
|
|
698
|
+
{ value: "auto", label: "Auto-generate from my projects" },
|
|
699
|
+
{ value: "manual", label: "Write manually" }
|
|
700
|
+
]
|
|
701
|
+
});
|
|
702
|
+
handleCancel(bioChoice);
|
|
703
|
+
let bio = "auto";
|
|
704
|
+
if (bioChoice === "manual") {
|
|
705
|
+
bio = await p.text({
|
|
706
|
+
message: "Your bio (2-3 sentences):"
|
|
707
|
+
});
|
|
708
|
+
handleCancel(bio);
|
|
709
|
+
}
|
|
710
|
+
const photoUrl = await p.text({
|
|
711
|
+
message: "Photo URL (optional):",
|
|
712
|
+
placeholder: "https://...",
|
|
713
|
+
defaultValue: ""
|
|
714
|
+
});
|
|
715
|
+
handleCancel(photoUrl);
|
|
716
|
+
const github = await p.text({
|
|
717
|
+
message: "GitHub username or URL:",
|
|
718
|
+
defaultValue: ""
|
|
719
|
+
});
|
|
720
|
+
handleCancel(github);
|
|
721
|
+
const twitter = await p.text({
|
|
722
|
+
message: "Twitter/X handle:",
|
|
723
|
+
defaultValue: ""
|
|
724
|
+
});
|
|
725
|
+
handleCancel(twitter);
|
|
726
|
+
const linkedin = await p.text({
|
|
727
|
+
message: "LinkedIn URL:",
|
|
728
|
+
defaultValue: ""
|
|
729
|
+
});
|
|
730
|
+
handleCancel(linkedin);
|
|
731
|
+
const blogUrl = await p.text({
|
|
732
|
+
message: "Blog URL:",
|
|
733
|
+
defaultValue: ""
|
|
734
|
+
});
|
|
735
|
+
handleCancel(blogUrl);
|
|
736
|
+
const email = await p.text({
|
|
737
|
+
message: "Contact email:",
|
|
738
|
+
defaultValue: ""
|
|
739
|
+
});
|
|
740
|
+
handleCancel(email);
|
|
741
|
+
const owner = {
|
|
742
|
+
name,
|
|
743
|
+
tagline,
|
|
744
|
+
bio,
|
|
745
|
+
photoUrl: photoUrl || null,
|
|
746
|
+
social: {
|
|
747
|
+
github: github || void 0,
|
|
748
|
+
twitter: twitter || void 0,
|
|
749
|
+
linkedin: linkedin || void 0,
|
|
750
|
+
blog: blogUrl || void 0,
|
|
751
|
+
email: email || void 0
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
logger.header("Design Preferences");
|
|
755
|
+
const theme = await p.select({
|
|
756
|
+
message: "Theme:",
|
|
757
|
+
options: THEME_OPTIONS
|
|
758
|
+
});
|
|
759
|
+
handleCancel(theme);
|
|
760
|
+
const accentColor = await p.select({
|
|
761
|
+
message: "Accent color:",
|
|
762
|
+
options: [
|
|
763
|
+
...DEFAULT_ACCENT_COLORS,
|
|
764
|
+
{ value: "custom", label: "Custom hex" }
|
|
765
|
+
]
|
|
766
|
+
});
|
|
767
|
+
handleCancel(accentColor);
|
|
768
|
+
let finalAccent = accentColor;
|
|
769
|
+
if (accentColor === "custom") {
|
|
770
|
+
finalAccent = await p.text({
|
|
771
|
+
message: "Custom accent color (hex):",
|
|
772
|
+
placeholder: "#7c3aed",
|
|
773
|
+
validate: (v) => /^#[0-9a-fA-F]{6}$/.test(v) ? void 0 : "Enter a valid hex color"
|
|
774
|
+
});
|
|
775
|
+
handleCancel(finalAccent);
|
|
776
|
+
}
|
|
777
|
+
const font = await p.select({
|
|
778
|
+
message: "Font:",
|
|
779
|
+
options: FONT_OPTIONS
|
|
780
|
+
});
|
|
781
|
+
handleCancel(font);
|
|
782
|
+
const animationLevel = await p.select({
|
|
783
|
+
message: "Animation level:",
|
|
784
|
+
options: ANIMATION_OPTIONS
|
|
785
|
+
});
|
|
786
|
+
handleCancel(animationLevel);
|
|
787
|
+
const style = {
|
|
788
|
+
theme,
|
|
789
|
+
accentColor: finalAccent,
|
|
790
|
+
font,
|
|
791
|
+
animationLevel
|
|
792
|
+
};
|
|
793
|
+
logger.header("Sections");
|
|
794
|
+
const additionalSections = await p.multiselect({
|
|
795
|
+
message: "Additional sections (Hero + Projects always included):",
|
|
796
|
+
options: SECTION_OPTIONS,
|
|
797
|
+
required: false
|
|
798
|
+
});
|
|
799
|
+
handleCancel(additionalSections);
|
|
800
|
+
const sections = [
|
|
801
|
+
"hero",
|
|
802
|
+
"projects",
|
|
803
|
+
...additionalSections
|
|
804
|
+
];
|
|
805
|
+
logger.header("AI Engine");
|
|
806
|
+
let engine;
|
|
807
|
+
if (availableEngines.length === 1) {
|
|
808
|
+
engine = availableEngines[0];
|
|
809
|
+
logger.info(`Using ${engine}`);
|
|
810
|
+
} else {
|
|
811
|
+
const filteredEngineOptions = ENGINE_OPTIONS.filter(
|
|
812
|
+
(o) => availableEngines.includes(o.value)
|
|
813
|
+
);
|
|
814
|
+
engine = await p.select({
|
|
815
|
+
message: "AI engine to use:",
|
|
816
|
+
options: filteredEngineOptions
|
|
817
|
+
});
|
|
818
|
+
handleCancel(engine);
|
|
819
|
+
}
|
|
820
|
+
logger.header("Deployment");
|
|
821
|
+
const deployPlatform = await p.select({
|
|
822
|
+
message: "Deploy to:",
|
|
823
|
+
options: DEPLOY_OPTIONS
|
|
824
|
+
});
|
|
825
|
+
handleCancel(deployPlatform);
|
|
826
|
+
let projectName = "";
|
|
827
|
+
if (deployPlatform !== "local") {
|
|
828
|
+
projectName = await p.text({
|
|
829
|
+
message: "Project name (used in URL):",
|
|
830
|
+
placeholder: "my-shipfolio",
|
|
831
|
+
validate: (v) => /^[a-z0-9-]+$/.test(v) ? void 0 : "Lowercase letters, numbers, and hyphens only"
|
|
832
|
+
});
|
|
833
|
+
handleCancel(projectName);
|
|
834
|
+
}
|
|
835
|
+
p.outro("Configuration complete. Generating your site...");
|
|
836
|
+
return {
|
|
837
|
+
projects: projectEntries,
|
|
838
|
+
owner,
|
|
839
|
+
style,
|
|
840
|
+
sections,
|
|
841
|
+
engine,
|
|
842
|
+
deploy: {
|
|
843
|
+
platform: deployPlatform,
|
|
844
|
+
projectName
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
var init_interviewer = __esm({
|
|
849
|
+
"src/interviewer/index.ts"() {
|
|
850
|
+
"use strict";
|
|
851
|
+
init_esm_shims();
|
|
852
|
+
init_questions();
|
|
853
|
+
init_logger();
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
// src/spec/builder.ts
|
|
858
|
+
function buildSpec(interview) {
|
|
859
|
+
return {
|
|
860
|
+
version: "1.0.0",
|
|
861
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
862
|
+
engine: interview.engine,
|
|
863
|
+
framework: "next",
|
|
864
|
+
style: interview.style,
|
|
865
|
+
owner: interview.owner,
|
|
866
|
+
projects: interview.projects,
|
|
867
|
+
sections: interview.sections,
|
|
868
|
+
deploy: {
|
|
869
|
+
platform: interview.deploy.platform,
|
|
870
|
+
projectName: interview.deploy.projectName
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
var init_builder = __esm({
|
|
875
|
+
"src/spec/builder.ts"() {
|
|
876
|
+
"use strict";
|
|
877
|
+
init_esm_shims();
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
// src/orchestrator/prompt-builder.ts
|
|
882
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
883
|
+
import { join as join2, dirname } from "path";
|
|
884
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
885
|
+
function getPromptsDir() {
|
|
886
|
+
const currentDir = dirname(fileURLToPath2(import.meta.url));
|
|
887
|
+
const candidates = [
|
|
888
|
+
join2(currentDir, "../../prompts"),
|
|
889
|
+
join2(currentDir, "../prompts"),
|
|
890
|
+
join2(currentDir, "../../../prompts")
|
|
891
|
+
];
|
|
892
|
+
return candidates[0];
|
|
893
|
+
}
|
|
894
|
+
async function loadTemplate(filename) {
|
|
895
|
+
const promptsDir = getPromptsDir();
|
|
896
|
+
const candidates = [
|
|
897
|
+
join2(promptsDir, filename),
|
|
898
|
+
join2(dirname(fileURLToPath2(import.meta.url)), "../../prompts", filename),
|
|
899
|
+
join2(dirname(fileURLToPath2(import.meta.url)), "../../../prompts", filename)
|
|
900
|
+
];
|
|
901
|
+
for (const candidate of candidates) {
|
|
902
|
+
try {
|
|
903
|
+
return await readFile2(candidate, "utf-8");
|
|
904
|
+
} catch {
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
throw new Error(`Prompt template not found: ${filename}`);
|
|
909
|
+
}
|
|
910
|
+
async function buildFreshPrompt(spec) {
|
|
911
|
+
let template = await loadTemplate("fresh-build.md");
|
|
912
|
+
const sectionsText = spec.sections.map((s) => `- ${s}`).join("\n");
|
|
913
|
+
template = template.replace("{{SPEC_JSON}}", JSON.stringify(spec, null, 2)).replace("{{THEME}}", spec.style.theme).replace("{{ACCENT_COLOR}}", spec.style.accentColor).replace("{{ANIMATION_LEVEL}}", spec.style.animationLevel).replace("{{FONT}}", spec.style.font).replace("{{SECTIONS_LIST}}", sectionsText);
|
|
914
|
+
return template;
|
|
915
|
+
}
|
|
916
|
+
async function buildUpdatePrompt(existingConfig, diff) {
|
|
917
|
+
let template = await loadTemplate("update.md");
|
|
918
|
+
const newProjectsText = diff.newProjects.length > 0 ? diff.newProjects.map(
|
|
919
|
+
(p6) => `- ${p6.name}: ${p6.description}
|
|
920
|
+
Tech: ${p6.techStack.join(", ")}
|
|
921
|
+
README excerpt: ${(p6.readmeContent || "").slice(0, 500)}`
|
|
922
|
+
).join("\n\n") : "None";
|
|
923
|
+
const updatedProjectsText = diff.updatedProjects.length > 0 ? diff.updatedProjects.map(
|
|
924
|
+
(u) => `- ${u.project.name}: ${u.newCommits} new commits
|
|
925
|
+
README changed: ${u.readmeChanged}
|
|
926
|
+
Dependencies changed: ${u.depsChanged}`
|
|
927
|
+
).join("\n\n") : "None";
|
|
928
|
+
const removedProjectsText = diff.removedProjects.length > 0 ? diff.removedProjects.map((p6) => `- ${p6.name}`).join("\n") : "None";
|
|
929
|
+
template = template.replace(
|
|
930
|
+
"{{EXISTING_CONFIG_JSON}}",
|
|
931
|
+
JSON.stringify(existingConfig, null, 2)
|
|
932
|
+
).replace("{{NEW_PROJECTS}}", newProjectsText).replace("{{UPDATED_PROJECTS}}", updatedProjectsText).replace("{{REMOVED_PROJECTS}}", removedProjectsText).replace("{{PERSONAL_INFO_DIFF}}", "No changes");
|
|
933
|
+
return template;
|
|
934
|
+
}
|
|
935
|
+
var init_prompt_builder = __esm({
|
|
936
|
+
"src/orchestrator/prompt-builder.ts"() {
|
|
937
|
+
"use strict";
|
|
938
|
+
init_esm_shims();
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
// src/utils/exec.ts
|
|
943
|
+
import { execa } from "execa";
|
|
944
|
+
async function run(command, args, options) {
|
|
945
|
+
return execa(command, args, {
|
|
946
|
+
stdio: "pipe",
|
|
947
|
+
...options
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
async function commandExists(command) {
|
|
951
|
+
try {
|
|
952
|
+
await execa("which", [command]);
|
|
953
|
+
return true;
|
|
954
|
+
} catch {
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
async function runWithOutput(command, args, options) {
|
|
959
|
+
const result = await execa(command, args, {
|
|
960
|
+
stdio: "pipe",
|
|
961
|
+
...options
|
|
962
|
+
});
|
|
963
|
+
return result.stdout;
|
|
964
|
+
}
|
|
965
|
+
var init_exec = __esm({
|
|
966
|
+
"src/utils/exec.ts"() {
|
|
967
|
+
"use strict";
|
|
968
|
+
init_esm_shims();
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
// src/orchestrator/detect.ts
|
|
973
|
+
async function detectEngines() {
|
|
974
|
+
const results = [];
|
|
975
|
+
if (process.env.V0_API_KEY) {
|
|
976
|
+
results.push({ type: "v0", available: true });
|
|
977
|
+
}
|
|
978
|
+
if (await commandExists("claude")) {
|
|
979
|
+
results.push({ type: "claude", available: true });
|
|
980
|
+
}
|
|
981
|
+
if (await commandExists("codex")) {
|
|
982
|
+
results.push({ type: "codex", available: true });
|
|
983
|
+
}
|
|
984
|
+
return results;
|
|
985
|
+
}
|
|
986
|
+
function getAvailableEngineTypes(engines) {
|
|
987
|
+
return engines.filter((e) => e.available).map((e) => e.type);
|
|
988
|
+
}
|
|
989
|
+
var init_detect = __esm({
|
|
990
|
+
"src/orchestrator/detect.ts"() {
|
|
991
|
+
"use strict";
|
|
992
|
+
init_esm_shims();
|
|
993
|
+
init_exec();
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
// src/orchestrator/engines/claude.ts
|
|
998
|
+
import { execa as execa2 } from "execa";
|
|
999
|
+
import ora2 from "ora";
|
|
1000
|
+
async function generateWithClaude(prompt, outputDir) {
|
|
1001
|
+
const spinner = ora2("Generating site with Claude Code...").start();
|
|
1002
|
+
try {
|
|
1003
|
+
const fullPrompt = `${prompt}
|
|
1004
|
+
|
|
1005
|
+
Create all files in the current working directory. Do not ask questions, just generate all files.`;
|
|
1006
|
+
await execa2("claude", ["-p", fullPrompt], {
|
|
1007
|
+
cwd: outputDir,
|
|
1008
|
+
stdio: "pipe",
|
|
1009
|
+
timeout: 3e5
|
|
1010
|
+
// 5 minute timeout
|
|
1011
|
+
});
|
|
1012
|
+
spinner.succeed("Site generated with Claude Code");
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
spinner.fail("Claude Code generation failed");
|
|
1015
|
+
throw error;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
var init_claude = __esm({
|
|
1019
|
+
"src/orchestrator/engines/claude.ts"() {
|
|
1020
|
+
"use strict";
|
|
1021
|
+
init_esm_shims();
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
// src/orchestrator/engines/codex.ts
|
|
1026
|
+
import { execa as execa3 } from "execa";
|
|
1027
|
+
import ora3 from "ora";
|
|
1028
|
+
async function generateWithCodex(prompt, outputDir) {
|
|
1029
|
+
const spinner = ora3("Generating site with Codex...").start();
|
|
1030
|
+
try {
|
|
1031
|
+
const fullPrompt = `${prompt}
|
|
1032
|
+
|
|
1033
|
+
Create all files in the current working directory. Do not ask questions, just generate all files.`;
|
|
1034
|
+
await execa3("codex", ["--quiet", "--full-auto", fullPrompt], {
|
|
1035
|
+
cwd: outputDir,
|
|
1036
|
+
stdio: "pipe",
|
|
1037
|
+
timeout: 3e5
|
|
1038
|
+
});
|
|
1039
|
+
spinner.succeed("Site generated with Codex");
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
spinner.fail("Codex generation failed");
|
|
1042
|
+
throw error;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
var init_codex = __esm({
|
|
1046
|
+
"src/orchestrator/engines/codex.ts"() {
|
|
1047
|
+
"use strict";
|
|
1048
|
+
init_esm_shims();
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
// src/orchestrator/engines/v0.ts
|
|
1053
|
+
import OpenAI from "openai";
|
|
1054
|
+
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
1055
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
1056
|
+
import ora4 from "ora";
|
|
1057
|
+
async function generateWithV0(prompt, outputDir, apiKey) {
|
|
1058
|
+
const spinner = ora4("Generating site with v0...").start();
|
|
1059
|
+
const client = new OpenAI({
|
|
1060
|
+
apiKey,
|
|
1061
|
+
baseURL: "https://api.v0.dev/v1"
|
|
1062
|
+
});
|
|
1063
|
+
try {
|
|
1064
|
+
const response = await client.chat.completions.create({
|
|
1065
|
+
model: "v0-1.0-md",
|
|
1066
|
+
messages: [
|
|
1067
|
+
{
|
|
1068
|
+
role: "system",
|
|
1069
|
+
content: "Generate a complete Next.js 15 portfolio website using TypeScript, Tailwind CSS, and shadcn/ui. Return all files with their paths clearly marked. Use ```tsx filename.tsx or ```ts filename.ts code blocks with the filename on the same line as the opening backticks. No emoji in any output."
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
role: "user",
|
|
1073
|
+
content: prompt
|
|
1074
|
+
}
|
|
1075
|
+
]
|
|
1076
|
+
});
|
|
1077
|
+
const content = response.choices[0]?.message?.content;
|
|
1078
|
+
if (!content) {
|
|
1079
|
+
throw new Error("v0 returned empty response");
|
|
1080
|
+
}
|
|
1081
|
+
spinner.text = "Parsing v0 output...";
|
|
1082
|
+
const files = extractFiles(content);
|
|
1083
|
+
if (files.length === 0) {
|
|
1084
|
+
throw new Error("No files extracted from v0 response");
|
|
1085
|
+
}
|
|
1086
|
+
spinner.text = `Writing ${files.length} files...`;
|
|
1087
|
+
for (const file of files) {
|
|
1088
|
+
const filePath = join3(outputDir, file.filename);
|
|
1089
|
+
await mkdir2(dirname2(filePath), { recursive: true });
|
|
1090
|
+
await writeFile2(filePath, file.content, "utf-8");
|
|
1091
|
+
}
|
|
1092
|
+
const hasPkgJson = files.some((f) => f.filename === "package.json");
|
|
1093
|
+
if (!hasPkgJson) {
|
|
1094
|
+
await writeFile2(
|
|
1095
|
+
join3(outputDir, "package.json"),
|
|
1096
|
+
JSON.stringify(
|
|
1097
|
+
{
|
|
1098
|
+
name: "shipfolio-site",
|
|
1099
|
+
version: "1.0.0",
|
|
1100
|
+
private: true,
|
|
1101
|
+
scripts: {
|
|
1102
|
+
dev: "next dev",
|
|
1103
|
+
build: "next build",
|
|
1104
|
+
start: "next start"
|
|
1105
|
+
},
|
|
1106
|
+
dependencies: {
|
|
1107
|
+
next: "^15.0.0",
|
|
1108
|
+
react: "^19.0.0",
|
|
1109
|
+
"react-dom": "^19.0.0",
|
|
1110
|
+
"class-variance-authority": "^0.7.0",
|
|
1111
|
+
clsx: "^2.1.0",
|
|
1112
|
+
"tailwind-merge": "^2.6.0",
|
|
1113
|
+
"lucide-react": "^0.460.0"
|
|
1114
|
+
},
|
|
1115
|
+
devDependencies: {
|
|
1116
|
+
typescript: "^5.7.0",
|
|
1117
|
+
"@types/node": "^22.0.0",
|
|
1118
|
+
"@types/react": "^19.0.0",
|
|
1119
|
+
"@types/react-dom": "^19.0.0",
|
|
1120
|
+
tailwindcss: "^4.0.0",
|
|
1121
|
+
"@tailwindcss/postcss": "^4.0.0"
|
|
1122
|
+
}
|
|
1123
|
+
},
|
|
1124
|
+
null,
|
|
1125
|
+
2
|
|
1126
|
+
),
|
|
1127
|
+
"utf-8"
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
spinner.succeed(`Site generated with v0 (${files.length} files)`);
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
spinner.fail("v0 generation failed");
|
|
1133
|
+
throw error;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
function extractFiles(content) {
|
|
1137
|
+
const files = [];
|
|
1138
|
+
const patterns = [
|
|
1139
|
+
// ```tsx src/app/page.tsx
|
|
1140
|
+
/```(?:tsx?|jsx?|css|json|mjs|cjs)\s+([^\n`]+)\n([\s\S]*?)```/g,
|
|
1141
|
+
// ```tsx filename="src/app/page.tsx"
|
|
1142
|
+
/```(?:tsx?|jsx?|css|json|mjs|cjs)\s+filename="([^"]+)"\n([\s\S]*?)```/g,
|
|
1143
|
+
// ```tsx {filename: "src/app/page.tsx"}
|
|
1144
|
+
/```(?:tsx?|jsx?|css|json|mjs|cjs).*?filename:\s*"([^"]+)".*?\n([\s\S]*?)```/g
|
|
1145
|
+
];
|
|
1146
|
+
for (const pattern of patterns) {
|
|
1147
|
+
let match;
|
|
1148
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
1149
|
+
const filename = match[1].trim();
|
|
1150
|
+
const fileContent = match[2].trim();
|
|
1151
|
+
if (filename.includes(" ") && !filename.includes("/") && !filename.includes(".")) {
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
const cleanFilename = filename.replace(/^["']|["']$/g, "").replace(/^\.\//g, "");
|
|
1155
|
+
if (cleanFilename && fileContent) {
|
|
1156
|
+
files.push({ filename: cleanFilename, content: fileContent });
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return files;
|
|
1161
|
+
}
|
|
1162
|
+
var init_v0 = __esm({
|
|
1163
|
+
"src/orchestrator/engines/v0.ts"() {
|
|
1164
|
+
"use strict";
|
|
1165
|
+
init_esm_shims();
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
// src/orchestrator/validator.ts
|
|
1170
|
+
async function validateGeneratedSite(siteDir) {
|
|
1171
|
+
const errors = [];
|
|
1172
|
+
const requiredFiles = [
|
|
1173
|
+
"package.json",
|
|
1174
|
+
"next.config.ts",
|
|
1175
|
+
"src/app/layout.tsx",
|
|
1176
|
+
"src/app/page.tsx"
|
|
1177
|
+
];
|
|
1178
|
+
const alternativeChecks = [
|
|
1179
|
+
["next.config.ts", ["next.config.ts", "next.config.js", "next.config.mjs"]]
|
|
1180
|
+
];
|
|
1181
|
+
for (const file of requiredFiles) {
|
|
1182
|
+
const alternatives = alternativeChecks.find(([key]) => key === file);
|
|
1183
|
+
if (alternatives) {
|
|
1184
|
+
const anyExists = await Promise.any(
|
|
1185
|
+
alternatives[1].map(async (alt) => {
|
|
1186
|
+
if (await fileExists(join(siteDir, alt))) return true;
|
|
1187
|
+
throw new Error("not found");
|
|
1188
|
+
})
|
|
1189
|
+
).catch(() => false);
|
|
1190
|
+
if (!anyExists) {
|
|
1191
|
+
errors.push(`Missing: ${file} (or any alternative)`);
|
|
1192
|
+
}
|
|
1193
|
+
} else {
|
|
1194
|
+
if (!await fileExists(join(siteDir, file))) {
|
|
1195
|
+
errors.push(`Missing: ${file}`);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
valid: errors.length === 0,
|
|
1201
|
+
errors
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
async function validateBuildOutput(siteDir) {
|
|
1205
|
+
const errors = [];
|
|
1206
|
+
const outDir = join(siteDir, "out");
|
|
1207
|
+
if (!await fileExists(outDir)) {
|
|
1208
|
+
errors.push("Build output directory 'out/' not found");
|
|
1209
|
+
return { valid: false, errors };
|
|
1210
|
+
}
|
|
1211
|
+
const indexHtml = join(outDir, "index.html");
|
|
1212
|
+
if (!await fileExists(indexHtml)) {
|
|
1213
|
+
errors.push("Missing: out/index.html");
|
|
1214
|
+
}
|
|
1215
|
+
return {
|
|
1216
|
+
valid: errors.length === 0,
|
|
1217
|
+
errors
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
var init_validator = __esm({
|
|
1221
|
+
"src/orchestrator/validator.ts"() {
|
|
1222
|
+
"use strict";
|
|
1223
|
+
init_esm_shims();
|
|
1224
|
+
init_fs();
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
// src/orchestrator/index.ts
|
|
1229
|
+
import ora5 from "ora";
|
|
1230
|
+
async function generateSite(engine, prompt, outputDir) {
|
|
1231
|
+
await ensureDir(outputDir);
|
|
1232
|
+
switch (engine) {
|
|
1233
|
+
case "claude":
|
|
1234
|
+
await generateWithClaude(prompt, outputDir);
|
|
1235
|
+
break;
|
|
1236
|
+
case "codex":
|
|
1237
|
+
await generateWithCodex(prompt, outputDir);
|
|
1238
|
+
break;
|
|
1239
|
+
case "v0": {
|
|
1240
|
+
const apiKey = process.env.V0_API_KEY;
|
|
1241
|
+
if (!apiKey) {
|
|
1242
|
+
throw new Error(
|
|
1243
|
+
"V0_API_KEY environment variable is required for v0 engine"
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
await generateWithV0(prompt, outputDir, apiKey);
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
const validation = await validateGeneratedSite(outputDir);
|
|
1251
|
+
if (!validation.valid) {
|
|
1252
|
+
logger.warn("Generated site has issues:");
|
|
1253
|
+
for (const err of validation.errors) {
|
|
1254
|
+
logger.warn(` ${err}`);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
async function buildSite(siteDir) {
|
|
1259
|
+
const spinner = ora5("Installing dependencies...").start();
|
|
1260
|
+
try {
|
|
1261
|
+
await run("npm", ["install"], { cwd: siteDir });
|
|
1262
|
+
spinner.text = "Building site...";
|
|
1263
|
+
await run("npm", ["run", "build"], { cwd: siteDir });
|
|
1264
|
+
spinner.succeed("Site built successfully");
|
|
1265
|
+
} catch (error) {
|
|
1266
|
+
spinner.fail("Build failed");
|
|
1267
|
+
logger.error(error.stderr || error.message || String(error));
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
const validation = await validateBuildOutput(siteDir);
|
|
1271
|
+
if (!validation.valid) {
|
|
1272
|
+
logger.error("Build output validation failed:");
|
|
1273
|
+
for (const err of validation.errors) {
|
|
1274
|
+
logger.error(` ${err}`);
|
|
1275
|
+
}
|
|
1276
|
+
return false;
|
|
1277
|
+
}
|
|
1278
|
+
return true;
|
|
1279
|
+
}
|
|
1280
|
+
var init_orchestrator = __esm({
|
|
1281
|
+
"src/orchestrator/index.ts"() {
|
|
1282
|
+
"use strict";
|
|
1283
|
+
init_esm_shims();
|
|
1284
|
+
init_claude();
|
|
1285
|
+
init_codex();
|
|
1286
|
+
init_v0();
|
|
1287
|
+
init_validator();
|
|
1288
|
+
init_fs();
|
|
1289
|
+
init_exec();
|
|
1290
|
+
init_logger();
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
// src/deployer/auth.ts
|
|
1295
|
+
async function checkAuth(platform) {
|
|
1296
|
+
if (platform === "local") return true;
|
|
1297
|
+
try {
|
|
1298
|
+
if (platform === "cloudflare") {
|
|
1299
|
+
await runWithOutput("npx", ["wrangler", "whoami"]);
|
|
1300
|
+
return true;
|
|
1301
|
+
}
|
|
1302
|
+
if (platform === "vercel") {
|
|
1303
|
+
await runWithOutput("npx", ["vercel", "whoami"]);
|
|
1304
|
+
return true;
|
|
1305
|
+
}
|
|
1306
|
+
} catch {
|
|
1307
|
+
return false;
|
|
1308
|
+
}
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
async function login(platform) {
|
|
1312
|
+
if (platform === "cloudflare") {
|
|
1313
|
+
logger.info("Opening Cloudflare login in browser...");
|
|
1314
|
+
await run("npx", ["wrangler", "login"], { stdio: "inherit" });
|
|
1315
|
+
} else if (platform === "vercel") {
|
|
1316
|
+
logger.info("Opening Vercel login in browser...");
|
|
1317
|
+
await run("npx", ["vercel", "login"], { stdio: "inherit" });
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
async function ensureAuth(platform) {
|
|
1321
|
+
if (platform === "local") return;
|
|
1322
|
+
const isAuthed = await checkAuth(platform);
|
|
1323
|
+
if (!isAuthed) {
|
|
1324
|
+
await login(platform);
|
|
1325
|
+
const nowAuthed = await checkAuth(platform);
|
|
1326
|
+
if (!nowAuthed) {
|
|
1327
|
+
throw new Error(`Authentication failed for ${platform}`);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
var init_auth = __esm({
|
|
1332
|
+
"src/deployer/auth.ts"() {
|
|
1333
|
+
"use strict";
|
|
1334
|
+
init_esm_shims();
|
|
1335
|
+
init_exec();
|
|
1336
|
+
init_logger();
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
// src/deployer/cloudflare.ts
|
|
1341
|
+
import ora6 from "ora";
|
|
1342
|
+
async function deployToCloudflare(distDir, projectName) {
|
|
1343
|
+
const spinner = ora6("Deploying to Cloudflare Pages...").start();
|
|
1344
|
+
try {
|
|
1345
|
+
const output = await runWithOutput("npx", [
|
|
1346
|
+
"wrangler",
|
|
1347
|
+
"pages",
|
|
1348
|
+
"deploy",
|
|
1349
|
+
distDir,
|
|
1350
|
+
`--project-name=${projectName}`
|
|
1351
|
+
]);
|
|
1352
|
+
const urlMatch = output.match(
|
|
1353
|
+
/https:\/\/[^\s]+\.pages\.dev/
|
|
1354
|
+
);
|
|
1355
|
+
const url = urlMatch?.[0] || `https://${projectName}.pages.dev`;
|
|
1356
|
+
spinner.succeed(`Deployed to Cloudflare Pages`);
|
|
1357
|
+
logger.info(`URL: ${url}`);
|
|
1358
|
+
return url;
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
spinner.fail("Cloudflare Pages deployment failed");
|
|
1361
|
+
throw error;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
var init_cloudflare = __esm({
|
|
1365
|
+
"src/deployer/cloudflare.ts"() {
|
|
1366
|
+
"use strict";
|
|
1367
|
+
init_esm_shims();
|
|
1368
|
+
init_exec();
|
|
1369
|
+
init_logger();
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
// src/deployer/vercel.ts
|
|
1374
|
+
import ora7 from "ora";
|
|
1375
|
+
async function deployToVercel(distDir) {
|
|
1376
|
+
const spinner = ora7("Deploying to Vercel...").start();
|
|
1377
|
+
try {
|
|
1378
|
+
const output = await runWithOutput("npx", [
|
|
1379
|
+
"vercel",
|
|
1380
|
+
"deploy",
|
|
1381
|
+
distDir,
|
|
1382
|
+
"--yes",
|
|
1383
|
+
"--prod"
|
|
1384
|
+
]);
|
|
1385
|
+
const urlMatch = output.match(
|
|
1386
|
+
/https:\/\/[^\s]+\.vercel\.app/
|
|
1387
|
+
);
|
|
1388
|
+
const url = urlMatch?.[0] || output.trim().split("\n").pop() || "";
|
|
1389
|
+
spinner.succeed("Deployed to Vercel");
|
|
1390
|
+
logger.info(`URL: ${url}`);
|
|
1391
|
+
return url;
|
|
1392
|
+
} catch (error) {
|
|
1393
|
+
spinner.fail("Vercel deployment failed");
|
|
1394
|
+
throw error;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
var init_vercel = __esm({
|
|
1398
|
+
"src/deployer/vercel.ts"() {
|
|
1399
|
+
"use strict";
|
|
1400
|
+
init_esm_shims();
|
|
1401
|
+
init_exec();
|
|
1402
|
+
init_logger();
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
// src/deployer/github.ts
|
|
1407
|
+
import * as p2 from "@clack/prompts";
|
|
1408
|
+
async function setupGitHubAutoDeploy(siteDir, projectName) {
|
|
1409
|
+
const hasGh = await commandExists("gh");
|
|
1410
|
+
if (!hasGh) {
|
|
1411
|
+
logger.info(
|
|
1412
|
+
"GitHub CLI (gh) not found. Install it to enable auto-deploy setup."
|
|
1413
|
+
);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
const shouldSetup = await p2.confirm({
|
|
1417
|
+
message: "Set up GitHub auto-deploy? (push = redeploy)",
|
|
1418
|
+
initialValue: false
|
|
1419
|
+
});
|
|
1420
|
+
if (p2.isCancel(shouldSetup) || !shouldSetup) return;
|
|
1421
|
+
const visibility = await p2.select({
|
|
1422
|
+
message: "Repository visibility:",
|
|
1423
|
+
options: [
|
|
1424
|
+
{ value: "private", label: "Private" },
|
|
1425
|
+
{ value: "public", label: "Public" }
|
|
1426
|
+
]
|
|
1427
|
+
});
|
|
1428
|
+
if (p2.isCancel(visibility)) return;
|
|
1429
|
+
try {
|
|
1430
|
+
const gitDir = join(siteDir, ".git");
|
|
1431
|
+
if (!await fileExists(gitDir)) {
|
|
1432
|
+
await run("git", ["init"], { cwd: siteDir });
|
|
1433
|
+
}
|
|
1434
|
+
await run(
|
|
1435
|
+
"gh",
|
|
1436
|
+
[
|
|
1437
|
+
"repo",
|
|
1438
|
+
"create",
|
|
1439
|
+
projectName,
|
|
1440
|
+
`--${visibility}`,
|
|
1441
|
+
"--source",
|
|
1442
|
+
siteDir,
|
|
1443
|
+
"--push"
|
|
1444
|
+
],
|
|
1445
|
+
{ cwd: siteDir, stdio: "inherit" }
|
|
1446
|
+
);
|
|
1447
|
+
logger.success(`GitHub repository created: ${projectName}`);
|
|
1448
|
+
logger.info(
|
|
1449
|
+
"Connect this repo in your Cloudflare Pages or Vercel dashboard for auto-deploy."
|
|
1450
|
+
);
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
logger.error(`GitHub setup failed: ${error.message}`);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
var init_github = __esm({
|
|
1456
|
+
"src/deployer/github.ts"() {
|
|
1457
|
+
"use strict";
|
|
1458
|
+
init_esm_shims();
|
|
1459
|
+
init_exec();
|
|
1460
|
+
init_fs();
|
|
1461
|
+
init_logger();
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
// src/deployer/index.ts
|
|
1466
|
+
import { join as join4 } from "path";
|
|
1467
|
+
async function deploy(siteDir, platform, projectName) {
|
|
1468
|
+
if (platform === "local") {
|
|
1469
|
+
logger.info(`Site built at ${join4(siteDir, "out/")}`);
|
|
1470
|
+
logger.info("Run `npx serve ./out` in the site directory to preview.");
|
|
1471
|
+
return null;
|
|
1472
|
+
}
|
|
1473
|
+
await ensureAuth(platform);
|
|
1474
|
+
const distDir = join4(siteDir, "out");
|
|
1475
|
+
let url;
|
|
1476
|
+
if (platform === "cloudflare") {
|
|
1477
|
+
url = await deployToCloudflare(distDir, projectName);
|
|
1478
|
+
} else {
|
|
1479
|
+
url = await deployToVercel(distDir);
|
|
1480
|
+
}
|
|
1481
|
+
await setupGitHubAutoDeploy(siteDir, projectName);
|
|
1482
|
+
return url;
|
|
1483
|
+
}
|
|
1484
|
+
var init_deployer = __esm({
|
|
1485
|
+
"src/deployer/index.ts"() {
|
|
1486
|
+
"use strict";
|
|
1487
|
+
init_esm_shims();
|
|
1488
|
+
init_auth();
|
|
1489
|
+
init_cloudflare();
|
|
1490
|
+
init_vercel();
|
|
1491
|
+
init_github();
|
|
1492
|
+
init_logger();
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
// src/pdf/index.ts
|
|
1497
|
+
import { createServer } from "http";
|
|
1498
|
+
import { join as join5 } from "path";
|
|
1499
|
+
import ora8 from "ora";
|
|
1500
|
+
async function exportPdf(siteDir, outputPath) {
|
|
1501
|
+
const distDir = join5(siteDir, "out");
|
|
1502
|
+
const pdfPath = outputPath || join5(distDir, "shipfolio.pdf");
|
|
1503
|
+
const spinner = ora8("Exporting PDF...").start();
|
|
1504
|
+
const server = await startStaticServer(distDir);
|
|
1505
|
+
const port = server.address()?.port || 3456;
|
|
1506
|
+
try {
|
|
1507
|
+
const { chromium } = await import("playwright");
|
|
1508
|
+
const browser = await chromium.launch();
|
|
1509
|
+
const page = await browser.newPage();
|
|
1510
|
+
await page.goto(`http://localhost:${port}`, {
|
|
1511
|
+
waitUntil: "networkidle"
|
|
1512
|
+
});
|
|
1513
|
+
await page.waitForTimeout(1e3);
|
|
1514
|
+
await page.pdf({
|
|
1515
|
+
path: pdfPath,
|
|
1516
|
+
format: "A4",
|
|
1517
|
+
printBackground: true,
|
|
1518
|
+
margin: {
|
|
1519
|
+
top: "1cm",
|
|
1520
|
+
right: "1cm",
|
|
1521
|
+
bottom: "1cm",
|
|
1522
|
+
left: "1cm"
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
await browser.close();
|
|
1526
|
+
spinner.succeed(`PDF exported: ${pdfPath}`);
|
|
1527
|
+
return pdfPath;
|
|
1528
|
+
} catch (error) {
|
|
1529
|
+
spinner.fail("PDF export failed");
|
|
1530
|
+
if (error.message?.includes("browserType.launch")) {
|
|
1531
|
+
logger.error(
|
|
1532
|
+
"Playwright browsers not installed. Run: npx playwright install chromium"
|
|
1533
|
+
);
|
|
1534
|
+
} else {
|
|
1535
|
+
logger.error(error.message || String(error));
|
|
1536
|
+
}
|
|
1537
|
+
throw error;
|
|
1538
|
+
} finally {
|
|
1539
|
+
server.close();
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
function startStaticServer(dir) {
|
|
1543
|
+
return new Promise(async (resolve7, reject) => {
|
|
1544
|
+
const handler = await import("serve-handler");
|
|
1545
|
+
const server = createServer((req, res) => {
|
|
1546
|
+
return handler.default(req, res, {
|
|
1547
|
+
public: dir,
|
|
1548
|
+
cleanUrls: true
|
|
1549
|
+
});
|
|
1550
|
+
});
|
|
1551
|
+
server.listen(0, () => {
|
|
1552
|
+
resolve7(server);
|
|
1553
|
+
});
|
|
1554
|
+
server.on("error", reject);
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
async function startPreviewServer(siteDir, port = 3e3) {
|
|
1558
|
+
const distDir = join5(siteDir, "out");
|
|
1559
|
+
const handler = await import("serve-handler");
|
|
1560
|
+
const server = createServer((req, res) => {
|
|
1561
|
+
return handler.default(req, res, {
|
|
1562
|
+
public: distDir,
|
|
1563
|
+
cleanUrls: true
|
|
1564
|
+
});
|
|
1565
|
+
});
|
|
1566
|
+
return new Promise((resolve7, reject) => {
|
|
1567
|
+
server.listen(port, () => {
|
|
1568
|
+
logger.info(`Preview: http://localhost:${port}`);
|
|
1569
|
+
resolve7(server);
|
|
1570
|
+
});
|
|
1571
|
+
server.on("error", reject);
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
var init_pdf = __esm({
|
|
1575
|
+
"src/pdf/index.ts"() {
|
|
1576
|
+
"use strict";
|
|
1577
|
+
init_esm_shims();
|
|
1578
|
+
init_logger();
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
// src/commands/init.ts
|
|
1583
|
+
var init_exports = {};
|
|
1584
|
+
__export(init_exports, {
|
|
1585
|
+
initCommand: () => initCommand
|
|
1586
|
+
});
|
|
1587
|
+
import { resolve } from "path";
|
|
1588
|
+
import { join as join6 } from "path";
|
|
1589
|
+
import * as p3 from "@clack/prompts";
|
|
1590
|
+
async function initCommand(options) {
|
|
1591
|
+
logger.header("shipfolio v1.0.0");
|
|
1592
|
+
logger.info("Detecting AI engines...");
|
|
1593
|
+
const engines = await detectEngines();
|
|
1594
|
+
const availableTypes = getAvailableEngineTypes(engines);
|
|
1595
|
+
if (availableTypes.length === 0) {
|
|
1596
|
+
logger.error("No AI engine found.");
|
|
1597
|
+
logger.plain("");
|
|
1598
|
+
logger.plain(" shipfolio requires one of the following:");
|
|
1599
|
+
logger.plain(" - Claude Code: npm install -g @anthropic-ai/claude-code");
|
|
1600
|
+
logger.plain(" - Codex: npm install -g @openai/codex");
|
|
1601
|
+
logger.plain(" - v0: Set V0_API_KEY environment variable");
|
|
1602
|
+
logger.plain("");
|
|
1603
|
+
process.exit(1);
|
|
1604
|
+
}
|
|
1605
|
+
logger.info(
|
|
1606
|
+
`Available engines: ${availableTypes.join(", ")}`
|
|
1607
|
+
);
|
|
1608
|
+
const scanDirs = options.scan?.map((d) => resolve(d)) || [
|
|
1609
|
+
resolve(process.cwd())
|
|
1610
|
+
];
|
|
1611
|
+
const scannedProjects = await scanProjects(scanDirs);
|
|
1612
|
+
if (scannedProjects.length === 0) {
|
|
1613
|
+
logger.error("No projects found. Specify a different directory with --scan.");
|
|
1614
|
+
process.exit(1);
|
|
1615
|
+
}
|
|
1616
|
+
logger.header(`Found ${scannedProjects.length} projects:`);
|
|
1617
|
+
const tableRows = scannedProjects.map((proj) => [
|
|
1618
|
+
proj.name,
|
|
1619
|
+
proj.techStack.slice(0, 3).join(", "),
|
|
1620
|
+
proj.firstCommitDate?.slice(0, 10) || "?",
|
|
1621
|
+
`${proj.totalCommits} commits`
|
|
1622
|
+
]);
|
|
1623
|
+
logger.table(tableRows);
|
|
1624
|
+
logger.blank();
|
|
1625
|
+
const interviewResult = await runInterview(scannedProjects, availableTypes);
|
|
1626
|
+
const spec = buildSpec(interviewResult);
|
|
1627
|
+
const prompt = await buildFreshPrompt(spec);
|
|
1628
|
+
const outputDir = resolve(options.output || "./shipfolio-site");
|
|
1629
|
+
await ensureDir(outputDir);
|
|
1630
|
+
await generateSite(spec.engine, prompt, outputDir);
|
|
1631
|
+
let buildSuccess = await buildSite(outputDir);
|
|
1632
|
+
if (!buildSuccess) {
|
|
1633
|
+
logger.info("Attempting retry...");
|
|
1634
|
+
buildSuccess = await buildSite(outputDir);
|
|
1635
|
+
if (!buildSuccess) {
|
|
1636
|
+
logger.error("Build failed after retry. Check the output directory for details.");
|
|
1637
|
+
process.exit(1);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
if (!options.noPreview) {
|
|
1641
|
+
const server = await startPreviewServer(outputDir);
|
|
1642
|
+
logger.blank();
|
|
1643
|
+
const proceed = await p3.confirm({
|
|
1644
|
+
message: "Deploy now?",
|
|
1645
|
+
initialValue: true
|
|
1646
|
+
});
|
|
1647
|
+
server.close();
|
|
1648
|
+
if (p3.isCancel(proceed) || !proceed) {
|
|
1649
|
+
if (spec.deploy.platform === "local") {
|
|
1650
|
+
} else {
|
|
1651
|
+
logger.info("Skipping deploy. Run `npx shipfolio deploy` later.");
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
if (!options.noPdf) {
|
|
1656
|
+
try {
|
|
1657
|
+
await exportPdf(outputDir);
|
|
1658
|
+
} catch {
|
|
1659
|
+
logger.warn("PDF export skipped due to error.");
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
if (spec.deploy.platform !== "local") {
|
|
1663
|
+
const url = await deploy(
|
|
1664
|
+
outputDir,
|
|
1665
|
+
spec.deploy.platform,
|
|
1666
|
+
spec.deploy.projectName
|
|
1667
|
+
);
|
|
1668
|
+
if (url) {
|
|
1669
|
+
spec.deploy.url = url;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
await writeJson(join6(outputDir, "shipfolio.config.json"), {
|
|
1673
|
+
...spec,
|
|
1674
|
+
sitePath: outputDir
|
|
1675
|
+
});
|
|
1676
|
+
logger.header("Done.");
|
|
1677
|
+
if (spec.deploy.url) {
|
|
1678
|
+
logger.info(`Site: ${spec.deploy.url}`);
|
|
1679
|
+
}
|
|
1680
|
+
logger.info(`Local: ${outputDir}`);
|
|
1681
|
+
logger.info(
|
|
1682
|
+
`Update: npx shipfolio update --site ${outputDir}`
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
var init_init = __esm({
|
|
1686
|
+
"src/commands/init.ts"() {
|
|
1687
|
+
"use strict";
|
|
1688
|
+
init_esm_shims();
|
|
1689
|
+
init_scanner();
|
|
1690
|
+
init_interviewer();
|
|
1691
|
+
init_builder();
|
|
1692
|
+
init_prompt_builder();
|
|
1693
|
+
init_detect();
|
|
1694
|
+
init_orchestrator();
|
|
1695
|
+
init_deployer();
|
|
1696
|
+
init_pdf();
|
|
1697
|
+
init_pdf();
|
|
1698
|
+
init_fs();
|
|
1699
|
+
init_logger();
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
// src/index.ts
|
|
1704
|
+
init_esm_shims();
|
|
1705
|
+
init_init();
|
|
1706
|
+
import { Command } from "commander";
|
|
1707
|
+
|
|
1708
|
+
// src/commands/update.ts
|
|
1709
|
+
init_esm_shims();
|
|
1710
|
+
init_scanner();
|
|
1711
|
+
import { resolve as resolve2 } from "path";
|
|
1712
|
+
|
|
1713
|
+
// src/spec/diff.ts
|
|
1714
|
+
init_esm_shims();
|
|
1715
|
+
function computeDiff(oldConfig, newScan) {
|
|
1716
|
+
const oldProjectMap = new Map(
|
|
1717
|
+
oldConfig.projects.map((p6) => [p6.localPath, p6])
|
|
1718
|
+
);
|
|
1719
|
+
const newProjectMap = new Map(newScan.map((p6) => [p6.localPath, p6]));
|
|
1720
|
+
const newProjects = [];
|
|
1721
|
+
const updatedProjects = [];
|
|
1722
|
+
const removedProjects = [];
|
|
1723
|
+
const unchangedProjects = [];
|
|
1724
|
+
for (const [path2, meta] of newProjectMap) {
|
|
1725
|
+
const oldProject = oldProjectMap.get(path2);
|
|
1726
|
+
if (!oldProject) {
|
|
1727
|
+
newProjects.push(meta);
|
|
1728
|
+
} else if (meta.lastScannedCommit !== oldProject.lastScannedCommit) {
|
|
1729
|
+
const newCommits = meta.totalCommits - oldProject.totalCommits;
|
|
1730
|
+
updatedProjects.push({
|
|
1731
|
+
project: meta,
|
|
1732
|
+
newCommits: Math.max(newCommits, 0),
|
|
1733
|
+
readmeChanged: meta.readmeContent !== oldProject.readmeContent,
|
|
1734
|
+
depsChanged: JSON.stringify(meta.techStack) !== JSON.stringify(oldProject.techStack)
|
|
1735
|
+
});
|
|
1736
|
+
} else {
|
|
1737
|
+
unchangedProjects.push(oldProject);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
for (const [path2, project] of oldProjectMap) {
|
|
1741
|
+
if (!newProjectMap.has(path2)) {
|
|
1742
|
+
removedProjects.push(project);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
return {
|
|
1746
|
+
newProjects,
|
|
1747
|
+
updatedProjects,
|
|
1748
|
+
removedProjects,
|
|
1749
|
+
unchangedProjects
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// src/commands/update.ts
|
|
1754
|
+
init_prompt_builder();
|
|
1755
|
+
init_detect();
|
|
1756
|
+
init_orchestrator();
|
|
1757
|
+
init_deployer();
|
|
1758
|
+
init_pdf();
|
|
1759
|
+
init_fs();
|
|
1760
|
+
init_logger();
|
|
1761
|
+
import * as p4 from "@clack/prompts";
|
|
1762
|
+
async function updateCommand(options) {
|
|
1763
|
+
logger.header("shipfolio update");
|
|
1764
|
+
const siteDir = resolve2(options.site);
|
|
1765
|
+
const configPath = join(siteDir, "shipfolio.config.json");
|
|
1766
|
+
if (!await fileExists(configPath)) {
|
|
1767
|
+
logger.error(
|
|
1768
|
+
"shipfolio.config.json not found. This directory was not generated by shipfolio."
|
|
1769
|
+
);
|
|
1770
|
+
const fresh = await p4.confirm({
|
|
1771
|
+
message: "Proceed with fresh build instead?",
|
|
1772
|
+
initialValue: true
|
|
1773
|
+
});
|
|
1774
|
+
if (p4.isCancel(fresh) || !fresh) {
|
|
1775
|
+
process.exit(0);
|
|
1776
|
+
}
|
|
1777
|
+
const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
1778
|
+
await initCommand2({ output: siteDir });
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
const config = await readJson(configPath);
|
|
1782
|
+
logger.info(
|
|
1783
|
+
`Site: ${config.deploy.projectName || "local"} (generated ${config.generatedAt.slice(0, 10)})`
|
|
1784
|
+
);
|
|
1785
|
+
logger.info(`Theme: ${config.style.theme}`);
|
|
1786
|
+
logger.info(`Projects: ${config.projects.length}`);
|
|
1787
|
+
const engines = await detectEngines();
|
|
1788
|
+
const availableTypes = getAvailableEngineTypes(engines);
|
|
1789
|
+
if (availableTypes.length === 0) {
|
|
1790
|
+
logger.error("No AI engine found. See `npx shipfolio` for install instructions.");
|
|
1791
|
+
process.exit(1);
|
|
1792
|
+
}
|
|
1793
|
+
const scanDirs = options.scan?.map((d) => resolve2(d)) || [...new Set(config.projects.map((p6) => {
|
|
1794
|
+
const parts = p6.localPath.split("/");
|
|
1795
|
+
parts.pop();
|
|
1796
|
+
return parts.join("/");
|
|
1797
|
+
}))];
|
|
1798
|
+
const scannedProjects = await scanProjects(scanDirs);
|
|
1799
|
+
const diff = computeDiff(config, scannedProjects);
|
|
1800
|
+
logger.header("Changes detected:");
|
|
1801
|
+
if (diff.newProjects.length > 0) {
|
|
1802
|
+
logger.plain(" New:");
|
|
1803
|
+
for (const proj of diff.newProjects) {
|
|
1804
|
+
logger.plain(` + ${proj.name} ${proj.techStack.slice(0, 3).join(", ")}`);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
if (diff.updatedProjects.length > 0) {
|
|
1808
|
+
logger.plain(" Updated:");
|
|
1809
|
+
for (const upd of diff.updatedProjects) {
|
|
1810
|
+
logger.plain(
|
|
1811
|
+
` ~ ${upd.project.name} ${upd.newCommits} new commits`
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
if (diff.removedProjects.length > 0) {
|
|
1816
|
+
logger.plain(" Removed:");
|
|
1817
|
+
for (const proj of diff.removedProjects) {
|
|
1818
|
+
logger.plain(` - ${proj.name} (directory gone)`);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
if (diff.newProjects.length === 0 && diff.updatedProjects.length === 0 && diff.removedProjects.length === 0) {
|
|
1822
|
+
logger.info("No changes detected.");
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
const proceed = await p4.confirm({
|
|
1826
|
+
message: "Apply these changes?",
|
|
1827
|
+
initialValue: true
|
|
1828
|
+
});
|
|
1829
|
+
if (p4.isCancel(proceed) || !proceed) {
|
|
1830
|
+
process.exit(0);
|
|
1831
|
+
}
|
|
1832
|
+
const engine = availableTypes.includes(config.engine) ? config.engine : availableTypes[0];
|
|
1833
|
+
const prompt = await buildUpdatePrompt(config, diff);
|
|
1834
|
+
await generateSite(engine, prompt, siteDir);
|
|
1835
|
+
const buildSuccess = await buildSite(siteDir);
|
|
1836
|
+
if (!buildSuccess) {
|
|
1837
|
+
logger.error("Build failed. Check the site directory.");
|
|
1838
|
+
process.exit(1);
|
|
1839
|
+
}
|
|
1840
|
+
if (!options.noPreview) {
|
|
1841
|
+
const server = await startPreviewServer(siteDir);
|
|
1842
|
+
const ok = await p4.confirm({
|
|
1843
|
+
message: "Looks good?",
|
|
1844
|
+
initialValue: true
|
|
1845
|
+
});
|
|
1846
|
+
server.close();
|
|
1847
|
+
if (p4.isCancel(ok) || !ok) {
|
|
1848
|
+
logger.info("Update cancelled at preview.");
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
if (!options.noPdf) {
|
|
1853
|
+
try {
|
|
1854
|
+
await exportPdf(siteDir);
|
|
1855
|
+
} catch {
|
|
1856
|
+
logger.warn("PDF export skipped.");
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
if (!options.noDeploy && config.deploy.platform !== "local") {
|
|
1860
|
+
await deploy(siteDir, config.deploy.platform, config.deploy.projectName);
|
|
1861
|
+
}
|
|
1862
|
+
const updatedConfig = {
|
|
1863
|
+
...config,
|
|
1864
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1865
|
+
projects: [
|
|
1866
|
+
...diff.unchangedProjects,
|
|
1867
|
+
...diff.updatedProjects.map((u) => {
|
|
1868
|
+
const existing = config.projects.find(
|
|
1869
|
+
(p6) => p6.localPath === u.project.localPath
|
|
1870
|
+
);
|
|
1871
|
+
return {
|
|
1872
|
+
...u.project,
|
|
1873
|
+
included: true,
|
|
1874
|
+
overrideDescription: existing?.overrideDescription || null,
|
|
1875
|
+
showSourceLink: existing?.showSourceLink ?? !!u.project.remoteUrl,
|
|
1876
|
+
role: existing?.role || "solo",
|
|
1877
|
+
metrics: existing?.metrics || {}
|
|
1878
|
+
};
|
|
1879
|
+
}),
|
|
1880
|
+
...diff.newProjects.map(
|
|
1881
|
+
(proj) => ({
|
|
1882
|
+
...proj,
|
|
1883
|
+
included: true,
|
|
1884
|
+
overrideDescription: null,
|
|
1885
|
+
showSourceLink: !!proj.remoteUrl,
|
|
1886
|
+
role: "solo",
|
|
1887
|
+
metrics: {}
|
|
1888
|
+
})
|
|
1889
|
+
)
|
|
1890
|
+
]
|
|
1891
|
+
};
|
|
1892
|
+
await writeJson(join(siteDir, "shipfolio.config.json"), updatedConfig);
|
|
1893
|
+
logger.success("Update complete.");
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// src/commands/spec.ts
|
|
1897
|
+
init_esm_shims();
|
|
1898
|
+
init_scanner();
|
|
1899
|
+
init_interviewer();
|
|
1900
|
+
init_builder();
|
|
1901
|
+
init_prompt_builder();
|
|
1902
|
+
init_detect();
|
|
1903
|
+
init_fs();
|
|
1904
|
+
init_logger();
|
|
1905
|
+
import { resolve as resolve3 } from "path";
|
|
1906
|
+
async function specCommand(options) {
|
|
1907
|
+
logger.header("shipfolio spec");
|
|
1908
|
+
const engines = await detectEngines();
|
|
1909
|
+
const availableTypes = getAvailableEngineTypes(engines);
|
|
1910
|
+
const engineTypes = availableTypes.length > 0 ? availableTypes : ["claude"];
|
|
1911
|
+
const scanDirs = options.scan?.map((d) => resolve3(d)) || [
|
|
1912
|
+
resolve3(process.cwd())
|
|
1913
|
+
];
|
|
1914
|
+
const scannedProjects = await scanProjects(scanDirs);
|
|
1915
|
+
if (scannedProjects.length === 0) {
|
|
1916
|
+
logger.error("No projects found.");
|
|
1917
|
+
process.exit(1);
|
|
1918
|
+
}
|
|
1919
|
+
logger.info(`Found ${scannedProjects.length} projects`);
|
|
1920
|
+
const interviewResult = await runInterview(
|
|
1921
|
+
scannedProjects,
|
|
1922
|
+
[...engineTypes]
|
|
1923
|
+
);
|
|
1924
|
+
const spec = buildSpec(interviewResult);
|
|
1925
|
+
const prompt = await buildFreshPrompt(spec);
|
|
1926
|
+
const outputDir = options.output || ".";
|
|
1927
|
+
const specPath = resolve3(outputDir, "shipfolio-spec.json");
|
|
1928
|
+
const promptPath = resolve3(outputDir, "shipfolio-prompt.md");
|
|
1929
|
+
await writeJson(specPath, spec);
|
|
1930
|
+
await writeText(promptPath, prompt);
|
|
1931
|
+
logger.success(`Spec saved: ${specPath}`);
|
|
1932
|
+
logger.success(`Prompt saved: ${promptPath}`);
|
|
1933
|
+
logger.blank();
|
|
1934
|
+
logger.info(
|
|
1935
|
+
"Use these files inside your Claude Code or Codex session to generate the site."
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// src/commands/deploy.ts
|
|
1940
|
+
init_esm_shims();
|
|
1941
|
+
init_deployer();
|
|
1942
|
+
init_fs();
|
|
1943
|
+
init_logger();
|
|
1944
|
+
import { resolve as resolve4 } from "path";
|
|
1945
|
+
import * as p5 from "@clack/prompts";
|
|
1946
|
+
async function deployCommand(options) {
|
|
1947
|
+
const siteDir = resolve4(options.site);
|
|
1948
|
+
const configPath = join(siteDir, "shipfolio.config.json");
|
|
1949
|
+
let platform;
|
|
1950
|
+
let projectName;
|
|
1951
|
+
if (await fileExists(configPath)) {
|
|
1952
|
+
const config = await readJson(configPath);
|
|
1953
|
+
platform = options.platform || config.deploy.platform;
|
|
1954
|
+
projectName = config.deploy.projectName;
|
|
1955
|
+
} else {
|
|
1956
|
+
platform = options.platform || "cloudflare";
|
|
1957
|
+
projectName = await p5.text({
|
|
1958
|
+
message: "Project name:",
|
|
1959
|
+
validate: (v) => /^[a-z0-9-]+$/.test(v) ? void 0 : "Lowercase letters, numbers, and hyphens only"
|
|
1960
|
+
});
|
|
1961
|
+
if (p5.isCancel(projectName)) process.exit(0);
|
|
1962
|
+
}
|
|
1963
|
+
if (platform === "local") {
|
|
1964
|
+
logger.info("Deploy platform is set to local. Nothing to deploy.");
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
const outDir = join(siteDir, "out");
|
|
1968
|
+
if (!await fileExists(outDir)) {
|
|
1969
|
+
logger.error("Build output not found. Run `npm run build` in the site directory first.");
|
|
1970
|
+
process.exit(1);
|
|
1971
|
+
}
|
|
1972
|
+
await deploy(siteDir, platform, projectName);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// src/commands/pdf.ts
|
|
1976
|
+
init_esm_shims();
|
|
1977
|
+
init_pdf();
|
|
1978
|
+
init_fs();
|
|
1979
|
+
init_logger();
|
|
1980
|
+
import { resolve as resolve5 } from "path";
|
|
1981
|
+
async function pdfCommand(options) {
|
|
1982
|
+
const siteDir = resolve5(options.site);
|
|
1983
|
+
const outDir = join(siteDir, "out");
|
|
1984
|
+
if (!await fileExists(outDir)) {
|
|
1985
|
+
logger.error(
|
|
1986
|
+
"Build output not found. Run `npm run build` in the site directory first."
|
|
1987
|
+
);
|
|
1988
|
+
process.exit(1);
|
|
1989
|
+
}
|
|
1990
|
+
await exportPdf(siteDir, options.output);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// src/commands/preview.ts
|
|
1994
|
+
init_esm_shims();
|
|
1995
|
+
init_pdf();
|
|
1996
|
+
init_fs();
|
|
1997
|
+
init_logger();
|
|
1998
|
+
import { resolve as resolve6 } from "path";
|
|
1999
|
+
async function previewCommand(options) {
|
|
2000
|
+
const siteDir = resolve6(options.site);
|
|
2001
|
+
const outDir = join(siteDir, "out");
|
|
2002
|
+
if (!await fileExists(outDir)) {
|
|
2003
|
+
logger.error(
|
|
2004
|
+
"Build output not found. Run `npm run build` in the site directory first."
|
|
2005
|
+
);
|
|
2006
|
+
process.exit(1);
|
|
2007
|
+
}
|
|
2008
|
+
const port = options.port || 3e3;
|
|
2009
|
+
await startPreviewServer(siteDir, port);
|
|
2010
|
+
logger.info("Press Ctrl+C to stop.");
|
|
2011
|
+
await new Promise(() => {
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
// src/index.ts
|
|
2016
|
+
var program = new Command();
|
|
2017
|
+
program.name("shipfolio").description(
|
|
2018
|
+
"Generate and deploy your personal portfolio site from local projects using AI"
|
|
2019
|
+
).version("1.0.0");
|
|
2020
|
+
program.command("init", { isDefault: true }).description("Generate a new portfolio site").option("-s, --scan <dirs...>", "Directories to scan for projects").option("-e, --engine <engine>", "AI engine: claude | codex | v0").option("-d, --deploy <platform>", "Deploy target: cloudflare | vercel | local").option("--style <theme>", "Theme: dark-minimal | light-clean | monochrome").option("--accent <hex>", "Accent color (hex)").option("--auto", "Skip prompts, use defaults").option("-o, --output <dir>", "Output directory", "./shipfolio-site").option("--no-pdf", "Skip PDF export").option("--no-preview", "Skip local preview").option("-v, --verbose", "Verbose output").action(initCommand);
|
|
2021
|
+
program.command("update").description("Update an existing portfolio site").requiredOption("--site <path>", "Path to existing site directory").option("-s, --scan <dirs...>", "Directories to scan for projects").option("--no-pdf", "Skip PDF export").option("--no-preview", "Skip preview").option("--no-deploy", "Skip deployment").action(updateCommand);
|
|
2022
|
+
program.command("spec").description("Generate spec and prompt files only").option("-s, --scan <dirs...>", "Directories to scan for projects").option("-o, --output <dir>", "Output directory for spec files", ".").action(specCommand);
|
|
2023
|
+
program.command("deploy").description("Deploy an existing built site").requiredOption("--site <path>", "Path to site directory").option("-p, --platform <platform>", "Deploy platform: cloudflare | vercel").action(deployCommand);
|
|
2024
|
+
program.command("pdf").description("Export site to PDF").requiredOption("--site <path>", "Path to site directory").option("-o, --output <path>", "PDF output path").action(pdfCommand);
|
|
2025
|
+
program.command("preview").description("Start local preview server").requiredOption("--site <path>", "Path to site directory").option("--port <port>", "Port number", "3000").action((opts) => previewCommand({ ...opts, port: parseInt(opts.port) }));
|
|
2026
|
+
program.parse();
|
|
2027
|
+
//# sourceMappingURL=cli.js.map
|