shipfolio 1.0.7 → 1.0.9

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
@@ -378,10 +378,10 @@ function extractFirstParagraph(readme) {
378
378
  continue;
379
379
  }
380
380
  if (collecting && trimmed === "" && paragraphLines.length > 0) break;
381
- if (collecting && trimmed !== "") {
381
+ if (collecting && trimmed !== "" && !trimmed.startsWith("![") && !trimmed.startsWith("[![")) {
382
382
  paragraphLines.push(trimmed);
383
383
  }
384
- if (!collecting && trimmed !== "" && !trimmed.startsWith("[")) {
384
+ if (!collecting && trimmed !== "" && !trimmed.startsWith("[") && !trimmed.startsWith("!") && !trimmed.startsWith("[![")) {
385
385
  paragraphLines.push(trimmed);
386
386
  collecting = true;
387
387
  }
@@ -664,6 +664,8 @@ var init_i18n = __esm({
664
664
  deployTo: "Deploy to:",
665
665
  projectNamePrompt: "Project name (used in URL):",
666
666
  projectNameInvalid: "Lowercase letters, numbers, and hyphens only",
667
+ customDomainPrompt: "Custom domain (optional, e.g. portfolio.example.com):",
668
+ customDomainInvalid: "Enter a valid domain (e.g. portfolio.example.com)",
667
669
  configComplete: "Configuration complete. Generating your site...",
668
670
  // Draft
669
671
  draftFound: "Found a saved draft from a previous session.",
@@ -758,6 +760,8 @@ var init_i18n = __esm({
758
760
  deployTo: "\u90E8\u7F72\u5230:",
759
761
  projectNamePrompt: "\u9879\u76EE\u540D\u79F0 (\u7528\u4E8E URL):",
760
762
  projectNameInvalid: "\u4EC5\u9650\u5C0F\u5199\u5B57\u6BCD, \u6570\u5B57\u548C\u8FDE\u5B57\u7B26",
763
+ customDomainPrompt: "\u81EA\u5B9A\u4E49\u57DF\u540D (\u53EF\u9009, \u5982 portfolio.example.com):",
764
+ customDomainInvalid: "\u8BF7\u8F93\u5165\u6709\u6548\u7684\u57DF\u540D (\u5982 portfolio.example.com)",
761
765
  configComplete: "\u914D\u7F6E\u5B8C\u6210, \u6B63\u5728\u751F\u6210\u7F51\u7AD9...",
762
766
  draftFound: "\u53D1\u73B0\u4E0A\u6B21\u672A\u5B8C\u6210\u7684\u914D\u7F6E\u8349\u7A3F.",
763
767
  draftLoadPrompt: "\u662F\u5426\u52A0\u8F7D\u8349\u7A3F? (\u8DF3\u8FC7\u91CD\u590D\u586B\u5199)",
@@ -1181,6 +1185,7 @@ async function runInterview(scannedProjects, availableEngines) {
1181
1185
  });
1182
1186
  handleCancel(deployPlatform);
1183
1187
  let projectName = "";
1188
+ let customDomain = "";
1184
1189
  if (deployPlatform !== "local") {
1185
1190
  projectName = await p.text({
1186
1191
  message: t().projectNamePrompt,
@@ -1188,6 +1193,12 @@ async function runInterview(scannedProjects, availableEngines) {
1188
1193
  validate: (v) => /^[a-z0-9-]+$/.test(v) ? void 0 : t().projectNameInvalid
1189
1194
  });
1190
1195
  handleCancel(projectName);
1196
+ customDomain = await p.text({
1197
+ message: t().customDomainPrompt,
1198
+ placeholder: "portfolio.example.com",
1199
+ defaultValue: ""
1200
+ });
1201
+ handleCancel(customDomain);
1191
1202
  }
1192
1203
  p.outro(t().configComplete);
1193
1204
  return {
@@ -1198,7 +1209,8 @@ async function runInterview(scannedProjects, availableEngines) {
1198
1209
  engine,
1199
1210
  deploy: {
1200
1211
  platform: deployPlatform,
1201
- projectName
1212
+ projectName,
1213
+ customDomain: customDomain || void 0
1202
1214
  }
1203
1215
  };
1204
1216
  }
@@ -1225,7 +1237,8 @@ function buildSpec(interview) {
1225
1237
  sections: interview.sections,
1226
1238
  deploy: {
1227
1239
  platform: interview.deploy.platform,
1228
- projectName: interview.deploy.projectName
1240
+ projectName: interview.deploy.projectName,
1241
+ customDomain: interview.deploy.customDomain
1229
1242
  }
1230
1243
  };
1231
1244
  }
@@ -1754,9 +1767,9 @@ async function generateSite(engine, prompt, outputDir) {
1754
1767
  "The previous generation was incomplete. The following required files are missing:",
1755
1768
  ...validation.errors.map((e) => `- ${e}`),
1756
1769
  "",
1757
- "Please create ALL missing files. Here is the original specification:",
1758
- "",
1759
- prompt
1770
+ "Please create ALL missing files in the current working directory.",
1771
+ "This is a Next.js 15 + Tailwind CSS + shadcn/ui static site (output: 'export').",
1772
+ "npm install && npm run build must succeed."
1760
1773
  ].join("\n");
1761
1774
  await runEngine(engine, fixPrompt, outputDir);
1762
1775
  validation = await validateGeneratedSite(outputDir);
@@ -1777,8 +1790,9 @@ async function buildSite(siteDir) {
1777
1790
  spinner.succeed("Site built successfully");
1778
1791
  } catch (error) {
1779
1792
  spinner.fail("Build failed");
1780
- logger.error(error.stderr || error.message || String(error));
1781
- return false;
1793
+ const errorMsg = error.stderr || error.message || String(error);
1794
+ logger.error(errorMsg);
1795
+ return { success: false, error: errorMsg };
1782
1796
  }
1783
1797
  const validation = await validateBuildOutput(siteDir);
1784
1798
  if (!validation.valid) {
@@ -1786,23 +1800,26 @@ async function buildSite(siteDir) {
1786
1800
  for (const err of validation.errors) {
1787
1801
  logger.error(` ${err}`);
1788
1802
  }
1789
- return false;
1803
+ return { success: false, error: validation.errors.join("\n") };
1790
1804
  }
1791
- return true;
1805
+ return { success: true };
1792
1806
  }
1793
- async function retryBuild(engine, siteDir, buildError, originalPrompt) {
1807
+ async function retryBuild(engine, siteDir, buildError) {
1794
1808
  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)}`;
1809
+ const fixPrompt = [
1810
+ "The site build failed with the following errors. Fix them without changing the design or structure.",
1811
+ "",
1812
+ "Build errors:",
1813
+ buildError.slice(0, 4e3),
1814
+ "",
1815
+ "Fix the errors in the existing files in the current working directory.",
1816
+ "Do not recreate files that already work. Only fix what is broken."
1817
+ ].join("\n");
1801
1818
  try {
1802
1819
  await runEngine(engine, fixPrompt, siteDir);
1803
1820
  return await buildSite(siteDir);
1804
1821
  } catch {
1805
- return false;
1822
+ return { success: false, error: "Retry generation failed" };
1806
1823
  }
1807
1824
  }
1808
1825
  var init_orchestrator = __esm({
@@ -1880,7 +1897,26 @@ async function ensureProject(projectName) {
1880
1897
  } catch {
1881
1898
  }
1882
1899
  }
1883
- async function deployToCloudflare(distDir, projectName) {
1900
+ async function addCustomDomain(projectName, domain) {
1901
+ try {
1902
+ const whoami = await runWithOutput("npx", ["wrangler", "whoami"]);
1903
+ const accountMatch = whoami.match(/([0-9a-f]{32})/);
1904
+ if (!accountMatch) {
1905
+ logger.warn("Could not detect Cloudflare account ID for custom domain setup.");
1906
+ return false;
1907
+ }
1908
+ const accountId = accountMatch[1];
1909
+ logger.blank();
1910
+ logger.info(`To add custom domain "${domain}" to your Cloudflare Pages project:`);
1911
+ logger.plain(` 1. Go to https://dash.cloudflare.com/${accountId}/pages/${projectName}/custom-domains`);
1912
+ logger.plain(` 2. Click "Set up a custom domain" and enter: ${domain}`);
1913
+ logger.plain(` 3. Add the CNAME record as instructed`);
1914
+ return false;
1915
+ } catch {
1916
+ return false;
1917
+ }
1918
+ }
1919
+ async function deployToCloudflare(distDir, projectName, customDomain) {
1884
1920
  const spinner = ora6("Deploying to Cloudflare Pages...").start();
1885
1921
  try {
1886
1922
  spinner.text = "Creating Cloudflare Pages project...";
@@ -1899,6 +1935,9 @@ async function deployToCloudflare(distDir, projectName) {
1899
1935
  const url = urlMatch?.[0] || `https://${projectName}.pages.dev`;
1900
1936
  spinner.succeed(`Deployed to Cloudflare Pages`);
1901
1937
  logger.info(`URL: ${url}`);
1938
+ if (customDomain) {
1939
+ await addCustomDomain(projectName, customDomain);
1940
+ }
1902
1941
  return url;
1903
1942
  } catch (error) {
1904
1943
  spinner.fail("Cloudflare Pages deployment failed");
@@ -1916,7 +1955,7 @@ var init_cloudflare = __esm({
1916
1955
 
1917
1956
  // src/deployer/vercel.ts
1918
1957
  import ora7 from "ora";
1919
- async function deployToVercel(distDir) {
1958
+ async function deployToVercel(distDir, customDomain) {
1920
1959
  const spinner = ora7("Deploying to Vercel...").start();
1921
1960
  try {
1922
1961
  const output = await runWithOutput("npx", [
@@ -1932,6 +1971,24 @@ async function deployToVercel(distDir) {
1932
1971
  const url = urlMatch?.[0] || output.trim().split("\n").pop() || "";
1933
1972
  spinner.succeed("Deployed to Vercel");
1934
1973
  logger.info(`URL: ${url}`);
1974
+ if (customDomain) {
1975
+ spinner.start(`Adding custom domain: ${customDomain}...`);
1976
+ try {
1977
+ await runWithOutput("npx", [
1978
+ "vercel",
1979
+ "domains",
1980
+ "add",
1981
+ customDomain
1982
+ ]);
1983
+ spinner.succeed(`Custom domain added: ${customDomain}`);
1984
+ logger.blank();
1985
+ logger.info("Add a CNAME record pointing to cname.vercel-dns.com in your DNS provider.");
1986
+ logger.info(`Your site will be available at: https://${customDomain}`);
1987
+ } catch (err) {
1988
+ spinner.warn(`Could not add domain automatically: ${err.message || err}`);
1989
+ logger.info(`Add it manually: npx vercel domains add ${customDomain}`);
1990
+ }
1991
+ }
1935
1992
  return url;
1936
1993
  } catch (error) {
1937
1994
  spinner.fail("Vercel deployment failed");
@@ -2081,7 +2138,7 @@ var init_github = __esm({
2081
2138
 
2082
2139
  // src/deployer/index.ts
2083
2140
  import { join as join3 } from "path";
2084
- async function deploy(siteDir, platform, projectName) {
2141
+ async function deploy(siteDir, platform, projectName, customDomain) {
2085
2142
  if (platform === "local") {
2086
2143
  logger.info(`Site built at ${join3(siteDir, "out/")}`);
2087
2144
  logger.info("Run `npx serve ./out` in the site directory to preview.");
@@ -2091,9 +2148,9 @@ async function deploy(siteDir, platform, projectName) {
2091
2148
  const distDir = join3(siteDir, "out");
2092
2149
  let url;
2093
2150
  if (platform === "cloudflare") {
2094
- url = await deployToCloudflare(distDir, projectName);
2151
+ url = await deployToCloudflare(distDir, projectName, customDomain);
2095
2152
  } else {
2096
- url = await deployToVercel(distDir);
2153
+ url = await deployToVercel(distDir, customDomain);
2097
2154
  }
2098
2155
  await setupGitHubAutoDeploy(siteDir, projectName, platform);
2099
2156
  return url;
@@ -2180,12 +2237,22 @@ async function startPreviewServer(siteDir, port = 3e3) {
2180
2237
  cleanUrls: true
2181
2238
  });
2182
2239
  });
2183
- return new Promise((resolve7, reject) => {
2184
- server.listen(port, () => {
2185
- logger.info(`Preview: http://localhost:${port}`);
2186
- resolve7(server);
2187
- });
2188
- server.on("error", reject);
2240
+ return new Promise((resolve7) => {
2241
+ const tryListen = (p6) => {
2242
+ server.once("error", (err) => {
2243
+ if (err.code === "EADDRINUSE") {
2244
+ tryListen(0);
2245
+ } else {
2246
+ tryListen(0);
2247
+ }
2248
+ });
2249
+ server.listen(p6, () => {
2250
+ const actualPort = server.address()?.port || p6;
2251
+ logger.info(`Preview: http://localhost:${actualPort}`);
2252
+ resolve7(server);
2253
+ });
2254
+ };
2255
+ tryListen(port);
2189
2256
  });
2190
2257
  }
2191
2258
  var init_pdf = __esm({
@@ -2200,7 +2267,7 @@ var init_pdf = __esm({
2200
2267
  import { readFile as readFile2, writeFile as writeFile3, mkdir as mkdir3, unlink } from "fs/promises";
2201
2268
  import { join as join5 } from "path";
2202
2269
  import { homedir } from "os";
2203
- async function saveDraft(interviewResult) {
2270
+ async function saveDraft(interviewResult, silent = false) {
2204
2271
  try {
2205
2272
  await mkdir3(DRAFT_DIR, { recursive: true });
2206
2273
  const draft = {
@@ -2208,7 +2275,9 @@ async function saveDraft(interviewResult) {
2208
2275
  interviewResult
2209
2276
  };
2210
2277
  await writeFile3(DRAFT_FILE, JSON.stringify(draft, null, 2), "utf-8");
2211
- logger.info(t().draftSaved);
2278
+ if (!silent) {
2279
+ logger.info(t().draftSaved);
2280
+ }
2212
2281
  } catch {
2213
2282
  }
2214
2283
  }
@@ -2220,10 +2289,12 @@ async function loadDraft() {
2220
2289
  return null;
2221
2290
  }
2222
2291
  }
2223
- async function clearDraft() {
2292
+ async function clearDraft(silent = false) {
2224
2293
  try {
2225
2294
  await unlink(DRAFT_FILE);
2226
- logger.info(t().draftCleared);
2295
+ if (!silent) {
2296
+ logger.info(t().draftCleared);
2297
+ }
2227
2298
  } catch {
2228
2299
  }
2229
2300
  }
@@ -2248,7 +2319,7 @@ import { resolve } from "path";
2248
2319
  import { join as join6 } from "path";
2249
2320
  import * as p3 from "@clack/prompts";
2250
2321
  async function initCommand(options) {
2251
- logger.header("shipfolio v1.0.7");
2322
+ logger.header("shipfolio v1.0.9");
2252
2323
  logger.info("Detecting AI engines...");
2253
2324
  const engines = await detectEngines();
2254
2325
  const availableTypes = getAvailableEngineTypes(engines);
@@ -2299,16 +2370,16 @@ async function initCommand(options) {
2299
2370
  } else {
2300
2371
  interviewResult = await runInterview(scannedProjects, availableTypes);
2301
2372
  }
2302
- await saveDraft(interviewResult);
2373
+ await saveDraft(interviewResult, true);
2303
2374
  const spec = buildSpec(interviewResult);
2304
2375
  const prompt = await buildFreshPrompt(spec);
2305
2376
  const outputDir = resolve(options.output || "./shipfolio-site");
2306
2377
  await ensureDir(outputDir);
2307
2378
  await generateSite(spec.engine, prompt, outputDir);
2308
- let buildSuccess = await buildSite(outputDir);
2309
- if (!buildSuccess) {
2310
- buildSuccess = await retryBuild(spec.engine, outputDir, "Build failed. Check missing files and fix errors.", prompt);
2311
- if (!buildSuccess) {
2379
+ let buildResult = await buildSite(outputDir);
2380
+ if (!buildResult.success) {
2381
+ buildResult = await retryBuild(spec.engine, outputDir, buildResult.error || "Unknown build error");
2382
+ if (!buildResult.success) {
2312
2383
  logger.error("Build failed after retry. Check the output directory for details.");
2313
2384
  process.exit(1);
2314
2385
  }
@@ -2344,7 +2415,8 @@ async function initCommand(options) {
2344
2415
  const url = await deploy(
2345
2416
  outputDir,
2346
2417
  spec.deploy.platform,
2347
- spec.deploy.projectName
2418
+ spec.deploy.projectName,
2419
+ spec.deploy.customDomain
2348
2420
  );
2349
2421
  if (url) {
2350
2422
  spec.deploy.url = url;
@@ -2358,7 +2430,7 @@ async function initCommand(options) {
2358
2430
  ...spec,
2359
2431
  sitePath: outputDir
2360
2432
  });
2361
- await clearDraft();
2433
+ await clearDraft(true);
2362
2434
  logger.header("Done.");
2363
2435
  if (spec.deploy.url) {
2364
2436
  logger.info(`Site: ${spec.deploy.url}`);
@@ -2396,7 +2468,7 @@ import { Command } from "commander";
2396
2468
  // src/commands/update.ts
2397
2469
  init_esm_shims();
2398
2470
  init_scanner();
2399
- import { resolve as resolve2 } from "path";
2471
+ import { resolve as resolve2, dirname as dirname2 } from "path";
2400
2472
 
2401
2473
  // src/spec/diff.ts
2402
2474
  init_esm_shims();
@@ -2467,8 +2539,9 @@ async function updateCommand(options) {
2467
2539
  return;
2468
2540
  }
2469
2541
  const config = await readJson(configPath);
2542
+ const generatedDate = config.generatedAt?.slice(0, 10) || "unknown";
2470
2543
  logger.info(
2471
- `Site: ${config.deploy.projectName || "local"} (generated ${config.generatedAt.slice(0, 10)})`
2544
+ `Site: ${config.deploy.projectName || "local"} (generated ${generatedDate})`
2472
2545
  );
2473
2546
  logger.info(`Theme: ${config.style.theme}`);
2474
2547
  logger.info(`Projects: ${config.projects.length}`);
@@ -2478,11 +2551,7 @@ async function updateCommand(options) {
2478
2551
  logger.error("No AI engine found. See `npx shipfolio` for install instructions.");
2479
2552
  process.exit(1);
2480
2553
  }
2481
- const scanDirs = options.scan?.map((d) => resolve2(d)) || [...new Set(config.projects.map((p6) => {
2482
- const parts = p6.localPath.split("/");
2483
- parts.pop();
2484
- return parts.join("/");
2485
- }))];
2554
+ const scanDirs = options.scan?.map((d) => resolve2(d)) || [...new Set(config.projects.map((p6) => dirname2(p6.localPath)))];
2486
2555
  const scannedProjects = await scanProjects(scanDirs);
2487
2556
  const diff = computeDiff(config, scannedProjects);
2488
2557
  logger.header("Changes detected:");
@@ -2520,10 +2589,13 @@ async function updateCommand(options) {
2520
2589
  const engine = availableTypes.includes(config.engine) ? config.engine : availableTypes[0];
2521
2590
  const prompt = await buildUpdatePrompt(config, diff);
2522
2591
  await generateSite(engine, prompt, siteDir);
2523
- const buildSuccess = await buildSite(siteDir);
2524
- if (!buildSuccess) {
2525
- logger.error("Build failed. Check the site directory.");
2526
- process.exit(1);
2592
+ let buildResult = await buildSite(siteDir);
2593
+ if (!buildResult.success) {
2594
+ buildResult = await retryBuild(engine, siteDir, buildResult.error || "Unknown build error");
2595
+ if (!buildResult.success) {
2596
+ logger.error("Build failed after retry. Check the site directory.");
2597
+ process.exit(1);
2598
+ }
2527
2599
  }
2528
2600
  if (!options.noPreview) {
2529
2601
  const server = await startPreviewServer(siteDir);
@@ -2545,7 +2617,12 @@ async function updateCommand(options) {
2545
2617
  }
2546
2618
  }
2547
2619
  if (!options.noDeploy && config.deploy.platform !== "local") {
2548
- await deploy(siteDir, config.deploy.platform, config.deploy.projectName);
2620
+ try {
2621
+ await deploy(siteDir, config.deploy.platform, config.deploy.projectName, config.deploy.customDomain);
2622
+ } catch (err) {
2623
+ logger.error(`Deployment failed: ${err.message || err}`);
2624
+ logger.info("You can retry later with: npx shipfolio deploy --site " + siteDir);
2625
+ }
2549
2626
  }
2550
2627
  const updatedConfig = {
2551
2628
  ...config,
@@ -2595,7 +2672,7 @@ async function specCommand(options) {
2595
2672
  logger.header("shipfolio spec");
2596
2673
  const engines = await detectEngines();
2597
2674
  const availableTypes = getAvailableEngineTypes(engines);
2598
- const engineTypes = availableTypes.length > 0 ? availableTypes : ["claude"];
2675
+ const engineTypes = availableTypes.length > 0 ? availableTypes : ["claude", "codex", "v0"];
2599
2676
  const scanDirs = options.scan?.map((d) => resolve3(d)) || [
2600
2677
  resolve3(process.cwd())
2601
2678
  ];
@@ -2636,10 +2713,12 @@ async function deployCommand(options) {
2636
2713
  const configPath = join(siteDir, "shipfolio.config.json");
2637
2714
  let platform;
2638
2715
  let projectName;
2716
+ let customDomain;
2639
2717
  if (await fileExists(configPath)) {
2640
2718
  const config = await readJson(configPath);
2641
2719
  platform = options.platform || config.deploy.platform;
2642
2720
  projectName = config.deploy.projectName;
2721
+ customDomain = config.deploy.customDomain;
2643
2722
  } else {
2644
2723
  platform = options.platform || "cloudflare";
2645
2724
  projectName = await p5.text({
@@ -2657,7 +2736,7 @@ async function deployCommand(options) {
2657
2736
  logger.error("Build output not found. Run `npm run build` in the site directory first.");
2658
2737
  process.exit(1);
2659
2738
  }
2660
- await deploy(siteDir, platform, projectName);
2739
+ await deploy(siteDir, platform, projectName, customDomain);
2661
2740
  }
2662
2741
 
2663
2742
  // src/commands/pdf.ts
@@ -2706,8 +2785,8 @@ initLocale();
2706
2785
  var program = new Command();
2707
2786
  program.name("shipfolio").description(
2708
2787
  "Generate and deploy your personal portfolio site from local projects using AI"
2709
- ).version("1.0.7");
2710
- 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);
2788
+ ).version("1.0.9");
2789
+ program.command("init", { isDefault: true }).description("Generate a new portfolio site").option("-s, --scan <dirs...>", "Directories to scan for projects").option("-o, --output <dir>", "Output directory", "./shipfolio-site").option("--no-pdf", "Skip PDF export").option("--no-preview", "Skip local preview").action(initCommand);
2711
2790
  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);
2712
2791
  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);
2713
2792
  program.command("deploy").description("Deploy an existing built site").requiredOption("--site <path>", "Path to site directory").option("-p, --platform <platform>", "Deploy platform: cloudflare | vercel").action(deployCommand);