shipfolio 1.0.1 → 1.0.3
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 +648 -135
- 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
|
});
|
|
@@ -655,6 +865,108 @@ function handleCancel(value) {
|
|
|
655
865
|
process.exit(0);
|
|
656
866
|
}
|
|
657
867
|
}
|
|
868
|
+
async function runMergeStep(projects) {
|
|
869
|
+
const shouldMerge = await p.confirm({
|
|
870
|
+
message: t().mergePrompt,
|
|
871
|
+
initialValue: false
|
|
872
|
+
});
|
|
873
|
+
handleCancel(shouldMerge);
|
|
874
|
+
if (!shouldMerge) return projects;
|
|
875
|
+
let remaining = [...projects];
|
|
876
|
+
while (remaining.length >= 2) {
|
|
877
|
+
const mergeIds = await p.multiselect({
|
|
878
|
+
message: t().selectToMerge,
|
|
879
|
+
options: remaining.map((proj) => ({
|
|
880
|
+
value: proj.id,
|
|
881
|
+
label: `${proj.name} (${proj.localPath})`,
|
|
882
|
+
hint: proj.techStack.slice(0, 3).join(", ")
|
|
883
|
+
})),
|
|
884
|
+
required: true
|
|
885
|
+
});
|
|
886
|
+
handleCancel(mergeIds);
|
|
887
|
+
if (mergeIds.length < 2) {
|
|
888
|
+
logger.warn(t().mergeNeedTwo);
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
const toMerge = mergeIds.map(
|
|
892
|
+
(id) => remaining.find((p6) => p6.id === id)
|
|
893
|
+
);
|
|
894
|
+
const defaultName = toMerge.map((p6) => p6.name).join(" + ");
|
|
895
|
+
const mergedName = await p.text({
|
|
896
|
+
message: t().mergedNamePrompt,
|
|
897
|
+
placeholder: defaultName,
|
|
898
|
+
defaultValue: defaultName
|
|
899
|
+
});
|
|
900
|
+
handleCancel(mergedName);
|
|
901
|
+
const merged = mergeProjects(toMerge, mergedName);
|
|
902
|
+
remaining = remaining.filter((p6) => !mergeIds.includes(p6.id));
|
|
903
|
+
remaining.push(merged);
|
|
904
|
+
logger.info(
|
|
905
|
+
t().mergedResult(toMerge.map((p6) => p6.name).join(", "), mergedName)
|
|
906
|
+
);
|
|
907
|
+
if (remaining.length < 2) break;
|
|
908
|
+
const more = await p.confirm({
|
|
909
|
+
message: t().mergeMore,
|
|
910
|
+
initialValue: false
|
|
911
|
+
});
|
|
912
|
+
handleCancel(more);
|
|
913
|
+
if (!more) break;
|
|
914
|
+
}
|
|
915
|
+
return remaining;
|
|
916
|
+
}
|
|
917
|
+
function mergeProjects(projects, name) {
|
|
918
|
+
const allChildren = [];
|
|
919
|
+
for (const proj of projects) {
|
|
920
|
+
if (proj.children && proj.children.length > 0) {
|
|
921
|
+
allChildren.push(...proj.children);
|
|
922
|
+
} else {
|
|
923
|
+
const { children, ...rest } = proj;
|
|
924
|
+
allChildren.push(rest);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
const techStack = [
|
|
928
|
+
...new Set(allChildren.flatMap((p6) => p6.techStack))
|
|
929
|
+
];
|
|
930
|
+
const languages = {};
|
|
931
|
+
for (const child of allChildren) {
|
|
932
|
+
for (const [lang, count] of Object.entries(child.languages)) {
|
|
933
|
+
languages[lang] = (languages[lang] || 0) + count;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
const firstDates = allChildren.map((p6) => p6.firstCommitDate).filter(Boolean).sort();
|
|
937
|
+
const lastDates = allChildren.map((p6) => p6.lastCommitDate).filter(Boolean).sort();
|
|
938
|
+
const totalCommits = allChildren.reduce(
|
|
939
|
+
(sum, p6) => sum + p6.totalCommits,
|
|
940
|
+
0
|
|
941
|
+
);
|
|
942
|
+
const remoteUrl = allChildren.find((p6) => p6.remoteUrl)?.remoteUrl || null;
|
|
943
|
+
const demoUrl = allChildren.find((p6) => p6.demoUrl)?.demoUrl || null;
|
|
944
|
+
const readmeParts = allChildren.filter((p6) => p6.readmeContent).map(
|
|
945
|
+
(p6) => `--- ${p6.name} ---
|
|
946
|
+
${p6.readmeContent}`
|
|
947
|
+
);
|
|
948
|
+
const readmeContent = readmeParts.length > 0 ? readmeParts.join("\n\n") : null;
|
|
949
|
+
const descriptions = allChildren.map((p6) => p6.description).filter(Boolean);
|
|
950
|
+
const description = descriptions.join(" | ");
|
|
951
|
+
const localPath = allChildren[0].localPath;
|
|
952
|
+
const id = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
953
|
+
return {
|
|
954
|
+
id,
|
|
955
|
+
name,
|
|
956
|
+
localPath,
|
|
957
|
+
description,
|
|
958
|
+
techStack,
|
|
959
|
+
languages,
|
|
960
|
+
firstCommitDate: firstDates[0] || "",
|
|
961
|
+
lastCommitDate: lastDates[lastDates.length - 1] || "",
|
|
962
|
+
totalCommits,
|
|
963
|
+
remoteUrl,
|
|
964
|
+
demoUrl,
|
|
965
|
+
readmeContent,
|
|
966
|
+
lastScannedCommit: allChildren[0].lastScannedCommit || "",
|
|
967
|
+
children: allChildren
|
|
968
|
+
};
|
|
969
|
+
}
|
|
658
970
|
async function runInterview(scannedProjects, availableEngines) {
|
|
659
971
|
p.intro("shipfolio");
|
|
660
972
|
logger.header("Project Selection");
|
|
@@ -668,41 +980,47 @@ async function runInterview(scannedProjects, availableEngines) {
|
|
|
668
980
|
};
|
|
669
981
|
});
|
|
670
982
|
const selectedIds = await p.multiselect({
|
|
671
|
-
message:
|
|
983
|
+
message: t().selectProjects,
|
|
672
984
|
options: projectOptions,
|
|
673
985
|
required: true
|
|
674
986
|
});
|
|
675
987
|
handleCancel(selectedIds);
|
|
988
|
+
let selectedMetas = selectedIds.map(
|
|
989
|
+
(id) => scannedProjects.find((p6) => p6.id === id)
|
|
990
|
+
);
|
|
991
|
+
if (selectedMetas.length >= 2) {
|
|
992
|
+
selectedMetas = await runMergeStep(selectedMetas);
|
|
993
|
+
}
|
|
676
994
|
const projectEntries = [];
|
|
677
|
-
for (const
|
|
678
|
-
const
|
|
995
|
+
for (const meta of selectedMetas) {
|
|
996
|
+
const displayName = meta.children ? `${meta.name} (${meta.children.length} sub-projects)` : meta.name;
|
|
679
997
|
logger.plain(`
|
|
680
|
-
|
|
998
|
+
${t().configuring(displayName)}`);
|
|
681
999
|
const overrideDesc = await p.text({
|
|
682
|
-
message:
|
|
683
|
-
placeholder: meta.description?.slice(0, 80) ||
|
|
1000
|
+
message: t().descriptionFor(displayName),
|
|
1001
|
+
placeholder: meta.description?.slice(0, 80) || t().descriptionPlaceholder,
|
|
684
1002
|
defaultValue: ""
|
|
685
1003
|
});
|
|
686
1004
|
handleCancel(overrideDesc);
|
|
687
1005
|
const demoUrl = await p.text({
|
|
688
|
-
message:
|
|
1006
|
+
message: t().demoUrlFor(displayName),
|
|
689
1007
|
placeholder: meta.demoUrl || "none",
|
|
690
1008
|
defaultValue: meta.demoUrl || ""
|
|
691
1009
|
});
|
|
692
1010
|
handleCancel(demoUrl);
|
|
693
1011
|
const showSource = await p.confirm({
|
|
694
|
-
message:
|
|
1012
|
+
message: t().showSourceFor(displayName),
|
|
695
1013
|
initialValue: !!meta.remoteUrl
|
|
696
1014
|
});
|
|
697
1015
|
handleCancel(showSource);
|
|
698
1016
|
const role = await p.select({
|
|
699
|
-
message:
|
|
700
|
-
options: ROLE_OPTIONS
|
|
1017
|
+
message: t().roleFor(displayName),
|
|
1018
|
+
options: ROLE_OPTIONS()
|
|
701
1019
|
});
|
|
702
1020
|
handleCancel(role);
|
|
703
1021
|
const metricsInput = await p.text({
|
|
704
|
-
message:
|
|
705
|
-
placeholder:
|
|
1022
|
+
message: t().metricsFor(displayName),
|
|
1023
|
+
placeholder: t().optional,
|
|
706
1024
|
defaultValue: ""
|
|
707
1025
|
});
|
|
708
1026
|
handleCancel(metricsInput);
|
|
@@ -720,60 +1038,60 @@ async function runInterview(scannedProjects, availableEngines) {
|
|
|
720
1038
|
metrics
|
|
721
1039
|
});
|
|
722
1040
|
}
|
|
723
|
-
logger.header(
|
|
1041
|
+
logger.header(t().personalInfo);
|
|
724
1042
|
const name = await p.text({
|
|
725
|
-
message:
|
|
726
|
-
validate: (v) => v.length === 0 ?
|
|
1043
|
+
message: t().fullName,
|
|
1044
|
+
validate: (v) => v.length === 0 ? t().nameRequired : void 0
|
|
727
1045
|
});
|
|
728
1046
|
handleCancel(name);
|
|
729
1047
|
const tagline = await p.text({
|
|
730
|
-
message:
|
|
731
|
-
placeholder:
|
|
1048
|
+
message: t().tagline,
|
|
1049
|
+
placeholder: t().taglinePlaceholder
|
|
732
1050
|
});
|
|
733
1051
|
handleCancel(tagline);
|
|
734
1052
|
const bioChoice = await p.select({
|
|
735
|
-
message:
|
|
1053
|
+
message: t().bio,
|
|
736
1054
|
options: [
|
|
737
|
-
{ value: "auto", label:
|
|
738
|
-
{ value: "manual", label:
|
|
1055
|
+
{ value: "auto", label: t().bioAuto },
|
|
1056
|
+
{ value: "manual", label: t().bioManual }
|
|
739
1057
|
]
|
|
740
1058
|
});
|
|
741
1059
|
handleCancel(bioChoice);
|
|
742
1060
|
let bio = "auto";
|
|
743
1061
|
if (bioChoice === "manual") {
|
|
744
1062
|
bio = await p.text({
|
|
745
|
-
message:
|
|
1063
|
+
message: t().bioPrompt
|
|
746
1064
|
});
|
|
747
1065
|
handleCancel(bio);
|
|
748
1066
|
}
|
|
749
1067
|
const photoUrl = await p.text({
|
|
750
|
-
message:
|
|
1068
|
+
message: t().photoUrl,
|
|
751
1069
|
placeholder: "https://...",
|
|
752
1070
|
defaultValue: ""
|
|
753
1071
|
});
|
|
754
1072
|
handleCancel(photoUrl);
|
|
755
1073
|
const github = await p.text({
|
|
756
|
-
message:
|
|
1074
|
+
message: t().githubUser,
|
|
757
1075
|
defaultValue: ""
|
|
758
1076
|
});
|
|
759
1077
|
handleCancel(github);
|
|
760
1078
|
const twitter = await p.text({
|
|
761
|
-
message:
|
|
1079
|
+
message: t().twitterHandle,
|
|
762
1080
|
defaultValue: ""
|
|
763
1081
|
});
|
|
764
1082
|
handleCancel(twitter);
|
|
765
1083
|
const linkedin = await p.text({
|
|
766
|
-
message:
|
|
1084
|
+
message: t().linkedinUrl,
|
|
767
1085
|
defaultValue: ""
|
|
768
1086
|
});
|
|
769
1087
|
handleCancel(linkedin);
|
|
770
1088
|
const blogUrl = await p.text({
|
|
771
|
-
message:
|
|
1089
|
+
message: t().blogUrl,
|
|
772
1090
|
defaultValue: ""
|
|
773
1091
|
});
|
|
774
1092
|
handleCancel(blogUrl);
|
|
775
1093
|
const email = await p.text({
|
|
776
|
-
message:
|
|
1094
|
+
message: t().contactEmail,
|
|
777
1095
|
defaultValue: ""
|
|
778
1096
|
});
|
|
779
1097
|
handleCancel(email);
|
|
@@ -790,37 +1108,37 @@ async function runInterview(scannedProjects, availableEngines) {
|
|
|
790
1108
|
email: email || void 0
|
|
791
1109
|
}
|
|
792
1110
|
};
|
|
793
|
-
logger.header(
|
|
1111
|
+
logger.header(t().designPrefs);
|
|
794
1112
|
const theme = await p.select({
|
|
795
|
-
message:
|
|
796
|
-
options: THEME_OPTIONS
|
|
1113
|
+
message: t().theme,
|
|
1114
|
+
options: THEME_OPTIONS()
|
|
797
1115
|
});
|
|
798
1116
|
handleCancel(theme);
|
|
799
1117
|
const accentColor = await p.select({
|
|
800
|
-
message:
|
|
1118
|
+
message: t().accentColor,
|
|
801
1119
|
options: [
|
|
802
|
-
...DEFAULT_ACCENT_COLORS,
|
|
803
|
-
{ value: "custom", label:
|
|
1120
|
+
...DEFAULT_ACCENT_COLORS(),
|
|
1121
|
+
{ value: "custom", label: t().customHex }
|
|
804
1122
|
]
|
|
805
1123
|
});
|
|
806
1124
|
handleCancel(accentColor);
|
|
807
1125
|
let finalAccent = accentColor;
|
|
808
1126
|
if (accentColor === "custom") {
|
|
809
1127
|
finalAccent = await p.text({
|
|
810
|
-
message:
|
|
1128
|
+
message: t().customHexPrompt,
|
|
811
1129
|
placeholder: "#7c3aed",
|
|
812
|
-
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
|
|
813
1131
|
});
|
|
814
1132
|
handleCancel(finalAccent);
|
|
815
1133
|
}
|
|
816
1134
|
const font = await p.select({
|
|
817
|
-
message:
|
|
818
|
-
options: FONT_OPTIONS
|
|
1135
|
+
message: t().font,
|
|
1136
|
+
options: FONT_OPTIONS()
|
|
819
1137
|
});
|
|
820
1138
|
handleCancel(font);
|
|
821
1139
|
const animationLevel = await p.select({
|
|
822
|
-
message:
|
|
823
|
-
options: ANIMATION_OPTIONS
|
|
1140
|
+
message: t().animationLevel,
|
|
1141
|
+
options: ANIMATION_OPTIONS()
|
|
824
1142
|
});
|
|
825
1143
|
handleCancel(animationLevel);
|
|
826
1144
|
const style = {
|
|
@@ -829,10 +1147,10 @@ async function runInterview(scannedProjects, availableEngines) {
|
|
|
829
1147
|
font,
|
|
830
1148
|
animationLevel
|
|
831
1149
|
};
|
|
832
|
-
logger.header(
|
|
1150
|
+
logger.header(t().sections);
|
|
833
1151
|
const additionalSections = await p.multiselect({
|
|
834
|
-
message:
|
|
835
|
-
options: SECTION_OPTIONS,
|
|
1152
|
+
message: t().additionalSections,
|
|
1153
|
+
options: SECTION_OPTIONS(),
|
|
836
1154
|
required: false
|
|
837
1155
|
});
|
|
838
1156
|
handleCancel(additionalSections);
|
|
@@ -841,37 +1159,37 @@ async function runInterview(scannedProjects, availableEngines) {
|
|
|
841
1159
|
"projects",
|
|
842
1160
|
...additionalSections
|
|
843
1161
|
];
|
|
844
|
-
logger.header(
|
|
1162
|
+
logger.header(t().aiEngine);
|
|
845
1163
|
let engine;
|
|
846
1164
|
if (availableEngines.length === 1) {
|
|
847
1165
|
engine = availableEngines[0];
|
|
848
|
-
logger.info(
|
|
1166
|
+
logger.info(t().engineUsing(engine));
|
|
849
1167
|
} else {
|
|
850
|
-
const filteredEngineOptions = ENGINE_OPTIONS.filter(
|
|
1168
|
+
const filteredEngineOptions = ENGINE_OPTIONS().filter(
|
|
851
1169
|
(o) => availableEngines.includes(o.value)
|
|
852
1170
|
);
|
|
853
1171
|
engine = await p.select({
|
|
854
|
-
message:
|
|
1172
|
+
message: t().engineSelect,
|
|
855
1173
|
options: filteredEngineOptions
|
|
856
1174
|
});
|
|
857
1175
|
handleCancel(engine);
|
|
858
1176
|
}
|
|
859
|
-
logger.header(
|
|
1177
|
+
logger.header(t().deployment);
|
|
860
1178
|
const deployPlatform = await p.select({
|
|
861
|
-
message:
|
|
862
|
-
options: DEPLOY_OPTIONS
|
|
1179
|
+
message: t().deployTo,
|
|
1180
|
+
options: DEPLOY_OPTIONS()
|
|
863
1181
|
});
|
|
864
1182
|
handleCancel(deployPlatform);
|
|
865
1183
|
let projectName = "";
|
|
866
1184
|
if (deployPlatform !== "local") {
|
|
867
1185
|
projectName = await p.text({
|
|
868
|
-
message:
|
|
1186
|
+
message: t().projectNamePrompt,
|
|
869
1187
|
placeholder: "my-shipfolio",
|
|
870
|
-
validate: (v) => /^[a-z0-9-]+$/.test(v) ? void 0 :
|
|
1188
|
+
validate: (v) => /^[a-z0-9-]+$/.test(v) ? void 0 : t().projectNameInvalid
|
|
871
1189
|
});
|
|
872
1190
|
handleCancel(projectName);
|
|
873
1191
|
}
|
|
874
|
-
p.outro(
|
|
1192
|
+
p.outro(t().configComplete);
|
|
875
1193
|
return {
|
|
876
1194
|
projects: projectEntries,
|
|
877
1195
|
owner,
|
|
@@ -890,6 +1208,7 @@ var init_interviewer = __esm({
|
|
|
890
1208
|
init_esm_shims();
|
|
891
1209
|
init_questions();
|
|
892
1210
|
init_logger();
|
|
1211
|
+
init_i18n();
|
|
893
1212
|
}
|
|
894
1213
|
});
|
|
895
1214
|
|
|
@@ -918,42 +1237,14 @@ var init_builder = __esm({
|
|
|
918
1237
|
});
|
|
919
1238
|
|
|
920
1239
|
// src/orchestrator/prompt-builder.ts
|
|
921
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
922
|
-
import { join as join2, dirname } from "path";
|
|
923
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
924
|
-
function getPromptsDir() {
|
|
925
|
-
const currentDir = dirname(fileURLToPath2(import.meta.url));
|
|
926
|
-
const candidates = [
|
|
927
|
-
join2(currentDir, "../../prompts"),
|
|
928
|
-
join2(currentDir, "../prompts"),
|
|
929
|
-
join2(currentDir, "../../../prompts")
|
|
930
|
-
];
|
|
931
|
-
return candidates[0];
|
|
932
|
-
}
|
|
933
|
-
async function loadTemplate(filename) {
|
|
934
|
-
const promptsDir = getPromptsDir();
|
|
935
|
-
const candidates = [
|
|
936
|
-
join2(promptsDir, filename),
|
|
937
|
-
join2(dirname(fileURLToPath2(import.meta.url)), "../../prompts", filename),
|
|
938
|
-
join2(dirname(fileURLToPath2(import.meta.url)), "../../../prompts", filename)
|
|
939
|
-
];
|
|
940
|
-
for (const candidate of candidates) {
|
|
941
|
-
try {
|
|
942
|
-
return await readFile2(candidate, "utf-8");
|
|
943
|
-
} catch {
|
|
944
|
-
continue;
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
throw new Error(`Prompt template not found: ${filename}`);
|
|
948
|
-
}
|
|
949
1240
|
async function buildFreshPrompt(spec) {
|
|
950
|
-
let template =
|
|
1241
|
+
let template = FRESH_BUILD_TEMPLATE;
|
|
951
1242
|
const sectionsText = spec.sections.map((s) => `- ${s}`).join("\n");
|
|
952
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);
|
|
953
1244
|
return template;
|
|
954
1245
|
}
|
|
955
1246
|
async function buildUpdatePrompt(existingConfig, diff) {
|
|
956
|
-
let template =
|
|
1247
|
+
let template = UPDATE_TEMPLATE;
|
|
957
1248
|
const newProjectsText = diff.newProjects.length > 0 ? diff.newProjects.map(
|
|
958
1249
|
(p6) => `- ${p6.name}: ${p6.description}
|
|
959
1250
|
Tech: ${p6.techStack.join(", ")}
|
|
@@ -971,10 +1262,167 @@ async function buildUpdatePrompt(existingConfig, diff) {
|
|
|
971
1262
|
).replace("{{NEW_PROJECTS}}", newProjectsText).replace("{{UPDATED_PROJECTS}}", updatedProjectsText).replace("{{REMOVED_PROJECTS}}", removedProjectsText).replace("{{PERSONAL_INFO_DIFF}}", "No changes");
|
|
972
1263
|
return template;
|
|
973
1264
|
}
|
|
1265
|
+
var FRESH_BUILD_TEMPLATE, UPDATE_TEMPLATE;
|
|
974
1266
|
var init_prompt_builder = __esm({
|
|
975
1267
|
"src/orchestrator/prompt-builder.ts"() {
|
|
976
1268
|
"use strict";
|
|
977
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
|
+
`;
|
|
978
1426
|
}
|
|
979
1427
|
});
|
|
980
1428
|
|
|
@@ -1091,7 +1539,7 @@ var init_codex = __esm({
|
|
|
1091
1539
|
// src/orchestrator/engines/v0.ts
|
|
1092
1540
|
import OpenAI from "openai";
|
|
1093
1541
|
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
1094
|
-
import { join as
|
|
1542
|
+
import { join as join2, dirname } from "path";
|
|
1095
1543
|
import ora4 from "ora";
|
|
1096
1544
|
async function generateWithV0(prompt, outputDir, apiKey) {
|
|
1097
1545
|
const spinner = ora4("Generating site with v0...").start();
|
|
@@ -1124,14 +1572,14 @@ async function generateWithV0(prompt, outputDir, apiKey) {
|
|
|
1124
1572
|
}
|
|
1125
1573
|
spinner.text = `Writing ${files.length} files...`;
|
|
1126
1574
|
for (const file of files) {
|
|
1127
|
-
const filePath =
|
|
1128
|
-
await mkdir2(
|
|
1575
|
+
const filePath = join2(outputDir, file.filename);
|
|
1576
|
+
await mkdir2(dirname(filePath), { recursive: true });
|
|
1129
1577
|
await writeFile2(filePath, file.content, "utf-8");
|
|
1130
1578
|
}
|
|
1131
1579
|
const hasPkgJson = files.some((f) => f.filename === "package.json");
|
|
1132
1580
|
if (!hasPkgJson) {
|
|
1133
1581
|
await writeFile2(
|
|
1134
|
-
|
|
1582
|
+
join2(outputDir, "package.json"),
|
|
1135
1583
|
JSON.stringify(
|
|
1136
1584
|
{
|
|
1137
1585
|
name: "shipfolio-site",
|
|
@@ -1502,15 +1950,15 @@ var init_github = __esm({
|
|
|
1502
1950
|
});
|
|
1503
1951
|
|
|
1504
1952
|
// src/deployer/index.ts
|
|
1505
|
-
import { join as
|
|
1953
|
+
import { join as join3 } from "path";
|
|
1506
1954
|
async function deploy(siteDir, platform, projectName) {
|
|
1507
1955
|
if (platform === "local") {
|
|
1508
|
-
logger.info(`Site built at ${
|
|
1956
|
+
logger.info(`Site built at ${join3(siteDir, "out/")}`);
|
|
1509
1957
|
logger.info("Run `npx serve ./out` in the site directory to preview.");
|
|
1510
1958
|
return null;
|
|
1511
1959
|
}
|
|
1512
1960
|
await ensureAuth(platform);
|
|
1513
|
-
const distDir =
|
|
1961
|
+
const distDir = join3(siteDir, "out");
|
|
1514
1962
|
let url;
|
|
1515
1963
|
if (platform === "cloudflare") {
|
|
1516
1964
|
url = await deployToCloudflare(distDir, projectName);
|
|
@@ -1534,11 +1982,11 @@ var init_deployer = __esm({
|
|
|
1534
1982
|
|
|
1535
1983
|
// src/pdf/index.ts
|
|
1536
1984
|
import { createServer } from "http";
|
|
1537
|
-
import { join as
|
|
1985
|
+
import { join as join4 } from "path";
|
|
1538
1986
|
import ora8 from "ora";
|
|
1539
1987
|
async function exportPdf(siteDir, outputPath) {
|
|
1540
|
-
const distDir =
|
|
1541
|
-
const pdfPath = outputPath ||
|
|
1988
|
+
const distDir = join4(siteDir, "out");
|
|
1989
|
+
const pdfPath = outputPath || join4(distDir, "shipfolio.pdf");
|
|
1542
1990
|
const spinner = ora8("Exporting PDF...").start();
|
|
1543
1991
|
const server = await startStaticServer(distDir);
|
|
1544
1992
|
const port = server.address()?.port || 3456;
|
|
@@ -1594,7 +2042,7 @@ function startStaticServer(dir) {
|
|
|
1594
2042
|
});
|
|
1595
2043
|
}
|
|
1596
2044
|
async function startPreviewServer(siteDir, port = 3e3) {
|
|
1597
|
-
const distDir =
|
|
2045
|
+
const distDir = join4(siteDir, "out");
|
|
1598
2046
|
const handler = await import("serve-handler");
|
|
1599
2047
|
const server = createServer((req, res) => {
|
|
1600
2048
|
return handler.default(req, res, {
|
|
@@ -1618,6 +2066,49 @@ var init_pdf = __esm({
|
|
|
1618
2066
|
}
|
|
1619
2067
|
});
|
|
1620
2068
|
|
|
2069
|
+
// src/utils/draft.ts
|
|
2070
|
+
import { readFile as readFile2, writeFile as writeFile3, mkdir as mkdir3, unlink } from "fs/promises";
|
|
2071
|
+
import { join as join5 } from "path";
|
|
2072
|
+
import { homedir } from "os";
|
|
2073
|
+
async function saveDraft(interviewResult) {
|
|
2074
|
+
try {
|
|
2075
|
+
await mkdir3(DRAFT_DIR, { recursive: true });
|
|
2076
|
+
const draft = {
|
|
2077
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2078
|
+
interviewResult
|
|
2079
|
+
};
|
|
2080
|
+
await writeFile3(DRAFT_FILE, JSON.stringify(draft, null, 2), "utf-8");
|
|
2081
|
+
logger.info(t().draftSaved);
|
|
2082
|
+
} catch {
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
async function loadDraft() {
|
|
2086
|
+
try {
|
|
2087
|
+
const content = await readFile2(DRAFT_FILE, "utf-8");
|
|
2088
|
+
return JSON.parse(content);
|
|
2089
|
+
} catch {
|
|
2090
|
+
return null;
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
async function clearDraft() {
|
|
2094
|
+
try {
|
|
2095
|
+
await unlink(DRAFT_FILE);
|
|
2096
|
+
logger.info(t().draftCleared);
|
|
2097
|
+
} catch {
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
var DRAFT_DIR, DRAFT_FILE;
|
|
2101
|
+
var init_draft = __esm({
|
|
2102
|
+
"src/utils/draft.ts"() {
|
|
2103
|
+
"use strict";
|
|
2104
|
+
init_esm_shims();
|
|
2105
|
+
init_logger();
|
|
2106
|
+
init_i18n();
|
|
2107
|
+
DRAFT_DIR = join5(homedir(), ".shipfolio");
|
|
2108
|
+
DRAFT_FILE = join5(DRAFT_DIR, "draft.json");
|
|
2109
|
+
}
|
|
2110
|
+
});
|
|
2111
|
+
|
|
1621
2112
|
// src/commands/init.ts
|
|
1622
2113
|
var init_exports = {};
|
|
1623
2114
|
__export(init_exports, {
|
|
@@ -1627,7 +2118,7 @@ import { resolve } from "path";
|
|
|
1627
2118
|
import { join as join6 } from "path";
|
|
1628
2119
|
import * as p3 from "@clack/prompts";
|
|
1629
2120
|
async function initCommand(options) {
|
|
1630
|
-
logger.header("shipfolio v1.0.
|
|
2121
|
+
logger.header("shipfolio v1.0.3");
|
|
1631
2122
|
logger.info("Detecting AI engines...");
|
|
1632
2123
|
const engines = await detectEngines();
|
|
1633
2124
|
const availableTypes = getAvailableEngineTypes(engines);
|
|
@@ -1661,7 +2152,24 @@ async function initCommand(options) {
|
|
|
1661
2152
|
]);
|
|
1662
2153
|
logger.table(tableRows);
|
|
1663
2154
|
logger.blank();
|
|
1664
|
-
|
|
2155
|
+
let interviewResult;
|
|
2156
|
+
const draft = await loadDraft();
|
|
2157
|
+
if (draft) {
|
|
2158
|
+
logger.info(t().draftFound);
|
|
2159
|
+
const useDraft = await p3.confirm({
|
|
2160
|
+
message: t().draftLoadPrompt,
|
|
2161
|
+
initialValue: true
|
|
2162
|
+
});
|
|
2163
|
+
if (!p3.isCancel(useDraft) && useDraft) {
|
|
2164
|
+
interviewResult = draft.interviewResult;
|
|
2165
|
+
await clearDraft();
|
|
2166
|
+
} else {
|
|
2167
|
+
interviewResult = await runInterview(scannedProjects, availableTypes);
|
|
2168
|
+
}
|
|
2169
|
+
} else {
|
|
2170
|
+
interviewResult = await runInterview(scannedProjects, availableTypes);
|
|
2171
|
+
}
|
|
2172
|
+
await saveDraft(interviewResult);
|
|
1665
2173
|
const spec = buildSpec(interviewResult);
|
|
1666
2174
|
const prompt = await buildFreshPrompt(spec);
|
|
1667
2175
|
const outputDir = resolve(options.output || "./shipfolio-site");
|
|
@@ -1712,6 +2220,7 @@ async function initCommand(options) {
|
|
|
1712
2220
|
...spec,
|
|
1713
2221
|
sitePath: outputDir
|
|
1714
2222
|
});
|
|
2223
|
+
await clearDraft();
|
|
1715
2224
|
logger.header("Done.");
|
|
1716
2225
|
if (spec.deploy.url) {
|
|
1717
2226
|
logger.info(`Site: ${spec.deploy.url}`);
|
|
@@ -1736,6 +2245,8 @@ var init_init = __esm({
|
|
|
1736
2245
|
init_pdf();
|
|
1737
2246
|
init_fs();
|
|
1738
2247
|
init_logger();
|
|
2248
|
+
init_draft();
|
|
2249
|
+
init_i18n();
|
|
1739
2250
|
}
|
|
1740
2251
|
});
|
|
1741
2252
|
|
|
@@ -2052,10 +2563,12 @@ async function previewCommand(options) {
|
|
|
2052
2563
|
}
|
|
2053
2564
|
|
|
2054
2565
|
// src/index.ts
|
|
2566
|
+
init_i18n();
|
|
2567
|
+
initLocale();
|
|
2055
2568
|
var program = new Command();
|
|
2056
2569
|
program.name("shipfolio").description(
|
|
2057
2570
|
"Generate and deploy your personal portfolio site from local projects using AI"
|
|
2058
|
-
).version("1.0.
|
|
2571
|
+
).version("1.0.3");
|
|
2059
2572
|
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);
|
|
2060
2573
|
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);
|
|
2061
2574
|
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);
|