shipfolio 1.0.9 → 1.1.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 +246 -145
- package/dist/cli.js +1212 -441
- package/dist/cli.js.map +1 -1
- package/dist/lib/orchestrator/detect.js +3 -1
- package/dist/lib/orchestrator/detect.js.map +1 -1
- package/dist/lib/orchestrator/prompt-builder.js +93 -174
- package/dist/lib/orchestrator/prompt-builder.js.map +1 -1
- package/dist/lib/scanner/git.js +1 -1
- package/dist/lib/scanner/git.js.map +1 -1
- package/dist/lib/scanner/index.js +42 -18
- package/dist/lib/scanner/index.js.map +1 -1
- package/dist/lib/spec/builder.js +100 -3
- package/dist/lib/spec/builder.js.map +1 -1
- package/dist/lib/spec/diff.js +179 -15
- package/dist/lib/spec/diff.js.map +1 -1
- package/package.json +6 -4
- package/prompts/fresh-build.md +15 -0
- package/prompts/update.md +5 -2
package/dist/cli.js
CHANGED
|
@@ -81,7 +81,7 @@ function normalizeGitUrl(url) {
|
|
|
81
81
|
}
|
|
82
82
|
return url;
|
|
83
83
|
}
|
|
84
|
-
async function findGitRepos(rootPath, maxDepth =
|
|
84
|
+
async function findGitRepos(rootPath, maxDepth = 6) {
|
|
85
85
|
const { glob: glob2 } = await import("glob");
|
|
86
86
|
const gitDirs = await glob2("**/.git", {
|
|
87
87
|
cwd: rootPath,
|
|
@@ -455,15 +455,20 @@ var init_logger = __esm({
|
|
|
455
455
|
});
|
|
456
456
|
|
|
457
457
|
// src/scanner/index.ts
|
|
458
|
+
import { dirname, resolve as resolvePath } from "path";
|
|
458
459
|
import ora from "ora";
|
|
459
460
|
async function scanProjects(directories) {
|
|
460
461
|
const allPaths = [];
|
|
461
462
|
const spinner = ora("Scanning for projects...").start();
|
|
462
463
|
for (const dir of directories) {
|
|
463
464
|
try {
|
|
464
|
-
const repos = await findGitRepos(dir);
|
|
465
|
+
const repos = await findGitRepos(dir, DEFAULT_SCAN_DEPTH);
|
|
465
466
|
allPaths.push(...repos);
|
|
466
|
-
const nonGitDirs = await findNonGitProjects(
|
|
467
|
+
const nonGitDirs = await findNonGitProjects(
|
|
468
|
+
dir,
|
|
469
|
+
repos,
|
|
470
|
+
DEFAULT_SCAN_DEPTH
|
|
471
|
+
);
|
|
467
472
|
allPaths.push(...nonGitDirs);
|
|
468
473
|
} catch (err) {
|
|
469
474
|
logger.warn(`Could not scan ${dir}: ${err}`);
|
|
@@ -492,31 +497,29 @@ async function scanProjects(directories) {
|
|
|
492
497
|
spinner.succeed(`Scanned ${projects.length} projects`);
|
|
493
498
|
return projects;
|
|
494
499
|
}
|
|
495
|
-
async function findNonGitProjects(rootPath, gitRepos) {
|
|
500
|
+
async function findNonGitProjects(rootPath, gitRepos, maxDepth) {
|
|
496
501
|
const { glob: glob2 } = await import("glob");
|
|
497
|
-
const indicators = [
|
|
498
|
-
"*/package.json",
|
|
499
|
-
"*/Cargo.toml",
|
|
500
|
-
"*/go.mod",
|
|
501
|
-
"*/pyproject.toml",
|
|
502
|
-
"*/requirements.txt",
|
|
503
|
-
"*/setup.py"
|
|
504
|
-
];
|
|
505
502
|
const found = /* @__PURE__ */ new Set();
|
|
506
|
-
for (const
|
|
507
|
-
const matches = await glob2(
|
|
503
|
+
for (const indicator of NON_GIT_PROJECT_INDICATORS) {
|
|
504
|
+
const matches = await glob2(`**/${indicator}`, {
|
|
508
505
|
cwd: rootPath,
|
|
509
|
-
ignore:
|
|
506
|
+
ignore: SCAN_IGNORE_PATTERNS,
|
|
507
|
+
maxDepth
|
|
510
508
|
});
|
|
511
509
|
for (const match of matches) {
|
|
512
|
-
const
|
|
513
|
-
if (!gitRepos
|
|
514
|
-
found.add(
|
|
510
|
+
const candidateDir = dirname(resolvePath(rootPath, match));
|
|
511
|
+
if (!isInsideTrackedRepo(candidateDir, gitRepos) && !found.has(candidateDir)) {
|
|
512
|
+
found.add(candidateDir);
|
|
515
513
|
}
|
|
516
514
|
}
|
|
517
515
|
}
|
|
518
516
|
return [...found].sort();
|
|
519
517
|
}
|
|
518
|
+
function isInsideTrackedRepo(candidateDir, gitRepos) {
|
|
519
|
+
return gitRepos.some(
|
|
520
|
+
(repoPath) => candidateDir === repoPath || candidateDir.startsWith(`${repoPath}/`)
|
|
521
|
+
);
|
|
522
|
+
}
|
|
520
523
|
async function extractProjectMeta(projectPath) {
|
|
521
524
|
const gitMeta = await getGitMeta(projectPath);
|
|
522
525
|
let techStack = [];
|
|
@@ -576,6 +579,7 @@ async function extractProjectMeta(projectPath) {
|
|
|
576
579
|
lastScannedCommit: gitMeta.lastCommitHash || ""
|
|
577
580
|
};
|
|
578
581
|
}
|
|
582
|
+
var DEFAULT_SCAN_DEPTH, NON_GIT_PROJECT_INDICATORS, SCAN_IGNORE_PATTERNS;
|
|
579
583
|
var init_scanner = __esm({
|
|
580
584
|
"src/scanner/index.ts"() {
|
|
581
585
|
"use strict";
|
|
@@ -588,6 +592,25 @@ var init_scanner = __esm({
|
|
|
588
592
|
init_generic();
|
|
589
593
|
init_extractors();
|
|
590
594
|
init_logger();
|
|
595
|
+
DEFAULT_SCAN_DEPTH = 6;
|
|
596
|
+
NON_GIT_PROJECT_INDICATORS = [
|
|
597
|
+
"package.json",
|
|
598
|
+
"Cargo.toml",
|
|
599
|
+
"go.mod",
|
|
600
|
+
"pyproject.toml",
|
|
601
|
+
"requirements.txt",
|
|
602
|
+
"setup.py"
|
|
603
|
+
];
|
|
604
|
+
SCAN_IGNORE_PATTERNS = [
|
|
605
|
+
"**/node_modules/**",
|
|
606
|
+
"**/vendor/**",
|
|
607
|
+
"**/.git/**",
|
|
608
|
+
"**/.next/**",
|
|
609
|
+
"**/dist/**",
|
|
610
|
+
"**/build/**",
|
|
611
|
+
"**/out/**",
|
|
612
|
+
"**/coverage/**"
|
|
613
|
+
];
|
|
591
614
|
}
|
|
592
615
|
});
|
|
593
616
|
|
|
@@ -626,6 +649,14 @@ var init_i18n = __esm({
|
|
|
626
649
|
showSourceFor: (name) => `Show source code link for ${name}?`,
|
|
627
650
|
roleFor: (name) => `Your role in ${name}:`,
|
|
628
651
|
metricsFor: (name) => `Key metrics for ${name} (e.g. "1k users, $5k MRR"):`,
|
|
652
|
+
featureProjectFor: (name) => `Feature ${name} more prominently on the site?`,
|
|
653
|
+
caseStudyDetailsFor: (name) => `Add richer case-study details for ${name}?`,
|
|
654
|
+
audienceFor: (name) => `Primary audience for ${name}:`,
|
|
655
|
+
problemFor: (name) => `Problem or goal for ${name}:`,
|
|
656
|
+
solutionFor: (name) => `Your solution or key decision in ${name}:`,
|
|
657
|
+
impactFor: (name) => `Outcome / impact for ${name}:`,
|
|
658
|
+
evidenceFor: (name) => `Evidence for ${name} (comma-separated facts, proof points, links):`,
|
|
659
|
+
screenshotsFor: (name) => `Screenshot URLs for ${name} (comma-separated):`,
|
|
629
660
|
optional: "optional",
|
|
630
661
|
// Personal info
|
|
631
662
|
personalInfo: "Personal Information",
|
|
@@ -672,6 +703,8 @@ var init_i18n = __esm({
|
|
|
672
703
|
draftLoadPrompt: "Load draft? (skip re-entering previous answers)",
|
|
673
704
|
draftSaved: "Draft saved. Your progress will be restored next time.",
|
|
674
705
|
draftCleared: "Draft cleared.",
|
|
706
|
+
updateProfilePrompt: "Update profile / contact info too?",
|
|
707
|
+
reviewSectionsPrompt: "Review enabled sections too?",
|
|
675
708
|
// Roles
|
|
676
709
|
roleSolo: "Solo",
|
|
677
710
|
roleLead: "Lead",
|
|
@@ -727,6 +760,14 @@ var init_i18n = __esm({
|
|
|
727
760
|
showSourceFor: (name) => `\u662F\u5426\u5C55\u793A ${name} \u7684\u6E90\u7801\u94FE\u63A5?`,
|
|
728
761
|
roleFor: (name) => `\u4F60\u5728 ${name} \u4E2D\u7684\u89D2\u8272:`,
|
|
729
762
|
metricsFor: (name) => `${name} \u7684\u5173\u952E\u6307\u6807 (\u5982 "1k \u7528\u6237, $5k MRR"):`,
|
|
763
|
+
featureProjectFor: (name) => `\u662F\u5426\u5C06 ${name} \u8BBE\u4E3A\u91CD\u70B9\u9879\u76EE?`,
|
|
764
|
+
caseStudyDetailsFor: (name) => `\u662F\u5426\u4E3A ${name} \u8865\u5145\u66F4\u5B8C\u6574\u7684\u6848\u4F8B\u4FE1\u606F?`,
|
|
765
|
+
audienceFor: (name) => `${name} \u7684\u4E3B\u8981\u7528\u6237/\u53D7\u4F17:`,
|
|
766
|
+
problemFor: (name) => `${name} \u89E3\u51B3\u7684\u95EE\u9898\u6216\u76EE\u6807:`,
|
|
767
|
+
solutionFor: (name) => `\u4F60\u5728 ${name} \u4E2D\u7684\u65B9\u6848\u6216\u5173\u952E\u51B3\u7B56:`,
|
|
768
|
+
impactFor: (name) => `${name} \u7684\u7ED3\u679C / \u5F71\u54CD:`,
|
|
769
|
+
evidenceFor: (name) => `${name} \u7684\u8BC1\u660E\u6750\u6599 (\u9017\u53F7\u5206\u9694: \u4E8B\u5B9E\u3001\u6307\u6807\u3001\u94FE\u63A5):`,
|
|
770
|
+
screenshotsFor: (name) => `${name} \u7684\u622A\u56FE\u94FE\u63A5 (\u9017\u53F7\u5206\u9694):`,
|
|
730
771
|
optional: "\u53EF\u9009",
|
|
731
772
|
personalInfo: "\u4E2A\u4EBA\u4FE1\u606F",
|
|
732
773
|
fullName: "\u4F60\u7684\u5168\u540D:",
|
|
@@ -767,6 +808,8 @@ var init_i18n = __esm({
|
|
|
767
808
|
draftLoadPrompt: "\u662F\u5426\u52A0\u8F7D\u8349\u7A3F? (\u8DF3\u8FC7\u91CD\u590D\u586B\u5199)",
|
|
768
809
|
draftSaved: "\u8349\u7A3F\u5DF2\u4FDD\u5B58, \u4E0B\u6B21\u8FD0\u884C\u65F6\u53EF\u6062\u590D.",
|
|
769
810
|
draftCleared: "\u8349\u7A3F\u5DF2\u6E05\u9664.",
|
|
811
|
+
updateProfilePrompt: "\u662F\u5426\u540C\u65F6\u66F4\u65B0\u4E2A\u4EBA\u8D44\u6599 / \u8054\u7CFB\u65B9\u5F0F?",
|
|
812
|
+
reviewSectionsPrompt: "\u662F\u5426\u540C\u65F6\u68C0\u67E5\u542F\u7528\u7684\u9875\u9762\u533A\u5757?",
|
|
770
813
|
roleSolo: "\u72EC\u7ACB\u5B8C\u6210",
|
|
771
814
|
roleLead: "\u4E3B\u5BFC\u5F00\u53D1",
|
|
772
815
|
roleContributor: "\u53C2\u4E0E\u8D21\u732E",
|
|
@@ -861,6 +904,102 @@ var init_questions = __esm({
|
|
|
861
904
|
}
|
|
862
905
|
});
|
|
863
906
|
|
|
907
|
+
// src/spec/project-utils.ts
|
|
908
|
+
function slugify(value) {
|
|
909
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
910
|
+
}
|
|
911
|
+
function uniqueStrings(values) {
|
|
912
|
+
return [...new Set(values.filter(Boolean))];
|
|
913
|
+
}
|
|
914
|
+
function defaultCaseStudy(featured = false) {
|
|
915
|
+
return {
|
|
916
|
+
featured,
|
|
917
|
+
audience: null,
|
|
918
|
+
problem: null,
|
|
919
|
+
solution: null,
|
|
920
|
+
impact: null,
|
|
921
|
+
evidence: [],
|
|
922
|
+
screenshots: []
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
function buildTrackingSignature(projects) {
|
|
926
|
+
return projects.map((project) => `${project.localPath}:${project.lastScannedCommit || "none"}`).sort().join("|");
|
|
927
|
+
}
|
|
928
|
+
function buildMergedProjectMeta(projects, name) {
|
|
929
|
+
const techStack = uniqueStrings(projects.flatMap((project) => project.techStack));
|
|
930
|
+
const languages = {};
|
|
931
|
+
for (const project of projects) {
|
|
932
|
+
for (const [lang, count] of Object.entries(project.languages)) {
|
|
933
|
+
languages[lang] = (languages[lang] || 0) + count;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
const firstDates = projects.map((project) => project.firstCommitDate).filter(Boolean).sort();
|
|
937
|
+
const lastDates = projects.map((project) => project.lastCommitDate).filter(Boolean).sort();
|
|
938
|
+
const totalCommits = projects.reduce(
|
|
939
|
+
(sum, project) => sum + project.totalCommits,
|
|
940
|
+
0
|
|
941
|
+
);
|
|
942
|
+
const remoteUrl = projects.find((project) => project.remoteUrl)?.remoteUrl || null;
|
|
943
|
+
const demoUrl = projects.find((project) => project.demoUrl)?.demoUrl || null;
|
|
944
|
+
const readmeParts = projects.filter((project) => project.readmeContent).map((project) => `--- ${project.name} ---
|
|
945
|
+
${project.readmeContent}`);
|
|
946
|
+
const readmeContent = readmeParts.length > 0 ? readmeParts.join("\n\n") : null;
|
|
947
|
+
const descriptions = projects.map((project) => project.description).filter(Boolean);
|
|
948
|
+
return {
|
|
949
|
+
id: slugify(name),
|
|
950
|
+
name,
|
|
951
|
+
localPath: projects[0]?.localPath || "",
|
|
952
|
+
description: descriptions.join(" | "),
|
|
953
|
+
techStack,
|
|
954
|
+
languages,
|
|
955
|
+
firstCommitDate: firstDates[0] || "",
|
|
956
|
+
lastCommitDate: lastDates[lastDates.length - 1] || "",
|
|
957
|
+
totalCommits,
|
|
958
|
+
remoteUrl,
|
|
959
|
+
demoUrl,
|
|
960
|
+
readmeContent,
|
|
961
|
+
lastScannedCommit: buildTrackingSignature(projects)
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
function getTrackedProjectPaths(project) {
|
|
965
|
+
if (project.trackedProjectPaths && project.trackedProjectPaths.length > 0) {
|
|
966
|
+
return uniqueStrings(project.trackedProjectPaths);
|
|
967
|
+
}
|
|
968
|
+
if (project.children && project.children.length > 0) {
|
|
969
|
+
return uniqueStrings(project.children.map((child) => child.localPath));
|
|
970
|
+
}
|
|
971
|
+
return project.localPath ? [project.localPath] : [];
|
|
972
|
+
}
|
|
973
|
+
function createProjectEntry(meta, overrides = {}) {
|
|
974
|
+
const baseMetrics = overrides.metrics || {};
|
|
975
|
+
const baseCaseStudy = overrides.caseStudy ? {
|
|
976
|
+
...defaultCaseStudy(overrides.caseStudy.featured),
|
|
977
|
+
...overrides.caseStudy,
|
|
978
|
+
evidence: overrides.caseStudy.evidence || [],
|
|
979
|
+
screenshots: overrides.caseStudy.screenshots || []
|
|
980
|
+
} : defaultCaseStudy(false);
|
|
981
|
+
return {
|
|
982
|
+
...meta,
|
|
983
|
+
included: overrides.included ?? true,
|
|
984
|
+
overrideDescription: overrides.overrideDescription ?? null,
|
|
985
|
+
showSourceLink: overrides.showSourceLink ?? !!meta.remoteUrl,
|
|
986
|
+
role: overrides.role ?? "solo",
|
|
987
|
+
trackedProjectPaths: overrides.trackedProjectPaths && overrides.trackedProjectPaths.length > 0 ? uniqueStrings(overrides.trackedProjectPaths) : [meta.localPath],
|
|
988
|
+
metrics: {
|
|
989
|
+
...baseMetrics,
|
|
990
|
+
custom: baseMetrics.custom || void 0
|
|
991
|
+
},
|
|
992
|
+
caseStudy: baseCaseStudy,
|
|
993
|
+
children: overrides.children
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
var init_project_utils = __esm({
|
|
997
|
+
"src/spec/project-utils.ts"() {
|
|
998
|
+
"use strict";
|
|
999
|
+
init_esm_shims();
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
|
|
864
1003
|
// src/interviewer/index.ts
|
|
865
1004
|
import * as p from "@clack/prompts";
|
|
866
1005
|
function handleCancel(value) {
|
|
@@ -869,6 +1008,38 @@ function handleCancel(value) {
|
|
|
869
1008
|
process.exit(0);
|
|
870
1009
|
}
|
|
871
1010
|
}
|
|
1011
|
+
function inferOwnerName() {
|
|
1012
|
+
return process.env.SHIPFOLIO_NAME || process.env.GIT_AUTHOR_NAME || process.env.USER || process.env.USERNAME || "Portfolio Owner";
|
|
1013
|
+
}
|
|
1014
|
+
function sanitizeProjectName(value) {
|
|
1015
|
+
const sanitized = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1016
|
+
return sanitized || "my-shipfolio";
|
|
1017
|
+
}
|
|
1018
|
+
function parseCommaSeparatedList(value) {
|
|
1019
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1020
|
+
}
|
|
1021
|
+
function getAdditionalSectionOptions(projects, owner) {
|
|
1022
|
+
return SECTION_OPTIONS().filter((option) => {
|
|
1023
|
+
switch (option.value) {
|
|
1024
|
+
case "blog":
|
|
1025
|
+
return !!owner.social.blog;
|
|
1026
|
+
case "timeline":
|
|
1027
|
+
return projects.some(
|
|
1028
|
+
(project) => !!project.firstCommitDate || !!project.lastCommitDate
|
|
1029
|
+
);
|
|
1030
|
+
case "metrics":
|
|
1031
|
+
return projects.some(
|
|
1032
|
+
(project) => !!project.metrics.users || !!project.metrics.revenue || !!project.metrics.downloads || !!project.metrics.stars || !!project.metrics.custom || !!project.caseStudy.impact || project.caseStudy.evidence.length > 0
|
|
1033
|
+
);
|
|
1034
|
+
case "contact":
|
|
1035
|
+
return Boolean(
|
|
1036
|
+
owner.social.email || owner.social.github || owner.social.linkedin || owner.social.twitter || owner.social.blog
|
|
1037
|
+
);
|
|
1038
|
+
default:
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
872
1043
|
async function runMergeStep(projects) {
|
|
873
1044
|
const shouldMerge = await p.confirm({
|
|
874
1045
|
message: t().mergePrompt,
|
|
@@ -928,51 +1099,15 @@ function mergeProjects(projects, name) {
|
|
|
928
1099
|
allChildren.push(rest);
|
|
929
1100
|
}
|
|
930
1101
|
}
|
|
931
|
-
const
|
|
932
|
-
...new Set(allChildren.flatMap((p6) => p6.techStack))
|
|
933
|
-
];
|
|
934
|
-
const languages = {};
|
|
935
|
-
for (const child of allChildren) {
|
|
936
|
-
for (const [lang, count] of Object.entries(child.languages)) {
|
|
937
|
-
languages[lang] = (languages[lang] || 0) + count;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
const firstDates = allChildren.map((p6) => p6.firstCommitDate).filter(Boolean).sort();
|
|
941
|
-
const lastDates = allChildren.map((p6) => p6.lastCommitDate).filter(Boolean).sort();
|
|
942
|
-
const totalCommits = allChildren.reduce(
|
|
943
|
-
(sum, p6) => sum + p6.totalCommits,
|
|
944
|
-
0
|
|
945
|
-
);
|
|
946
|
-
const remoteUrl = allChildren.find((p6) => p6.remoteUrl)?.remoteUrl || null;
|
|
947
|
-
const demoUrl = allChildren.find((p6) => p6.demoUrl)?.demoUrl || null;
|
|
948
|
-
const readmeParts = allChildren.filter((p6) => p6.readmeContent).map(
|
|
949
|
-
(p6) => `--- ${p6.name} ---
|
|
950
|
-
${p6.readmeContent}`
|
|
951
|
-
);
|
|
952
|
-
const readmeContent = readmeParts.length > 0 ? readmeParts.join("\n\n") : null;
|
|
953
|
-
const descriptions = allChildren.map((p6) => p6.description).filter(Boolean);
|
|
954
|
-
const description = descriptions.join(" | ");
|
|
955
|
-
const localPath = allChildren[0].localPath;
|
|
956
|
-
const id = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1102
|
+
const mergedMeta = buildMergedProjectMeta(allChildren, name);
|
|
957
1103
|
return {
|
|
958
|
-
|
|
959
|
-
name,
|
|
960
|
-
localPath,
|
|
961
|
-
description,
|
|
962
|
-
techStack,
|
|
963
|
-
languages,
|
|
964
|
-
firstCommitDate: firstDates[0] || "",
|
|
965
|
-
lastCommitDate: lastDates[lastDates.length - 1] || "",
|
|
966
|
-
totalCommits,
|
|
967
|
-
remoteUrl,
|
|
968
|
-
demoUrl,
|
|
969
|
-
readmeContent,
|
|
970
|
-
lastScannedCommit: allChildren[0].lastScannedCommit || "",
|
|
1104
|
+
...mergedMeta,
|
|
971
1105
|
children: allChildren
|
|
972
1106
|
};
|
|
973
1107
|
}
|
|
974
|
-
async function runInterview(scannedProjects, availableEngines) {
|
|
1108
|
+
async function runInterview(scannedProjects, availableEngines, options = {}) {
|
|
975
1109
|
p.intro("shipfolio");
|
|
1110
|
+
const auto = options.auto === true;
|
|
976
1111
|
logger.header("Project Selection");
|
|
977
1112
|
const projectOptions = scannedProjects.map((proj) => {
|
|
978
1113
|
const techInfo = proj.techStack.slice(0, 3).join(", ");
|
|
@@ -983,23 +1118,45 @@ async function runInterview(scannedProjects, availableEngines) {
|
|
|
983
1118
|
hint: techInfo ? `${techInfo} | ${commitInfo}` : commitInfo
|
|
984
1119
|
};
|
|
985
1120
|
});
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
}
|
|
991
|
-
|
|
1121
|
+
let selectedIds;
|
|
1122
|
+
if (auto) {
|
|
1123
|
+
selectedIds = scannedProjects.map((proj) => proj.id);
|
|
1124
|
+
logger.info(`Auto mode: selected all ${selectedIds.length} scanned projects.`);
|
|
1125
|
+
} else {
|
|
1126
|
+
selectedIds = await p.multiselect({
|
|
1127
|
+
message: t().selectProjects,
|
|
1128
|
+
options: projectOptions,
|
|
1129
|
+
required: true
|
|
1130
|
+
});
|
|
1131
|
+
handleCancel(selectedIds);
|
|
1132
|
+
}
|
|
992
1133
|
let selectedMetas = selectedIds.map(
|
|
993
1134
|
(id) => scannedProjects.find((p6) => p6.id === id)
|
|
994
1135
|
);
|
|
995
|
-
if (selectedMetas.length >= 2) {
|
|
1136
|
+
if (!auto && selectedMetas.length >= 2) {
|
|
996
1137
|
selectedMetas = await runMergeStep(selectedMetas);
|
|
997
1138
|
}
|
|
1139
|
+
const autoFeaturedIds = new Set(
|
|
1140
|
+
[...selectedMetas].sort((a, b) => b.totalCommits - a.totalCommits).slice(0, 3).map((project) => project.id)
|
|
1141
|
+
);
|
|
998
1142
|
const projectEntries = [];
|
|
999
1143
|
for (const meta of selectedMetas) {
|
|
1000
1144
|
const displayName = meta.children ? `${meta.name} (${meta.children.length} sub-projects)` : meta.name;
|
|
1001
1145
|
logger.plain(`
|
|
1002
1146
|
${t().configuring(displayName)}`);
|
|
1147
|
+
if (auto) {
|
|
1148
|
+
projectEntries.push(
|
|
1149
|
+
createProjectEntry(meta, {
|
|
1150
|
+
showSourceLink: !!meta.remoteUrl,
|
|
1151
|
+
trackedProjectPaths: meta.children?.map((child) => child.localPath) || [
|
|
1152
|
+
meta.localPath
|
|
1153
|
+
],
|
|
1154
|
+
children: meta.children,
|
|
1155
|
+
caseStudy: defaultCaseStudy(autoFeaturedIds.has(meta.id))
|
|
1156
|
+
})
|
|
1157
|
+
);
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1003
1160
|
const overrideDesc = await p.text({
|
|
1004
1161
|
message: t().descriptionFor(displayName),
|
|
1005
1162
|
placeholder: meta.description?.slice(0, 80) || t().descriptionPlaceholder,
|
|
@@ -1032,119 +1189,229 @@ async function runInterview(scannedProjects, availableEngines) {
|
|
|
1032
1189
|
if (metricsInput) {
|
|
1033
1190
|
metrics.custom = { summary: metricsInput };
|
|
1034
1191
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1192
|
+
const featured = await p.confirm({
|
|
1193
|
+
message: t().featureProjectFor(displayName),
|
|
1194
|
+
initialValue: autoFeaturedIds.has(meta.id)
|
|
1195
|
+
});
|
|
1196
|
+
handleCancel(featured);
|
|
1197
|
+
const addCaseStudyDetails = await p.confirm({
|
|
1198
|
+
message: t().caseStudyDetailsFor(displayName),
|
|
1199
|
+
initialValue: false
|
|
1043
1200
|
});
|
|
1201
|
+
handleCancel(addCaseStudyDetails);
|
|
1202
|
+
let audience = null;
|
|
1203
|
+
let problem = null;
|
|
1204
|
+
let solution = null;
|
|
1205
|
+
let impact = null;
|
|
1206
|
+
let evidence = [];
|
|
1207
|
+
let screenshots = [];
|
|
1208
|
+
if (addCaseStudyDetails) {
|
|
1209
|
+
const audienceInput = await p.text({
|
|
1210
|
+
message: t().audienceFor(displayName),
|
|
1211
|
+
placeholder: t().optional,
|
|
1212
|
+
defaultValue: ""
|
|
1213
|
+
});
|
|
1214
|
+
handleCancel(audienceInput);
|
|
1215
|
+
audience = audienceInput || null;
|
|
1216
|
+
const problemInput = await p.text({
|
|
1217
|
+
message: t().problemFor(displayName),
|
|
1218
|
+
placeholder: t().optional,
|
|
1219
|
+
defaultValue: ""
|
|
1220
|
+
});
|
|
1221
|
+
handleCancel(problemInput);
|
|
1222
|
+
problem = problemInput || null;
|
|
1223
|
+
const solutionInput = await p.text({
|
|
1224
|
+
message: t().solutionFor(displayName),
|
|
1225
|
+
placeholder: t().optional,
|
|
1226
|
+
defaultValue: ""
|
|
1227
|
+
});
|
|
1228
|
+
handleCancel(solutionInput);
|
|
1229
|
+
solution = solutionInput || null;
|
|
1230
|
+
const impactInput = await p.text({
|
|
1231
|
+
message: t().impactFor(displayName),
|
|
1232
|
+
placeholder: t().optional,
|
|
1233
|
+
defaultValue: metricsInput || ""
|
|
1234
|
+
});
|
|
1235
|
+
handleCancel(impactInput);
|
|
1236
|
+
impact = impactInput || null;
|
|
1237
|
+
const evidenceInput = await p.text({
|
|
1238
|
+
message: t().evidenceFor(displayName),
|
|
1239
|
+
placeholder: t().optional,
|
|
1240
|
+
defaultValue: ""
|
|
1241
|
+
});
|
|
1242
|
+
handleCancel(evidenceInput);
|
|
1243
|
+
evidence = parseCommaSeparatedList(evidenceInput);
|
|
1244
|
+
const screenshotsInput = await p.text({
|
|
1245
|
+
message: t().screenshotsFor(displayName),
|
|
1246
|
+
placeholder: t().optional,
|
|
1247
|
+
defaultValue: ""
|
|
1248
|
+
});
|
|
1249
|
+
handleCancel(screenshotsInput);
|
|
1250
|
+
screenshots = parseCommaSeparatedList(screenshotsInput);
|
|
1251
|
+
}
|
|
1252
|
+
projectEntries.push(
|
|
1253
|
+
createProjectEntry(
|
|
1254
|
+
{
|
|
1255
|
+
...meta,
|
|
1256
|
+
demoUrl: demoUrl || meta.demoUrl
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
overrideDescription: overrideDesc || null,
|
|
1260
|
+
showSourceLink: showSource,
|
|
1261
|
+
role,
|
|
1262
|
+
metrics,
|
|
1263
|
+
trackedProjectPaths: meta.children?.map((child) => child.localPath) || [
|
|
1264
|
+
meta.localPath
|
|
1265
|
+
],
|
|
1266
|
+
children: meta.children,
|
|
1267
|
+
caseStudy: {
|
|
1268
|
+
featured,
|
|
1269
|
+
audience,
|
|
1270
|
+
problem,
|
|
1271
|
+
solution,
|
|
1272
|
+
impact,
|
|
1273
|
+
evidence,
|
|
1274
|
+
screenshots
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
)
|
|
1278
|
+
);
|
|
1044
1279
|
}
|
|
1045
1280
|
logger.header(t().personalInfo);
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
{ value: "manual", label: t().bioManual }
|
|
1061
|
-
]
|
|
1062
|
-
});
|
|
1063
|
-
handleCancel(bioChoice);
|
|
1064
|
-
let bio = "auto";
|
|
1065
|
-
if (bioChoice === "manual") {
|
|
1066
|
-
bio = await p.text({
|
|
1067
|
-
message: t().bioPrompt
|
|
1281
|
+
let owner;
|
|
1282
|
+
if (auto) {
|
|
1283
|
+
owner = {
|
|
1284
|
+
name: inferOwnerName(),
|
|
1285
|
+
tagline: t().taglinePlaceholder,
|
|
1286
|
+
bio: "auto",
|
|
1287
|
+
photoUrl: null,
|
|
1288
|
+
social: {}
|
|
1289
|
+
};
|
|
1290
|
+
logger.info(`Auto mode: using "${owner.name}" as the portfolio owner name.`);
|
|
1291
|
+
} else {
|
|
1292
|
+
const name = await p.text({
|
|
1293
|
+
message: t().fullName,
|
|
1294
|
+
validate: (v) => v.length === 0 ? t().nameRequired : void 0
|
|
1068
1295
|
});
|
|
1069
|
-
handleCancel(
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
defaultValue: ""
|
|
1090
|
-
});
|
|
1091
|
-
handleCancel(linkedin);
|
|
1092
|
-
const blogUrl = await p.text({
|
|
1093
|
-
message: t().blogUrl,
|
|
1094
|
-
defaultValue: ""
|
|
1095
|
-
});
|
|
1096
|
-
handleCancel(blogUrl);
|
|
1097
|
-
const email = await p.text({
|
|
1098
|
-
message: t().contactEmail,
|
|
1099
|
-
defaultValue: ""
|
|
1100
|
-
});
|
|
1101
|
-
handleCancel(email);
|
|
1102
|
-
const owner = {
|
|
1103
|
-
name,
|
|
1104
|
-
tagline,
|
|
1105
|
-
bio,
|
|
1106
|
-
photoUrl: photoUrl || null,
|
|
1107
|
-
social: {
|
|
1108
|
-
github: github || void 0,
|
|
1109
|
-
twitter: twitter || void 0,
|
|
1110
|
-
linkedin: linkedin || void 0,
|
|
1111
|
-
blog: blogUrl || void 0,
|
|
1112
|
-
email: email || void 0
|
|
1296
|
+
handleCancel(name);
|
|
1297
|
+
const tagline = await p.text({
|
|
1298
|
+
message: t().tagline,
|
|
1299
|
+
placeholder: t().taglinePlaceholder
|
|
1300
|
+
});
|
|
1301
|
+
handleCancel(tagline);
|
|
1302
|
+
const bioChoice = await p.select({
|
|
1303
|
+
message: t().bio,
|
|
1304
|
+
options: [
|
|
1305
|
+
{ value: "auto", label: t().bioAuto },
|
|
1306
|
+
{ value: "manual", label: t().bioManual }
|
|
1307
|
+
]
|
|
1308
|
+
});
|
|
1309
|
+
handleCancel(bioChoice);
|
|
1310
|
+
let bio = "auto";
|
|
1311
|
+
if (bioChoice === "manual") {
|
|
1312
|
+
bio = await p.text({
|
|
1313
|
+
message: t().bioPrompt
|
|
1314
|
+
});
|
|
1315
|
+
handleCancel(bio);
|
|
1113
1316
|
}
|
|
1114
|
-
|
|
1317
|
+
const photoUrl = await p.text({
|
|
1318
|
+
message: t().photoUrl,
|
|
1319
|
+
placeholder: "https://...",
|
|
1320
|
+
defaultValue: ""
|
|
1321
|
+
});
|
|
1322
|
+
handleCancel(photoUrl);
|
|
1323
|
+
const github = await p.text({
|
|
1324
|
+
message: t().githubUser,
|
|
1325
|
+
defaultValue: ""
|
|
1326
|
+
});
|
|
1327
|
+
handleCancel(github);
|
|
1328
|
+
const twitter = await p.text({
|
|
1329
|
+
message: t().twitterHandle,
|
|
1330
|
+
defaultValue: ""
|
|
1331
|
+
});
|
|
1332
|
+
handleCancel(twitter);
|
|
1333
|
+
const linkedin = await p.text({
|
|
1334
|
+
message: t().linkedinUrl,
|
|
1335
|
+
defaultValue: ""
|
|
1336
|
+
});
|
|
1337
|
+
handleCancel(linkedin);
|
|
1338
|
+
const blogUrl = await p.text({
|
|
1339
|
+
message: t().blogUrl,
|
|
1340
|
+
defaultValue: ""
|
|
1341
|
+
});
|
|
1342
|
+
handleCancel(blogUrl);
|
|
1343
|
+
const email = await p.text({
|
|
1344
|
+
message: t().contactEmail,
|
|
1345
|
+
defaultValue: ""
|
|
1346
|
+
});
|
|
1347
|
+
handleCancel(email);
|
|
1348
|
+
owner = {
|
|
1349
|
+
name,
|
|
1350
|
+
tagline,
|
|
1351
|
+
bio,
|
|
1352
|
+
photoUrl: photoUrl || null,
|
|
1353
|
+
social: {
|
|
1354
|
+
github: github || void 0,
|
|
1355
|
+
twitter: twitter || void 0,
|
|
1356
|
+
linkedin: linkedin || void 0,
|
|
1357
|
+
blog: blogUrl || void 0,
|
|
1358
|
+
email: email || void 0
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1115
1362
|
logger.header(t().designPrefs);
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
options
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
]
|
|
1127
|
-
});
|
|
1128
|
-
handleCancel(accentColor);
|
|
1129
|
-
let finalAccent = accentColor;
|
|
1130
|
-
if (accentColor === "custom") {
|
|
1131
|
-
finalAccent = await p.text({
|
|
1132
|
-
message: t().customHexPrompt,
|
|
1133
|
-
placeholder: "#7c3aed",
|
|
1134
|
-
validate: (v) => /^#[0-9a-fA-F]{6}$/.test(v) ? void 0 : t().invalidHex
|
|
1363
|
+
let theme;
|
|
1364
|
+
if (options.style) {
|
|
1365
|
+
theme = options.style;
|
|
1366
|
+
logger.info(`Theme preset: ${theme}`);
|
|
1367
|
+
} else if (auto) {
|
|
1368
|
+
theme = "dark-minimal";
|
|
1369
|
+
} else {
|
|
1370
|
+
theme = await p.select({
|
|
1371
|
+
message: t().theme,
|
|
1372
|
+
options: THEME_OPTIONS()
|
|
1135
1373
|
});
|
|
1136
|
-
handleCancel(
|
|
1374
|
+
handleCancel(theme);
|
|
1375
|
+
}
|
|
1376
|
+
let finalAccent;
|
|
1377
|
+
if (options.accent) {
|
|
1378
|
+
finalAccent = options.accent;
|
|
1379
|
+
logger.info(`Accent preset: ${finalAccent}`);
|
|
1380
|
+
} else if (auto) {
|
|
1381
|
+
finalAccent = "#7c3aed";
|
|
1382
|
+
} else {
|
|
1383
|
+
const accentColor = await p.select({
|
|
1384
|
+
message: t().accentColor,
|
|
1385
|
+
options: [
|
|
1386
|
+
...DEFAULT_ACCENT_COLORS(),
|
|
1387
|
+
{ value: "custom", label: t().customHex }
|
|
1388
|
+
]
|
|
1389
|
+
});
|
|
1390
|
+
handleCancel(accentColor);
|
|
1391
|
+
finalAccent = accentColor;
|
|
1392
|
+
if (accentColor === "custom") {
|
|
1393
|
+
finalAccent = await p.text({
|
|
1394
|
+
message: t().customHexPrompt,
|
|
1395
|
+
placeholder: "#7c3aed",
|
|
1396
|
+
validate: (v) => /^#[0-9a-fA-F]{6}$/.test(v) ? void 0 : t().invalidHex
|
|
1397
|
+
});
|
|
1398
|
+
handleCancel(finalAccent);
|
|
1399
|
+
}
|
|
1137
1400
|
}
|
|
1138
|
-
const font = await p.select({
|
|
1401
|
+
const font = auto ? "Inter" : await p.select({
|
|
1139
1402
|
message: t().font,
|
|
1140
1403
|
options: FONT_OPTIONS()
|
|
1141
1404
|
});
|
|
1142
|
-
|
|
1143
|
-
|
|
1405
|
+
if (!auto) {
|
|
1406
|
+
handleCancel(font);
|
|
1407
|
+
}
|
|
1408
|
+
const animationLevel = auto ? "subtle" : await p.select({
|
|
1144
1409
|
message: t().animationLevel,
|
|
1145
1410
|
options: ANIMATION_OPTIONS()
|
|
1146
1411
|
});
|
|
1147
|
-
|
|
1412
|
+
if (!auto) {
|
|
1413
|
+
handleCancel(animationLevel);
|
|
1414
|
+
}
|
|
1148
1415
|
const style = {
|
|
1149
1416
|
theme,
|
|
1150
1417
|
accentColor: finalAccent,
|
|
@@ -1152,20 +1419,34 @@ async function runInterview(scannedProjects, availableEngines) {
|
|
|
1152
1419
|
animationLevel
|
|
1153
1420
|
};
|
|
1154
1421
|
logger.header(t().sections);
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1422
|
+
let sections;
|
|
1423
|
+
if (auto) {
|
|
1424
|
+
sections = ["hero", "projects", "about", "contact"];
|
|
1425
|
+
} else {
|
|
1426
|
+
const sectionOptions = getAdditionalSectionOptions(projectEntries, owner);
|
|
1427
|
+
const additionalSections = sectionOptions.length > 0 ? await p.multiselect({
|
|
1428
|
+
message: t().additionalSections,
|
|
1429
|
+
options: sectionOptions,
|
|
1430
|
+
required: false
|
|
1431
|
+
}) : [];
|
|
1432
|
+
if (sectionOptions.length > 0) {
|
|
1433
|
+
handleCancel(additionalSections);
|
|
1434
|
+
}
|
|
1435
|
+
sections = [
|
|
1436
|
+
"hero",
|
|
1437
|
+
"projects",
|
|
1438
|
+
...additionalSections
|
|
1439
|
+
];
|
|
1440
|
+
}
|
|
1166
1441
|
logger.header(t().aiEngine);
|
|
1167
1442
|
let engine;
|
|
1168
|
-
if (
|
|
1443
|
+
if (options.engine && availableEngines.includes(options.engine)) {
|
|
1444
|
+
engine = options.engine;
|
|
1445
|
+
logger.info(t().engineUsing(engine));
|
|
1446
|
+
} else if (availableEngines.length === 1) {
|
|
1447
|
+
engine = availableEngines[0];
|
|
1448
|
+
logger.info(t().engineUsing(engine));
|
|
1449
|
+
} else if (auto) {
|
|
1169
1450
|
engine = availableEngines[0];
|
|
1170
1451
|
logger.info(t().engineUsing(engine));
|
|
1171
1452
|
} else {
|
|
@@ -1179,26 +1460,38 @@ async function runInterview(scannedProjects, availableEngines) {
|
|
|
1179
1460
|
handleCancel(engine);
|
|
1180
1461
|
}
|
|
1181
1462
|
logger.header(t().deployment);
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
options
|
|
1185
|
-
|
|
1186
|
-
|
|
1463
|
+
let deployPlatform;
|
|
1464
|
+
if (options.deploy) {
|
|
1465
|
+
deployPlatform = options.deploy;
|
|
1466
|
+
logger.info(`Deploy preset: ${deployPlatform}`);
|
|
1467
|
+
} else if (auto) {
|
|
1468
|
+
deployPlatform = "local";
|
|
1469
|
+
} else {
|
|
1470
|
+
deployPlatform = await p.select({
|
|
1471
|
+
message: t().deployTo,
|
|
1472
|
+
options: DEPLOY_OPTIONS()
|
|
1473
|
+
});
|
|
1474
|
+
handleCancel(deployPlatform);
|
|
1475
|
+
}
|
|
1187
1476
|
let projectName = "";
|
|
1188
1477
|
let customDomain = "";
|
|
1189
1478
|
if (deployPlatform !== "local") {
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1479
|
+
if (auto) {
|
|
1480
|
+
projectName = sanitizeProjectName(`${owner.name}-shipfolio`);
|
|
1481
|
+
} else {
|
|
1482
|
+
projectName = await p.text({
|
|
1483
|
+
message: t().projectNamePrompt,
|
|
1484
|
+
placeholder: "my-shipfolio",
|
|
1485
|
+
validate: (v) => /^[a-z0-9-]+$/.test(v) ? void 0 : t().projectNameInvalid
|
|
1486
|
+
});
|
|
1487
|
+
handleCancel(projectName);
|
|
1488
|
+
customDomain = await p.text({
|
|
1489
|
+
message: t().customDomainPrompt,
|
|
1490
|
+
placeholder: "portfolio.example.com",
|
|
1491
|
+
defaultValue: ""
|
|
1492
|
+
});
|
|
1493
|
+
handleCancel(customDomain);
|
|
1494
|
+
}
|
|
1202
1495
|
}
|
|
1203
1496
|
p.outro(t().configComplete);
|
|
1204
1497
|
return {
|
|
@@ -1221,13 +1514,121 @@ var init_interviewer = __esm({
|
|
|
1221
1514
|
init_questions();
|
|
1222
1515
|
init_logger();
|
|
1223
1516
|
init_i18n();
|
|
1517
|
+
init_project_utils();
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
// src/spec/schema.ts
|
|
1522
|
+
import { z } from "zod";
|
|
1523
|
+
function parseShipfolioSpec(input) {
|
|
1524
|
+
return shipfolioSpecSchema.parse(input);
|
|
1525
|
+
}
|
|
1526
|
+
function parseShipfolioConfig(input) {
|
|
1527
|
+
return shipfolioConfigSchema.parse(input);
|
|
1528
|
+
}
|
|
1529
|
+
var sectionIdSchema, engineTypeSchema, deployPlatformSchema, styleConfigSchema, projectMetaSchema, projectCaseStudySchema, ownerInfoSchema, projectEntrySchema, deployConfigSchema, shipfolioSpecSchema, shipfolioConfigSchema;
|
|
1530
|
+
var init_schema = __esm({
|
|
1531
|
+
"src/spec/schema.ts"() {
|
|
1532
|
+
"use strict";
|
|
1533
|
+
init_esm_shims();
|
|
1534
|
+
sectionIdSchema = z.enum([
|
|
1535
|
+
"hero",
|
|
1536
|
+
"projects",
|
|
1537
|
+
"skills",
|
|
1538
|
+
"about",
|
|
1539
|
+
"timeline",
|
|
1540
|
+
"blog",
|
|
1541
|
+
"metrics",
|
|
1542
|
+
"contact"
|
|
1543
|
+
]);
|
|
1544
|
+
engineTypeSchema = z.enum(["claude", "codex", "v0"]);
|
|
1545
|
+
deployPlatformSchema = z.enum(["cloudflare", "vercel", "local"]);
|
|
1546
|
+
styleConfigSchema = z.object({
|
|
1547
|
+
theme: z.enum(["dark-minimal", "light-clean", "monochrome", "custom"]),
|
|
1548
|
+
accentColor: z.string(),
|
|
1549
|
+
font: z.string(),
|
|
1550
|
+
animationLevel: z.enum(["none", "subtle", "moderate"])
|
|
1551
|
+
});
|
|
1552
|
+
projectMetaSchema = z.object({
|
|
1553
|
+
id: z.string(),
|
|
1554
|
+
name: z.string(),
|
|
1555
|
+
localPath: z.string(),
|
|
1556
|
+
description: z.string(),
|
|
1557
|
+
techStack: z.array(z.string()),
|
|
1558
|
+
languages: z.record(z.number()),
|
|
1559
|
+
firstCommitDate: z.string(),
|
|
1560
|
+
lastCommitDate: z.string(),
|
|
1561
|
+
totalCommits: z.number().int().nonnegative(),
|
|
1562
|
+
remoteUrl: z.string().nullable(),
|
|
1563
|
+
demoUrl: z.string().nullable(),
|
|
1564
|
+
readmeContent: z.string().nullable(),
|
|
1565
|
+
lastScannedCommit: z.string()
|
|
1566
|
+
});
|
|
1567
|
+
projectCaseStudySchema = z.object({
|
|
1568
|
+
featured: z.boolean(),
|
|
1569
|
+
audience: z.string().nullable(),
|
|
1570
|
+
problem: z.string().nullable(),
|
|
1571
|
+
solution: z.string().nullable(),
|
|
1572
|
+
impact: z.string().nullable(),
|
|
1573
|
+
evidence: z.array(z.string()),
|
|
1574
|
+
screenshots: z.array(z.string())
|
|
1575
|
+
});
|
|
1576
|
+
ownerInfoSchema = z.object({
|
|
1577
|
+
name: z.string(),
|
|
1578
|
+
tagline: z.string(),
|
|
1579
|
+
bio: z.union([z.literal("auto"), z.string()]),
|
|
1580
|
+
photoUrl: z.string().nullable(),
|
|
1581
|
+
social: z.object({
|
|
1582
|
+
github: z.string().optional(),
|
|
1583
|
+
twitter: z.string().optional(),
|
|
1584
|
+
linkedin: z.string().optional(),
|
|
1585
|
+
blog: z.string().optional(),
|
|
1586
|
+
email: z.string().optional()
|
|
1587
|
+
})
|
|
1588
|
+
});
|
|
1589
|
+
projectEntrySchema = projectMetaSchema.extend({
|
|
1590
|
+
included: z.boolean(),
|
|
1591
|
+
overrideDescription: z.string().nullable(),
|
|
1592
|
+
showSourceLink: z.boolean(),
|
|
1593
|
+
role: z.enum(["solo", "lead", "contributor"]),
|
|
1594
|
+
trackedProjectPaths: z.array(z.string()),
|
|
1595
|
+
metrics: z.object({
|
|
1596
|
+
users: z.string().optional(),
|
|
1597
|
+
revenue: z.string().optional(),
|
|
1598
|
+
downloads: z.string().optional(),
|
|
1599
|
+
stars: z.number().int().optional(),
|
|
1600
|
+
custom: z.record(z.string()).optional()
|
|
1601
|
+
}),
|
|
1602
|
+
caseStudy: projectCaseStudySchema,
|
|
1603
|
+
children: z.array(projectMetaSchema).optional()
|
|
1604
|
+
});
|
|
1605
|
+
deployConfigSchema = z.object({
|
|
1606
|
+
platform: deployPlatformSchema,
|
|
1607
|
+
projectName: z.string(),
|
|
1608
|
+
customDomain: z.string().optional(),
|
|
1609
|
+
url: z.string().optional()
|
|
1610
|
+
});
|
|
1611
|
+
shipfolioSpecSchema = z.object({
|
|
1612
|
+
version: z.string(),
|
|
1613
|
+
generatedAt: z.string(),
|
|
1614
|
+
engine: engineTypeSchema,
|
|
1615
|
+
framework: z.literal("next"),
|
|
1616
|
+
style: styleConfigSchema,
|
|
1617
|
+
owner: ownerInfoSchema,
|
|
1618
|
+
projects: z.array(projectEntrySchema),
|
|
1619
|
+
sections: z.array(sectionIdSchema),
|
|
1620
|
+
deploy: deployConfigSchema
|
|
1621
|
+
});
|
|
1622
|
+
shipfolioConfigSchema = shipfolioSpecSchema.extend({
|
|
1623
|
+
sitePath: z.string()
|
|
1624
|
+
});
|
|
1224
1625
|
}
|
|
1225
1626
|
});
|
|
1226
1627
|
|
|
1227
1628
|
// src/spec/builder.ts
|
|
1228
1629
|
function buildSpec(interview) {
|
|
1229
|
-
return {
|
|
1230
|
-
version: "1.
|
|
1630
|
+
return parseShipfolioSpec({
|
|
1631
|
+
version: "1.1.0",
|
|
1231
1632
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1232
1633
|
engine: interview.engine,
|
|
1233
1634
|
framework: "next",
|
|
@@ -1240,208 +1641,114 @@ function buildSpec(interview) {
|
|
|
1240
1641
|
projectName: interview.deploy.projectName,
|
|
1241
1642
|
customDomain: interview.deploy.customDomain
|
|
1242
1643
|
}
|
|
1243
|
-
};
|
|
1644
|
+
});
|
|
1244
1645
|
}
|
|
1245
1646
|
var init_builder = __esm({
|
|
1246
1647
|
"src/spec/builder.ts"() {
|
|
1247
1648
|
"use strict";
|
|
1248
1649
|
init_esm_shims();
|
|
1650
|
+
init_schema();
|
|
1249
1651
|
}
|
|
1250
1652
|
});
|
|
1251
1653
|
|
|
1252
1654
|
// src/orchestrator/prompt-builder.ts
|
|
1655
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1656
|
+
async function loadPromptTemplate(filename) {
|
|
1657
|
+
const cached = promptTemplateCache.get(filename);
|
|
1658
|
+
if (cached) {
|
|
1659
|
+
return cached;
|
|
1660
|
+
}
|
|
1661
|
+
const candidates = [
|
|
1662
|
+
fileURLToPath2(new URL(`../prompts/${filename}`, import.meta.url)),
|
|
1663
|
+
fileURLToPath2(new URL(`../../prompts/${filename}`, import.meta.url)),
|
|
1664
|
+
fileURLToPath2(new URL(`../../../prompts/${filename}`, import.meta.url))
|
|
1665
|
+
];
|
|
1666
|
+
for (const path2 of candidates) {
|
|
1667
|
+
if (await fileExists(path2)) {
|
|
1668
|
+
const template = await readText(path2);
|
|
1669
|
+
promptTemplateCache.set(filename, template);
|
|
1670
|
+
return template;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
throw new Error(`Prompt template not found: ${filename}`);
|
|
1674
|
+
}
|
|
1675
|
+
function fillTemplate(template, replacements) {
|
|
1676
|
+
return Object.entries(replacements).reduce((text4, [key, value]) => {
|
|
1677
|
+
return text4.split(`{{${key}}}`).join(value);
|
|
1678
|
+
}, template);
|
|
1679
|
+
}
|
|
1253
1680
|
async function buildFreshPrompt(spec) {
|
|
1254
|
-
|
|
1255
|
-
const sectionsText = spec.sections.map((
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1681
|
+
const template = await loadPromptTemplate("fresh-build.md");
|
|
1682
|
+
const sectionsText = spec.sections.map((section) => `- ${section}`).join("\n");
|
|
1683
|
+
return fillTemplate(template, {
|
|
1684
|
+
SPEC_JSON: JSON.stringify(spec, null, 2),
|
|
1685
|
+
THEME: spec.style.theme,
|
|
1686
|
+
ACCENT_COLOR: spec.style.accentColor,
|
|
1687
|
+
ANIMATION_LEVEL: spec.style.animationLevel,
|
|
1688
|
+
FONT: spec.style.font,
|
|
1689
|
+
SECTIONS_LIST: sectionsText
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
async function buildUpdatePrompt(existingConfig, diff, preparedNewProjects = [], personalInfoDiff = "No changes") {
|
|
1693
|
+
const template = await loadPromptTemplate("update.md");
|
|
1694
|
+
const newProjects = preparedNewProjects.length > 0 ? preparedNewProjects : diff.newProjects.map((project) => ({
|
|
1695
|
+
...project,
|
|
1696
|
+
included: true,
|
|
1697
|
+
overrideDescription: null,
|
|
1698
|
+
showSourceLink: !!project.remoteUrl,
|
|
1699
|
+
role: "solo",
|
|
1700
|
+
trackedProjectPaths: [project.localPath],
|
|
1701
|
+
metrics: {},
|
|
1702
|
+
caseStudy: {
|
|
1703
|
+
featured: false,
|
|
1704
|
+
audience: null,
|
|
1705
|
+
problem: null,
|
|
1706
|
+
solution: null,
|
|
1707
|
+
impact: null,
|
|
1708
|
+
evidence: [],
|
|
1709
|
+
screenshots: []
|
|
1710
|
+
}
|
|
1711
|
+
}));
|
|
1712
|
+
const newProjectsText = newProjects.length > 0 ? newProjects.map(
|
|
1713
|
+
(project) => `- ${project.name}: ${project.overrideDescription || project.description}
|
|
1714
|
+
Tech: ${project.techStack.join(", ")}
|
|
1715
|
+
Demo: ${project.demoUrl || "none"}
|
|
1716
|
+
Featured: ${project.caseStudy.featured}
|
|
1717
|
+
Case study: ${JSON.stringify(project.caseStudy)}
|
|
1718
|
+
README excerpt: ${(project.readmeContent || "").slice(0, 500)}`
|
|
1265
1719
|
).join("\n\n") : "None";
|
|
1266
1720
|
const updatedProjectsText = diff.updatedProjects.length > 0 ? diff.updatedProjects.map(
|
|
1267
|
-
(
|
|
1268
|
-
README changed: ${
|
|
1269
|
-
Dependencies changed: ${
|
|
1721
|
+
(update) => `- ${update.project.name}: ${update.newCommits} new commits
|
|
1722
|
+
README changed: ${update.readmeChanged}
|
|
1723
|
+
Dependencies changed: ${update.depsChanged}
|
|
1724
|
+
Changed paths: ${update.changedPaths.join(", ") || "none"}
|
|
1725
|
+
Removed paths: ${update.removedPaths.join(", ") || "none"}
|
|
1726
|
+
Case study: ${JSON.stringify(update.project.caseStudy)}`
|
|
1270
1727
|
).join("\n\n") : "None";
|
|
1271
|
-
const removedProjectsText = diff.removedProjects.length > 0 ? diff.removedProjects.map((
|
|
1272
|
-
template
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1728
|
+
const removedProjectsText = diff.removedProjects.length > 0 ? diff.removedProjects.map((project) => `- ${project.name}`).join("\n") : "None";
|
|
1729
|
+
return fillTemplate(template, {
|
|
1730
|
+
EXISTING_CONFIG_JSON: JSON.stringify(existingConfig, null, 2),
|
|
1731
|
+
NEW_PROJECTS: newProjectsText,
|
|
1732
|
+
UPDATED_PROJECTS: updatedProjectsText,
|
|
1733
|
+
REMOVED_PROJECTS: removedProjectsText,
|
|
1734
|
+
PERSONAL_INFO_DIFF: personalInfoDiff
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
var promptTemplateCache;
|
|
1279
1738
|
var init_prompt_builder = __esm({
|
|
1280
1739
|
"src/orchestrator/prompt-builder.ts"() {
|
|
1281
1740
|
"use strict";
|
|
1282
1741
|
init_esm_shims();
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
Generate a complete, production-ready personal portfolio website.
|
|
1286
|
-
All data and design preferences are provided in the spec below.
|
|
1287
|
-
Output a fully working Next.js project that builds and deploys as a static site.
|
|
1288
|
-
|
|
1289
|
-
# Spec
|
|
1290
|
-
|
|
1291
|
-
{{SPEC_JSON}}
|
|
1292
|
-
|
|
1293
|
-
# Technical Requirements
|
|
1294
|
-
|
|
1295
|
-
- Next.js 15 with App Router and TypeScript
|
|
1296
|
-
- Tailwind CSS v4 for styling
|
|
1297
|
-
- shadcn/ui components (initialize with \`npx shadcn@latest init --yes\`)
|
|
1298
|
-
- Static export: set \`output: 'export'\` in next.config.ts
|
|
1299
|
-
- Build command: \`npm run build\` producing \`out/\` directory
|
|
1300
|
-
- Zero external API calls at runtime -- all data is embedded in source
|
|
1301
|
-
- All project data in \`src/data/projects.ts\` as typed constants
|
|
1302
|
-
- Include \`@media print\` stylesheet in \`src/app/globals.css\` for PDF export
|
|
1303
|
-
- Responsive: mobile-first with Tailwind breakpoints (sm, md, lg, xl)
|
|
1304
|
-
- Lighthouse performance score 95+
|
|
1305
|
-
- Semantic HTML with ARIA labels and keyboard navigation
|
|
1306
|
-
- No emoji anywhere in text, UI, code, or comments
|
|
1307
|
-
- Use lucide-react for icons
|
|
1308
|
-
|
|
1309
|
-
# Design Direction
|
|
1310
|
-
|
|
1311
|
-
- Theme: {{THEME}}
|
|
1312
|
-
- Accent color: {{ACCENT_COLOR}}
|
|
1313
|
-
- Animation level: {{ANIMATION_LEVEL}}
|
|
1314
|
-
- Font: {{FONT}}
|
|
1315
|
-
|
|
1316
|
-
Design guidelines:
|
|
1317
|
-
- Typography-driven layout with large, bold headings
|
|
1318
|
-
- Generous whitespace between sections
|
|
1319
|
-
- Single accent color for interactive elements and highlights only
|
|
1320
|
-
- Project cards should feel substantial but clean
|
|
1321
|
-
- The site must look hand-crafted, not template-generated
|
|
1322
|
-
- Dark themes: use zinc/slate backgrounds, not pure black
|
|
1323
|
-
- Light themes: use warm whites and subtle grays
|
|
1324
|
-
|
|
1325
|
-
# Content Generation
|
|
1326
|
-
|
|
1327
|
-
For each project in the spec:
|
|
1328
|
-
- Write a 2-3 sentence narrative description based on the README content and tech stack
|
|
1329
|
-
- Focus on what it does and why it matters
|
|
1330
|
-
- If user provided an override description, use that instead
|
|
1331
|
-
- Maintain consistent voice across all descriptions
|
|
1332
|
-
|
|
1333
|
-
For the bio (if set to "auto"):
|
|
1334
|
-
- Generate a professional, authentic bio based on the project portfolio
|
|
1335
|
-
- Emphasize shipping velocity and breadth
|
|
1336
|
-
- Tone: confident, direct, no buzzwords or fluff
|
|
1337
|
-
|
|
1338
|
-
# Sections to Include
|
|
1339
|
-
|
|
1340
|
-
{{SECTIONS_LIST}}
|
|
1341
|
-
|
|
1342
|
-
Hero and Projects are always included. Additional sections as specified.
|
|
1343
|
-
|
|
1344
|
-
# Print / PDF Styles
|
|
1345
|
-
|
|
1346
|
-
In globals.css, add @media print rules:
|
|
1347
|
-
- Single-column layout
|
|
1348
|
-
- Hide navigation, footer, animations
|
|
1349
|
-
- Preserve background colors (user will print with backgrounds enabled)
|
|
1350
|
-
- Show link URLs inline: \`a[href]::after { content: " (" attr(href) ")"; }\`
|
|
1351
|
-
- Avoid page breaks inside project cards
|
|
1352
|
-
- A4-friendly margins and font sizes
|
|
1353
|
-
|
|
1354
|
-
# File Structure to Generate
|
|
1355
|
-
|
|
1356
|
-
\`\`\`
|
|
1357
|
-
next.config.ts
|
|
1358
|
-
package.json
|
|
1359
|
-
tailwind.config.ts
|
|
1360
|
-
tsconfig.json
|
|
1361
|
-
postcss.config.mjs
|
|
1362
|
-
components.json
|
|
1363
|
-
src/app/layout.tsx
|
|
1364
|
-
src/app/page.tsx
|
|
1365
|
-
src/app/globals.css
|
|
1366
|
-
src/components/ui/ (shadcn components as needed)
|
|
1367
|
-
src/components/hero.tsx
|
|
1368
|
-
src/components/project-card.tsx
|
|
1369
|
-
src/components/project-grid.tsx
|
|
1370
|
-
src/components/skills.tsx
|
|
1371
|
-
src/components/about.tsx
|
|
1372
|
-
src/components/timeline.tsx
|
|
1373
|
-
src/components/contact.tsx
|
|
1374
|
-
src/components/navigation.tsx
|
|
1375
|
-
src/components/footer.tsx
|
|
1376
|
-
src/data/projects.ts
|
|
1377
|
-
src/data/owner.ts
|
|
1378
|
-
src/lib/utils.ts
|
|
1379
|
-
public/favicon.svg
|
|
1380
|
-
shipfolio.config.json
|
|
1381
|
-
\`\`\`
|
|
1382
|
-
|
|
1383
|
-
# Important
|
|
1384
|
-
|
|
1385
|
-
- Generate ALL files needed for the project to build successfully
|
|
1386
|
-
- Include all shadcn/ui component files that are referenced
|
|
1387
|
-
- The \`package.json\` must include all dependencies
|
|
1388
|
-
- \`npm install && npm run build\` must succeed without errors
|
|
1389
|
-
- Do not use next/image (incompatible with static export) -- use standard <img> tags
|
|
1390
|
-
- Do not use features that require a server (API routes, middleware, ISR)
|
|
1391
|
-
`;
|
|
1392
|
-
UPDATE_TEMPLATE = `# Task
|
|
1393
|
-
|
|
1394
|
-
Update an existing portfolio website previously generated by shipfolio.
|
|
1395
|
-
You must preserve the existing design system, layout structure, component
|
|
1396
|
-
architecture, and any custom modifications the user has made.
|
|
1397
|
-
|
|
1398
|
-
# Existing Site Configuration
|
|
1399
|
-
|
|
1400
|
-
{{EXISTING_CONFIG_JSON}}
|
|
1401
|
-
|
|
1402
|
-
# Changes to Apply
|
|
1403
|
-
|
|
1404
|
-
## New Projects to Add
|
|
1405
|
-
{{NEW_PROJECTS}}
|
|
1406
|
-
|
|
1407
|
-
## Projects to Update (new commits since last scan)
|
|
1408
|
-
{{UPDATED_PROJECTS}}
|
|
1409
|
-
|
|
1410
|
-
## Projects to Remove
|
|
1411
|
-
{{REMOVED_PROJECTS}}
|
|
1412
|
-
|
|
1413
|
-
## Updated Personal Info
|
|
1414
|
-
{{PERSONAL_INFO_DIFF}}
|
|
1415
|
-
|
|
1416
|
-
# Rules
|
|
1417
|
-
|
|
1418
|
-
1. Do NOT change the overall layout, color scheme, or design system
|
|
1419
|
-
2. Do NOT reorganize existing components or rename files
|
|
1420
|
-
3. Only modify files that need changes for the specified updates
|
|
1421
|
-
4. For new projects: follow the exact same card format and component pattern
|
|
1422
|
-
as existing project cards
|
|
1423
|
-
5. Preserve all custom CSS, custom components, and manual edits
|
|
1424
|
-
6. Update src/data/projects.ts with new/changed/removed project data
|
|
1425
|
-
7. Update src/data/owner.ts if personal info changed
|
|
1426
|
-
8. Update shipfolio.config.json with new timestamps and project list
|
|
1427
|
-
9. If a new section type is needed, create it following the existing
|
|
1428
|
-
component patterns and design tokens in the codebase
|
|
1429
|
-
10. No emoji anywhere
|
|
1430
|
-
|
|
1431
|
-
# Technical Notes
|
|
1432
|
-
|
|
1433
|
-
- The site uses Next.js 15 + Tailwind CSS + shadcn/ui
|
|
1434
|
-
- Static export via \`output: 'export'\` in next.config.ts
|
|
1435
|
-
- Do not break the build -- \`npm run build\` must succeed
|
|
1436
|
-
- Do not add new dependencies unless absolutely necessary
|
|
1437
|
-
- Keep the existing @media print styles working
|
|
1438
|
-
`;
|
|
1742
|
+
init_fs();
|
|
1743
|
+
promptTemplateCache = /* @__PURE__ */ new Map();
|
|
1439
1744
|
}
|
|
1440
1745
|
});
|
|
1441
1746
|
|
|
1442
1747
|
// src/utils/exec.ts
|
|
1443
|
-
import {
|
|
1444
|
-
|
|
1748
|
+
import {
|
|
1749
|
+
execa
|
|
1750
|
+
} from "execa";
|
|
1751
|
+
function run(command, args, options) {
|
|
1445
1752
|
return execa(command, args, {
|
|
1446
1753
|
stdio: "pipe",
|
|
1447
1754
|
...options
|
|
@@ -1460,7 +1767,16 @@ async function runWithOutput(command, args, options) {
|
|
|
1460
1767
|
stdio: "pipe",
|
|
1461
1768
|
...options
|
|
1462
1769
|
});
|
|
1463
|
-
|
|
1770
|
+
if (typeof result.stdout === "string") {
|
|
1771
|
+
return result.stdout;
|
|
1772
|
+
}
|
|
1773
|
+
if (result.stdout instanceof Uint8Array) {
|
|
1774
|
+
return Buffer.from(result.stdout).toString("utf-8");
|
|
1775
|
+
}
|
|
1776
|
+
if (Array.isArray(result.stdout)) {
|
|
1777
|
+
return result.stdout.join("\n");
|
|
1778
|
+
}
|
|
1779
|
+
return "";
|
|
1464
1780
|
}
|
|
1465
1781
|
var init_exec = __esm({
|
|
1466
1782
|
"src/utils/exec.ts"() {
|
|
@@ -1537,9 +1853,11 @@ async function generateWithCodex(prompt, outputDir) {
|
|
|
1537
1853
|
const fullPrompt = `${prompt}
|
|
1538
1854
|
|
|
1539
1855
|
Create all files in the current working directory. Do not ask questions, just generate all files.`;
|
|
1540
|
-
await execa3("codex", ["
|
|
1856
|
+
await execa3("codex", ["exec", "--full-auto", "--skip-git-repo-check", fullPrompt], {
|
|
1541
1857
|
cwd: outputDir,
|
|
1542
|
-
|
|
1858
|
+
stdin: "ignore",
|
|
1859
|
+
stdout: "ignore",
|
|
1860
|
+
stderr: "pipe",
|
|
1543
1861
|
timeout: 6e5
|
|
1544
1862
|
});
|
|
1545
1863
|
spinner.succeed("Site generated with Codex");
|
|
@@ -1558,7 +1876,7 @@ var init_codex = __esm({
|
|
|
1558
1876
|
// src/orchestrator/engines/v0.ts
|
|
1559
1877
|
import OpenAI from "openai";
|
|
1560
1878
|
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
1561
|
-
import { join as join2, dirname } from "path";
|
|
1879
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
1562
1880
|
import ora4 from "ora";
|
|
1563
1881
|
async function generateWithV0(prompt, outputDir, apiKey) {
|
|
1564
1882
|
const spinner = ora4("Generating site with v0...").start();
|
|
@@ -1592,7 +1910,7 @@ async function generateWithV0(prompt, outputDir, apiKey) {
|
|
|
1592
1910
|
spinner.text = `Writing ${files.length} files...`;
|
|
1593
1911
|
for (const file of files) {
|
|
1594
1912
|
const filePath = join2(outputDir, file.filename);
|
|
1595
|
-
await mkdir2(
|
|
1913
|
+
await mkdir2(dirname2(filePath), { recursive: true });
|
|
1596
1914
|
await writeFile2(filePath, file.content, "utf-8");
|
|
1597
1915
|
}
|
|
1598
1916
|
const hasPkgJson = files.some((f) => f.filename === "package.json");
|
|
@@ -2310,6 +2628,44 @@ var init_draft = __esm({
|
|
|
2310
2628
|
}
|
|
2311
2629
|
});
|
|
2312
2630
|
|
|
2631
|
+
// src/spec/io.ts
|
|
2632
|
+
import { ZodError } from "zod";
|
|
2633
|
+
function formatValidationError(error) {
|
|
2634
|
+
return error.issues.map((issue) => {
|
|
2635
|
+
const path2 = issue.path.length > 0 ? issue.path.join(".") : "<root>";
|
|
2636
|
+
return `${path2}: ${issue.message}`;
|
|
2637
|
+
}).join("; ");
|
|
2638
|
+
}
|
|
2639
|
+
function toValidationError(label, error) {
|
|
2640
|
+
if (error instanceof ZodError) {
|
|
2641
|
+
return new Error(`${label} is invalid: ${formatValidationError(error)}`);
|
|
2642
|
+
}
|
|
2643
|
+
return error instanceof Error ? error : new Error(`${label} is invalid`);
|
|
2644
|
+
}
|
|
2645
|
+
async function readShipfolioConfig(path2) {
|
|
2646
|
+
try {
|
|
2647
|
+
return parseShipfolioConfig(await readJson(path2));
|
|
2648
|
+
} catch (error) {
|
|
2649
|
+
throw toValidationError("shipfolio.config.json", error);
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
async function writeShipfolioConfig(path2, config) {
|
|
2653
|
+
const validated = parseShipfolioConfig(config);
|
|
2654
|
+
await writeJson(path2, validated);
|
|
2655
|
+
}
|
|
2656
|
+
async function writeShipfolioSpec(path2, spec) {
|
|
2657
|
+
const validated = parseShipfolioSpec(spec);
|
|
2658
|
+
await writeJson(path2, validated);
|
|
2659
|
+
}
|
|
2660
|
+
var init_io = __esm({
|
|
2661
|
+
"src/spec/io.ts"() {
|
|
2662
|
+
"use strict";
|
|
2663
|
+
init_esm_shims();
|
|
2664
|
+
init_fs();
|
|
2665
|
+
init_schema();
|
|
2666
|
+
}
|
|
2667
|
+
});
|
|
2668
|
+
|
|
2313
2669
|
// src/commands/init.ts
|
|
2314
2670
|
var init_exports = {};
|
|
2315
2671
|
__export(init_exports, {
|
|
@@ -2318,8 +2674,45 @@ __export(init_exports, {
|
|
|
2318
2674
|
import { resolve } from "path";
|
|
2319
2675
|
import { join as join6 } from "path";
|
|
2320
2676
|
import * as p3 from "@clack/prompts";
|
|
2677
|
+
function resolveEngineOption(value, available) {
|
|
2678
|
+
if (!value) return void 0;
|
|
2679
|
+
if (!["claude", "codex", "v0"].includes(value)) {
|
|
2680
|
+
logger.warn(`Ignoring invalid engine "${value}".`);
|
|
2681
|
+
return void 0;
|
|
2682
|
+
}
|
|
2683
|
+
const engine = value;
|
|
2684
|
+
if (!available.includes(engine)) {
|
|
2685
|
+
logger.warn(`Engine "${value}" is not available in the current environment.`);
|
|
2686
|
+
return void 0;
|
|
2687
|
+
}
|
|
2688
|
+
return engine;
|
|
2689
|
+
}
|
|
2690
|
+
function resolveDeployOption(value) {
|
|
2691
|
+
if (!value) return void 0;
|
|
2692
|
+
if (!["cloudflare", "vercel", "local"].includes(value)) {
|
|
2693
|
+
logger.warn(`Ignoring invalid deploy target "${value}".`);
|
|
2694
|
+
return void 0;
|
|
2695
|
+
}
|
|
2696
|
+
return value;
|
|
2697
|
+
}
|
|
2698
|
+
function resolveStyleOption(value) {
|
|
2699
|
+
if (!value) return void 0;
|
|
2700
|
+
if (!["dark-minimal", "light-clean", "monochrome", "custom"].includes(value)) {
|
|
2701
|
+
logger.warn(`Ignoring invalid theme "${value}".`);
|
|
2702
|
+
return void 0;
|
|
2703
|
+
}
|
|
2704
|
+
return value;
|
|
2705
|
+
}
|
|
2706
|
+
function resolveAccentOption(value) {
|
|
2707
|
+
if (!value) return void 0;
|
|
2708
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(value)) {
|
|
2709
|
+
logger.warn(`Ignoring invalid accent color "${value}". Expected #RRGGBB.`);
|
|
2710
|
+
return void 0;
|
|
2711
|
+
}
|
|
2712
|
+
return value;
|
|
2713
|
+
}
|
|
2321
2714
|
async function initCommand(options) {
|
|
2322
|
-
logger.header("shipfolio v1.0
|
|
2715
|
+
logger.header("shipfolio v1.1.0");
|
|
2323
2716
|
logger.info("Detecting AI engines...");
|
|
2324
2717
|
const engines = await detectEngines();
|
|
2325
2718
|
const availableTypes = getAvailableEngineTypes(engines);
|
|
@@ -2353,8 +2746,15 @@ async function initCommand(options) {
|
|
|
2353
2746
|
]);
|
|
2354
2747
|
logger.table(tableRows);
|
|
2355
2748
|
logger.blank();
|
|
2749
|
+
const interviewOptions = {
|
|
2750
|
+
engine: resolveEngineOption(options.engine, availableTypes),
|
|
2751
|
+
deploy: resolveDeployOption(options.deploy),
|
|
2752
|
+
style: resolveStyleOption(options.style),
|
|
2753
|
+
accent: resolveAccentOption(options.accent),
|
|
2754
|
+
auto: options.auto === true
|
|
2755
|
+
};
|
|
2356
2756
|
let interviewResult;
|
|
2357
|
-
const draft = await loadDraft();
|
|
2757
|
+
const draft = options.auto ? null : await loadDraft();
|
|
2358
2758
|
if (draft) {
|
|
2359
2759
|
logger.info(t().draftFound);
|
|
2360
2760
|
const useDraft = await p3.confirm({
|
|
@@ -2365,10 +2765,18 @@ async function initCommand(options) {
|
|
|
2365
2765
|
interviewResult = draft.interviewResult;
|
|
2366
2766
|
await clearDraft();
|
|
2367
2767
|
} else {
|
|
2368
|
-
interviewResult = await runInterview(
|
|
2768
|
+
interviewResult = await runInterview(
|
|
2769
|
+
scannedProjects,
|
|
2770
|
+
availableTypes,
|
|
2771
|
+
interviewOptions
|
|
2772
|
+
);
|
|
2369
2773
|
}
|
|
2370
2774
|
} else {
|
|
2371
|
-
interviewResult = await runInterview(
|
|
2775
|
+
interviewResult = await runInterview(
|
|
2776
|
+
scannedProjects,
|
|
2777
|
+
availableTypes,
|
|
2778
|
+
interviewOptions
|
|
2779
|
+
);
|
|
2372
2780
|
}
|
|
2373
2781
|
await saveDraft(interviewResult, true);
|
|
2374
2782
|
const spec = buildSpec(interviewResult);
|
|
@@ -2384,15 +2792,9 @@ async function initCommand(options) {
|
|
|
2384
2792
|
process.exit(1);
|
|
2385
2793
|
}
|
|
2386
2794
|
}
|
|
2387
|
-
if (!options.noPdf) {
|
|
2388
|
-
try {
|
|
2389
|
-
await exportPdf(outputDir);
|
|
2390
|
-
} catch {
|
|
2391
|
-
logger.warn("PDF export skipped due to error.");
|
|
2392
|
-
}
|
|
2393
|
-
}
|
|
2394
2795
|
let shouldDeploy = spec.deploy.platform !== "local";
|
|
2395
|
-
|
|
2796
|
+
const shouldPreview = !options.noPreview && !options.auto;
|
|
2797
|
+
if (shouldPreview) {
|
|
2396
2798
|
const server = await startPreviewServer(outputDir);
|
|
2397
2799
|
logger.blank();
|
|
2398
2800
|
if (shouldDeploy) {
|
|
@@ -2410,6 +2812,13 @@ async function initCommand(options) {
|
|
|
2410
2812
|
server.close();
|
|
2411
2813
|
}
|
|
2412
2814
|
}
|
|
2815
|
+
if (!options.noPdf) {
|
|
2816
|
+
try {
|
|
2817
|
+
await exportPdf(outputDir);
|
|
2818
|
+
} catch {
|
|
2819
|
+
logger.warn("PDF export skipped due to error.");
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2413
2822
|
if (shouldDeploy) {
|
|
2414
2823
|
try {
|
|
2415
2824
|
const url = await deploy(
|
|
@@ -2426,7 +2835,7 @@ async function initCommand(options) {
|
|
|
2426
2835
|
logger.info("You can retry later with: npx shipfolio deploy --site " + outputDir);
|
|
2427
2836
|
}
|
|
2428
2837
|
}
|
|
2429
|
-
await
|
|
2838
|
+
await writeShipfolioConfig(join6(outputDir, "shipfolio.config.json"), {
|
|
2430
2839
|
...spec,
|
|
2431
2840
|
sitePath: outputDir
|
|
2432
2841
|
});
|
|
@@ -2457,6 +2866,7 @@ var init_init = __esm({
|
|
|
2457
2866
|
init_logger();
|
|
2458
2867
|
init_draft();
|
|
2459
2868
|
init_i18n();
|
|
2869
|
+
init_io();
|
|
2460
2870
|
}
|
|
2461
2871
|
});
|
|
2462
2872
|
|
|
@@ -2468,38 +2878,113 @@ import { Command } from "commander";
|
|
|
2468
2878
|
// src/commands/update.ts
|
|
2469
2879
|
init_esm_shims();
|
|
2470
2880
|
init_scanner();
|
|
2471
|
-
import { resolve as resolve2, dirname as
|
|
2881
|
+
import { resolve as resolve2, dirname as dirname3 } from "path";
|
|
2472
2882
|
|
|
2473
2883
|
// src/spec/diff.ts
|
|
2474
2884
|
init_esm_shims();
|
|
2885
|
+
init_project_utils();
|
|
2475
2886
|
function computeDiff(oldConfig, newScan) {
|
|
2476
|
-
const oldProjectMap = new Map(
|
|
2477
|
-
oldConfig.projects.map((p6) => [p6.localPath, p6])
|
|
2478
|
-
);
|
|
2479
2887
|
const newProjectMap = new Map(newScan.map((p6) => [p6.localPath, p6]));
|
|
2888
|
+
const trackedOldPaths = /* @__PURE__ */ new Set();
|
|
2480
2889
|
const newProjects = [];
|
|
2481
2890
|
const updatedProjects = [];
|
|
2482
2891
|
const removedProjects = [];
|
|
2483
2892
|
const unchangedProjects = [];
|
|
2484
|
-
for (const
|
|
2485
|
-
const
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
}
|
|
2489
|
-
|
|
2893
|
+
for (const oldProject of oldConfig.projects) {
|
|
2894
|
+
const trackedPaths = getTrackedProjectPaths(oldProject);
|
|
2895
|
+
for (const trackedPath of trackedPaths) {
|
|
2896
|
+
trackedOldPaths.add(trackedPath);
|
|
2897
|
+
}
|
|
2898
|
+
const currentMatches = trackedPaths.map((path2) => newProjectMap.get(path2)).filter((project) => Boolean(project));
|
|
2899
|
+
const removedPaths = trackedPaths.filter((path2) => !newProjectMap.has(path2));
|
|
2900
|
+
if (currentMatches.length === 0) {
|
|
2901
|
+
removedProjects.push(oldProject);
|
|
2902
|
+
continue;
|
|
2903
|
+
}
|
|
2904
|
+
if (trackedPaths.length > 1) {
|
|
2905
|
+
const mergedMeta = buildMergedProjectMeta(currentMatches, oldProject.name);
|
|
2906
|
+
const oldChildrenByPath = new Map(
|
|
2907
|
+
(oldProject.children || []).map((child) => [child.localPath, child])
|
|
2908
|
+
);
|
|
2909
|
+
const oldTrackingSignature = oldProject.children && oldProject.children.length > 0 ? buildTrackingSignature(oldProject.children) : oldProject.lastScannedCommit;
|
|
2910
|
+
let readmeChanged = removedPaths.length > 0;
|
|
2911
|
+
let depsChanged = removedPaths.length > 0;
|
|
2912
|
+
let newCommits = 0;
|
|
2913
|
+
for (const currentProject2 of currentMatches) {
|
|
2914
|
+
const oldChild = oldChildrenByPath.get(currentProject2.localPath);
|
|
2915
|
+
if (!oldChild) {
|
|
2916
|
+
readmeChanged = true;
|
|
2917
|
+
depsChanged = true;
|
|
2918
|
+
newCommits += currentProject2.totalCommits;
|
|
2919
|
+
continue;
|
|
2920
|
+
}
|
|
2921
|
+
if (currentProject2.readmeContent !== oldChild.readmeContent) {
|
|
2922
|
+
readmeChanged = true;
|
|
2923
|
+
}
|
|
2924
|
+
if (JSON.stringify(currentProject2.techStack) !== JSON.stringify(oldChild.techStack)) {
|
|
2925
|
+
depsChanged = true;
|
|
2926
|
+
}
|
|
2927
|
+
newCommits += Math.max(
|
|
2928
|
+
currentProject2.totalCommits - oldChild.totalCommits,
|
|
2929
|
+
0
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2932
|
+
const changedPaths = currentMatches.filter((currentProject2) => {
|
|
2933
|
+
const oldChild = oldChildrenByPath.get(currentProject2.localPath);
|
|
2934
|
+
if (!oldChild) return true;
|
|
2935
|
+
return currentProject2.lastScannedCommit !== oldChild.lastScannedCommit;
|
|
2936
|
+
}).map((project) => project.localPath);
|
|
2937
|
+
const hasUpdate = removedPaths.length > 0 || trackedPaths.length !== currentMatches.length || mergedMeta.lastScannedCommit !== oldTrackingSignature || readmeChanged || depsChanged;
|
|
2938
|
+
if (hasUpdate) {
|
|
2939
|
+
updatedProjects.push({
|
|
2940
|
+
project: createProjectEntry(mergedMeta, {
|
|
2941
|
+
included: oldProject.included,
|
|
2942
|
+
overrideDescription: oldProject.overrideDescription,
|
|
2943
|
+
showSourceLink: oldProject.showSourceLink,
|
|
2944
|
+
role: oldProject.role,
|
|
2945
|
+
metrics: oldProject.metrics,
|
|
2946
|
+
caseStudy: oldProject.caseStudy,
|
|
2947
|
+
trackedProjectPaths: currentMatches.map((project) => project.localPath),
|
|
2948
|
+
children: currentMatches
|
|
2949
|
+
}),
|
|
2950
|
+
newCommits,
|
|
2951
|
+
readmeChanged,
|
|
2952
|
+
depsChanged,
|
|
2953
|
+
changedPaths,
|
|
2954
|
+
removedPaths
|
|
2955
|
+
});
|
|
2956
|
+
} else {
|
|
2957
|
+
unchangedProjects.push(oldProject);
|
|
2958
|
+
}
|
|
2959
|
+
continue;
|
|
2960
|
+
}
|
|
2961
|
+
const currentProject = currentMatches[0];
|
|
2962
|
+
if (currentProject.lastScannedCommit !== oldProject.lastScannedCommit) {
|
|
2963
|
+
const newCommits = currentProject.totalCommits - oldProject.totalCommits;
|
|
2490
2964
|
updatedProjects.push({
|
|
2491
|
-
project:
|
|
2965
|
+
project: createProjectEntry(currentProject, {
|
|
2966
|
+
included: oldProject.included,
|
|
2967
|
+
overrideDescription: oldProject.overrideDescription,
|
|
2968
|
+
showSourceLink: oldProject.showSourceLink,
|
|
2969
|
+
role: oldProject.role,
|
|
2970
|
+
metrics: oldProject.metrics,
|
|
2971
|
+
caseStudy: oldProject.caseStudy,
|
|
2972
|
+
trackedProjectPaths: trackedPaths,
|
|
2973
|
+
children: oldProject.children
|
|
2974
|
+
}),
|
|
2492
2975
|
newCommits: Math.max(newCommits, 0),
|
|
2493
|
-
readmeChanged:
|
|
2494
|
-
depsChanged: JSON.stringify(
|
|
2976
|
+
readmeChanged: currentProject.readmeContent !== oldProject.readmeContent,
|
|
2977
|
+
depsChanged: JSON.stringify(currentProject.techStack) !== JSON.stringify(oldProject.techStack),
|
|
2978
|
+
changedPaths: [currentProject.localPath],
|
|
2979
|
+
removedPaths
|
|
2495
2980
|
});
|
|
2496
2981
|
} else {
|
|
2497
2982
|
unchangedProjects.push(oldProject);
|
|
2498
2983
|
}
|
|
2499
2984
|
}
|
|
2500
|
-
for (const [path2,
|
|
2501
|
-
if (!
|
|
2502
|
-
|
|
2985
|
+
for (const [path2, meta] of newProjectMap) {
|
|
2986
|
+
if (!trackedOldPaths.has(path2)) {
|
|
2987
|
+
newProjects.push(meta);
|
|
2503
2988
|
}
|
|
2504
2989
|
}
|
|
2505
2990
|
return {
|
|
@@ -2518,7 +3003,54 @@ init_deployer();
|
|
|
2518
3003
|
init_pdf();
|
|
2519
3004
|
init_fs();
|
|
2520
3005
|
init_logger();
|
|
3006
|
+
init_io();
|
|
3007
|
+
init_project_utils();
|
|
3008
|
+
init_questions();
|
|
3009
|
+
init_interviewer();
|
|
3010
|
+
init_i18n();
|
|
2521
3011
|
import * as p4 from "@clack/prompts";
|
|
3012
|
+
function parseCommaSeparatedList2(value) {
|
|
3013
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
3014
|
+
}
|
|
3015
|
+
function buildPersonalInfoDiff(previousOwner, nextOwner, previousSections, nextSections) {
|
|
3016
|
+
const changes = [];
|
|
3017
|
+
if (previousOwner.name !== nextOwner.name) {
|
|
3018
|
+
changes.push(`Name: ${previousOwner.name || "none"} -> ${nextOwner.name || "none"}`);
|
|
3019
|
+
}
|
|
3020
|
+
if (previousOwner.tagline !== nextOwner.tagline) {
|
|
3021
|
+
changes.push(`Tagline: ${previousOwner.tagline || "none"} -> ${nextOwner.tagline || "none"}`);
|
|
3022
|
+
}
|
|
3023
|
+
if (previousOwner.bio !== nextOwner.bio) {
|
|
3024
|
+
changes.push("Bio updated");
|
|
3025
|
+
}
|
|
3026
|
+
if (previousOwner.photoUrl !== nextOwner.photoUrl) {
|
|
3027
|
+
changes.push(`Photo URL: ${previousOwner.photoUrl || "none"} -> ${nextOwner.photoUrl || "none"}`);
|
|
3028
|
+
}
|
|
3029
|
+
const socialKeys = [
|
|
3030
|
+
"github",
|
|
3031
|
+
"twitter",
|
|
3032
|
+
"linkedin",
|
|
3033
|
+
"blog",
|
|
3034
|
+
"email"
|
|
3035
|
+
];
|
|
3036
|
+
for (const key of socialKeys) {
|
|
3037
|
+
if (previousOwner.social[key] !== nextOwner.social[key]) {
|
|
3038
|
+
changes.push(`${key}: ${previousOwner.social[key] || "none"} -> ${nextOwner.social[key] || "none"}`);
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
if (JSON.stringify(previousSections) !== JSON.stringify(nextSections)) {
|
|
3042
|
+
changes.push(`Sections: ${previousSections.join(", ")} -> ${nextSections.join(", ")}`);
|
|
3043
|
+
}
|
|
3044
|
+
if (changes.length === 0) {
|
|
3045
|
+
return "No changes";
|
|
3046
|
+
}
|
|
3047
|
+
return [
|
|
3048
|
+
...changes,
|
|
3049
|
+
"",
|
|
3050
|
+
`Final owner: ${JSON.stringify(nextOwner, null, 2)}`,
|
|
3051
|
+
`Final sections: ${nextSections.join(", ")}`
|
|
3052
|
+
].join("\n");
|
|
3053
|
+
}
|
|
2522
3054
|
async function updateCommand(options) {
|
|
2523
3055
|
logger.header("shipfolio update");
|
|
2524
3056
|
const siteDir = resolve2(options.site);
|
|
@@ -2538,7 +3070,7 @@ async function updateCommand(options) {
|
|
|
2538
3070
|
await initCommand2({ output: siteDir });
|
|
2539
3071
|
return;
|
|
2540
3072
|
}
|
|
2541
|
-
const config = await
|
|
3073
|
+
const config = await readShipfolioConfig(configPath);
|
|
2542
3074
|
const generatedDate = config.generatedAt?.slice(0, 10) || "unknown";
|
|
2543
3075
|
logger.info(
|
|
2544
3076
|
`Site: ${config.deploy.projectName || "local"} (generated ${generatedDate})`
|
|
@@ -2551,7 +3083,13 @@ async function updateCommand(options) {
|
|
|
2551
3083
|
logger.error("No AI engine found. See `npx shipfolio` for install instructions.");
|
|
2552
3084
|
process.exit(1);
|
|
2553
3085
|
}
|
|
2554
|
-
const scanDirs = options.scan?.map((d) => resolve2(d)) || [
|
|
3086
|
+
const scanDirs = options.scan?.map((d) => resolve2(d)) || [
|
|
3087
|
+
...new Set(
|
|
3088
|
+
config.projects.flatMap(
|
|
3089
|
+
(project) => getTrackedProjectPaths(project).map((path2) => dirname3(path2))
|
|
3090
|
+
)
|
|
3091
|
+
)
|
|
3092
|
+
];
|
|
2555
3093
|
const scannedProjects = await scanProjects(scanDirs);
|
|
2556
3094
|
const diff = computeDiff(config, scannedProjects);
|
|
2557
3095
|
logger.header("Changes detected:");
|
|
@@ -2564,9 +3102,16 @@ async function updateCommand(options) {
|
|
|
2564
3102
|
if (diff.updatedProjects.length > 0) {
|
|
2565
3103
|
logger.plain(" Updated:");
|
|
2566
3104
|
for (const upd of diff.updatedProjects) {
|
|
3105
|
+
const changedSummary = upd.changedPaths.length > 0 ? ` paths: ${upd.changedPaths.join(", ")}` : "";
|
|
2567
3106
|
logger.plain(
|
|
2568
3107
|
` ~ ${upd.project.name} ${upd.newCommits} new commits`
|
|
2569
3108
|
);
|
|
3109
|
+
if (changedSummary) {
|
|
3110
|
+
logger.plain(changedSummary);
|
|
3111
|
+
}
|
|
3112
|
+
if (upd.removedPaths.length > 0) {
|
|
3113
|
+
logger.plain(` removed: ${upd.removedPaths.join(", ")}`);
|
|
3114
|
+
}
|
|
2570
3115
|
}
|
|
2571
3116
|
}
|
|
2572
3117
|
if (diff.removedProjects.length > 0) {
|
|
@@ -2586,8 +3131,251 @@ async function updateCommand(options) {
|
|
|
2586
3131
|
if (p4.isCancel(proceed) || !proceed) {
|
|
2587
3132
|
process.exit(0);
|
|
2588
3133
|
}
|
|
3134
|
+
const preparedNewProjects = [];
|
|
3135
|
+
for (const project of diff.newProjects) {
|
|
3136
|
+
logger.plain(`
|
|
3137
|
+
${t().configuring(project.name)}`);
|
|
3138
|
+
const overrideDesc = await p4.text({
|
|
3139
|
+
message: t().descriptionFor(project.name),
|
|
3140
|
+
placeholder: project.description?.slice(0, 80) || t().descriptionPlaceholder,
|
|
3141
|
+
defaultValue: ""
|
|
3142
|
+
});
|
|
3143
|
+
if (p4.isCancel(overrideDesc)) process.exit(0);
|
|
3144
|
+
const demoUrl = await p4.text({
|
|
3145
|
+
message: t().demoUrlFor(project.name),
|
|
3146
|
+
placeholder: project.demoUrl || "none",
|
|
3147
|
+
defaultValue: project.demoUrl || ""
|
|
3148
|
+
});
|
|
3149
|
+
if (p4.isCancel(demoUrl)) process.exit(0);
|
|
3150
|
+
const showSource = await p4.confirm({
|
|
3151
|
+
message: t().showSourceFor(project.name),
|
|
3152
|
+
initialValue: !!project.remoteUrl
|
|
3153
|
+
});
|
|
3154
|
+
if (p4.isCancel(showSource)) process.exit(0);
|
|
3155
|
+
const role = await p4.select({
|
|
3156
|
+
message: t().roleFor(project.name),
|
|
3157
|
+
options: ROLE_OPTIONS()
|
|
3158
|
+
});
|
|
3159
|
+
if (p4.isCancel(role)) process.exit(0);
|
|
3160
|
+
const metricsInput = await p4.text({
|
|
3161
|
+
message: t().metricsFor(project.name),
|
|
3162
|
+
placeholder: t().optional,
|
|
3163
|
+
defaultValue: ""
|
|
3164
|
+
});
|
|
3165
|
+
if (p4.isCancel(metricsInput)) process.exit(0);
|
|
3166
|
+
const featured = await p4.confirm({
|
|
3167
|
+
message: t().featureProjectFor(project.name),
|
|
3168
|
+
initialValue: false
|
|
3169
|
+
});
|
|
3170
|
+
if (p4.isCancel(featured)) process.exit(0);
|
|
3171
|
+
const addCaseStudyDetails = await p4.confirm({
|
|
3172
|
+
message: t().caseStudyDetailsFor(project.name),
|
|
3173
|
+
initialValue: false
|
|
3174
|
+
});
|
|
3175
|
+
if (p4.isCancel(addCaseStudyDetails)) process.exit(0);
|
|
3176
|
+
let audience = null;
|
|
3177
|
+
let problem = null;
|
|
3178
|
+
let solution = null;
|
|
3179
|
+
let impact = null;
|
|
3180
|
+
let evidence = [];
|
|
3181
|
+
let screenshots = [];
|
|
3182
|
+
if (addCaseStudyDetails) {
|
|
3183
|
+
const audienceInput = await p4.text({
|
|
3184
|
+
message: t().audienceFor(project.name),
|
|
3185
|
+
placeholder: t().optional,
|
|
3186
|
+
defaultValue: ""
|
|
3187
|
+
});
|
|
3188
|
+
if (p4.isCancel(audienceInput)) process.exit(0);
|
|
3189
|
+
audience = audienceInput || null;
|
|
3190
|
+
const problemInput = await p4.text({
|
|
3191
|
+
message: t().problemFor(project.name),
|
|
3192
|
+
placeholder: t().optional,
|
|
3193
|
+
defaultValue: ""
|
|
3194
|
+
});
|
|
3195
|
+
if (p4.isCancel(problemInput)) process.exit(0);
|
|
3196
|
+
problem = problemInput || null;
|
|
3197
|
+
const solutionInput = await p4.text({
|
|
3198
|
+
message: t().solutionFor(project.name),
|
|
3199
|
+
placeholder: t().optional,
|
|
3200
|
+
defaultValue: ""
|
|
3201
|
+
});
|
|
3202
|
+
if (p4.isCancel(solutionInput)) process.exit(0);
|
|
3203
|
+
solution = solutionInput || null;
|
|
3204
|
+
const impactInput = await p4.text({
|
|
3205
|
+
message: t().impactFor(project.name),
|
|
3206
|
+
placeholder: t().optional,
|
|
3207
|
+
defaultValue: metricsInput || ""
|
|
3208
|
+
});
|
|
3209
|
+
if (p4.isCancel(impactInput)) process.exit(0);
|
|
3210
|
+
impact = impactInput || null;
|
|
3211
|
+
const evidenceInput = await p4.text({
|
|
3212
|
+
message: t().evidenceFor(project.name),
|
|
3213
|
+
placeholder: t().optional,
|
|
3214
|
+
defaultValue: ""
|
|
3215
|
+
});
|
|
3216
|
+
if (p4.isCancel(evidenceInput)) process.exit(0);
|
|
3217
|
+
evidence = parseCommaSeparatedList2(evidenceInput);
|
|
3218
|
+
const screenshotsInput = await p4.text({
|
|
3219
|
+
message: t().screenshotsFor(project.name),
|
|
3220
|
+
placeholder: t().optional,
|
|
3221
|
+
defaultValue: ""
|
|
3222
|
+
});
|
|
3223
|
+
if (p4.isCancel(screenshotsInput)) process.exit(0);
|
|
3224
|
+
screenshots = parseCommaSeparatedList2(screenshotsInput);
|
|
3225
|
+
}
|
|
3226
|
+
const metrics = metricsInput ? { custom: { summary: metricsInput } } : {};
|
|
3227
|
+
preparedNewProjects.push(
|
|
3228
|
+
createProjectEntry(
|
|
3229
|
+
{
|
|
3230
|
+
...project,
|
|
3231
|
+
demoUrl: demoUrl || project.demoUrl
|
|
3232
|
+
},
|
|
3233
|
+
{
|
|
3234
|
+
overrideDescription: overrideDesc || null,
|
|
3235
|
+
showSourceLink: showSource,
|
|
3236
|
+
role,
|
|
3237
|
+
metrics,
|
|
3238
|
+
caseStudy: {
|
|
3239
|
+
...defaultCaseStudy(featured),
|
|
3240
|
+
audience,
|
|
3241
|
+
problem,
|
|
3242
|
+
solution,
|
|
3243
|
+
impact,
|
|
3244
|
+
evidence,
|
|
3245
|
+
screenshots
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
)
|
|
3249
|
+
);
|
|
3250
|
+
}
|
|
3251
|
+
let nextOwner = config.owner;
|
|
3252
|
+
const shouldUpdateProfile = await p4.confirm({
|
|
3253
|
+
message: t().updateProfilePrompt,
|
|
3254
|
+
initialValue: false
|
|
3255
|
+
});
|
|
3256
|
+
if (p4.isCancel(shouldUpdateProfile)) {
|
|
3257
|
+
process.exit(0);
|
|
3258
|
+
}
|
|
3259
|
+
if (shouldUpdateProfile) {
|
|
3260
|
+
const name = await p4.text({
|
|
3261
|
+
message: t().fullName,
|
|
3262
|
+
defaultValue: config.owner.name,
|
|
3263
|
+
validate: (v) => v.length === 0 ? t().nameRequired : void 0
|
|
3264
|
+
});
|
|
3265
|
+
if (p4.isCancel(name)) process.exit(0);
|
|
3266
|
+
const tagline = await p4.text({
|
|
3267
|
+
message: t().tagline,
|
|
3268
|
+
placeholder: t().taglinePlaceholder,
|
|
3269
|
+
defaultValue: config.owner.tagline
|
|
3270
|
+
});
|
|
3271
|
+
if (p4.isCancel(tagline)) process.exit(0);
|
|
3272
|
+
const bioChoice = await p4.select({
|
|
3273
|
+
message: t().bio,
|
|
3274
|
+
options: [
|
|
3275
|
+
{ value: "auto", label: t().bioAuto },
|
|
3276
|
+
{ value: "manual", label: t().bioManual }
|
|
3277
|
+
],
|
|
3278
|
+
initialValue: config.owner.bio === "auto" ? "auto" : "manual"
|
|
3279
|
+
});
|
|
3280
|
+
if (p4.isCancel(bioChoice)) process.exit(0);
|
|
3281
|
+
let bio = "auto";
|
|
3282
|
+
if (bioChoice === "manual") {
|
|
3283
|
+
bio = await p4.text({
|
|
3284
|
+
message: t().bioPrompt,
|
|
3285
|
+
defaultValue: config.owner.bio === "auto" ? "" : config.owner.bio
|
|
3286
|
+
});
|
|
3287
|
+
if (p4.isCancel(bio)) process.exit(0);
|
|
3288
|
+
}
|
|
3289
|
+
const photoUrl = await p4.text({
|
|
3290
|
+
message: t().photoUrl,
|
|
3291
|
+
placeholder: "https://...",
|
|
3292
|
+
defaultValue: config.owner.photoUrl || ""
|
|
3293
|
+
});
|
|
3294
|
+
if (p4.isCancel(photoUrl)) process.exit(0);
|
|
3295
|
+
const github = await p4.text({
|
|
3296
|
+
message: t().githubUser,
|
|
3297
|
+
defaultValue: config.owner.social.github || ""
|
|
3298
|
+
});
|
|
3299
|
+
if (p4.isCancel(github)) process.exit(0);
|
|
3300
|
+
const twitter = await p4.text({
|
|
3301
|
+
message: t().twitterHandle,
|
|
3302
|
+
defaultValue: config.owner.social.twitter || ""
|
|
3303
|
+
});
|
|
3304
|
+
if (p4.isCancel(twitter)) process.exit(0);
|
|
3305
|
+
const linkedin = await p4.text({
|
|
3306
|
+
message: t().linkedinUrl,
|
|
3307
|
+
defaultValue: config.owner.social.linkedin || ""
|
|
3308
|
+
});
|
|
3309
|
+
if (p4.isCancel(linkedin)) process.exit(0);
|
|
3310
|
+
const blog = await p4.text({
|
|
3311
|
+
message: t().blogUrl,
|
|
3312
|
+
defaultValue: config.owner.social.blog || ""
|
|
3313
|
+
});
|
|
3314
|
+
if (p4.isCancel(blog)) process.exit(0);
|
|
3315
|
+
const email = await p4.text({
|
|
3316
|
+
message: t().contactEmail,
|
|
3317
|
+
defaultValue: config.owner.social.email || ""
|
|
3318
|
+
});
|
|
3319
|
+
if (p4.isCancel(email)) process.exit(0);
|
|
3320
|
+
nextOwner = {
|
|
3321
|
+
name,
|
|
3322
|
+
tagline,
|
|
3323
|
+
bio,
|
|
3324
|
+
photoUrl: photoUrl || null,
|
|
3325
|
+
social: {
|
|
3326
|
+
github: github || void 0,
|
|
3327
|
+
twitter: twitter || void 0,
|
|
3328
|
+
linkedin: linkedin || void 0,
|
|
3329
|
+
blog: blog || void 0,
|
|
3330
|
+
email: email || void 0
|
|
3331
|
+
}
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
const projectedProjects = [
|
|
3335
|
+
...diff.unchangedProjects,
|
|
3336
|
+
...diff.updatedProjects.map((update) => update.project),
|
|
3337
|
+
...preparedNewProjects
|
|
3338
|
+
];
|
|
3339
|
+
let nextSections = config.sections;
|
|
3340
|
+
const shouldReviewSections = await p4.confirm({
|
|
3341
|
+
message: t().reviewSectionsPrompt,
|
|
3342
|
+
initialValue: shouldUpdateProfile
|
|
3343
|
+
});
|
|
3344
|
+
if (p4.isCancel(shouldReviewSections)) {
|
|
3345
|
+
process.exit(0);
|
|
3346
|
+
}
|
|
3347
|
+
if (shouldReviewSections) {
|
|
3348
|
+
const sectionOptions = getAdditionalSectionOptions(projectedProjects, nextOwner);
|
|
3349
|
+
const currentAdditionalSections = config.sections.filter(
|
|
3350
|
+
(section) => section !== "hero" && section !== "projects"
|
|
3351
|
+
);
|
|
3352
|
+
const allowedCurrentValues = currentAdditionalSections.filter(
|
|
3353
|
+
(section) => sectionOptions.some((option) => option.value === section)
|
|
3354
|
+
);
|
|
3355
|
+
const additionalSections = sectionOptions.length > 0 ? await p4.multiselect({
|
|
3356
|
+
message: t().additionalSections,
|
|
3357
|
+
options: sectionOptions,
|
|
3358
|
+
required: false,
|
|
3359
|
+
initialValues: allowedCurrentValues
|
|
3360
|
+
}) : [];
|
|
3361
|
+
if (sectionOptions.length > 0 && p4.isCancel(additionalSections)) {
|
|
3362
|
+
process.exit(0);
|
|
3363
|
+
}
|
|
3364
|
+
nextSections = ["hero", "projects", ...additionalSections];
|
|
3365
|
+
}
|
|
3366
|
+
const personalInfoDiff = buildPersonalInfoDiff(
|
|
3367
|
+
config.owner,
|
|
3368
|
+
nextOwner,
|
|
3369
|
+
config.sections,
|
|
3370
|
+
nextSections
|
|
3371
|
+
);
|
|
2589
3372
|
const engine = availableTypes.includes(config.engine) ? config.engine : availableTypes[0];
|
|
2590
|
-
const prompt = await buildUpdatePrompt(
|
|
3373
|
+
const prompt = await buildUpdatePrompt(
|
|
3374
|
+
config,
|
|
3375
|
+
diff,
|
|
3376
|
+
preparedNewProjects,
|
|
3377
|
+
personalInfoDiff
|
|
3378
|
+
);
|
|
2591
3379
|
await generateSite(engine, prompt, siteDir);
|
|
2592
3380
|
let buildResult = await buildSite(siteDir);
|
|
2593
3381
|
if (!buildResult.success) {
|
|
@@ -2627,34 +3415,15 @@ async function updateCommand(options) {
|
|
|
2627
3415
|
const updatedConfig = {
|
|
2628
3416
|
...config,
|
|
2629
3417
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3418
|
+
owner: nextOwner,
|
|
3419
|
+
sections: nextSections,
|
|
2630
3420
|
projects: [
|
|
2631
3421
|
...diff.unchangedProjects,
|
|
2632
|
-
...diff.updatedProjects.map((u) =>
|
|
2633
|
-
|
|
2634
|
-
(p6) => p6.localPath === u.project.localPath
|
|
2635
|
-
);
|
|
2636
|
-
return {
|
|
2637
|
-
...u.project,
|
|
2638
|
-
included: true,
|
|
2639
|
-
overrideDescription: existing?.overrideDescription || null,
|
|
2640
|
-
showSourceLink: existing?.showSourceLink ?? !!u.project.remoteUrl,
|
|
2641
|
-
role: existing?.role || "solo",
|
|
2642
|
-
metrics: existing?.metrics || {}
|
|
2643
|
-
};
|
|
2644
|
-
}),
|
|
2645
|
-
...diff.newProjects.map(
|
|
2646
|
-
(proj) => ({
|
|
2647
|
-
...proj,
|
|
2648
|
-
included: true,
|
|
2649
|
-
overrideDescription: null,
|
|
2650
|
-
showSourceLink: !!proj.remoteUrl,
|
|
2651
|
-
role: "solo",
|
|
2652
|
-
metrics: {}
|
|
2653
|
-
})
|
|
2654
|
-
)
|
|
3422
|
+
...diff.updatedProjects.map((u) => u.project),
|
|
3423
|
+
...preparedNewProjects
|
|
2655
3424
|
]
|
|
2656
3425
|
};
|
|
2657
|
-
await
|
|
3426
|
+
await writeShipfolioConfig(join(siteDir, "shipfolio.config.json"), updatedConfig);
|
|
2658
3427
|
logger.success("Update complete.");
|
|
2659
3428
|
}
|
|
2660
3429
|
|
|
@@ -2663,6 +3432,7 @@ init_esm_shims();
|
|
|
2663
3432
|
init_scanner();
|
|
2664
3433
|
init_interviewer();
|
|
2665
3434
|
init_builder();
|
|
3435
|
+
init_io();
|
|
2666
3436
|
init_prompt_builder();
|
|
2667
3437
|
init_detect();
|
|
2668
3438
|
init_fs();
|
|
@@ -2691,7 +3461,7 @@ async function specCommand(options) {
|
|
|
2691
3461
|
const outputDir = options.output || ".";
|
|
2692
3462
|
const specPath = resolve3(outputDir, "shipfolio-spec.json");
|
|
2693
3463
|
const promptPath = resolve3(outputDir, "shipfolio-prompt.md");
|
|
2694
|
-
await
|
|
3464
|
+
await writeShipfolioSpec(specPath, spec);
|
|
2695
3465
|
await writeText(promptPath, prompt);
|
|
2696
3466
|
logger.success(`Spec saved: ${specPath}`);
|
|
2697
3467
|
logger.success(`Prompt saved: ${promptPath}`);
|
|
@@ -2706,6 +3476,7 @@ init_esm_shims();
|
|
|
2706
3476
|
init_deployer();
|
|
2707
3477
|
init_fs();
|
|
2708
3478
|
init_logger();
|
|
3479
|
+
init_io();
|
|
2709
3480
|
import { resolve as resolve4 } from "path";
|
|
2710
3481
|
import * as p5 from "@clack/prompts";
|
|
2711
3482
|
async function deployCommand(options) {
|
|
@@ -2715,7 +3486,7 @@ async function deployCommand(options) {
|
|
|
2715
3486
|
let projectName;
|
|
2716
3487
|
let customDomain;
|
|
2717
3488
|
if (await fileExists(configPath)) {
|
|
2718
|
-
const config = await
|
|
3489
|
+
const config = await readShipfolioConfig(configPath);
|
|
2719
3490
|
platform = options.platform || config.deploy.platform;
|
|
2720
3491
|
projectName = config.deploy.projectName;
|
|
2721
3492
|
customDomain = config.deploy.customDomain;
|
|
@@ -2785,8 +3556,8 @@ initLocale();
|
|
|
2785
3556
|
var program = new Command();
|
|
2786
3557
|
program.name("shipfolio").description(
|
|
2787
3558
|
"Generate and deploy your personal portfolio site from local projects using AI"
|
|
2788
|
-
).version("1.0
|
|
2789
|
-
program.command("init", { isDefault: true }).description("Generate a new portfolio site").option("-s, --scan <dirs...>", "Directories to scan for projects").option("-o, --output <dir>", "Output directory", "./shipfolio-site").option("--no-pdf", "Skip PDF export").option("--no-preview", "Skip local preview").action(initCommand);
|
|
3559
|
+
).version("1.1.0");
|
|
3560
|
+
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 | custom").option("--accent <hex>", "Accent color (hex)").option("--auto", "Skip prompts and use safe defaults").option("-o, --output <dir>", "Output directory", "./shipfolio-site").option("--no-pdf", "Skip PDF export").option("--no-preview", "Skip local preview").action(initCommand);
|
|
2790
3561
|
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);
|
|
2791
3562
|
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);
|
|
2792
3563
|
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);
|