shipfolio 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +594 -149
- package/dist/cli.js.map +1 -1
- package/dist/lib/orchestrator/prompt-builder.js +158 -30
- package/dist/lib/orchestrator/prompt-builder.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -591,58 +591,268 @@ var init_scanner = __esm({
|
|
|
591
591
|
}
|
|
592
592
|
});
|
|
593
593
|
|
|
594
|
+
// src/utils/i18n.ts
|
|
595
|
+
function detectLocale() {
|
|
596
|
+
const lang = (process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANGUAGE || "").toLowerCase();
|
|
597
|
+
if (lang.startsWith("zh")) return "zh";
|
|
598
|
+
return "en";
|
|
599
|
+
}
|
|
600
|
+
function initLocale() {
|
|
601
|
+
currentLocale = detectLocale();
|
|
602
|
+
}
|
|
603
|
+
function t() {
|
|
604
|
+
return messages[currentLocale];
|
|
605
|
+
}
|
|
606
|
+
var messages, currentLocale;
|
|
607
|
+
var init_i18n = __esm({
|
|
608
|
+
"src/utils/i18n.ts"() {
|
|
609
|
+
"use strict";
|
|
610
|
+
init_esm_shims();
|
|
611
|
+
messages = {
|
|
612
|
+
en: {
|
|
613
|
+
// Project selection
|
|
614
|
+
selectProjects: "Select projects to include (space = select, enter = confirm):",
|
|
615
|
+
mergePrompt: "Merge any related projects into one entry? (e.g. web + mobile)",
|
|
616
|
+
selectToMerge: "Select projects to merge together (space = select, enter = confirm):",
|
|
617
|
+
mergedNamePrompt: "Name for the merged entry:",
|
|
618
|
+
mergeNeedTwo: "Need at least 2 projects to merge. Skipping.",
|
|
619
|
+
mergedResult: (names, target) => `Merged ${names} -> "${target}"`,
|
|
620
|
+
mergeMore: "Merge more projects?",
|
|
621
|
+
// Per-project
|
|
622
|
+
configuring: (name) => `Configuring: ${name}`,
|
|
623
|
+
descriptionFor: (name) => `Description for ${name} (enter to use auto-generated):`,
|
|
624
|
+
descriptionPlaceholder: "Auto-generate from README",
|
|
625
|
+
demoUrlFor: (name) => `Demo URL for ${name}:`,
|
|
626
|
+
showSourceFor: (name) => `Show source code link for ${name}?`,
|
|
627
|
+
roleFor: (name) => `Your role in ${name}:`,
|
|
628
|
+
metricsFor: (name) => `Key metrics for ${name} (e.g. "1k users, $5k MRR"):`,
|
|
629
|
+
optional: "optional",
|
|
630
|
+
// Personal info
|
|
631
|
+
personalInfo: "Personal Information",
|
|
632
|
+
fullName: "Your full name:",
|
|
633
|
+
nameRequired: "Name is required",
|
|
634
|
+
tagline: "Professional tagline (one line):",
|
|
635
|
+
taglinePlaceholder: "Full-stack developer. I ship things.",
|
|
636
|
+
bio: "Bio:",
|
|
637
|
+
bioAuto: "Auto-generate from my projects",
|
|
638
|
+
bioManual: "Write manually",
|
|
639
|
+
bioPrompt: "Your bio (2-3 sentences):",
|
|
640
|
+
photoUrl: "Photo URL (optional):",
|
|
641
|
+
githubUser: "GitHub username or URL:",
|
|
642
|
+
twitterHandle: "Twitter/X handle:",
|
|
643
|
+
linkedinUrl: "LinkedIn URL:",
|
|
644
|
+
blogUrl: "Blog URL:",
|
|
645
|
+
contactEmail: "Contact email:",
|
|
646
|
+
// Design
|
|
647
|
+
designPrefs: "Design Preferences",
|
|
648
|
+
theme: "Theme:",
|
|
649
|
+
accentColor: "Accent color:",
|
|
650
|
+
customHex: "Custom hex",
|
|
651
|
+
customHexPrompt: "Custom accent color (hex):",
|
|
652
|
+
invalidHex: "Enter a valid hex color",
|
|
653
|
+
font: "Font:",
|
|
654
|
+
animationLevel: "Animation level:",
|
|
655
|
+
// Sections
|
|
656
|
+
sections: "Sections",
|
|
657
|
+
additionalSections: "Additional sections (Hero + Projects always included):",
|
|
658
|
+
// Engine
|
|
659
|
+
aiEngine: "AI Engine",
|
|
660
|
+
engineUsing: (name) => `Using ${name}`,
|
|
661
|
+
engineSelect: "AI engine to use:",
|
|
662
|
+
// Deploy
|
|
663
|
+
deployment: "Deployment",
|
|
664
|
+
deployTo: "Deploy to:",
|
|
665
|
+
projectNamePrompt: "Project name (used in URL):",
|
|
666
|
+
projectNameInvalid: "Lowercase letters, numbers, and hyphens only",
|
|
667
|
+
configComplete: "Configuration complete. Generating your site...",
|
|
668
|
+
// Draft
|
|
669
|
+
draftFound: "Found a saved draft from a previous session.",
|
|
670
|
+
draftLoadPrompt: "Load draft? (skip re-entering previous answers)",
|
|
671
|
+
draftSaved: "Draft saved. Your progress will be restored next time.",
|
|
672
|
+
draftCleared: "Draft cleared.",
|
|
673
|
+
// Roles
|
|
674
|
+
roleSolo: "Solo",
|
|
675
|
+
roleLead: "Lead",
|
|
676
|
+
roleContributor: "Contributor",
|
|
677
|
+
// Themes
|
|
678
|
+
themeDarkMinimal: "Dark Minimal",
|
|
679
|
+
themeLightClean: "Light Clean",
|
|
680
|
+
themeMonochrome: "Monochrome",
|
|
681
|
+
themeCustom: "Custom (AI decides)",
|
|
682
|
+
// Fonts
|
|
683
|
+
fontInter: "Inter",
|
|
684
|
+
fontJetBrains: "JetBrains Mono",
|
|
685
|
+
fontSystem: "System Default",
|
|
686
|
+
// Animations
|
|
687
|
+
animSubtle: "Subtle",
|
|
688
|
+
animModerate: "Moderate",
|
|
689
|
+
animNone: "None",
|
|
690
|
+
// Section names
|
|
691
|
+
secSkills: "Skills / Tech Stack",
|
|
692
|
+
secAbout: "About Me",
|
|
693
|
+
secTimeline: "Timeline / Changelog",
|
|
694
|
+
secBlog: "Blog",
|
|
695
|
+
secMetrics: "Metrics Dashboard",
|
|
696
|
+
secContact: "Contact",
|
|
697
|
+
// Deploy platforms
|
|
698
|
+
deployCloudflare: "Cloudflare Pages",
|
|
699
|
+
deployVercel: "Vercel",
|
|
700
|
+
deployLocal: "Local only (no deploy)",
|
|
701
|
+
// Colors
|
|
702
|
+
colorPurple: "Purple",
|
|
703
|
+
colorGreen: "Green",
|
|
704
|
+
colorOrange: "Orange",
|
|
705
|
+
colorBlue: "Blue",
|
|
706
|
+
colorRed: "Red",
|
|
707
|
+
colorPink: "Pink",
|
|
708
|
+
// Engines
|
|
709
|
+
engineClaude: "Claude Code",
|
|
710
|
+
engineCodex: "Codex",
|
|
711
|
+
engineV0: "v0 (Vercel)"
|
|
712
|
+
},
|
|
713
|
+
zh: {
|
|
714
|
+
selectProjects: "\u9009\u62E9\u8981\u5C55\u793A\u7684\u9879\u76EE (\u7A7A\u683C = \u9009\u62E9, \u56DE\u8F66 = \u786E\u8BA4):",
|
|
715
|
+
mergePrompt: "\u662F\u5426\u8981\u5408\u5E76\u76F8\u5173\u9879\u76EE\u4E3A\u4E00\u4E2A\u6761\u76EE? (\u5982 web + mobile)",
|
|
716
|
+
selectToMerge: "\u9009\u62E9\u8981\u5408\u5E76\u7684\u9879\u76EE (\u7A7A\u683C = \u9009\u62E9, \u56DE\u8F66 = \u786E\u8BA4):",
|
|
717
|
+
mergedNamePrompt: "\u5408\u5E76\u540E\u7684\u540D\u79F0:",
|
|
718
|
+
mergeNeedTwo: "\u81F3\u5C11\u9700\u8981\u9009\u62E9 2 \u4E2A\u9879\u76EE\u624D\u80FD\u5408\u5E76, \u8DF3\u8FC7.",
|
|
719
|
+
mergedResult: (names, target) => `\u5DF2\u5408\u5E76 ${names} -> "${target}"`,
|
|
720
|
+
mergeMore: "\u7EE7\u7EED\u5408\u5E76\u5176\u4ED6\u9879\u76EE?",
|
|
721
|
+
configuring: (name) => `\u914D\u7F6E: ${name}`,
|
|
722
|
+
descriptionFor: (name) => `${name} \u7684\u63CF\u8FF0 (\u56DE\u8F66\u4F7F\u7528\u81EA\u52A8\u751F\u6210):`,
|
|
723
|
+
descriptionPlaceholder: "\u4ECE README \u81EA\u52A8\u751F\u6210",
|
|
724
|
+
demoUrlFor: (name) => `${name} \u7684 Demo \u5730\u5740:`,
|
|
725
|
+
showSourceFor: (name) => `\u662F\u5426\u5C55\u793A ${name} \u7684\u6E90\u7801\u94FE\u63A5?`,
|
|
726
|
+
roleFor: (name) => `\u4F60\u5728 ${name} \u4E2D\u7684\u89D2\u8272:`,
|
|
727
|
+
metricsFor: (name) => `${name} \u7684\u5173\u952E\u6307\u6807 (\u5982 "1k \u7528\u6237, $5k MRR"):`,
|
|
728
|
+
optional: "\u53EF\u9009",
|
|
729
|
+
personalInfo: "\u4E2A\u4EBA\u4FE1\u606F",
|
|
730
|
+
fullName: "\u4F60\u7684\u5168\u540D:",
|
|
731
|
+
nameRequired: "\u59D3\u540D\u4E0D\u80FD\u4E3A\u7A7A",
|
|
732
|
+
tagline: "\u804C\u4E1A\u6807\u8BED (\u4E00\u53E5\u8BDD):",
|
|
733
|
+
taglinePlaceholder: "\u5168\u6808\u5DE5\u7A0B\u5E08, \u6301\u7EED\u4EA4\u4ED8.",
|
|
734
|
+
bio: "\u4E2A\u4EBA\u7B80\u4ECB:",
|
|
735
|
+
bioAuto: "\u6839\u636E\u9879\u76EE\u81EA\u52A8\u751F\u6210",
|
|
736
|
+
bioManual: "\u624B\u52A8\u586B\u5199",
|
|
737
|
+
bioPrompt: "\u4F60\u7684\u7B80\u4ECB (2-3\u53E5\u8BDD):",
|
|
738
|
+
photoUrl: "\u5934\u50CF\u94FE\u63A5 (\u53EF\u9009):",
|
|
739
|
+
githubUser: "GitHub \u7528\u6237\u540D\u6216\u94FE\u63A5:",
|
|
740
|
+
twitterHandle: "Twitter/X \u8D26\u53F7:",
|
|
741
|
+
linkedinUrl: "LinkedIn \u94FE\u63A5:",
|
|
742
|
+
blogUrl: "\u535A\u5BA2\u94FE\u63A5:",
|
|
743
|
+
contactEmail: "\u8054\u7CFB\u90AE\u7BB1:",
|
|
744
|
+
designPrefs: "\u8BBE\u8BA1\u504F\u597D",
|
|
745
|
+
theme: "\u4E3B\u9898:",
|
|
746
|
+
accentColor: "\u5F3A\u8C03\u8272:",
|
|
747
|
+
customHex: "\u81EA\u5B9A\u4E49\u8272\u503C",
|
|
748
|
+
customHexPrompt: "\u81EA\u5B9A\u4E49\u5F3A\u8C03\u8272 (hex):",
|
|
749
|
+
invalidHex: "\u8BF7\u8F93\u5165\u6709\u6548\u7684 hex \u989C\u8272\u503C",
|
|
750
|
+
font: "\u5B57\u4F53:",
|
|
751
|
+
animationLevel: "\u52A8\u753B\u7EA7\u522B:",
|
|
752
|
+
sections: "\u9875\u9762\u533A\u5757",
|
|
753
|
+
additionalSections: "\u9644\u52A0\u533A\u5757 (Hero + \u9879\u76EE\u5C55\u793A \u59CB\u7EC8\u5305\u542B):",
|
|
754
|
+
aiEngine: "AI \u5F15\u64CE",
|
|
755
|
+
engineUsing: (name) => `\u4F7F\u7528 ${name}`,
|
|
756
|
+
engineSelect: "\u9009\u62E9 AI \u5F15\u64CE:",
|
|
757
|
+
deployment: "\u90E8\u7F72",
|
|
758
|
+
deployTo: "\u90E8\u7F72\u5230:",
|
|
759
|
+
projectNamePrompt: "\u9879\u76EE\u540D\u79F0 (\u7528\u4E8E URL):",
|
|
760
|
+
projectNameInvalid: "\u4EC5\u9650\u5C0F\u5199\u5B57\u6BCD, \u6570\u5B57\u548C\u8FDE\u5B57\u7B26",
|
|
761
|
+
configComplete: "\u914D\u7F6E\u5B8C\u6210, \u6B63\u5728\u751F\u6210\u7F51\u7AD9...",
|
|
762
|
+
draftFound: "\u53D1\u73B0\u4E0A\u6B21\u672A\u5B8C\u6210\u7684\u914D\u7F6E\u8349\u7A3F.",
|
|
763
|
+
draftLoadPrompt: "\u662F\u5426\u52A0\u8F7D\u8349\u7A3F? (\u8DF3\u8FC7\u91CD\u590D\u586B\u5199)",
|
|
764
|
+
draftSaved: "\u8349\u7A3F\u5DF2\u4FDD\u5B58, \u4E0B\u6B21\u8FD0\u884C\u65F6\u53EF\u6062\u590D.",
|
|
765
|
+
draftCleared: "\u8349\u7A3F\u5DF2\u6E05\u9664.",
|
|
766
|
+
roleSolo: "\u72EC\u7ACB\u5B8C\u6210",
|
|
767
|
+
roleLead: "\u4E3B\u5BFC\u5F00\u53D1",
|
|
768
|
+
roleContributor: "\u53C2\u4E0E\u8D21\u732E",
|
|
769
|
+
themeDarkMinimal: "\u6697\u8272\u6781\u7B80",
|
|
770
|
+
themeLightClean: "\u660E\u4EAE\u7B80\u6D01",
|
|
771
|
+
themeMonochrome: "\u9ED1\u767D",
|
|
772
|
+
themeCustom: "\u81EA\u5B9A\u4E49 (AI \u51B3\u5B9A)",
|
|
773
|
+
fontInter: "Inter",
|
|
774
|
+
fontJetBrains: "JetBrains Mono",
|
|
775
|
+
fontSystem: "\u7CFB\u7EDF\u9ED8\u8BA4",
|
|
776
|
+
animSubtle: "\u8F7B\u5FAE",
|
|
777
|
+
animModerate: "\u9002\u4E2D",
|
|
778
|
+
animNone: "\u65E0",
|
|
779
|
+
secSkills: "\u6280\u672F\u6808",
|
|
780
|
+
secAbout: "\u5173\u4E8E\u6211",
|
|
781
|
+
secTimeline: "\u65F6\u95F4\u7EBF",
|
|
782
|
+
secBlog: "\u535A\u5BA2",
|
|
783
|
+
secMetrics: "\u6570\u636E\u9762\u677F",
|
|
784
|
+
secContact: "\u8054\u7CFB\u65B9\u5F0F",
|
|
785
|
+
deployCloudflare: "Cloudflare Pages",
|
|
786
|
+
deployVercel: "Vercel",
|
|
787
|
+
deployLocal: "\u4EC5\u672C\u5730 (\u4E0D\u90E8\u7F72)",
|
|
788
|
+
colorPurple: "\u7D2B\u8272",
|
|
789
|
+
colorGreen: "\u7EFF\u8272",
|
|
790
|
+
colorOrange: "\u6A59\u8272",
|
|
791
|
+
colorBlue: "\u84DD\u8272",
|
|
792
|
+
colorRed: "\u7EA2\u8272",
|
|
793
|
+
colorPink: "\u7C89\u8272",
|
|
794
|
+
engineClaude: "Claude Code",
|
|
795
|
+
engineCodex: "Codex",
|
|
796
|
+
engineV0: "v0 (Vercel)"
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
currentLocale = "en";
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
|
|
594
803
|
// src/interviewer/questions.ts
|
|
595
804
|
var THEME_OPTIONS, FONT_OPTIONS, ANIMATION_OPTIONS, SECTION_OPTIONS, ENGINE_OPTIONS, DEPLOY_OPTIONS, ROLE_OPTIONS, DEFAULT_ACCENT_COLORS;
|
|
596
805
|
var init_questions = __esm({
|
|
597
806
|
"src/interviewer/questions.ts"() {
|
|
598
807
|
"use strict";
|
|
599
808
|
init_esm_shims();
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
{ value: "
|
|
603
|
-
{ value: "
|
|
604
|
-
{ value: "
|
|
809
|
+
init_i18n();
|
|
810
|
+
THEME_OPTIONS = () => [
|
|
811
|
+
{ value: "dark-minimal", label: t().themeDarkMinimal },
|
|
812
|
+
{ value: "light-clean", label: t().themeLightClean },
|
|
813
|
+
{ value: "monochrome", label: t().themeMonochrome },
|
|
814
|
+
{ value: "custom", label: t().themeCustom }
|
|
605
815
|
];
|
|
606
|
-
FONT_OPTIONS = [
|
|
607
|
-
{ value: "Inter", label:
|
|
608
|
-
{ value: "JetBrains Mono", label:
|
|
609
|
-
{ value: "system", label:
|
|
816
|
+
FONT_OPTIONS = () => [
|
|
817
|
+
{ value: "Inter", label: t().fontInter },
|
|
818
|
+
{ value: "JetBrains Mono", label: t().fontJetBrains },
|
|
819
|
+
{ value: "system", label: t().fontSystem }
|
|
610
820
|
];
|
|
611
|
-
ANIMATION_OPTIONS = [
|
|
612
|
-
{ value: "subtle", label:
|
|
613
|
-
{ value: "moderate", label:
|
|
614
|
-
{ value: "none", label:
|
|
821
|
+
ANIMATION_OPTIONS = () => [
|
|
822
|
+
{ value: "subtle", label: t().animSubtle },
|
|
823
|
+
{ value: "moderate", label: t().animModerate },
|
|
824
|
+
{ value: "none", label: t().animNone }
|
|
615
825
|
];
|
|
616
|
-
SECTION_OPTIONS = [
|
|
617
|
-
{ value: "skills", label:
|
|
618
|
-
{ value: "about", label:
|
|
619
|
-
{ value: "timeline", label:
|
|
620
|
-
{ value: "blog", label:
|
|
621
|
-
{ value: "metrics", label:
|
|
622
|
-
{ value: "contact", label:
|
|
826
|
+
SECTION_OPTIONS = () => [
|
|
827
|
+
{ value: "skills", label: t().secSkills },
|
|
828
|
+
{ value: "about", label: t().secAbout },
|
|
829
|
+
{ value: "timeline", label: t().secTimeline },
|
|
830
|
+
{ value: "blog", label: t().secBlog },
|
|
831
|
+
{ value: "metrics", label: t().secMetrics },
|
|
832
|
+
{ value: "contact", label: t().secContact }
|
|
623
833
|
];
|
|
624
|
-
ENGINE_OPTIONS = [
|
|
625
|
-
{ value: "claude", label:
|
|
626
|
-
{ value: "codex", label:
|
|
627
|
-
{ value: "v0", label:
|
|
834
|
+
ENGINE_OPTIONS = () => [
|
|
835
|
+
{ value: "claude", label: t().engineClaude },
|
|
836
|
+
{ value: "codex", label: t().engineCodex },
|
|
837
|
+
{ value: "v0", label: t().engineV0 }
|
|
628
838
|
];
|
|
629
|
-
DEPLOY_OPTIONS = [
|
|
630
|
-
{ value: "cloudflare", label:
|
|
631
|
-
{ value: "vercel", label:
|
|
632
|
-
{ value: "local", label:
|
|
839
|
+
DEPLOY_OPTIONS = () => [
|
|
840
|
+
{ value: "cloudflare", label: t().deployCloudflare },
|
|
841
|
+
{ value: "vercel", label: t().deployVercel },
|
|
842
|
+
{ value: "local", label: t().deployLocal }
|
|
633
843
|
];
|
|
634
|
-
ROLE_OPTIONS = [
|
|
635
|
-
{ value: "solo", label:
|
|
636
|
-
{ value: "lead", label:
|
|
637
|
-
{ value: "contributor", label:
|
|
844
|
+
ROLE_OPTIONS = () => [
|
|
845
|
+
{ value: "solo", label: t().roleSolo },
|
|
846
|
+
{ value: "lead", label: t().roleLead },
|
|
847
|
+
{ value: "contributor", label: t().roleContributor }
|
|
638
848
|
];
|
|
639
|
-
DEFAULT_ACCENT_COLORS = [
|
|
640
|
-
{ value: "#7c3aed", label:
|
|
641
|
-
{ value: "#10b981", label:
|
|
642
|
-
{ value: "#f97316", label:
|
|
643
|
-
{ value: "#3b82f6", label:
|
|
644
|
-
{ value: "#ef4444", label:
|
|
645
|
-
{ value: "#ec4899", label:
|
|
849
|
+
DEFAULT_ACCENT_COLORS = () => [
|
|
850
|
+
{ value: "#7c3aed", label: t().colorPurple },
|
|
851
|
+
{ value: "#10b981", label: t().colorGreen },
|
|
852
|
+
{ value: "#f97316", label: t().colorOrange },
|
|
853
|
+
{ value: "#3b82f6", label: t().colorBlue },
|
|
854
|
+
{ value: "#ef4444", label: t().colorRed },
|
|
855
|
+
{ value: "#ec4899", label: t().colorPink }
|
|
646
856
|
];
|
|
647
857
|
}
|
|
648
858
|
});
|
|
@@ -657,7 +867,7 @@ function handleCancel(value) {
|
|
|
657
867
|
}
|
|
658
868
|
async function runMergeStep(projects) {
|
|
659
869
|
const shouldMerge = await p.confirm({
|
|
660
|
-
message:
|
|
870
|
+
message: t().mergePrompt,
|
|
661
871
|
initialValue: false
|
|
662
872
|
});
|
|
663
873
|
handleCancel(shouldMerge);
|
|
@@ -665,7 +875,7 @@ async function runMergeStep(projects) {
|
|
|
665
875
|
let remaining = [...projects];
|
|
666
876
|
while (remaining.length >= 2) {
|
|
667
877
|
const mergeIds = await p.multiselect({
|
|
668
|
-
message:
|
|
878
|
+
message: t().selectToMerge,
|
|
669
879
|
options: remaining.map((proj) => ({
|
|
670
880
|
value: proj.id,
|
|
671
881
|
label: `${proj.name} (${proj.localPath})`,
|
|
@@ -675,7 +885,7 @@ async function runMergeStep(projects) {
|
|
|
675
885
|
});
|
|
676
886
|
handleCancel(mergeIds);
|
|
677
887
|
if (mergeIds.length < 2) {
|
|
678
|
-
logger.warn(
|
|
888
|
+
logger.warn(t().mergeNeedTwo);
|
|
679
889
|
break;
|
|
680
890
|
}
|
|
681
891
|
const toMerge = mergeIds.map(
|
|
@@ -683,7 +893,7 @@ async function runMergeStep(projects) {
|
|
|
683
893
|
);
|
|
684
894
|
const defaultName = toMerge.map((p6) => p6.name).join(" + ");
|
|
685
895
|
const mergedName = await p.text({
|
|
686
|
-
message:
|
|
896
|
+
message: t().mergedNamePrompt,
|
|
687
897
|
placeholder: defaultName,
|
|
688
898
|
defaultValue: defaultName
|
|
689
899
|
});
|
|
@@ -692,11 +902,11 @@ async function runMergeStep(projects) {
|
|
|
692
902
|
remaining = remaining.filter((p6) => !mergeIds.includes(p6.id));
|
|
693
903
|
remaining.push(merged);
|
|
694
904
|
logger.info(
|
|
695
|
-
|
|
905
|
+
t().mergedResult(toMerge.map((p6) => p6.name).join(", "), mergedName)
|
|
696
906
|
);
|
|
697
907
|
if (remaining.length < 2) break;
|
|
698
908
|
const more = await p.confirm({
|
|
699
|
-
message:
|
|
909
|
+
message: t().mergeMore,
|
|
700
910
|
initialValue: false
|
|
701
911
|
});
|
|
702
912
|
handleCancel(more);
|
|
@@ -770,7 +980,7 @@ async function runInterview(scannedProjects, availableEngines) {
|
|
|
770
980
|
};
|
|
771
981
|
});
|
|
772
982
|
const selectedIds = await p.multiselect({
|
|
773
|
-
message:
|
|
983
|
+
message: t().selectProjects,
|
|
774
984
|
options: projectOptions,
|
|
775
985
|
required: true
|
|
776
986
|
});
|
|
@@ -785,32 +995,32 @@ async function runInterview(scannedProjects, availableEngines) {
|
|
|
785
995
|
for (const meta of selectedMetas) {
|
|
786
996
|
const displayName = meta.children ? `${meta.name} (${meta.children.length} sub-projects)` : meta.name;
|
|
787
997
|
logger.plain(`
|
|
788
|
-
|
|
998
|
+
${t().configuring(displayName)}`);
|
|
789
999
|
const overrideDesc = await p.text({
|
|
790
|
-
message:
|
|
791
|
-
placeholder: meta.description?.slice(0, 80) ||
|
|
1000
|
+
message: t().descriptionFor(displayName),
|
|
1001
|
+
placeholder: meta.description?.slice(0, 80) || t().descriptionPlaceholder,
|
|
792
1002
|
defaultValue: ""
|
|
793
1003
|
});
|
|
794
1004
|
handleCancel(overrideDesc);
|
|
795
1005
|
const demoUrl = await p.text({
|
|
796
|
-
message:
|
|
1006
|
+
message: t().demoUrlFor(displayName),
|
|
797
1007
|
placeholder: meta.demoUrl || "none",
|
|
798
1008
|
defaultValue: meta.demoUrl || ""
|
|
799
1009
|
});
|
|
800
1010
|
handleCancel(demoUrl);
|
|
801
1011
|
const showSource = await p.confirm({
|
|
802
|
-
message:
|
|
1012
|
+
message: t().showSourceFor(displayName),
|
|
803
1013
|
initialValue: !!meta.remoteUrl
|
|
804
1014
|
});
|
|
805
1015
|
handleCancel(showSource);
|
|
806
1016
|
const role = await p.select({
|
|
807
|
-
message:
|
|
808
|
-
options: ROLE_OPTIONS
|
|
1017
|
+
message: t().roleFor(displayName),
|
|
1018
|
+
options: ROLE_OPTIONS()
|
|
809
1019
|
});
|
|
810
1020
|
handleCancel(role);
|
|
811
1021
|
const metricsInput = await p.text({
|
|
812
|
-
message:
|
|
813
|
-
placeholder:
|
|
1022
|
+
message: t().metricsFor(displayName),
|
|
1023
|
+
placeholder: t().optional,
|
|
814
1024
|
defaultValue: ""
|
|
815
1025
|
});
|
|
816
1026
|
handleCancel(metricsInput);
|
|
@@ -828,60 +1038,60 @@ async function runInterview(scannedProjects, availableEngines) {
|
|
|
828
1038
|
metrics
|
|
829
1039
|
});
|
|
830
1040
|
}
|
|
831
|
-
logger.header(
|
|
1041
|
+
logger.header(t().personalInfo);
|
|
832
1042
|
const name = await p.text({
|
|
833
|
-
message:
|
|
834
|
-
validate: (v) => v.length === 0 ?
|
|
1043
|
+
message: t().fullName,
|
|
1044
|
+
validate: (v) => v.length === 0 ? t().nameRequired : void 0
|
|
835
1045
|
});
|
|
836
1046
|
handleCancel(name);
|
|
837
1047
|
const tagline = await p.text({
|
|
838
|
-
message:
|
|
839
|
-
placeholder:
|
|
1048
|
+
message: t().tagline,
|
|
1049
|
+
placeholder: t().taglinePlaceholder
|
|
840
1050
|
});
|
|
841
1051
|
handleCancel(tagline);
|
|
842
1052
|
const bioChoice = await p.select({
|
|
843
|
-
message:
|
|
1053
|
+
message: t().bio,
|
|
844
1054
|
options: [
|
|
845
|
-
{ value: "auto", label:
|
|
846
|
-
{ value: "manual", label:
|
|
1055
|
+
{ value: "auto", label: t().bioAuto },
|
|
1056
|
+
{ value: "manual", label: t().bioManual }
|
|
847
1057
|
]
|
|
848
1058
|
});
|
|
849
1059
|
handleCancel(bioChoice);
|
|
850
1060
|
let bio = "auto";
|
|
851
1061
|
if (bioChoice === "manual") {
|
|
852
1062
|
bio = await p.text({
|
|
853
|
-
message:
|
|
1063
|
+
message: t().bioPrompt
|
|
854
1064
|
});
|
|
855
1065
|
handleCancel(bio);
|
|
856
1066
|
}
|
|
857
1067
|
const photoUrl = await p.text({
|
|
858
|
-
message:
|
|
1068
|
+
message: t().photoUrl,
|
|
859
1069
|
placeholder: "https://...",
|
|
860
1070
|
defaultValue: ""
|
|
861
1071
|
});
|
|
862
1072
|
handleCancel(photoUrl);
|
|
863
1073
|
const github = await p.text({
|
|
864
|
-
message:
|
|
1074
|
+
message: t().githubUser,
|
|
865
1075
|
defaultValue: ""
|
|
866
1076
|
});
|
|
867
1077
|
handleCancel(github);
|
|
868
1078
|
const twitter = await p.text({
|
|
869
|
-
message:
|
|
1079
|
+
message: t().twitterHandle,
|
|
870
1080
|
defaultValue: ""
|
|
871
1081
|
});
|
|
872
1082
|
handleCancel(twitter);
|
|
873
1083
|
const linkedin = await p.text({
|
|
874
|
-
message:
|
|
1084
|
+
message: t().linkedinUrl,
|
|
875
1085
|
defaultValue: ""
|
|
876
1086
|
});
|
|
877
1087
|
handleCancel(linkedin);
|
|
878
1088
|
const blogUrl = await p.text({
|
|
879
|
-
message:
|
|
1089
|
+
message: t().blogUrl,
|
|
880
1090
|
defaultValue: ""
|
|
881
1091
|
});
|
|
882
1092
|
handleCancel(blogUrl);
|
|
883
1093
|
const email = await p.text({
|
|
884
|
-
message:
|
|
1094
|
+
message: t().contactEmail,
|
|
885
1095
|
defaultValue: ""
|
|
886
1096
|
});
|
|
887
1097
|
handleCancel(email);
|
|
@@ -898,37 +1108,37 @@ async function runInterview(scannedProjects, availableEngines) {
|
|
|
898
1108
|
email: email || void 0
|
|
899
1109
|
}
|
|
900
1110
|
};
|
|
901
|
-
logger.header(
|
|
1111
|
+
logger.header(t().designPrefs);
|
|
902
1112
|
const theme = await p.select({
|
|
903
|
-
message:
|
|
904
|
-
options: THEME_OPTIONS
|
|
1113
|
+
message: t().theme,
|
|
1114
|
+
options: THEME_OPTIONS()
|
|
905
1115
|
});
|
|
906
1116
|
handleCancel(theme);
|
|
907
1117
|
const accentColor = await p.select({
|
|
908
|
-
message:
|
|
1118
|
+
message: t().accentColor,
|
|
909
1119
|
options: [
|
|
910
|
-
...DEFAULT_ACCENT_COLORS,
|
|
911
|
-
{ value: "custom", label:
|
|
1120
|
+
...DEFAULT_ACCENT_COLORS(),
|
|
1121
|
+
{ value: "custom", label: t().customHex }
|
|
912
1122
|
]
|
|
913
1123
|
});
|
|
914
1124
|
handleCancel(accentColor);
|
|
915
1125
|
let finalAccent = accentColor;
|
|
916
1126
|
if (accentColor === "custom") {
|
|
917
1127
|
finalAccent = await p.text({
|
|
918
|
-
message:
|
|
1128
|
+
message: t().customHexPrompt,
|
|
919
1129
|
placeholder: "#7c3aed",
|
|
920
|
-
validate: (v) => /^#[0-9a-fA-F]{6}$/.test(v) ? void 0 :
|
|
1130
|
+
validate: (v) => /^#[0-9a-fA-F]{6}$/.test(v) ? void 0 : t().invalidHex
|
|
921
1131
|
});
|
|
922
1132
|
handleCancel(finalAccent);
|
|
923
1133
|
}
|
|
924
1134
|
const font = await p.select({
|
|
925
|
-
message:
|
|
926
|
-
options: FONT_OPTIONS
|
|
1135
|
+
message: t().font,
|
|
1136
|
+
options: FONT_OPTIONS()
|
|
927
1137
|
});
|
|
928
1138
|
handleCancel(font);
|
|
929
1139
|
const animationLevel = await p.select({
|
|
930
|
-
message:
|
|
931
|
-
options: ANIMATION_OPTIONS
|
|
1140
|
+
message: t().animationLevel,
|
|
1141
|
+
options: ANIMATION_OPTIONS()
|
|
932
1142
|
});
|
|
933
1143
|
handleCancel(animationLevel);
|
|
934
1144
|
const style = {
|
|
@@ -937,10 +1147,10 @@ async function runInterview(scannedProjects, availableEngines) {
|
|
|
937
1147
|
font,
|
|
938
1148
|
animationLevel
|
|
939
1149
|
};
|
|
940
|
-
logger.header(
|
|
1150
|
+
logger.header(t().sections);
|
|
941
1151
|
const additionalSections = await p.multiselect({
|
|
942
|
-
message:
|
|
943
|
-
options: SECTION_OPTIONS,
|
|
1152
|
+
message: t().additionalSections,
|
|
1153
|
+
options: SECTION_OPTIONS(),
|
|
944
1154
|
required: false
|
|
945
1155
|
});
|
|
946
1156
|
handleCancel(additionalSections);
|
|
@@ -949,37 +1159,37 @@ async function runInterview(scannedProjects, availableEngines) {
|
|
|
949
1159
|
"projects",
|
|
950
1160
|
...additionalSections
|
|
951
1161
|
];
|
|
952
|
-
logger.header(
|
|
1162
|
+
logger.header(t().aiEngine);
|
|
953
1163
|
let engine;
|
|
954
1164
|
if (availableEngines.length === 1) {
|
|
955
1165
|
engine = availableEngines[0];
|
|
956
|
-
logger.info(
|
|
1166
|
+
logger.info(t().engineUsing(engine));
|
|
957
1167
|
} else {
|
|
958
|
-
const filteredEngineOptions = ENGINE_OPTIONS.filter(
|
|
1168
|
+
const filteredEngineOptions = ENGINE_OPTIONS().filter(
|
|
959
1169
|
(o) => availableEngines.includes(o.value)
|
|
960
1170
|
);
|
|
961
1171
|
engine = await p.select({
|
|
962
|
-
message:
|
|
1172
|
+
message: t().engineSelect,
|
|
963
1173
|
options: filteredEngineOptions
|
|
964
1174
|
});
|
|
965
1175
|
handleCancel(engine);
|
|
966
1176
|
}
|
|
967
|
-
logger.header(
|
|
1177
|
+
logger.header(t().deployment);
|
|
968
1178
|
const deployPlatform = await p.select({
|
|
969
|
-
message:
|
|
970
|
-
options: DEPLOY_OPTIONS
|
|
1179
|
+
message: t().deployTo,
|
|
1180
|
+
options: DEPLOY_OPTIONS()
|
|
971
1181
|
});
|
|
972
1182
|
handleCancel(deployPlatform);
|
|
973
1183
|
let projectName = "";
|
|
974
1184
|
if (deployPlatform !== "local") {
|
|
975
1185
|
projectName = await p.text({
|
|
976
|
-
message:
|
|
1186
|
+
message: t().projectNamePrompt,
|
|
977
1187
|
placeholder: "my-shipfolio",
|
|
978
|
-
validate: (v) => /^[a-z0-9-]+$/.test(v) ? void 0 :
|
|
1188
|
+
validate: (v) => /^[a-z0-9-]+$/.test(v) ? void 0 : t().projectNameInvalid
|
|
979
1189
|
});
|
|
980
1190
|
handleCancel(projectName);
|
|
981
1191
|
}
|
|
982
|
-
p.outro(
|
|
1192
|
+
p.outro(t().configComplete);
|
|
983
1193
|
return {
|
|
984
1194
|
projects: projectEntries,
|
|
985
1195
|
owner,
|
|
@@ -998,6 +1208,7 @@ var init_interviewer = __esm({
|
|
|
998
1208
|
init_esm_shims();
|
|
999
1209
|
init_questions();
|
|
1000
1210
|
init_logger();
|
|
1211
|
+
init_i18n();
|
|
1001
1212
|
}
|
|
1002
1213
|
});
|
|
1003
1214
|
|
|
@@ -1026,42 +1237,14 @@ var init_builder = __esm({
|
|
|
1026
1237
|
});
|
|
1027
1238
|
|
|
1028
1239
|
// src/orchestrator/prompt-builder.ts
|
|
1029
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
1030
|
-
import { join as join2, dirname } from "path";
|
|
1031
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1032
|
-
function getPromptsDir() {
|
|
1033
|
-
const currentDir = dirname(fileURLToPath2(import.meta.url));
|
|
1034
|
-
const candidates = [
|
|
1035
|
-
join2(currentDir, "../../prompts"),
|
|
1036
|
-
join2(currentDir, "../prompts"),
|
|
1037
|
-
join2(currentDir, "../../../prompts")
|
|
1038
|
-
];
|
|
1039
|
-
return candidates[0];
|
|
1040
|
-
}
|
|
1041
|
-
async function loadTemplate(filename) {
|
|
1042
|
-
const promptsDir = getPromptsDir();
|
|
1043
|
-
const candidates = [
|
|
1044
|
-
join2(promptsDir, filename),
|
|
1045
|
-
join2(dirname(fileURLToPath2(import.meta.url)), "../../prompts", filename),
|
|
1046
|
-
join2(dirname(fileURLToPath2(import.meta.url)), "../../../prompts", filename)
|
|
1047
|
-
];
|
|
1048
|
-
for (const candidate of candidates) {
|
|
1049
|
-
try {
|
|
1050
|
-
return await readFile2(candidate, "utf-8");
|
|
1051
|
-
} catch {
|
|
1052
|
-
continue;
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
throw new Error(`Prompt template not found: ${filename}`);
|
|
1056
|
-
}
|
|
1057
1240
|
async function buildFreshPrompt(spec) {
|
|
1058
|
-
let template =
|
|
1241
|
+
let template = FRESH_BUILD_TEMPLATE;
|
|
1059
1242
|
const sectionsText = spec.sections.map((s) => `- ${s}`).join("\n");
|
|
1060
1243
|
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);
|
|
1061
1244
|
return template;
|
|
1062
1245
|
}
|
|
1063
1246
|
async function buildUpdatePrompt(existingConfig, diff) {
|
|
1064
|
-
let template =
|
|
1247
|
+
let template = UPDATE_TEMPLATE;
|
|
1065
1248
|
const newProjectsText = diff.newProjects.length > 0 ? diff.newProjects.map(
|
|
1066
1249
|
(p6) => `- ${p6.name}: ${p6.description}
|
|
1067
1250
|
Tech: ${p6.techStack.join(", ")}
|
|
@@ -1079,10 +1262,167 @@ async function buildUpdatePrompt(existingConfig, diff) {
|
|
|
1079
1262
|
).replace("{{NEW_PROJECTS}}", newProjectsText).replace("{{UPDATED_PROJECTS}}", updatedProjectsText).replace("{{REMOVED_PROJECTS}}", removedProjectsText).replace("{{PERSONAL_INFO_DIFF}}", "No changes");
|
|
1080
1263
|
return template;
|
|
1081
1264
|
}
|
|
1265
|
+
var FRESH_BUILD_TEMPLATE, UPDATE_TEMPLATE;
|
|
1082
1266
|
var init_prompt_builder = __esm({
|
|
1083
1267
|
"src/orchestrator/prompt-builder.ts"() {
|
|
1084
1268
|
"use strict";
|
|
1085
1269
|
init_esm_shims();
|
|
1270
|
+
FRESH_BUILD_TEMPLATE = `# Task
|
|
1271
|
+
|
|
1272
|
+
Generate a complete, production-ready personal portfolio website.
|
|
1273
|
+
All data and design preferences are provided in the spec below.
|
|
1274
|
+
Output a fully working Next.js project that builds and deploys as a static site.
|
|
1275
|
+
|
|
1276
|
+
# Spec
|
|
1277
|
+
|
|
1278
|
+
{{SPEC_JSON}}
|
|
1279
|
+
|
|
1280
|
+
# Technical Requirements
|
|
1281
|
+
|
|
1282
|
+
- Next.js 15 with App Router and TypeScript
|
|
1283
|
+
- Tailwind CSS v4 for styling
|
|
1284
|
+
- shadcn/ui components (initialize with \`npx shadcn@latest init --yes\`)
|
|
1285
|
+
- Static export: set \`output: 'export'\` in next.config.ts
|
|
1286
|
+
- Build command: \`npm run build\` producing \`out/\` directory
|
|
1287
|
+
- Zero external API calls at runtime -- all data is embedded in source
|
|
1288
|
+
- All project data in \`src/data/projects.ts\` as typed constants
|
|
1289
|
+
- Include \`@media print\` stylesheet in \`src/app/globals.css\` for PDF export
|
|
1290
|
+
- Responsive: mobile-first with Tailwind breakpoints (sm, md, lg, xl)
|
|
1291
|
+
- Lighthouse performance score 95+
|
|
1292
|
+
- Semantic HTML with ARIA labels and keyboard navigation
|
|
1293
|
+
- No emoji anywhere in text, UI, code, or comments
|
|
1294
|
+
- Use lucide-react for icons
|
|
1295
|
+
|
|
1296
|
+
# Design Direction
|
|
1297
|
+
|
|
1298
|
+
- Theme: {{THEME}}
|
|
1299
|
+
- Accent color: {{ACCENT_COLOR}}
|
|
1300
|
+
- Animation level: {{ANIMATION_LEVEL}}
|
|
1301
|
+
- Font: {{FONT}}
|
|
1302
|
+
|
|
1303
|
+
Design guidelines:
|
|
1304
|
+
- Typography-driven layout with large, bold headings
|
|
1305
|
+
- Generous whitespace between sections
|
|
1306
|
+
- Single accent color for interactive elements and highlights only
|
|
1307
|
+
- Project cards should feel substantial but clean
|
|
1308
|
+
- The site must look hand-crafted, not template-generated
|
|
1309
|
+
- Dark themes: use zinc/slate backgrounds, not pure black
|
|
1310
|
+
- Light themes: use warm whites and subtle grays
|
|
1311
|
+
|
|
1312
|
+
# Content Generation
|
|
1313
|
+
|
|
1314
|
+
For each project in the spec:
|
|
1315
|
+
- Write a 2-3 sentence narrative description based on the README content and tech stack
|
|
1316
|
+
- Focus on what it does and why it matters
|
|
1317
|
+
- If user provided an override description, use that instead
|
|
1318
|
+
- Maintain consistent voice across all descriptions
|
|
1319
|
+
|
|
1320
|
+
For the bio (if set to "auto"):
|
|
1321
|
+
- Generate a professional, authentic bio based on the project portfolio
|
|
1322
|
+
- Emphasize shipping velocity and breadth
|
|
1323
|
+
- Tone: confident, direct, no buzzwords or fluff
|
|
1324
|
+
|
|
1325
|
+
# Sections to Include
|
|
1326
|
+
|
|
1327
|
+
{{SECTIONS_LIST}}
|
|
1328
|
+
|
|
1329
|
+
Hero and Projects are always included. Additional sections as specified.
|
|
1330
|
+
|
|
1331
|
+
# Print / PDF Styles
|
|
1332
|
+
|
|
1333
|
+
In globals.css, add @media print rules:
|
|
1334
|
+
- Single-column layout
|
|
1335
|
+
- Hide navigation, footer, animations
|
|
1336
|
+
- Preserve background colors (user will print with backgrounds enabled)
|
|
1337
|
+
- Show link URLs inline: \`a[href]::after { content: " (" attr(href) ")"; }\`
|
|
1338
|
+
- Avoid page breaks inside project cards
|
|
1339
|
+
- A4-friendly margins and font sizes
|
|
1340
|
+
|
|
1341
|
+
# File Structure to Generate
|
|
1342
|
+
|
|
1343
|
+
\`\`\`
|
|
1344
|
+
next.config.ts
|
|
1345
|
+
package.json
|
|
1346
|
+
tailwind.config.ts
|
|
1347
|
+
tsconfig.json
|
|
1348
|
+
postcss.config.mjs
|
|
1349
|
+
components.json
|
|
1350
|
+
src/app/layout.tsx
|
|
1351
|
+
src/app/page.tsx
|
|
1352
|
+
src/app/globals.css
|
|
1353
|
+
src/components/ui/ (shadcn components as needed)
|
|
1354
|
+
src/components/hero.tsx
|
|
1355
|
+
src/components/project-card.tsx
|
|
1356
|
+
src/components/project-grid.tsx
|
|
1357
|
+
src/components/skills.tsx
|
|
1358
|
+
src/components/about.tsx
|
|
1359
|
+
src/components/timeline.tsx
|
|
1360
|
+
src/components/contact.tsx
|
|
1361
|
+
src/components/navigation.tsx
|
|
1362
|
+
src/components/footer.tsx
|
|
1363
|
+
src/data/projects.ts
|
|
1364
|
+
src/data/owner.ts
|
|
1365
|
+
src/lib/utils.ts
|
|
1366
|
+
public/favicon.svg
|
|
1367
|
+
shipfolio.config.json
|
|
1368
|
+
\`\`\`
|
|
1369
|
+
|
|
1370
|
+
# Important
|
|
1371
|
+
|
|
1372
|
+
- Generate ALL files needed for the project to build successfully
|
|
1373
|
+
- Include all shadcn/ui component files that are referenced
|
|
1374
|
+
- The \`package.json\` must include all dependencies
|
|
1375
|
+
- \`npm install && npm run build\` must succeed without errors
|
|
1376
|
+
- Do not use next/image (incompatible with static export) -- use standard <img> tags
|
|
1377
|
+
- Do not use features that require a server (API routes, middleware, ISR)
|
|
1378
|
+
`;
|
|
1379
|
+
UPDATE_TEMPLATE = `# Task
|
|
1380
|
+
|
|
1381
|
+
Update an existing portfolio website previously generated by shipfolio.
|
|
1382
|
+
You must preserve the existing design system, layout structure, component
|
|
1383
|
+
architecture, and any custom modifications the user has made.
|
|
1384
|
+
|
|
1385
|
+
# Existing Site Configuration
|
|
1386
|
+
|
|
1387
|
+
{{EXISTING_CONFIG_JSON}}
|
|
1388
|
+
|
|
1389
|
+
# Changes to Apply
|
|
1390
|
+
|
|
1391
|
+
## New Projects to Add
|
|
1392
|
+
{{NEW_PROJECTS}}
|
|
1393
|
+
|
|
1394
|
+
## Projects to Update (new commits since last scan)
|
|
1395
|
+
{{UPDATED_PROJECTS}}
|
|
1396
|
+
|
|
1397
|
+
## Projects to Remove
|
|
1398
|
+
{{REMOVED_PROJECTS}}
|
|
1399
|
+
|
|
1400
|
+
## Updated Personal Info
|
|
1401
|
+
{{PERSONAL_INFO_DIFF}}
|
|
1402
|
+
|
|
1403
|
+
# Rules
|
|
1404
|
+
|
|
1405
|
+
1. Do NOT change the overall layout, color scheme, or design system
|
|
1406
|
+
2. Do NOT reorganize existing components or rename files
|
|
1407
|
+
3. Only modify files that need changes for the specified updates
|
|
1408
|
+
4. For new projects: follow the exact same card format and component pattern
|
|
1409
|
+
as existing project cards
|
|
1410
|
+
5. Preserve all custom CSS, custom components, and manual edits
|
|
1411
|
+
6. Update src/data/projects.ts with new/changed/removed project data
|
|
1412
|
+
7. Update src/data/owner.ts if personal info changed
|
|
1413
|
+
8. Update shipfolio.config.json with new timestamps and project list
|
|
1414
|
+
9. If a new section type is needed, create it following the existing
|
|
1415
|
+
component patterns and design tokens in the codebase
|
|
1416
|
+
10. No emoji anywhere
|
|
1417
|
+
|
|
1418
|
+
# Technical Notes
|
|
1419
|
+
|
|
1420
|
+
- The site uses Next.js 15 + Tailwind CSS + shadcn/ui
|
|
1421
|
+
- Static export via \`output: 'export'\` in next.config.ts
|
|
1422
|
+
- Do not break the build -- \`npm run build\` must succeed
|
|
1423
|
+
- Do not add new dependencies unless absolutely necessary
|
|
1424
|
+
- Keep the existing @media print styles working
|
|
1425
|
+
`;
|
|
1086
1426
|
}
|
|
1087
1427
|
});
|
|
1088
1428
|
|
|
@@ -1150,11 +1490,17 @@ async function generateWithClaude(prompt, outputDir) {
|
|
|
1150
1490
|
const fullPrompt = `${prompt}
|
|
1151
1491
|
|
|
1152
1492
|
Create all files in the current working directory. Do not ask questions, just generate all files.`;
|
|
1153
|
-
await execa2("claude", [
|
|
1493
|
+
await execa2("claude", [
|
|
1494
|
+
"-p",
|
|
1495
|
+
"--permission-mode",
|
|
1496
|
+
"bypassPermissions"
|
|
1497
|
+
], {
|
|
1154
1498
|
cwd: outputDir,
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1499
|
+
input: fullPrompt,
|
|
1500
|
+
stdout: "ignore",
|
|
1501
|
+
stderr: "pipe",
|
|
1502
|
+
timeout: 6e5
|
|
1503
|
+
// 10 minute timeout
|
|
1158
1504
|
});
|
|
1159
1505
|
spinner.succeed("Site generated with Claude Code");
|
|
1160
1506
|
} catch (error) {
|
|
@@ -1181,7 +1527,7 @@ Create all files in the current working directory. Do not ask questions, just ge
|
|
|
1181
1527
|
await execa3("codex", ["--quiet", "--full-auto", fullPrompt], {
|
|
1182
1528
|
cwd: outputDir,
|
|
1183
1529
|
stdio: "pipe",
|
|
1184
|
-
timeout:
|
|
1530
|
+
timeout: 6e5
|
|
1185
1531
|
});
|
|
1186
1532
|
spinner.succeed("Site generated with Codex");
|
|
1187
1533
|
} catch (error) {
|
|
@@ -1199,7 +1545,7 @@ var init_codex = __esm({
|
|
|
1199
1545
|
// src/orchestrator/engines/v0.ts
|
|
1200
1546
|
import OpenAI from "openai";
|
|
1201
1547
|
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
1202
|
-
import { join as
|
|
1548
|
+
import { join as join2, dirname } from "path";
|
|
1203
1549
|
import ora4 from "ora";
|
|
1204
1550
|
async function generateWithV0(prompt, outputDir, apiKey) {
|
|
1205
1551
|
const spinner = ora4("Generating site with v0...").start();
|
|
@@ -1232,14 +1578,14 @@ async function generateWithV0(prompt, outputDir, apiKey) {
|
|
|
1232
1578
|
}
|
|
1233
1579
|
spinner.text = `Writing ${files.length} files...`;
|
|
1234
1580
|
for (const file of files) {
|
|
1235
|
-
const filePath =
|
|
1236
|
-
await mkdir2(
|
|
1581
|
+
const filePath = join2(outputDir, file.filename);
|
|
1582
|
+
await mkdir2(dirname(filePath), { recursive: true });
|
|
1237
1583
|
await writeFile2(filePath, file.content, "utf-8");
|
|
1238
1584
|
}
|
|
1239
1585
|
const hasPkgJson = files.some((f) => f.filename === "package.json");
|
|
1240
1586
|
if (!hasPkgJson) {
|
|
1241
1587
|
await writeFile2(
|
|
1242
|
-
|
|
1588
|
+
join2(outputDir, "package.json"),
|
|
1243
1589
|
JSON.stringify(
|
|
1244
1590
|
{
|
|
1245
1591
|
name: "shipfolio-site",
|
|
@@ -1374,8 +1720,7 @@ var init_validator = __esm({
|
|
|
1374
1720
|
|
|
1375
1721
|
// src/orchestrator/index.ts
|
|
1376
1722
|
import ora5 from "ora";
|
|
1377
|
-
async function
|
|
1378
|
-
await ensureDir(outputDir);
|
|
1723
|
+
async function runEngine(engine, prompt, outputDir) {
|
|
1379
1724
|
switch (engine) {
|
|
1380
1725
|
case "claude":
|
|
1381
1726
|
await generateWithClaude(prompt, outputDir);
|
|
@@ -1394,12 +1739,33 @@ async function generateSite(engine, prompt, outputDir) {
|
|
|
1394
1739
|
break;
|
|
1395
1740
|
}
|
|
1396
1741
|
}
|
|
1397
|
-
|
|
1742
|
+
}
|
|
1743
|
+
async function generateSite(engine, prompt, outputDir) {
|
|
1744
|
+
await ensureDir(outputDir);
|
|
1745
|
+
await runEngine(engine, prompt, outputDir);
|
|
1746
|
+
let validation = await validateGeneratedSite(outputDir);
|
|
1398
1747
|
if (!validation.valid) {
|
|
1399
1748
|
logger.warn("Generated site has issues:");
|
|
1400
1749
|
for (const err of validation.errors) {
|
|
1401
1750
|
logger.warn(` ${err}`);
|
|
1402
1751
|
}
|
|
1752
|
+
logger.info("Retrying generation with error feedback...");
|
|
1753
|
+
const fixPrompt = [
|
|
1754
|
+
"The previous generation was incomplete. The following required files are missing:",
|
|
1755
|
+
...validation.errors.map((e) => `- ${e}`),
|
|
1756
|
+
"",
|
|
1757
|
+
"Please create ALL missing files. Here is the original specification:",
|
|
1758
|
+
"",
|
|
1759
|
+
prompt
|
|
1760
|
+
].join("\n");
|
|
1761
|
+
await runEngine(engine, fixPrompt, outputDir);
|
|
1762
|
+
validation = await validateGeneratedSite(outputDir);
|
|
1763
|
+
if (!validation.valid) {
|
|
1764
|
+
logger.warn("Still missing files after retry:");
|
|
1765
|
+
for (const err of validation.errors) {
|
|
1766
|
+
logger.warn(` ${err}`);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1403
1769
|
}
|
|
1404
1770
|
}
|
|
1405
1771
|
async function buildSite(siteDir) {
|
|
@@ -1424,6 +1790,21 @@ async function buildSite(siteDir) {
|
|
|
1424
1790
|
}
|
|
1425
1791
|
return true;
|
|
1426
1792
|
}
|
|
1793
|
+
async function retryBuild(engine, siteDir, buildError, originalPrompt) {
|
|
1794
|
+
logger.info("Retrying: feeding build error back to AI...");
|
|
1795
|
+
const fixPrompt = `The site generation produced build errors. Fix the following errors without changing the overall design or structure:
|
|
1796
|
+
|
|
1797
|
+
${buildError}
|
|
1798
|
+
|
|
1799
|
+
Original prompt for context:
|
|
1800
|
+
${originalPrompt.slice(0, 2e3)}`;
|
|
1801
|
+
try {
|
|
1802
|
+
await runEngine(engine, fixPrompt, siteDir);
|
|
1803
|
+
return await buildSite(siteDir);
|
|
1804
|
+
} catch {
|
|
1805
|
+
return false;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1427
1808
|
var init_orchestrator = __esm({
|
|
1428
1809
|
"src/orchestrator/index.ts"() {
|
|
1429
1810
|
"use strict";
|
|
@@ -1610,15 +1991,15 @@ var init_github = __esm({
|
|
|
1610
1991
|
});
|
|
1611
1992
|
|
|
1612
1993
|
// src/deployer/index.ts
|
|
1613
|
-
import { join as
|
|
1994
|
+
import { join as join3 } from "path";
|
|
1614
1995
|
async function deploy(siteDir, platform, projectName) {
|
|
1615
1996
|
if (platform === "local") {
|
|
1616
|
-
logger.info(`Site built at ${
|
|
1997
|
+
logger.info(`Site built at ${join3(siteDir, "out/")}`);
|
|
1617
1998
|
logger.info("Run `npx serve ./out` in the site directory to preview.");
|
|
1618
1999
|
return null;
|
|
1619
2000
|
}
|
|
1620
2001
|
await ensureAuth(platform);
|
|
1621
|
-
const distDir =
|
|
2002
|
+
const distDir = join3(siteDir, "out");
|
|
1622
2003
|
let url;
|
|
1623
2004
|
if (platform === "cloudflare") {
|
|
1624
2005
|
url = await deployToCloudflare(distDir, projectName);
|
|
@@ -1642,11 +2023,11 @@ var init_deployer = __esm({
|
|
|
1642
2023
|
|
|
1643
2024
|
// src/pdf/index.ts
|
|
1644
2025
|
import { createServer } from "http";
|
|
1645
|
-
import { join as
|
|
2026
|
+
import { join as join4 } from "path";
|
|
1646
2027
|
import ora8 from "ora";
|
|
1647
2028
|
async function exportPdf(siteDir, outputPath) {
|
|
1648
|
-
const distDir =
|
|
1649
|
-
const pdfPath = outputPath ||
|
|
2029
|
+
const distDir = join4(siteDir, "out");
|
|
2030
|
+
const pdfPath = outputPath || join4(distDir, "shipfolio.pdf");
|
|
1650
2031
|
const spinner = ora8("Exporting PDF...").start();
|
|
1651
2032
|
const server = await startStaticServer(distDir);
|
|
1652
2033
|
const port = server.address()?.port || 3456;
|
|
@@ -1702,7 +2083,7 @@ function startStaticServer(dir) {
|
|
|
1702
2083
|
});
|
|
1703
2084
|
}
|
|
1704
2085
|
async function startPreviewServer(siteDir, port = 3e3) {
|
|
1705
|
-
const distDir =
|
|
2086
|
+
const distDir = join4(siteDir, "out");
|
|
1706
2087
|
const handler = await import("serve-handler");
|
|
1707
2088
|
const server = createServer((req, res) => {
|
|
1708
2089
|
return handler.default(req, res, {
|
|
@@ -1726,6 +2107,49 @@ var init_pdf = __esm({
|
|
|
1726
2107
|
}
|
|
1727
2108
|
});
|
|
1728
2109
|
|
|
2110
|
+
// src/utils/draft.ts
|
|
2111
|
+
import { readFile as readFile2, writeFile as writeFile3, mkdir as mkdir3, unlink } from "fs/promises";
|
|
2112
|
+
import { join as join5 } from "path";
|
|
2113
|
+
import { homedir } from "os";
|
|
2114
|
+
async function saveDraft(interviewResult) {
|
|
2115
|
+
try {
|
|
2116
|
+
await mkdir3(DRAFT_DIR, { recursive: true });
|
|
2117
|
+
const draft = {
|
|
2118
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2119
|
+
interviewResult
|
|
2120
|
+
};
|
|
2121
|
+
await writeFile3(DRAFT_FILE, JSON.stringify(draft, null, 2), "utf-8");
|
|
2122
|
+
logger.info(t().draftSaved);
|
|
2123
|
+
} catch {
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
async function loadDraft() {
|
|
2127
|
+
try {
|
|
2128
|
+
const content = await readFile2(DRAFT_FILE, "utf-8");
|
|
2129
|
+
return JSON.parse(content);
|
|
2130
|
+
} catch {
|
|
2131
|
+
return null;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
async function clearDraft() {
|
|
2135
|
+
try {
|
|
2136
|
+
await unlink(DRAFT_FILE);
|
|
2137
|
+
logger.info(t().draftCleared);
|
|
2138
|
+
} catch {
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
var DRAFT_DIR, DRAFT_FILE;
|
|
2142
|
+
var init_draft = __esm({
|
|
2143
|
+
"src/utils/draft.ts"() {
|
|
2144
|
+
"use strict";
|
|
2145
|
+
init_esm_shims();
|
|
2146
|
+
init_logger();
|
|
2147
|
+
init_i18n();
|
|
2148
|
+
DRAFT_DIR = join5(homedir(), ".shipfolio");
|
|
2149
|
+
DRAFT_FILE = join5(DRAFT_DIR, "draft.json");
|
|
2150
|
+
}
|
|
2151
|
+
});
|
|
2152
|
+
|
|
1729
2153
|
// src/commands/init.ts
|
|
1730
2154
|
var init_exports = {};
|
|
1731
2155
|
__export(init_exports, {
|
|
@@ -1735,7 +2159,7 @@ import { resolve } from "path";
|
|
|
1735
2159
|
import { join as join6 } from "path";
|
|
1736
2160
|
import * as p3 from "@clack/prompts";
|
|
1737
2161
|
async function initCommand(options) {
|
|
1738
|
-
logger.header("shipfolio v1.0.
|
|
2162
|
+
logger.header("shipfolio v1.0.4");
|
|
1739
2163
|
logger.info("Detecting AI engines...");
|
|
1740
2164
|
const engines = await detectEngines();
|
|
1741
2165
|
const availableTypes = getAvailableEngineTypes(engines);
|
|
@@ -1769,7 +2193,24 @@ async function initCommand(options) {
|
|
|
1769
2193
|
]);
|
|
1770
2194
|
logger.table(tableRows);
|
|
1771
2195
|
logger.blank();
|
|
1772
|
-
|
|
2196
|
+
let interviewResult;
|
|
2197
|
+
const draft = await loadDraft();
|
|
2198
|
+
if (draft) {
|
|
2199
|
+
logger.info(t().draftFound);
|
|
2200
|
+
const useDraft = await p3.confirm({
|
|
2201
|
+
message: t().draftLoadPrompt,
|
|
2202
|
+
initialValue: true
|
|
2203
|
+
});
|
|
2204
|
+
if (!p3.isCancel(useDraft) && useDraft) {
|
|
2205
|
+
interviewResult = draft.interviewResult;
|
|
2206
|
+
await clearDraft();
|
|
2207
|
+
} else {
|
|
2208
|
+
interviewResult = await runInterview(scannedProjects, availableTypes);
|
|
2209
|
+
}
|
|
2210
|
+
} else {
|
|
2211
|
+
interviewResult = await runInterview(scannedProjects, availableTypes);
|
|
2212
|
+
}
|
|
2213
|
+
await saveDraft(interviewResult);
|
|
1773
2214
|
const spec = buildSpec(interviewResult);
|
|
1774
2215
|
const prompt = await buildFreshPrompt(spec);
|
|
1775
2216
|
const outputDir = resolve(options.output || "./shipfolio-site");
|
|
@@ -1777,8 +2218,7 @@ async function initCommand(options) {
|
|
|
1777
2218
|
await generateSite(spec.engine, prompt, outputDir);
|
|
1778
2219
|
let buildSuccess = await buildSite(outputDir);
|
|
1779
2220
|
if (!buildSuccess) {
|
|
1780
|
-
|
|
1781
|
-
buildSuccess = await buildSite(outputDir);
|
|
2221
|
+
buildSuccess = await retryBuild(spec.engine, outputDir, "Build failed. Check missing files and fix errors.", prompt);
|
|
1782
2222
|
if (!buildSuccess) {
|
|
1783
2223
|
logger.error("Build failed after retry. Check the output directory for details.");
|
|
1784
2224
|
process.exit(1);
|
|
@@ -1820,6 +2260,7 @@ async function initCommand(options) {
|
|
|
1820
2260
|
...spec,
|
|
1821
2261
|
sitePath: outputDir
|
|
1822
2262
|
});
|
|
2263
|
+
await clearDraft();
|
|
1823
2264
|
logger.header("Done.");
|
|
1824
2265
|
if (spec.deploy.url) {
|
|
1825
2266
|
logger.info(`Site: ${spec.deploy.url}`);
|
|
@@ -1844,6 +2285,8 @@ var init_init = __esm({
|
|
|
1844
2285
|
init_pdf();
|
|
1845
2286
|
init_fs();
|
|
1846
2287
|
init_logger();
|
|
2288
|
+
init_draft();
|
|
2289
|
+
init_i18n();
|
|
1847
2290
|
}
|
|
1848
2291
|
});
|
|
1849
2292
|
|
|
@@ -2160,10 +2603,12 @@ async function previewCommand(options) {
|
|
|
2160
2603
|
}
|
|
2161
2604
|
|
|
2162
2605
|
// src/index.ts
|
|
2606
|
+
init_i18n();
|
|
2607
|
+
initLocale();
|
|
2163
2608
|
var program = new Command();
|
|
2164
2609
|
program.name("shipfolio").description(
|
|
2165
2610
|
"Generate and deploy your personal portfolio site from local projects using AI"
|
|
2166
|
-
).version("1.0.
|
|
2611
|
+
).version("1.0.4");
|
|
2167
2612
|
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);
|
|
2168
2613
|
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);
|
|
2169
2614
|
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);
|