shipfolio 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js 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
  });
@@ -657,7 +867,7 @@ function handleCancel(value) {
657
867
  }
658
868
  async function runMergeStep(projects) {
659
869
  const shouldMerge = await p.confirm({
660
- message: "Merge any related projects into one entry? (e.g. web + mobile)",
870
+ message: t().mergePrompt,
661
871
  initialValue: false
662
872
  });
663
873
  handleCancel(shouldMerge);
@@ -665,7 +875,7 @@ async function runMergeStep(projects) {
665
875
  let remaining = [...projects];
666
876
  while (remaining.length >= 2) {
667
877
  const mergeIds = await p.multiselect({
668
- message: "Select projects to merge together (space = select, enter = confirm):",
878
+ message: t().selectToMerge,
669
879
  options: remaining.map((proj) => ({
670
880
  value: proj.id,
671
881
  label: `${proj.name} (${proj.localPath})`,
@@ -675,7 +885,7 @@ async function runMergeStep(projects) {
675
885
  });
676
886
  handleCancel(mergeIds);
677
887
  if (mergeIds.length < 2) {
678
- logger.warn("Need at least 2 projects to merge. Skipping.");
888
+ logger.warn(t().mergeNeedTwo);
679
889
  break;
680
890
  }
681
891
  const toMerge = mergeIds.map(
@@ -683,7 +893,7 @@ async function runMergeStep(projects) {
683
893
  );
684
894
  const defaultName = toMerge.map((p6) => p6.name).join(" + ");
685
895
  const mergedName = await p.text({
686
- message: "Name for the merged entry:",
896
+ message: t().mergedNamePrompt,
687
897
  placeholder: defaultName,
688
898
  defaultValue: defaultName
689
899
  });
@@ -692,11 +902,11 @@ async function runMergeStep(projects) {
692
902
  remaining = remaining.filter((p6) => !mergeIds.includes(p6.id));
693
903
  remaining.push(merged);
694
904
  logger.info(
695
- `Merged ${toMerge.map((p6) => p6.name).join(", ")} -> "${mergedName}"`
905
+ t().mergedResult(toMerge.map((p6) => p6.name).join(", "), mergedName)
696
906
  );
697
907
  if (remaining.length < 2) break;
698
908
  const more = await p.confirm({
699
- message: "Merge more projects?",
909
+ message: t().mergeMore,
700
910
  initialValue: false
701
911
  });
702
912
  handleCancel(more);
@@ -770,7 +980,7 @@ async function runInterview(scannedProjects, availableEngines) {
770
980
  };
771
981
  });
772
982
  const selectedIds = await p.multiselect({
773
- message: "Select projects to include (space = select, enter = confirm):",
983
+ message: t().selectProjects,
774
984
  options: projectOptions,
775
985
  required: true
776
986
  });
@@ -785,32 +995,32 @@ async function runInterview(scannedProjects, availableEngines) {
785
995
  for (const meta of selectedMetas) {
786
996
  const displayName = meta.children ? `${meta.name} (${meta.children.length} sub-projects)` : meta.name;
787
997
  logger.plain(`
788
- Configuring: ${displayName}`);
998
+ ${t().configuring(displayName)}`);
789
999
  const overrideDesc = await p.text({
790
- message: `Description for ${displayName} (enter to use auto-generated):`,
791
- placeholder: meta.description?.slice(0, 80) || "Auto-generate from README",
1000
+ message: t().descriptionFor(displayName),
1001
+ placeholder: meta.description?.slice(0, 80) || t().descriptionPlaceholder,
792
1002
  defaultValue: ""
793
1003
  });
794
1004
  handleCancel(overrideDesc);
795
1005
  const demoUrl = await p.text({
796
- message: `Demo URL for ${displayName}:`,
1006
+ message: t().demoUrlFor(displayName),
797
1007
  placeholder: meta.demoUrl || "none",
798
1008
  defaultValue: meta.demoUrl || ""
799
1009
  });
800
1010
  handleCancel(demoUrl);
801
1011
  const showSource = await p.confirm({
802
- message: `Show source code link for ${displayName}?`,
1012
+ message: t().showSourceFor(displayName),
803
1013
  initialValue: !!meta.remoteUrl
804
1014
  });
805
1015
  handleCancel(showSource);
806
1016
  const role = await p.select({
807
- message: `Your role in ${displayName}:`,
808
- options: ROLE_OPTIONS
1017
+ message: t().roleFor(displayName),
1018
+ options: ROLE_OPTIONS()
809
1019
  });
810
1020
  handleCancel(role);
811
1021
  const metricsInput = await p.text({
812
- message: `Key metrics for ${displayName} (e.g. "1k users, $5k MRR"):`,
813
- placeholder: "optional",
1022
+ message: t().metricsFor(displayName),
1023
+ placeholder: t().optional,
814
1024
  defaultValue: ""
815
1025
  });
816
1026
  handleCancel(metricsInput);
@@ -828,60 +1038,60 @@ async function runInterview(scannedProjects, availableEngines) {
828
1038
  metrics
829
1039
  });
830
1040
  }
831
- logger.header("Personal Information");
1041
+ logger.header(t().personalInfo);
832
1042
  const name = await p.text({
833
- message: "Your full name:",
834
- validate: (v) => v.length === 0 ? "Name is required" : void 0
1043
+ message: t().fullName,
1044
+ validate: (v) => v.length === 0 ? t().nameRequired : void 0
835
1045
  });
836
1046
  handleCancel(name);
837
1047
  const tagline = await p.text({
838
- message: "Professional tagline (one line):",
839
- placeholder: "Full-stack developer. I ship things."
1048
+ message: t().tagline,
1049
+ placeholder: t().taglinePlaceholder
840
1050
  });
841
1051
  handleCancel(tagline);
842
1052
  const bioChoice = await p.select({
843
- message: "Bio:",
1053
+ message: t().bio,
844
1054
  options: [
845
- { value: "auto", label: "Auto-generate from my projects" },
846
- { value: "manual", label: "Write manually" }
1055
+ { value: "auto", label: t().bioAuto },
1056
+ { value: "manual", label: t().bioManual }
847
1057
  ]
848
1058
  });
849
1059
  handleCancel(bioChoice);
850
1060
  let bio = "auto";
851
1061
  if (bioChoice === "manual") {
852
1062
  bio = await p.text({
853
- message: "Your bio (2-3 sentences):"
1063
+ message: t().bioPrompt
854
1064
  });
855
1065
  handleCancel(bio);
856
1066
  }
857
1067
  const photoUrl = await p.text({
858
- message: "Photo URL (optional):",
1068
+ message: t().photoUrl,
859
1069
  placeholder: "https://...",
860
1070
  defaultValue: ""
861
1071
  });
862
1072
  handleCancel(photoUrl);
863
1073
  const github = await p.text({
864
- message: "GitHub username or URL:",
1074
+ message: t().githubUser,
865
1075
  defaultValue: ""
866
1076
  });
867
1077
  handleCancel(github);
868
1078
  const twitter = await p.text({
869
- message: "Twitter/X handle:",
1079
+ message: t().twitterHandle,
870
1080
  defaultValue: ""
871
1081
  });
872
1082
  handleCancel(twitter);
873
1083
  const linkedin = await p.text({
874
- message: "LinkedIn URL:",
1084
+ message: t().linkedinUrl,
875
1085
  defaultValue: ""
876
1086
  });
877
1087
  handleCancel(linkedin);
878
1088
  const blogUrl = await p.text({
879
- message: "Blog URL:",
1089
+ message: t().blogUrl,
880
1090
  defaultValue: ""
881
1091
  });
882
1092
  handleCancel(blogUrl);
883
1093
  const email = await p.text({
884
- message: "Contact email:",
1094
+ message: t().contactEmail,
885
1095
  defaultValue: ""
886
1096
  });
887
1097
  handleCancel(email);
@@ -898,37 +1108,37 @@ async function runInterview(scannedProjects, availableEngines) {
898
1108
  email: email || void 0
899
1109
  }
900
1110
  };
901
- logger.header("Design Preferences");
1111
+ logger.header(t().designPrefs);
902
1112
  const theme = await p.select({
903
- message: "Theme:",
904
- options: THEME_OPTIONS
1113
+ message: t().theme,
1114
+ options: THEME_OPTIONS()
905
1115
  });
906
1116
  handleCancel(theme);
907
1117
  const accentColor = await p.select({
908
- message: "Accent color:",
1118
+ message: t().accentColor,
909
1119
  options: [
910
- ...DEFAULT_ACCENT_COLORS,
911
- { value: "custom", label: "Custom hex" }
1120
+ ...DEFAULT_ACCENT_COLORS(),
1121
+ { value: "custom", label: t().customHex }
912
1122
  ]
913
1123
  });
914
1124
  handleCancel(accentColor);
915
1125
  let finalAccent = accentColor;
916
1126
  if (accentColor === "custom") {
917
1127
  finalAccent = await p.text({
918
- message: "Custom accent color (hex):",
1128
+ message: t().customHexPrompt,
919
1129
  placeholder: "#7c3aed",
920
- 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
921
1131
  });
922
1132
  handleCancel(finalAccent);
923
1133
  }
924
1134
  const font = await p.select({
925
- message: "Font:",
926
- options: FONT_OPTIONS
1135
+ message: t().font,
1136
+ options: FONT_OPTIONS()
927
1137
  });
928
1138
  handleCancel(font);
929
1139
  const animationLevel = await p.select({
930
- message: "Animation level:",
931
- options: ANIMATION_OPTIONS
1140
+ message: t().animationLevel,
1141
+ options: ANIMATION_OPTIONS()
932
1142
  });
933
1143
  handleCancel(animationLevel);
934
1144
  const style = {
@@ -937,10 +1147,10 @@ async function runInterview(scannedProjects, availableEngines) {
937
1147
  font,
938
1148
  animationLevel
939
1149
  };
940
- logger.header("Sections");
1150
+ logger.header(t().sections);
941
1151
  const additionalSections = await p.multiselect({
942
- message: "Additional sections (Hero + Projects always included):",
943
- options: SECTION_OPTIONS,
1152
+ message: t().additionalSections,
1153
+ options: SECTION_OPTIONS(),
944
1154
  required: false
945
1155
  });
946
1156
  handleCancel(additionalSections);
@@ -949,37 +1159,37 @@ async function runInterview(scannedProjects, availableEngines) {
949
1159
  "projects",
950
1160
  ...additionalSections
951
1161
  ];
952
- logger.header("AI Engine");
1162
+ logger.header(t().aiEngine);
953
1163
  let engine;
954
1164
  if (availableEngines.length === 1) {
955
1165
  engine = availableEngines[0];
956
- logger.info(`Using ${engine}`);
1166
+ logger.info(t().engineUsing(engine));
957
1167
  } else {
958
- const filteredEngineOptions = ENGINE_OPTIONS.filter(
1168
+ const filteredEngineOptions = ENGINE_OPTIONS().filter(
959
1169
  (o) => availableEngines.includes(o.value)
960
1170
  );
961
1171
  engine = await p.select({
962
- message: "AI engine to use:",
1172
+ message: t().engineSelect,
963
1173
  options: filteredEngineOptions
964
1174
  });
965
1175
  handleCancel(engine);
966
1176
  }
967
- logger.header("Deployment");
1177
+ logger.header(t().deployment);
968
1178
  const deployPlatform = await p.select({
969
- message: "Deploy to:",
970
- options: DEPLOY_OPTIONS
1179
+ message: t().deployTo,
1180
+ options: DEPLOY_OPTIONS()
971
1181
  });
972
1182
  handleCancel(deployPlatform);
973
1183
  let projectName = "";
974
1184
  if (deployPlatform !== "local") {
975
1185
  projectName = await p.text({
976
- message: "Project name (used in URL):",
1186
+ message: t().projectNamePrompt,
977
1187
  placeholder: "my-shipfolio",
978
- 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
979
1189
  });
980
1190
  handleCancel(projectName);
981
1191
  }
982
- p.outro("Configuration complete. Generating your site...");
1192
+ p.outro(t().configComplete);
983
1193
  return {
984
1194
  projects: projectEntries,
985
1195
  owner,
@@ -998,6 +1208,7 @@ var init_interviewer = __esm({
998
1208
  init_esm_shims();
999
1209
  init_questions();
1000
1210
  init_logger();
1211
+ init_i18n();
1001
1212
  }
1002
1213
  });
1003
1214
 
@@ -1026,42 +1237,14 @@ var init_builder = __esm({
1026
1237
  });
1027
1238
 
1028
1239
  // src/orchestrator/prompt-builder.ts
1029
- import { readFile as readFile2 } from "fs/promises";
1030
- import { join as join2, dirname } from "path";
1031
- import { fileURLToPath as fileURLToPath2 } from "url";
1032
- function getPromptsDir() {
1033
- const currentDir = dirname(fileURLToPath2(import.meta.url));
1034
- const candidates = [
1035
- join2(currentDir, "../../prompts"),
1036
- join2(currentDir, "../prompts"),
1037
- join2(currentDir, "../../../prompts")
1038
- ];
1039
- return candidates[0];
1040
- }
1041
- async function loadTemplate(filename) {
1042
- const promptsDir = getPromptsDir();
1043
- const candidates = [
1044
- join2(promptsDir, filename),
1045
- join2(dirname(fileURLToPath2(import.meta.url)), "../../prompts", filename),
1046
- join2(dirname(fileURLToPath2(import.meta.url)), "../../../prompts", filename)
1047
- ];
1048
- for (const candidate of candidates) {
1049
- try {
1050
- return await readFile2(candidate, "utf-8");
1051
- } catch {
1052
- continue;
1053
- }
1054
- }
1055
- throw new Error(`Prompt template not found: ${filename}`);
1056
- }
1057
1240
  async function buildFreshPrompt(spec) {
1058
- let template = await loadTemplate("fresh-build.md");
1241
+ let template = FRESH_BUILD_TEMPLATE;
1059
1242
  const sectionsText = spec.sections.map((s) => `- ${s}`).join("\n");
1060
1243
  template = template.replace("{{SPEC_JSON}}", JSON.stringify(spec, null, 2)).replace("{{THEME}}", spec.style.theme).replace("{{ACCENT_COLOR}}", spec.style.accentColor).replace("{{ANIMATION_LEVEL}}", spec.style.animationLevel).replace("{{FONT}}", spec.style.font).replace("{{SECTIONS_LIST}}", sectionsText);
1061
1244
  return template;
1062
1245
  }
1063
1246
  async function buildUpdatePrompt(existingConfig, diff) {
1064
- let template = await loadTemplate("update.md");
1247
+ let template = UPDATE_TEMPLATE;
1065
1248
  const newProjectsText = diff.newProjects.length > 0 ? diff.newProjects.map(
1066
1249
  (p6) => `- ${p6.name}: ${p6.description}
1067
1250
  Tech: ${p6.techStack.join(", ")}
@@ -1079,10 +1262,167 @@ async function buildUpdatePrompt(existingConfig, diff) {
1079
1262
  ).replace("{{NEW_PROJECTS}}", newProjectsText).replace("{{UPDATED_PROJECTS}}", updatedProjectsText).replace("{{REMOVED_PROJECTS}}", removedProjectsText).replace("{{PERSONAL_INFO_DIFF}}", "No changes");
1080
1263
  return template;
1081
1264
  }
1265
+ var FRESH_BUILD_TEMPLATE, UPDATE_TEMPLATE;
1082
1266
  var init_prompt_builder = __esm({
1083
1267
  "src/orchestrator/prompt-builder.ts"() {
1084
1268
  "use strict";
1085
1269
  init_esm_shims();
1270
+ FRESH_BUILD_TEMPLATE = `# Task
1271
+
1272
+ Generate a complete, production-ready personal portfolio website.
1273
+ All data and design preferences are provided in the spec below.
1274
+ Output a fully working Next.js project that builds and deploys as a static site.
1275
+
1276
+ # Spec
1277
+
1278
+ {{SPEC_JSON}}
1279
+
1280
+ # Technical Requirements
1281
+
1282
+ - Next.js 15 with App Router and TypeScript
1283
+ - Tailwind CSS v4 for styling
1284
+ - shadcn/ui components (initialize with \`npx shadcn@latest init --yes\`)
1285
+ - Static export: set \`output: 'export'\` in next.config.ts
1286
+ - Build command: \`npm run build\` producing \`out/\` directory
1287
+ - Zero external API calls at runtime -- all data is embedded in source
1288
+ - All project data in \`src/data/projects.ts\` as typed constants
1289
+ - Include \`@media print\` stylesheet in \`src/app/globals.css\` for PDF export
1290
+ - Responsive: mobile-first with Tailwind breakpoints (sm, md, lg, xl)
1291
+ - Lighthouse performance score 95+
1292
+ - Semantic HTML with ARIA labels and keyboard navigation
1293
+ - No emoji anywhere in text, UI, code, or comments
1294
+ - Use lucide-react for icons
1295
+
1296
+ # Design Direction
1297
+
1298
+ - Theme: {{THEME}}
1299
+ - Accent color: {{ACCENT_COLOR}}
1300
+ - Animation level: {{ANIMATION_LEVEL}}
1301
+ - Font: {{FONT}}
1302
+
1303
+ Design guidelines:
1304
+ - Typography-driven layout with large, bold headings
1305
+ - Generous whitespace between sections
1306
+ - Single accent color for interactive elements and highlights only
1307
+ - Project cards should feel substantial but clean
1308
+ - The site must look hand-crafted, not template-generated
1309
+ - Dark themes: use zinc/slate backgrounds, not pure black
1310
+ - Light themes: use warm whites and subtle grays
1311
+
1312
+ # Content Generation
1313
+
1314
+ For each project in the spec:
1315
+ - Write a 2-3 sentence narrative description based on the README content and tech stack
1316
+ - Focus on what it does and why it matters
1317
+ - If user provided an override description, use that instead
1318
+ - Maintain consistent voice across all descriptions
1319
+
1320
+ For the bio (if set to "auto"):
1321
+ - Generate a professional, authentic bio based on the project portfolio
1322
+ - Emphasize shipping velocity and breadth
1323
+ - Tone: confident, direct, no buzzwords or fluff
1324
+
1325
+ # Sections to Include
1326
+
1327
+ {{SECTIONS_LIST}}
1328
+
1329
+ Hero and Projects are always included. Additional sections as specified.
1330
+
1331
+ # Print / PDF Styles
1332
+
1333
+ In globals.css, add @media print rules:
1334
+ - Single-column layout
1335
+ - Hide navigation, footer, animations
1336
+ - Preserve background colors (user will print with backgrounds enabled)
1337
+ - Show link URLs inline: \`a[href]::after { content: " (" attr(href) ")"; }\`
1338
+ - Avoid page breaks inside project cards
1339
+ - A4-friendly margins and font sizes
1340
+
1341
+ # File Structure to Generate
1342
+
1343
+ \`\`\`
1344
+ next.config.ts
1345
+ package.json
1346
+ tailwind.config.ts
1347
+ tsconfig.json
1348
+ postcss.config.mjs
1349
+ components.json
1350
+ src/app/layout.tsx
1351
+ src/app/page.tsx
1352
+ src/app/globals.css
1353
+ src/components/ui/ (shadcn components as needed)
1354
+ src/components/hero.tsx
1355
+ src/components/project-card.tsx
1356
+ src/components/project-grid.tsx
1357
+ src/components/skills.tsx
1358
+ src/components/about.tsx
1359
+ src/components/timeline.tsx
1360
+ src/components/contact.tsx
1361
+ src/components/navigation.tsx
1362
+ src/components/footer.tsx
1363
+ src/data/projects.ts
1364
+ src/data/owner.ts
1365
+ src/lib/utils.ts
1366
+ public/favicon.svg
1367
+ shipfolio.config.json
1368
+ \`\`\`
1369
+
1370
+ # Important
1371
+
1372
+ - Generate ALL files needed for the project to build successfully
1373
+ - Include all shadcn/ui component files that are referenced
1374
+ - The \`package.json\` must include all dependencies
1375
+ - \`npm install && npm run build\` must succeed without errors
1376
+ - Do not use next/image (incompatible with static export) -- use standard <img> tags
1377
+ - Do not use features that require a server (API routes, middleware, ISR)
1378
+ `;
1379
+ UPDATE_TEMPLATE = `# Task
1380
+
1381
+ Update an existing portfolio website previously generated by shipfolio.
1382
+ You must preserve the existing design system, layout structure, component
1383
+ architecture, and any custom modifications the user has made.
1384
+
1385
+ # Existing Site Configuration
1386
+
1387
+ {{EXISTING_CONFIG_JSON}}
1388
+
1389
+ # Changes to Apply
1390
+
1391
+ ## New Projects to Add
1392
+ {{NEW_PROJECTS}}
1393
+
1394
+ ## Projects to Update (new commits since last scan)
1395
+ {{UPDATED_PROJECTS}}
1396
+
1397
+ ## Projects to Remove
1398
+ {{REMOVED_PROJECTS}}
1399
+
1400
+ ## Updated Personal Info
1401
+ {{PERSONAL_INFO_DIFF}}
1402
+
1403
+ # Rules
1404
+
1405
+ 1. Do NOT change the overall layout, color scheme, or design system
1406
+ 2. Do NOT reorganize existing components or rename files
1407
+ 3. Only modify files that need changes for the specified updates
1408
+ 4. For new projects: follow the exact same card format and component pattern
1409
+ as existing project cards
1410
+ 5. Preserve all custom CSS, custom components, and manual edits
1411
+ 6. Update src/data/projects.ts with new/changed/removed project data
1412
+ 7. Update src/data/owner.ts if personal info changed
1413
+ 8. Update shipfolio.config.json with new timestamps and project list
1414
+ 9. If a new section type is needed, create it following the existing
1415
+ component patterns and design tokens in the codebase
1416
+ 10. No emoji anywhere
1417
+
1418
+ # Technical Notes
1419
+
1420
+ - The site uses Next.js 15 + Tailwind CSS + shadcn/ui
1421
+ - Static export via \`output: 'export'\` in next.config.ts
1422
+ - Do not break the build -- \`npm run build\` must succeed
1423
+ - Do not add new dependencies unless absolutely necessary
1424
+ - Keep the existing @media print styles working
1425
+ `;
1086
1426
  }
1087
1427
  });
1088
1428
 
@@ -1150,11 +1490,17 @@ async function generateWithClaude(prompt, outputDir) {
1150
1490
  const fullPrompt = `${prompt}
1151
1491
 
1152
1492
  Create all files in the current working directory. Do not ask questions, just generate all files.`;
1153
- await execa2("claude", ["-p", fullPrompt], {
1493
+ await execa2("claude", [
1494
+ "-p",
1495
+ "--permission-mode",
1496
+ "bypassPermissions"
1497
+ ], {
1154
1498
  cwd: outputDir,
1155
- stdio: "pipe",
1156
- timeout: 3e5
1157
- // 5 minute timeout
1499
+ input: fullPrompt,
1500
+ stdout: "ignore",
1501
+ stderr: "pipe",
1502
+ timeout: 6e5
1503
+ // 10 minute timeout
1158
1504
  });
1159
1505
  spinner.succeed("Site generated with Claude Code");
1160
1506
  } catch (error) {
@@ -1181,7 +1527,7 @@ Create all files in the current working directory. Do not ask questions, just ge
1181
1527
  await execa3("codex", ["--quiet", "--full-auto", fullPrompt], {
1182
1528
  cwd: outputDir,
1183
1529
  stdio: "pipe",
1184
- timeout: 3e5
1530
+ timeout: 6e5
1185
1531
  });
1186
1532
  spinner.succeed("Site generated with Codex");
1187
1533
  } catch (error) {
@@ -1199,7 +1545,7 @@ var init_codex = __esm({
1199
1545
  // src/orchestrator/engines/v0.ts
1200
1546
  import OpenAI from "openai";
1201
1547
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1202
- import { join as join3, dirname as dirname2 } from "path";
1548
+ import { join as join2, dirname } from "path";
1203
1549
  import ora4 from "ora";
1204
1550
  async function generateWithV0(prompt, outputDir, apiKey) {
1205
1551
  const spinner = ora4("Generating site with v0...").start();
@@ -1232,14 +1578,14 @@ async function generateWithV0(prompt, outputDir, apiKey) {
1232
1578
  }
1233
1579
  spinner.text = `Writing ${files.length} files...`;
1234
1580
  for (const file of files) {
1235
- const filePath = join3(outputDir, file.filename);
1236
- await mkdir2(dirname2(filePath), { recursive: true });
1581
+ const filePath = join2(outputDir, file.filename);
1582
+ await mkdir2(dirname(filePath), { recursive: true });
1237
1583
  await writeFile2(filePath, file.content, "utf-8");
1238
1584
  }
1239
1585
  const hasPkgJson = files.some((f) => f.filename === "package.json");
1240
1586
  if (!hasPkgJson) {
1241
1587
  await writeFile2(
1242
- join3(outputDir, "package.json"),
1588
+ join2(outputDir, "package.json"),
1243
1589
  JSON.stringify(
1244
1590
  {
1245
1591
  name: "shipfolio-site",
@@ -1374,8 +1720,7 @@ var init_validator = __esm({
1374
1720
 
1375
1721
  // src/orchestrator/index.ts
1376
1722
  import ora5 from "ora";
1377
- async function generateSite(engine, prompt, outputDir) {
1378
- await ensureDir(outputDir);
1723
+ async function runEngine(engine, prompt, outputDir) {
1379
1724
  switch (engine) {
1380
1725
  case "claude":
1381
1726
  await generateWithClaude(prompt, outputDir);
@@ -1394,12 +1739,33 @@ async function generateSite(engine, prompt, outputDir) {
1394
1739
  break;
1395
1740
  }
1396
1741
  }
1397
- const validation = await validateGeneratedSite(outputDir);
1742
+ }
1743
+ async function generateSite(engine, prompt, outputDir) {
1744
+ await ensureDir(outputDir);
1745
+ await runEngine(engine, prompt, outputDir);
1746
+ let validation = await validateGeneratedSite(outputDir);
1398
1747
  if (!validation.valid) {
1399
1748
  logger.warn("Generated site has issues:");
1400
1749
  for (const err of validation.errors) {
1401
1750
  logger.warn(` ${err}`);
1402
1751
  }
1752
+ logger.info("Retrying generation with error feedback...");
1753
+ const fixPrompt = [
1754
+ "The previous generation was incomplete. The following required files are missing:",
1755
+ ...validation.errors.map((e) => `- ${e}`),
1756
+ "",
1757
+ "Please create ALL missing files. Here is the original specification:",
1758
+ "",
1759
+ prompt
1760
+ ].join("\n");
1761
+ await runEngine(engine, fixPrompt, outputDir);
1762
+ validation = await validateGeneratedSite(outputDir);
1763
+ if (!validation.valid) {
1764
+ logger.warn("Still missing files after retry:");
1765
+ for (const err of validation.errors) {
1766
+ logger.warn(` ${err}`);
1767
+ }
1768
+ }
1403
1769
  }
1404
1770
  }
1405
1771
  async function buildSite(siteDir) {
@@ -1424,6 +1790,21 @@ async function buildSite(siteDir) {
1424
1790
  }
1425
1791
  return true;
1426
1792
  }
1793
+ async function retryBuild(engine, siteDir, buildError, originalPrompt) {
1794
+ logger.info("Retrying: feeding build error back to AI...");
1795
+ const fixPrompt = `The site generation produced build errors. Fix the following errors without changing the overall design or structure:
1796
+
1797
+ ${buildError}
1798
+
1799
+ Original prompt for context:
1800
+ ${originalPrompt.slice(0, 2e3)}`;
1801
+ try {
1802
+ await runEngine(engine, fixPrompt, siteDir);
1803
+ return await buildSite(siteDir);
1804
+ } catch {
1805
+ return false;
1806
+ }
1807
+ }
1427
1808
  var init_orchestrator = __esm({
1428
1809
  "src/orchestrator/index.ts"() {
1429
1810
  "use strict";
@@ -1610,15 +1991,15 @@ var init_github = __esm({
1610
1991
  });
1611
1992
 
1612
1993
  // src/deployer/index.ts
1613
- import { join as join4 } from "path";
1994
+ import { join as join3 } from "path";
1614
1995
  async function deploy(siteDir, platform, projectName) {
1615
1996
  if (platform === "local") {
1616
- logger.info(`Site built at ${join4(siteDir, "out/")}`);
1997
+ logger.info(`Site built at ${join3(siteDir, "out/")}`);
1617
1998
  logger.info("Run `npx serve ./out` in the site directory to preview.");
1618
1999
  return null;
1619
2000
  }
1620
2001
  await ensureAuth(platform);
1621
- const distDir = join4(siteDir, "out");
2002
+ const distDir = join3(siteDir, "out");
1622
2003
  let url;
1623
2004
  if (platform === "cloudflare") {
1624
2005
  url = await deployToCloudflare(distDir, projectName);
@@ -1642,11 +2023,11 @@ var init_deployer = __esm({
1642
2023
 
1643
2024
  // src/pdf/index.ts
1644
2025
  import { createServer } from "http";
1645
- import { join as join5 } from "path";
2026
+ import { join as join4 } from "path";
1646
2027
  import ora8 from "ora";
1647
2028
  async function exportPdf(siteDir, outputPath) {
1648
- const distDir = join5(siteDir, "out");
1649
- const pdfPath = outputPath || join5(distDir, "shipfolio.pdf");
2029
+ const distDir = join4(siteDir, "out");
2030
+ const pdfPath = outputPath || join4(distDir, "shipfolio.pdf");
1650
2031
  const spinner = ora8("Exporting PDF...").start();
1651
2032
  const server = await startStaticServer(distDir);
1652
2033
  const port = server.address()?.port || 3456;
@@ -1702,7 +2083,7 @@ function startStaticServer(dir) {
1702
2083
  });
1703
2084
  }
1704
2085
  async function startPreviewServer(siteDir, port = 3e3) {
1705
- const distDir = join5(siteDir, "out");
2086
+ const distDir = join4(siteDir, "out");
1706
2087
  const handler = await import("serve-handler");
1707
2088
  const server = createServer((req, res) => {
1708
2089
  return handler.default(req, res, {
@@ -1726,6 +2107,49 @@ var init_pdf = __esm({
1726
2107
  }
1727
2108
  });
1728
2109
 
2110
+ // src/utils/draft.ts
2111
+ import { readFile as readFile2, writeFile as writeFile3, mkdir as mkdir3, unlink } from "fs/promises";
2112
+ import { join as join5 } from "path";
2113
+ import { homedir } from "os";
2114
+ async function saveDraft(interviewResult) {
2115
+ try {
2116
+ await mkdir3(DRAFT_DIR, { recursive: true });
2117
+ const draft = {
2118
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2119
+ interviewResult
2120
+ };
2121
+ await writeFile3(DRAFT_FILE, JSON.stringify(draft, null, 2), "utf-8");
2122
+ logger.info(t().draftSaved);
2123
+ } catch {
2124
+ }
2125
+ }
2126
+ async function loadDraft() {
2127
+ try {
2128
+ const content = await readFile2(DRAFT_FILE, "utf-8");
2129
+ return JSON.parse(content);
2130
+ } catch {
2131
+ return null;
2132
+ }
2133
+ }
2134
+ async function clearDraft() {
2135
+ try {
2136
+ await unlink(DRAFT_FILE);
2137
+ logger.info(t().draftCleared);
2138
+ } catch {
2139
+ }
2140
+ }
2141
+ var DRAFT_DIR, DRAFT_FILE;
2142
+ var init_draft = __esm({
2143
+ "src/utils/draft.ts"() {
2144
+ "use strict";
2145
+ init_esm_shims();
2146
+ init_logger();
2147
+ init_i18n();
2148
+ DRAFT_DIR = join5(homedir(), ".shipfolio");
2149
+ DRAFT_FILE = join5(DRAFT_DIR, "draft.json");
2150
+ }
2151
+ });
2152
+
1729
2153
  // src/commands/init.ts
1730
2154
  var init_exports = {};
1731
2155
  __export(init_exports, {
@@ -1735,7 +2159,7 @@ import { resolve } from "path";
1735
2159
  import { join as join6 } from "path";
1736
2160
  import * as p3 from "@clack/prompts";
1737
2161
  async function initCommand(options) {
1738
- logger.header("shipfolio v1.0.0");
2162
+ logger.header("shipfolio v1.0.4");
1739
2163
  logger.info("Detecting AI engines...");
1740
2164
  const engines = await detectEngines();
1741
2165
  const availableTypes = getAvailableEngineTypes(engines);
@@ -1769,7 +2193,24 @@ async function initCommand(options) {
1769
2193
  ]);
1770
2194
  logger.table(tableRows);
1771
2195
  logger.blank();
1772
- const interviewResult = await runInterview(scannedProjects, availableTypes);
2196
+ let interviewResult;
2197
+ const draft = await loadDraft();
2198
+ if (draft) {
2199
+ logger.info(t().draftFound);
2200
+ const useDraft = await p3.confirm({
2201
+ message: t().draftLoadPrompt,
2202
+ initialValue: true
2203
+ });
2204
+ if (!p3.isCancel(useDraft) && useDraft) {
2205
+ interviewResult = draft.interviewResult;
2206
+ await clearDraft();
2207
+ } else {
2208
+ interviewResult = await runInterview(scannedProjects, availableTypes);
2209
+ }
2210
+ } else {
2211
+ interviewResult = await runInterview(scannedProjects, availableTypes);
2212
+ }
2213
+ await saveDraft(interviewResult);
1773
2214
  const spec = buildSpec(interviewResult);
1774
2215
  const prompt = await buildFreshPrompt(spec);
1775
2216
  const outputDir = resolve(options.output || "./shipfolio-site");
@@ -1777,8 +2218,7 @@ async function initCommand(options) {
1777
2218
  await generateSite(spec.engine, prompt, outputDir);
1778
2219
  let buildSuccess = await buildSite(outputDir);
1779
2220
  if (!buildSuccess) {
1780
- logger.info("Attempting retry...");
1781
- buildSuccess = await buildSite(outputDir);
2221
+ buildSuccess = await retryBuild(spec.engine, outputDir, "Build failed. Check missing files and fix errors.", prompt);
1782
2222
  if (!buildSuccess) {
1783
2223
  logger.error("Build failed after retry. Check the output directory for details.");
1784
2224
  process.exit(1);
@@ -1820,6 +2260,7 @@ async function initCommand(options) {
1820
2260
  ...spec,
1821
2261
  sitePath: outputDir
1822
2262
  });
2263
+ await clearDraft();
1823
2264
  logger.header("Done.");
1824
2265
  if (spec.deploy.url) {
1825
2266
  logger.info(`Site: ${spec.deploy.url}`);
@@ -1844,6 +2285,8 @@ var init_init = __esm({
1844
2285
  init_pdf();
1845
2286
  init_fs();
1846
2287
  init_logger();
2288
+ init_draft();
2289
+ init_i18n();
1847
2290
  }
1848
2291
  });
1849
2292
 
@@ -2160,10 +2603,12 @@ async function previewCommand(options) {
2160
2603
  }
2161
2604
 
2162
2605
  // src/index.ts
2606
+ init_i18n();
2607
+ initLocale();
2163
2608
  var program = new Command();
2164
2609
  program.name("shipfolio").description(
2165
2610
  "Generate and deploy your personal portfolio site from local projects using AI"
2166
- ).version("1.0.0");
2611
+ ).version("1.0.4");
2167
2612
  program.command("init", { isDefault: true }).description("Generate a new portfolio site").option("-s, --scan <dirs...>", "Directories to scan for projects").option("-e, --engine <engine>", "AI engine: claude | codex | v0").option("-d, --deploy <platform>", "Deploy target: cloudflare | vercel | local").option("--style <theme>", "Theme: dark-minimal | light-clean | monochrome").option("--accent <hex>", "Accent color (hex)").option("--auto", "Skip prompts, use defaults").option("-o, --output <dir>", "Output directory", "./shipfolio-site").option("--no-pdf", "Skip PDF export").option("--no-preview", "Skip local preview").option("-v, --verbose", "Verbose output").action(initCommand);
2168
2613
  program.command("update").description("Update an existing portfolio site").requiredOption("--site <path>", "Path to existing site directory").option("-s, --scan <dirs...>", "Directories to scan for projects").option("--no-pdf", "Skip PDF export").option("--no-preview", "Skip preview").option("--no-deploy", "Skip deployment").action(updateCommand);
2169
2614
  program.command("spec").description("Generate spec and prompt files only").option("-s, --scan <dirs...>", "Directories to scan for projects").option("-o, --output <dir>", "Output directory for spec files", ".").action(specCommand);