shipfolio 1.0.2 → 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
  });
@@ -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
 
@@ -1199,7 +1539,7 @@ var init_codex = __esm({
1199
1539
  // src/orchestrator/engines/v0.ts
1200
1540
  import OpenAI from "openai";
1201
1541
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1202
- import { join as join3, dirname as dirname2 } from "path";
1542
+ import { join as join2, dirname } from "path";
1203
1543
  import ora4 from "ora";
1204
1544
  async function generateWithV0(prompt, outputDir, apiKey) {
1205
1545
  const spinner = ora4("Generating site with v0...").start();
@@ -1232,14 +1572,14 @@ async function generateWithV0(prompt, outputDir, apiKey) {
1232
1572
  }
1233
1573
  spinner.text = `Writing ${files.length} files...`;
1234
1574
  for (const file of files) {
1235
- const filePath = join3(outputDir, file.filename);
1236
- await mkdir2(dirname2(filePath), { recursive: true });
1575
+ const filePath = join2(outputDir, file.filename);
1576
+ await mkdir2(dirname(filePath), { recursive: true });
1237
1577
  await writeFile2(filePath, file.content, "utf-8");
1238
1578
  }
1239
1579
  const hasPkgJson = files.some((f) => f.filename === "package.json");
1240
1580
  if (!hasPkgJson) {
1241
1581
  await writeFile2(
1242
- join3(outputDir, "package.json"),
1582
+ join2(outputDir, "package.json"),
1243
1583
  JSON.stringify(
1244
1584
  {
1245
1585
  name: "shipfolio-site",
@@ -1610,15 +1950,15 @@ var init_github = __esm({
1610
1950
  });
1611
1951
 
1612
1952
  // src/deployer/index.ts
1613
- import { join as join4 } from "path";
1953
+ import { join as join3 } from "path";
1614
1954
  async function deploy(siteDir, platform, projectName) {
1615
1955
  if (platform === "local") {
1616
- logger.info(`Site built at ${join4(siteDir, "out/")}`);
1956
+ logger.info(`Site built at ${join3(siteDir, "out/")}`);
1617
1957
  logger.info("Run `npx serve ./out` in the site directory to preview.");
1618
1958
  return null;
1619
1959
  }
1620
1960
  await ensureAuth(platform);
1621
- const distDir = join4(siteDir, "out");
1961
+ const distDir = join3(siteDir, "out");
1622
1962
  let url;
1623
1963
  if (platform === "cloudflare") {
1624
1964
  url = await deployToCloudflare(distDir, projectName);
@@ -1642,11 +1982,11 @@ var init_deployer = __esm({
1642
1982
 
1643
1983
  // src/pdf/index.ts
1644
1984
  import { createServer } from "http";
1645
- import { join as join5 } from "path";
1985
+ import { join as join4 } from "path";
1646
1986
  import ora8 from "ora";
1647
1987
  async function exportPdf(siteDir, outputPath) {
1648
- const distDir = join5(siteDir, "out");
1649
- const pdfPath = outputPath || join5(distDir, "shipfolio.pdf");
1988
+ const distDir = join4(siteDir, "out");
1989
+ const pdfPath = outputPath || join4(distDir, "shipfolio.pdf");
1650
1990
  const spinner = ora8("Exporting PDF...").start();
1651
1991
  const server = await startStaticServer(distDir);
1652
1992
  const port = server.address()?.port || 3456;
@@ -1702,7 +2042,7 @@ function startStaticServer(dir) {
1702
2042
  });
1703
2043
  }
1704
2044
  async function startPreviewServer(siteDir, port = 3e3) {
1705
- const distDir = join5(siteDir, "out");
2045
+ const distDir = join4(siteDir, "out");
1706
2046
  const handler = await import("serve-handler");
1707
2047
  const server = createServer((req, res) => {
1708
2048
  return handler.default(req, res, {
@@ -1726,6 +2066,49 @@ var init_pdf = __esm({
1726
2066
  }
1727
2067
  });
1728
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
+
1729
2112
  // src/commands/init.ts
1730
2113
  var init_exports = {};
1731
2114
  __export(init_exports, {
@@ -1735,7 +2118,7 @@ import { resolve } from "path";
1735
2118
  import { join as join6 } from "path";
1736
2119
  import * as p3 from "@clack/prompts";
1737
2120
  async function initCommand(options) {
1738
- logger.header("shipfolio v1.0.0");
2121
+ logger.header("shipfolio v1.0.3");
1739
2122
  logger.info("Detecting AI engines...");
1740
2123
  const engines = await detectEngines();
1741
2124
  const availableTypes = getAvailableEngineTypes(engines);
@@ -1769,7 +2152,24 @@ async function initCommand(options) {
1769
2152
  ]);
1770
2153
  logger.table(tableRows);
1771
2154
  logger.blank();
1772
- 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);
1773
2173
  const spec = buildSpec(interviewResult);
1774
2174
  const prompt = await buildFreshPrompt(spec);
1775
2175
  const outputDir = resolve(options.output || "./shipfolio-site");
@@ -1820,6 +2220,7 @@ async function initCommand(options) {
1820
2220
  ...spec,
1821
2221
  sitePath: outputDir
1822
2222
  });
2223
+ await clearDraft();
1823
2224
  logger.header("Done.");
1824
2225
  if (spec.deploy.url) {
1825
2226
  logger.info(`Site: ${spec.deploy.url}`);
@@ -1844,6 +2245,8 @@ var init_init = __esm({
1844
2245
  init_pdf();
1845
2246
  init_fs();
1846
2247
  init_logger();
2248
+ init_draft();
2249
+ init_i18n();
1847
2250
  }
1848
2251
  });
1849
2252
 
@@ -2160,10 +2563,12 @@ async function previewCommand(options) {
2160
2563
  }
2161
2564
 
2162
2565
  // src/index.ts
2566
+ init_i18n();
2567
+ initLocale();
2163
2568
  var program = new Command();
2164
2569
  program.name("shipfolio").description(
2165
2570
  "Generate and deploy your personal portfolio site from local projects using AI"
2166
- ).version("1.0.0");
2571
+ ).version("1.0.3");
2167
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);
2168
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);
2169
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);