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 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
- THEME_OPTIONS = [
601
- { value: "dark-minimal", label: "Dark Minimal" },
602
- { value: "light-clean", label: "Light Clean" },
603
- { value: "monochrome", label: "Monochrome" },
604
- { value: "custom", label: "Custom (AI decides)" }
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: "Inter" },
608
- { value: "JetBrains Mono", label: "JetBrains Mono" },
609
- { value: "system", label: "System Default" }
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: "Subtle" },
613
- { value: "moderate", label: "Moderate" },
614
- { value: "none", label: "None" }
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: "Skills / Tech Stack" },
618
- { value: "about", label: "About Me" },
619
- { value: "timeline", label: "Timeline / Changelog" },
620
- { value: "blog", label: "Blog" },
621
- { value: "metrics", label: "Metrics Dashboard" },
622
- { value: "contact", label: "Contact" }
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: "Claude Code" },
626
- { value: "codex", label: "Codex" },
627
- { value: "v0", label: "v0 (Vercel)" }
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: "Cloudflare Pages" },
631
- { value: "vercel", label: "Vercel" },
632
- { value: "local", label: "Local only (no deploy)" }
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: "Solo" },
636
- { value: "lead", label: "Lead" },
637
- { value: "contributor", label: "Contributor" }
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: "Purple" },
641
- { value: "#10b981", label: "Green" },
642
- { value: "#f97316", label: "Orange" },
643
- { value: "#3b82f6", label: "Blue" },
644
- { value: "#ef4444", label: "Red" },
645
- { value: "#ec4899", label: "Pink" }
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: "Select projects to include (space = select, enter = confirm):",
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 id of selectedIds) {
678
- const meta = scannedProjects.find((p6) => p6.id === id);
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
- Configuring: ${meta.name}`);
998
+ ${t().configuring(displayName)}`);
681
999
  const overrideDesc = await p.text({
682
- message: `Description for ${meta.name} (enter to use auto-generated):`,
683
- placeholder: meta.description?.slice(0, 80) || "Auto-generate from README",
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: `Demo URL for ${meta.name}:`,
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: `Show source code link for ${meta.name}?`,
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: `Your role in ${meta.name}:`,
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: `Key metrics for ${meta.name} (e.g. "1k users, $5k MRR"):`,
705
- placeholder: "optional",
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("Personal Information");
1041
+ logger.header(t().personalInfo);
724
1042
  const name = await p.text({
725
- message: "Your full name:",
726
- validate: (v) => v.length === 0 ? "Name is required" : void 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: "Professional tagline (one line):",
731
- placeholder: "Full-stack developer. I ship things."
1048
+ message: t().tagline,
1049
+ placeholder: t().taglinePlaceholder
732
1050
  });
733
1051
  handleCancel(tagline);
734
1052
  const bioChoice = await p.select({
735
- message: "Bio:",
1053
+ message: t().bio,
736
1054
  options: [
737
- { value: "auto", label: "Auto-generate from my projects" },
738
- { value: "manual", label: "Write manually" }
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: "Your bio (2-3 sentences):"
1063
+ message: t().bioPrompt
746
1064
  });
747
1065
  handleCancel(bio);
748
1066
  }
749
1067
  const photoUrl = await p.text({
750
- message: "Photo URL (optional):",
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: "GitHub username or URL:",
1074
+ message: t().githubUser,
757
1075
  defaultValue: ""
758
1076
  });
759
1077
  handleCancel(github);
760
1078
  const twitter = await p.text({
761
- message: "Twitter/X handle:",
1079
+ message: t().twitterHandle,
762
1080
  defaultValue: ""
763
1081
  });
764
1082
  handleCancel(twitter);
765
1083
  const linkedin = await p.text({
766
- message: "LinkedIn URL:",
1084
+ message: t().linkedinUrl,
767
1085
  defaultValue: ""
768
1086
  });
769
1087
  handleCancel(linkedin);
770
1088
  const blogUrl = await p.text({
771
- message: "Blog URL:",
1089
+ message: t().blogUrl,
772
1090
  defaultValue: ""
773
1091
  });
774
1092
  handleCancel(blogUrl);
775
1093
  const email = await p.text({
776
- message: "Contact email:",
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("Design Preferences");
1111
+ logger.header(t().designPrefs);
794
1112
  const theme = await p.select({
795
- message: "Theme:",
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: "Accent color:",
1118
+ message: t().accentColor,
801
1119
  options: [
802
- ...DEFAULT_ACCENT_COLORS,
803
- { value: "custom", label: "Custom hex" }
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: "Custom accent color (hex):",
1128
+ message: t().customHexPrompt,
811
1129
  placeholder: "#7c3aed",
812
- validate: (v) => /^#[0-9a-fA-F]{6}$/.test(v) ? void 0 : "Enter a valid hex color"
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: "Font:",
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: "Animation level:",
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("Sections");
1150
+ logger.header(t().sections);
833
1151
  const additionalSections = await p.multiselect({
834
- message: "Additional sections (Hero + Projects always included):",
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("AI Engine");
1162
+ logger.header(t().aiEngine);
845
1163
  let engine;
846
1164
  if (availableEngines.length === 1) {
847
1165
  engine = availableEngines[0];
848
- logger.info(`Using ${engine}`);
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: "AI engine to use:",
1172
+ message: t().engineSelect,
855
1173
  options: filteredEngineOptions
856
1174
  });
857
1175
  handleCancel(engine);
858
1176
  }
859
- logger.header("Deployment");
1177
+ logger.header(t().deployment);
860
1178
  const deployPlatform = await p.select({
861
- message: "Deploy to:",
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: "Project name (used in URL):",
1186
+ message: t().projectNamePrompt,
869
1187
  placeholder: "my-shipfolio",
870
- validate: (v) => /^[a-z0-9-]+$/.test(v) ? void 0 : "Lowercase letters, numbers, and hyphens only"
1188
+ validate: (v) => /^[a-z0-9-]+$/.test(v) ? void 0 : t().projectNameInvalid
871
1189
  });
872
1190
  handleCancel(projectName);
873
1191
  }
874
- p.outro("Configuration complete. Generating your site...");
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 = await loadTemplate("fresh-build.md");
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 = await loadTemplate("update.md");
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 join3, dirname as dirname2 } from "path";
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 = join3(outputDir, file.filename);
1128
- await mkdir2(dirname2(filePath), { recursive: true });
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
- join3(outputDir, "package.json"),
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 join4 } from "path";
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 ${join4(siteDir, "out/")}`);
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 = join4(siteDir, "out");
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 join5 } from "path";
1985
+ import { join as join4 } from "path";
1538
1986
  import ora8 from "ora";
1539
1987
  async function exportPdf(siteDir, outputPath) {
1540
- const distDir = join5(siteDir, "out");
1541
- const pdfPath = outputPath || join5(distDir, "shipfolio.pdf");
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 = join5(siteDir, "out");
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.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
- const interviewResult = await runInterview(scannedProjects, availableTypes);
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.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);