ship-em 0.2.3 → 0.2.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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # shipem
1
+ # shipem 🐑
2
2
 
3
3
  **One command. Your app is live.**
4
4
 
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import chalk4 from "chalk";
9
9
  import axios2 from "axios";
10
10
  import FormData2 from "form-data";
11
11
  import { createReadStream } from "fs";
12
- import { rmSync, existsSync as existsSync6, appendFileSync, writeFileSync as writeFileSync4, readFileSync as readFileSync7, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
12
+ import { rmSync, existsSync as existsSync6, appendFileSync, writeFileSync as writeFileSync5, readFileSync as readFileSync8, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
13
13
  import { join as join8 } from "path";
14
14
  import { tmpdir } from "os";
15
15
  import { create as tarCreate } from "tar";
@@ -68,7 +68,7 @@ var ui = {
68
68
  );
69
69
  console.log("");
70
70
  console.log(
71
- ` ${brand.gray("Your AI built it.")} ${brand.blue.bold("We'll ship it.")}`
71
+ ` \u{1F411} ${brand.gray("Your AI built it.")} ${brand.blue.bold("We'll ship it.")}`
72
72
  );
73
73
  console.log("");
74
74
  },
@@ -546,7 +546,7 @@ function scanProjectInternal(cwd) {
546
546
  return {
547
547
  framework: "nextjs",
548
548
  buildCommand: pkg.scripts?.build ?? "npm run build",
549
- outputDirectory: ".next",
549
+ outputDirectory: "out",
550
550
  installCommand: detectPackageManager(cwd),
551
551
  serverType,
552
552
  deployTarget: "cloudflare-pages",
@@ -794,9 +794,34 @@ function findWorkspacePackages(cwd) {
794
794
 
795
795
  // src/build/builder.ts
796
796
  import { execa } from "execa";
797
- import { existsSync as existsSync2 } from "fs";
797
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
798
798
  import { join as join2 } from "path";
799
799
  import chalk2 from "chalk";
800
+ function patchNextConfig(cwd) {
801
+ const candidates = ["next.config.ts", "next.config.mjs", "next.config.js"];
802
+ for (const filename of candidates) {
803
+ const configPath = join2(cwd, filename);
804
+ if (existsSync2(configPath)) {
805
+ const content = readFileSync2(configPath, "utf-8");
806
+ if (content.includes("output")) return;
807
+ const patched = content.replace(
808
+ /const nextConfig[^=]*=\s*\{/,
809
+ (match) => match + "\n output: 'export',"
810
+ );
811
+ if (patched !== content) {
812
+ writeFileSync(configPath, patched, "utf-8");
813
+ ui.dim(" \u2139\uFE0F Added output: 'export' to " + filename + " for static deployment");
814
+ return;
815
+ }
816
+ }
817
+ }
818
+ writeFileSync(
819
+ join2(cwd, "next.config.js"),
820
+ "/** @type {import('next').NextConfig} */\nconst nextConfig = { output: 'export' };\nmodule.exports = nextConfig;\n",
821
+ "utf-8"
822
+ );
823
+ ui.dim(" \u2139\uFE0F Created next.config.js with output: 'export' for static deployment");
824
+ }
800
825
  async function buildProject(config, cwd = process.cwd()) {
801
826
  const start = Date.now();
802
827
  if (config.installCommand) {
@@ -804,8 +829,8 @@ async function buildProject(config, cwd = process.cwd()) {
804
829
  const outputLines = [];
805
830
  let lineCount = 0;
806
831
  try {
807
- const [installBin, ...installArgs] = config.installCommand.split(" ");
808
- const installProc = execa(installBin, installArgs, {
832
+ const installProc = execa(config.installCommand, {
833
+ shell: true,
809
834
  cwd,
810
835
  env: { ...process.env, CI: "true" },
811
836
  timeout: 5 * 60 * 1e3,
@@ -850,6 +875,9 @@ async function buildProject(config, cwd = process.cwd()) {
850
875
  }
851
876
  console.log("");
852
877
  }
878
+ if (config.framework === "nextjs") {
879
+ patchNextConfig(cwd);
880
+ }
853
881
  if (config.buildCommand) {
854
882
  const buildSpinner = ui.spinner("Building...");
855
883
  const outputLines = [];
@@ -865,8 +893,8 @@ async function buildProject(config, cwd = process.cwd()) {
865
893
  }
866
894
  }
867
895
  try {
868
- const [finalBin, ...finalArgs] = config.buildCommand.split(" ");
869
- const buildProc = execa(finalBin, finalArgs, {
896
+ const buildProc = execa(config.buildCommand, {
897
+ shell: true,
870
898
  cwd,
871
899
  env: buildEnv,
872
900
  timeout: 10 * 60 * 1e3,
@@ -944,7 +972,7 @@ function parseErrorMessage(raw, outputLines) {
944
972
 
945
973
  // src/config.ts
946
974
  import Conf from "conf";
947
- import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3 } from "fs";
975
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
948
976
  import { join as join3 } from "path";
949
977
  var SHIPEM_API_URL = process.env.SHIPEM_API_URL ?? "https://api.shipem.dev";
950
978
  var globalConf = new Conf({
@@ -985,7 +1013,7 @@ function readProjectConfig(cwd = process.cwd()) {
985
1013
  return {};
986
1014
  }
987
1015
  try {
988
- const raw = readFileSync2(configPath, "utf-8");
1016
+ const raw = readFileSync3(configPath, "utf-8");
989
1017
  const config = JSON.parse(raw);
990
1018
  warnIfConfigContainsSecrets(config, configPath);
991
1019
  return config;
@@ -995,7 +1023,7 @@ function readProjectConfig(cwd = process.cwd()) {
995
1023
  }
996
1024
  function writeProjectConfig(config, cwd = process.cwd()) {
997
1025
  const configPath = join3(cwd, SHIPIT_CONFIG_FILE);
998
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1026
+ writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
999
1027
  }
1000
1028
  var SECRET_KEY_PATTERN = /token|secret|key|password|credential|api_?key/i;
1001
1029
  function warnIfConfigContainsSecrets(config, configPath) {
@@ -1162,7 +1190,7 @@ async function loginCommand(opts = {}) {
1162
1190
  }
1163
1191
 
1164
1192
  // src/commands/fix.ts
1165
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync as readdirSync2 } from "fs";
1193
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, readdirSync as readdirSync2 } from "fs";
1166
1194
  import { join as join4 } from "path";
1167
1195
  import { execa as execa2 } from "execa";
1168
1196
  function findMissingModules(errorOutput) {
@@ -1239,12 +1267,12 @@ function applyTsConfigFixes(cwd, fixes) {
1239
1267
  const tsconfigPath = join4(cwd, "tsconfig.json");
1240
1268
  if (!existsSync4(tsconfigPath)) return false;
1241
1269
  try {
1242
- const content = readFileSync3(tsconfigPath, "utf-8");
1270
+ const content = readFileSync4(tsconfigPath, "utf-8");
1243
1271
  const stripped = content.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
1244
1272
  const tsconfig = JSON.parse(stripped);
1245
1273
  if (!tsconfig.compilerOptions) tsconfig.compilerOptions = {};
1246
1274
  Object.assign(tsconfig.compilerOptions, fixes);
1247
- writeFileSync2(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n", "utf-8");
1275
+ writeFileSync3(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n", "utf-8");
1248
1276
  return true;
1249
1277
  } catch {
1250
1278
  return false;
@@ -1267,7 +1295,7 @@ function findMissingEnvVars(cwd) {
1267
1295
  scanDir(fullPath, depth + 1);
1268
1296
  } else if (/\.(ts|tsx|js|jsx|mjs|mts|vue|svelte|astro)$/.test(entry.name)) {
1269
1297
  try {
1270
- const content = readFileSync3(fullPath, "utf-8");
1298
+ const content = readFileSync4(fullPath, "utf-8");
1271
1299
  for (const pat of patterns) {
1272
1300
  pat.lastIndex = 0;
1273
1301
  let match;
@@ -1296,7 +1324,7 @@ function generateEnvExample(cwd, vars) {
1296
1324
  const envExamplePath = join4(cwd, ".env.example");
1297
1325
  if (existsSync4(envExamplePath)) return false;
1298
1326
  const content = vars.map((v) => `${v}=`).join("\n") + "\n";
1299
- writeFileSync2(envExamplePath, content, "utf-8");
1327
+ writeFileSync3(envExamplePath, content, "utf-8");
1300
1328
  return true;
1301
1329
  }
1302
1330
  function detectFrameworkConfigFixes(errorOutput, cwd, framework) {
@@ -1305,7 +1333,7 @@ function detectFrameworkConfigFixes(errorOutput, cwd, framework) {
1305
1333
  const nextConfigPath = existsSync4(join4(cwd, "next.config.mjs")) ? join4(cwd, "next.config.mjs") : existsSync4(join4(cwd, "next.config.js")) ? join4(cwd, "next.config.js") : null;
1306
1334
  if (nextConfigPath && errorOutput.includes("output")) {
1307
1335
  try {
1308
- const content = readFileSync3(nextConfigPath, "utf-8");
1336
+ const content = readFileSync4(nextConfigPath, "utf-8");
1309
1337
  if (!content.includes("output")) {
1310
1338
  fixes.push(`Add output: 'export' to ${nextConfigPath.split("/").pop()}`);
1311
1339
  }
@@ -1316,7 +1344,7 @@ function detectFrameworkConfigFixes(errorOutput, cwd, framework) {
1316
1344
  if ((framework === "vite-react" || framework === "vite-vue" || framework === "vite-svelte") && errorOutput.includes("plugin")) {
1317
1345
  const pkgPath = join4(cwd, "package.json");
1318
1346
  try {
1319
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1347
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
1320
1348
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1321
1349
  if (framework === "vite-react" && !deps["@vitejs/plugin-react"]) {
1322
1350
  fixes.push("Install @vitejs/plugin-react");
@@ -1488,11 +1516,11 @@ async function tryInlineFix(errorOutput, cwd, config) {
1488
1516
  // src/deploy/cloudflare.ts
1489
1517
  import axios from "axios";
1490
1518
  import { createHash } from "crypto";
1491
- import { readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync5 } from "fs";
1519
+ import { readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync6 } from "fs";
1492
1520
  import { join as join6, relative } from "path";
1493
1521
 
1494
1522
  // src/deploy/exclude.ts
1495
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1523
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
1496
1524
  import { join as join5 } from "path";
1497
1525
  import chalk3 from "chalk";
1498
1526
  var DEFAULT_PATTERNS = [
@@ -1547,7 +1575,7 @@ function matchesIgnoreLine(relPath, line) {
1547
1575
  function loadIgnoreLines(filePath) {
1548
1576
  if (!existsSync5(filePath)) return [];
1549
1577
  try {
1550
- return readFileSync4(filePath, "utf-8").split("\n");
1578
+ return readFileSync5(filePath, "utf-8").split("\n");
1551
1579
  } catch {
1552
1580
  return [];
1553
1581
  }
@@ -1680,7 +1708,7 @@ var CloudflarePages = class {
1680
1708
  let totalBytes = 0;
1681
1709
  const fileMap = /* @__PURE__ */ new Map();
1682
1710
  for (const filePath of filePaths) {
1683
- const content = readFileSync5(filePath);
1711
+ const content = readFileSync6(filePath);
1684
1712
  const hash = createHash("sha256").update(content).digest("hex");
1685
1713
  const urlPath = "/" + relative(fullOutputPath, filePath).replace(/\\/g, "/");
1686
1714
  fileMap.set(urlPath, { hash, content });
@@ -1695,6 +1723,9 @@ var CloudflarePages = class {
1695
1723
  for (const [urlPath, { hash }] of fileMap) {
1696
1724
  manifest[urlPath] = hash;
1697
1725
  }
1726
+ if (Object.keys(manifest).length === 0) {
1727
+ throw new DeployError(`Output directory '${outputDir}' contains no files to deploy. Check your build output.`);
1728
+ }
1698
1729
  const deploySpinner = ui.spinner("Creating deployment...");
1699
1730
  let jwt;
1700
1731
  let requiredFiles;
@@ -1759,7 +1790,7 @@ var CloudflarePages = class {
1759
1790
  let totalBytes = 0;
1760
1791
  const fileMap = /* @__PURE__ */ new Map();
1761
1792
  for (const filePath of filePaths) {
1762
- const content = readFileSync5(filePath);
1793
+ const content = readFileSync6(filePath);
1763
1794
  const hash = createHash("sha256").update(content).digest("hex");
1764
1795
  const urlPath = "/" + relative(fullOutputPath, filePath).replace(/\\/g, "/");
1765
1796
  fileMap.set(urlPath, { hash, content });
@@ -1774,6 +1805,9 @@ var CloudflarePages = class {
1774
1805
  for (const [urlPath, { hash }] of fileMap) {
1775
1806
  manifest[urlPath] = hash;
1776
1807
  }
1808
+ if (Object.keys(manifest).length === 0) {
1809
+ throw new DeployError(`Output directory '${outputDir}' contains no files to deploy. Check your build output.`);
1810
+ }
1777
1811
  const deploySpinner = ui.spinner(`Creating preview deployment (branch: ${branch})...`);
1778
1812
  let jwt;
1779
1813
  let requiredFiles;
@@ -1948,7 +1982,7 @@ function sanitizeProjectName(name) {
1948
1982
  }
1949
1983
 
1950
1984
  // src/deploy/badge.ts
1951
- import { readdirSync as readdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
1985
+ import { readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
1952
1986
  import { join as join7 } from "path";
1953
1987
  var BADGE_HTML = `<!-- Shipped with Shipem -->
1954
1988
  <div id="shipem-badge" style="position:fixed;bottom:12px;right:12px;z-index:9999;font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:12px;background:rgba(13,17,23,0.9);color:#94A3B8;padding:6px 12px;border-radius:20px;border:1px solid rgba(59,130,246,0.3);text-decoration:none;display:flex;align-items:center;gap:6px;backdrop-filter:blur(8px);transition:opacity 0.2s"><a href="https://shipem.dev" target="_blank" rel="noopener" style="color:#94A3B8;text-decoration:none;display:flex;align-items:center;gap:6px">Shipped with <span style="color:#3B82F6">\u26A1</span> Shipem</a></div>`;
@@ -1958,14 +1992,14 @@ function injectBadge(outputDir) {
1958
1992
  const files = readdirSync4(outputDir).filter((f) => f.endsWith(".html"));
1959
1993
  for (const file of files) {
1960
1994
  const filePath = join7(outputDir, file);
1961
- const content = readFileSync6(filePath, "utf-8");
1995
+ const content = readFileSync7(filePath, "utf-8");
1962
1996
  if (content.includes("shipem-badge")) continue;
1963
1997
  if (content.includes("</body>")) {
1964
- writeFileSync3(filePath, content.replace("</body>", `${BADGE_HTML}
1998
+ writeFileSync4(filePath, content.replace("</body>", `${BADGE_HTML}
1965
1999
  </body>`), "utf-8");
1966
2000
  injected++;
1967
2001
  } else if (content.includes("</html>")) {
1968
- writeFileSync3(filePath, content.replace("</html>", `${BADGE_HTML}
2002
+ writeFileSync4(filePath, content.replace("</html>", `${BADGE_HTML}
1969
2003
  </html>`), "utf-8");
1970
2004
  injected++;
1971
2005
  }
@@ -2299,6 +2333,10 @@ async function deployCommand(options) {
2299
2333
  }
2300
2334
  }
2301
2335
  phases.push({ name: "Scan", durationMs: Date.now() - phaseStart });
2336
+ if (projectConfig.framework === "nextjs") {
2337
+ patchNextConfigForStaticExport(cwd);
2338
+ patchNextLayoutFonts(cwd);
2339
+ }
2302
2340
  if (!options.skipBuild && !options.turbo) {
2303
2341
  phaseStart = Date.now();
2304
2342
  ui.section("Building...");
@@ -2554,13 +2592,13 @@ async function deployCommand(options) {
2554
2592
  }, cwd);
2555
2593
  const gitignorePath = join8(cwd, ".gitignore");
2556
2594
  if (existsSync6(gitignorePath)) {
2557
- const gitignoreContent = readFileSync7(gitignorePath, "utf-8");
2595
+ const gitignoreContent = readFileSync8(gitignorePath, "utf-8");
2558
2596
  const lines = gitignoreContent.split("\n").map((l) => l.trim());
2559
2597
  if (!lines.includes("shipem.json")) {
2560
2598
  appendFileSync(gitignorePath, "\n# Shipem config\nshipem.json\n");
2561
2599
  }
2562
2600
  } else {
2563
- writeFileSync4(gitignorePath, "# Shipem config\nshipem.json\n");
2601
+ writeFileSync5(gitignorePath, "# Shipem config\nshipem.json\n");
2564
2602
  }
2565
2603
  ui.deployBoxEnhanced(
2566
2604
  projectConfig.name,
@@ -2572,9 +2610,77 @@ async function deployCommand(options) {
2572
2610
  isAnonymous
2573
2611
  );
2574
2612
  }
2613
+ function patchNextConfigForStaticExport(cwd) {
2614
+ const tsConfig = join8(cwd, "next.config.ts");
2615
+ const jsConfig = join8(cwd, "next.config.js");
2616
+ const mjsConfig = join8(cwd, "next.config.mjs");
2617
+ for (const configPath of [tsConfig, jsConfig, mjsConfig]) {
2618
+ if (existsSync6(configPath)) {
2619
+ const content = readFileSync8(configPath, "utf-8");
2620
+ if (/output\s*:\s*['"]export['"]/.test(content)) {
2621
+ return;
2622
+ }
2623
+ }
2624
+ }
2625
+ if (existsSync6(tsConfig)) {
2626
+ const content = readFileSync8(tsConfig, "utf-8");
2627
+ const patched = content.replace(
2628
+ /({[\s\S]*?)(})\s*(?:satisfies|as)\s/,
2629
+ "$1 output: 'export',\n$2 satisfies "
2630
+ );
2631
+ if (patched === content) {
2632
+ const fallback = content.replace(/(\{)/, "$1\n output: 'export',");
2633
+ writeFileSync5(tsConfig, fallback);
2634
+ } else {
2635
+ writeFileSync5(tsConfig, patched);
2636
+ }
2637
+ console.log(` \u2139\uFE0F Added output: 'export' to next.config.ts for static deployment`);
2638
+ } else if (existsSync6(mjsConfig)) {
2639
+ const content = readFileSync8(mjsConfig, "utf-8");
2640
+ const patched = content.replace(/(\{)/, "$1\n output: 'export',");
2641
+ writeFileSync5(mjsConfig, patched);
2642
+ console.log(` \u2139\uFE0F Added output: 'export' to next.config.mjs for static deployment`);
2643
+ } else if (existsSync6(jsConfig)) {
2644
+ const content = readFileSync8(jsConfig, "utf-8");
2645
+ const patched = content.replace(/(\{)/, "$1\n output: 'export',");
2646
+ writeFileSync5(jsConfig, patched);
2647
+ console.log(` \u2139\uFE0F Added output: 'export' to next.config.js for static deployment`);
2648
+ } else {
2649
+ writeFileSync5(jsConfig, `/** @type {import('next').NextConfig} */
2650
+ module.exports = { output: 'export' };
2651
+ `);
2652
+ console.log(` \u2139\uFE0F Created next.config.js with output: 'export' for static deployment`);
2653
+ }
2654
+ }
2655
+ function patchNextLayoutFonts(cwd) {
2656
+ const layoutPaths = [
2657
+ join8(cwd, "app/layout.tsx"),
2658
+ join8(cwd, "app/layout.jsx"),
2659
+ join8(cwd, "src/app/layout.tsx"),
2660
+ join8(cwd, "src/app/layout.jsx")
2661
+ ];
2662
+ for (const layoutPath of layoutPaths) {
2663
+ if (!existsSync6(layoutPath)) continue;
2664
+ const content = readFileSync8(layoutPath, "utf-8");
2665
+ if (!/import.*next\/font/.test(content)) continue;
2666
+ let patched = content;
2667
+ patched = patched.replace(/import\s+\{[^}]*\}\s+from\s+['"]next\/font\/[^'"]+['"];?\n?/g, "");
2668
+ patched = patched.replace(/const\s+\w+\s*=\s*\w+\(\s*\{[^}]*\}\s*\)\s*;?\n?/g, "");
2669
+ patched = patched.replace(/\$\{\w+\.className\}/g, "");
2670
+ patched = patched.replace(/className=\{`([^`]*)`\}/g, (match, inner) => {
2671
+ const cleaned = inner.trim();
2672
+ return cleaned ? `className={\`${cleaned}\`}` : "";
2673
+ });
2674
+ patched = patched.replace(/className=\{\w+\.className\}/g, "");
2675
+ if (patched !== content) {
2676
+ writeFileSync5(layoutPath, patched);
2677
+ console.log(` \u2139\uFE0F Removed next/font imports from ${layoutPath.replace(cwd + "/", "")} for static export`);
2678
+ }
2679
+ }
2680
+ }
2575
2681
 
2576
2682
  // src/commands/env.ts
2577
- import { existsSync as existsSync7, readFileSync as readFileSync8, readdirSync as readdirSync6, writeFileSync as writeFileSync5 } from "fs";
2683
+ import { existsSync as existsSync7, readFileSync as readFileSync9, readdirSync as readdirSync6, writeFileSync as writeFileSync6 } from "fs";
2578
2684
  import { join as join9 } from "path";
2579
2685
  var SERVICE_LINKS = {
2580
2686
  SUPABASE_URL: { name: "Supabase", url: "https://app.supabase.com/project/_/settings/api" },
@@ -2614,7 +2720,7 @@ function scanSourceForEnvVars(cwd) {
2614
2720
  scanDir(fullPath, depth + 1);
2615
2721
  } else if (/\.(ts|tsx|js|jsx|mjs|mts|vue|svelte|astro)$/.test(entry.name)) {
2616
2722
  try {
2617
- const content = readFileSync8(fullPath, "utf-8");
2723
+ const content = readFileSync9(fullPath, "utf-8");
2618
2724
  for (const pat of patterns) {
2619
2725
  pat.lastIndex = 0;
2620
2726
  let match;
@@ -2653,7 +2759,7 @@ function readEnvFile(cwd) {
2653
2759
  }
2654
2760
  function readFile2(path) {
2655
2761
  try {
2656
- return readFileSync8(path, "utf-8");
2762
+ return readFileSync9(path, "utf-8");
2657
2763
  } catch {
2658
2764
  return null;
2659
2765
  }
@@ -2718,7 +2824,7 @@ async function envCommand() {
2718
2824
  const comment = service ? ` # ${service.name}` : "";
2719
2825
  return `${v}=${comment}`;
2720
2826
  }).join("\n") + "\n";
2721
- writeFileSync5(envExamplePath, content, "utf-8");
2827
+ writeFileSync6(envExamplePath, content, "utf-8");
2722
2828
  ui.success("Generated .env.example from detected variables");
2723
2829
  console.log("");
2724
2830
  } else if (existsSync7(envExamplePath)) {
@@ -2739,7 +2845,7 @@ async function envCommand() {
2739
2845
  // src/commands/init.ts
2740
2846
  import inquirer2 from "inquirer";
2741
2847
  import { execa as execa3 } from "execa";
2742
- import { existsSync as existsSync8, writeFileSync as writeFileSync6, mkdirSync } from "fs";
2848
+ import { existsSync as existsSync8, writeFileSync as writeFileSync7, mkdirSync } from "fs";
2743
2849
  import { join as join10 } from "path";
2744
2850
  import chalk6 from "chalk";
2745
2851
 
@@ -2933,7 +3039,7 @@ async function initCommand(options = {}) {
2933
3039
  }
2934
3040
  const projectDir = join10(process.cwd(), projectName);
2935
3041
  if (existsSync8(projectDir)) {
2936
- writeFileSync6(
3042
+ writeFileSync7(
2937
3043
  join10(projectDir, "shipem.json"),
2938
3044
  JSON.stringify({ project: { name: projectName, framework: matched.value } }, null, 2) + "\n",
2939
3045
  "utf-8"
@@ -3014,7 +3120,7 @@ async function scaffoldFromTemplate(tpl, options) {
3014
3120
  }
3015
3121
  const projectDir = join10(process.cwd(), projectName);
3016
3122
  if (existsSync8(projectDir)) {
3017
- writeFileSync6(
3123
+ writeFileSync7(
3018
3124
  join10(projectDir, "shipem.json"),
3019
3125
  JSON.stringify({ project: { name: projectName, framework: tpl.framework } }, null, 2) + "\n",
3020
3126
  "utf-8"
@@ -3030,7 +3136,7 @@ function createMinimalProject(projectName, description) {
3030
3136
  const dir = join10(process.cwd(), projectName);
3031
3137
  if (!existsSync8(dir)) {
3032
3138
  mkdirSync(dir, { recursive: true });
3033
- writeFileSync6(join10(dir, "index.html"), `<!DOCTYPE html>
3139
+ writeFileSync7(join10(dir, "index.html"), `<!DOCTYPE html>
3034
3140
  <html lang="en">
3035
3141
  <head>
3036
3142
  <meta charset="UTF-8">
@@ -3056,7 +3162,7 @@ function createMinimalProject(projectName, description) {
3056
3162
  function createStaticPortfolio(projectName) {
3057
3163
  const dir = join10(process.cwd(), projectName);
3058
3164
  mkdirSync(dir, { recursive: true });
3059
- writeFileSync6(join10(dir, "index.html"), `<!DOCTYPE html>
3165
+ writeFileSync7(join10(dir, "index.html"), `<!DOCTYPE html>
3060
3166
  <html lang="en">
3061
3167
  <head>
3062
3168
  <meta charset="UTF-8">
@@ -3093,7 +3199,7 @@ function createStaticPortfolio(projectName) {
3093
3199
  </footer>
3094
3200
  </body>
3095
3201
  </html>`, "utf-8");
3096
- writeFileSync6(join10(dir, "style.css"), `* { margin: 0; padding: 0; box-sizing: border-box; }
3202
+ writeFileSync7(join10(dir, "style.css"), `* { margin: 0; padding: 0; box-sizing: border-box; }
3097
3203
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0a0a; color: #ededed; min-height: 100vh; }
3098
3204
  header { text-align: center; padding: 4rem 1rem 2rem; }
3099
3205
  header h1 { font-size: 2.5rem; margin-bottom: 0.5rem; }
@@ -3532,7 +3638,7 @@ async function watchCommand() {
3532
3638
  import chalk11 from "chalk";
3533
3639
 
3534
3640
  // src/memory/index.ts
3535
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync2, existsSync as existsSync9 } from "fs";
3641
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, mkdirSync as mkdirSync2, existsSync as existsSync9 } from "fs";
3536
3642
  import { join as join12 } from "path";
3537
3643
  import { homedir } from "os";
3538
3644
  var SHIPEM_HOME = join12(homedir(), ".shipem");
@@ -3548,14 +3654,14 @@ function ensureDir() {
3548
3654
  function readJson2(path, fallback) {
3549
3655
  try {
3550
3656
  if (!existsSync9(path)) return fallback;
3551
- return JSON.parse(readFileSync9(path, "utf-8"));
3657
+ return JSON.parse(readFileSync10(path, "utf-8"));
3552
3658
  } catch {
3553
3659
  return fallback;
3554
3660
  }
3555
3661
  }
3556
3662
  function writeJson(path, data) {
3557
3663
  ensureDir();
3558
- writeFileSync7(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
3664
+ writeFileSync8(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
3559
3665
  }
3560
3666
  function getMemory() {
3561
3667
  return readJson2(MEMORY_PATH, {
@@ -3650,7 +3756,7 @@ async function configCommand(action, key, value) {
3650
3756
  // src/commands/monitor.ts
3651
3757
  import axios5 from "axios";
3652
3758
  import chalk12 from "chalk";
3653
- import { writeFileSync as writeFileSync8, readFileSync as readFileSync10, existsSync as existsSync10, unlinkSync } from "fs";
3759
+ import { writeFileSync as writeFileSync9, readFileSync as readFileSync11, existsSync as existsSync10, unlinkSync } from "fs";
3654
3760
  import { join as join13 } from "path";
3655
3761
  import { tmpdir as tmpdir2 } from "os";
3656
3762
 
@@ -3729,7 +3835,7 @@ async function monitorCommand(options = {}) {
3729
3835
  if (options.stop) {
3730
3836
  if (existsSync10(PID_FILE)) {
3731
3837
  try {
3732
- const pid = parseInt(readFileSync10(PID_FILE, "utf-8").trim(), 10);
3838
+ const pid = parseInt(readFileSync11(PID_FILE, "utf-8").trim(), 10);
3733
3839
  process.kill(pid, "SIGTERM");
3734
3840
  unlinkSync(PID_FILE);
3735
3841
  ui.success("Monitor daemon stopped.");
@@ -3803,11 +3909,11 @@ async function monitorCommand(options = {}) {
3803
3909
  });
3804
3910
  }
3805
3911
  function writePidFile() {
3806
- writeFileSync8(PID_FILE, String(process.pid), "utf-8");
3912
+ writeFileSync9(PID_FILE, String(process.pid), "utf-8");
3807
3913
  }
3808
3914
 
3809
3915
  // src/commands/hooks.ts
3810
- import { existsSync as existsSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync2, chmodSync, mkdirSync as mkdirSync3 } from "fs";
3916
+ import { existsSync as existsSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync10, unlinkSync as unlinkSync2, chmodSync, mkdirSync as mkdirSync3 } from "fs";
3811
3917
  import { join as join14 } from "path";
3812
3918
  var HOOK_MARKER = "# shipem-auto-deploy";
3813
3919
  var HOOK_CONTENT = `#!/bin/sh
@@ -3821,7 +3927,7 @@ function getHookPath(cwd) {
3821
3927
  }
3822
3928
  function isShipemHook(path) {
3823
3929
  if (!existsSync11(path)) return false;
3824
- const content = readFileSync11(path, "utf-8");
3930
+ const content = readFileSync12(path, "utf-8");
3825
3931
  return content.includes(HOOK_MARKER);
3826
3932
  }
3827
3933
  async function hooksCommand(action) {
@@ -3861,7 +3967,7 @@ async function hooksCommand(action) {
3861
3967
  if (!existsSync11(hooksDir)) {
3862
3968
  mkdirSync3(hooksDir, { recursive: true });
3863
3969
  }
3864
- writeFileSync9(hookPath, HOOK_CONTENT, "utf-8");
3970
+ writeFileSync10(hookPath, HOOK_CONTENT, "utf-8");
3865
3971
  chmodSync(hookPath, 493);
3866
3972
  ui.success("Installed post-commit hook \u2192 auto-deploy on commit");
3867
3973
  ui.dim(`Remove with: shipem hooks remove`);
@@ -4014,7 +4120,7 @@ async function previewCommand(options = {}) {
4014
4120
  }
4015
4121
 
4016
4122
  // src/index.ts
4017
- import { readFileSync as readFileSync12 } from "fs";
4123
+ import { readFileSync as readFileSync13 } from "fs";
4018
4124
  import { fileURLToPath } from "url";
4019
4125
  import { dirname, join as join15 } from "path";
4020
4126
  var __filename2 = fileURLToPath(import.meta.url);
@@ -4022,7 +4128,7 @@ var __dirname2 = dirname(__filename2);
4022
4128
  var version = "0.1.0";
4023
4129
  try {
4024
4130
  const pkg = JSON.parse(
4025
- readFileSync12(join15(__dirname2, "../package.json"), "utf-8")
4131
+ readFileSync13(join15(__dirname2, "../package.json"), "utf-8")
4026
4132
  );
4027
4133
  version = pkg.version;
4028
4134
  } catch {
package/dist/lib.js CHANGED
@@ -137,7 +137,7 @@ function scanProjectInternal(cwd) {
137
137
  return {
138
138
  framework: "nextjs",
139
139
  buildCommand: pkg.scripts?.build ?? "npm run build",
140
- outputDirectory: ".next",
140
+ outputDirectory: "out",
141
141
  installCommand: detectPackageManager(cwd),
142
142
  serverType,
143
143
  deployTarget: "cloudflare-pages",
@@ -450,7 +450,7 @@ function warnIfConfigContainsSecrets(config, configPath) {
450
450
 
451
451
  // src/build/builder.ts
452
452
  import { execa } from "execa";
453
- import { existsSync as existsSync3 } from "fs";
453
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
454
454
  import { join as join3 } from "path";
455
455
  import chalk2 from "chalk";
456
456
 
@@ -508,7 +508,7 @@ var ui = {
508
508
  );
509
509
  console.log("");
510
510
  console.log(
511
- ` ${brand.gray("Your AI built it.")} ${brand.blue.bold("We'll ship it.")}`
511
+ ` \u{1F411} ${brand.gray("Your AI built it.")} ${brand.blue.bold("We'll ship it.")}`
512
512
  );
513
513
  console.log("");
514
514
  },
@@ -848,6 +848,31 @@ ${url}`);
848
848
  };
849
849
 
850
850
  // src/build/builder.ts
851
+ function patchNextConfig(cwd) {
852
+ const candidates = ["next.config.ts", "next.config.mjs", "next.config.js"];
853
+ for (const filename of candidates) {
854
+ const configPath = join3(cwd, filename);
855
+ if (existsSync3(configPath)) {
856
+ const content = readFileSync3(configPath, "utf-8");
857
+ if (content.includes("output")) return;
858
+ const patched = content.replace(
859
+ /const nextConfig[^=]*=\s*\{/,
860
+ (match) => match + "\n output: 'export',"
861
+ );
862
+ if (patched !== content) {
863
+ writeFileSync2(configPath, patched, "utf-8");
864
+ ui.dim(" \u2139\uFE0F Added output: 'export' to " + filename + " for static deployment");
865
+ return;
866
+ }
867
+ }
868
+ }
869
+ writeFileSync2(
870
+ join3(cwd, "next.config.js"),
871
+ "/** @type {import('next').NextConfig} */\nconst nextConfig = { output: 'export' };\nmodule.exports = nextConfig;\n",
872
+ "utf-8"
873
+ );
874
+ ui.dim(" \u2139\uFE0F Created next.config.js with output: 'export' for static deployment");
875
+ }
851
876
  async function buildProject(config, cwd = process.cwd()) {
852
877
  const start = Date.now();
853
878
  if (config.installCommand) {
@@ -855,8 +880,8 @@ async function buildProject(config, cwd = process.cwd()) {
855
880
  const outputLines = [];
856
881
  let lineCount = 0;
857
882
  try {
858
- const [installBin, ...installArgs] = config.installCommand.split(" ");
859
- const installProc = execa(installBin, installArgs, {
883
+ const installProc = execa(config.installCommand, {
884
+ shell: true,
860
885
  cwd,
861
886
  env: { ...process.env, CI: "true" },
862
887
  timeout: 5 * 60 * 1e3,
@@ -901,6 +926,9 @@ async function buildProject(config, cwd = process.cwd()) {
901
926
  }
902
927
  console.log("");
903
928
  }
929
+ if (config.framework === "nextjs") {
930
+ patchNextConfig(cwd);
931
+ }
904
932
  if (config.buildCommand) {
905
933
  const buildSpinner = ui.spinner("Building...");
906
934
  const outputLines = [];
@@ -916,8 +944,8 @@ async function buildProject(config, cwd = process.cwd()) {
916
944
  }
917
945
  }
918
946
  try {
919
- const [finalBin, ...finalArgs] = config.buildCommand.split(" ");
920
- const buildProc = execa(finalBin, finalArgs, {
947
+ const buildProc = execa(config.buildCommand, {
948
+ shell: true,
921
949
  cwd,
922
950
  env: buildEnv,
923
951
  timeout: 10 * 60 * 1e3,
@@ -996,11 +1024,11 @@ function parseErrorMessage(raw, outputLines) {
996
1024
  // src/deploy/cloudflare.ts
997
1025
  import axios from "axios";
998
1026
  import { createHash } from "crypto";
999
- import { readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync4 } from "fs";
1027
+ import { readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync5 } from "fs";
1000
1028
  import { join as join5, relative } from "path";
1001
1029
 
1002
1030
  // src/deploy/exclude.ts
1003
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1031
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
1004
1032
  import { join as join4 } from "path";
1005
1033
  import chalk3 from "chalk";
1006
1034
  var DEFAULT_PATTERNS = [
@@ -1055,7 +1083,7 @@ function matchesIgnoreLine(relPath, line) {
1055
1083
  function loadIgnoreLines(filePath) {
1056
1084
  if (!existsSync4(filePath)) return [];
1057
1085
  try {
1058
- return readFileSync3(filePath, "utf-8").split("\n");
1086
+ return readFileSync4(filePath, "utf-8").split("\n");
1059
1087
  } catch {
1060
1088
  return [];
1061
1089
  }
@@ -1220,7 +1248,7 @@ var CloudflarePages = class {
1220
1248
  let totalBytes = 0;
1221
1249
  const fileMap = /* @__PURE__ */ new Map();
1222
1250
  for (const filePath of filePaths) {
1223
- const content = readFileSync4(filePath);
1251
+ const content = readFileSync5(filePath);
1224
1252
  const hash = createHash("sha256").update(content).digest("hex");
1225
1253
  const urlPath = "/" + relative(fullOutputPath, filePath).replace(/\\/g, "/");
1226
1254
  fileMap.set(urlPath, { hash, content });
@@ -1235,6 +1263,9 @@ var CloudflarePages = class {
1235
1263
  for (const [urlPath, { hash }] of fileMap) {
1236
1264
  manifest[urlPath] = hash;
1237
1265
  }
1266
+ if (Object.keys(manifest).length === 0) {
1267
+ throw new DeployError(`Output directory '${outputDir}' contains no files to deploy. Check your build output.`);
1268
+ }
1238
1269
  const deploySpinner = ui.spinner("Creating deployment...");
1239
1270
  let jwt;
1240
1271
  let requiredFiles;
@@ -1299,7 +1330,7 @@ var CloudflarePages = class {
1299
1330
  let totalBytes = 0;
1300
1331
  const fileMap = /* @__PURE__ */ new Map();
1301
1332
  for (const filePath of filePaths) {
1302
- const content = readFileSync4(filePath);
1333
+ const content = readFileSync5(filePath);
1303
1334
  const hash = createHash("sha256").update(content).digest("hex");
1304
1335
  const urlPath = "/" + relative(fullOutputPath, filePath).replace(/\\/g, "/");
1305
1336
  fileMap.set(urlPath, { hash, content });
@@ -1314,6 +1345,9 @@ var CloudflarePages = class {
1314
1345
  for (const [urlPath, { hash }] of fileMap) {
1315
1346
  manifest[urlPath] = hash;
1316
1347
  }
1348
+ if (Object.keys(manifest).length === 0) {
1349
+ throw new DeployError(`Output directory '${outputDir}' contains no files to deploy. Check your build output.`);
1350
+ }
1317
1351
  const deploySpinner = ui.spinner(`Creating preview deployment (branch: ${branch})...`);
1318
1352
  let jwt;
1319
1353
  let requiredFiles;
@@ -1488,7 +1522,7 @@ function sanitizeProjectName(name) {
1488
1522
  }
1489
1523
 
1490
1524
  // src/commands/fix.ts
1491
- import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync2, readdirSync as readdirSync3 } from "fs";
1525
+ import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync3, readdirSync as readdirSync3 } from "fs";
1492
1526
  import { join as join6 } from "path";
1493
1527
  import { execa as execa2 } from "execa";
1494
1528
  function findMissingModules(errorOutput) {
@@ -1565,12 +1599,12 @@ function applyTsConfigFixes(cwd, fixes) {
1565
1599
  const tsconfigPath = join6(cwd, "tsconfig.json");
1566
1600
  if (!existsSync5(tsconfigPath)) return false;
1567
1601
  try {
1568
- const content = readFileSync5(tsconfigPath, "utf-8");
1602
+ const content = readFileSync6(tsconfigPath, "utf-8");
1569
1603
  const stripped = content.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
1570
1604
  const tsconfig = JSON.parse(stripped);
1571
1605
  if (!tsconfig.compilerOptions) tsconfig.compilerOptions = {};
1572
1606
  Object.assign(tsconfig.compilerOptions, fixes);
1573
- writeFileSync2(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n", "utf-8");
1607
+ writeFileSync3(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n", "utf-8");
1574
1608
  return true;
1575
1609
  } catch {
1576
1610
  return false;
@@ -1593,7 +1627,7 @@ function findMissingEnvVars(cwd) {
1593
1627
  scanDir(fullPath, depth + 1);
1594
1628
  } else if (/\.(ts|tsx|js|jsx|mjs|mts|vue|svelte|astro)$/.test(entry.name)) {
1595
1629
  try {
1596
- const content = readFileSync5(fullPath, "utf-8");
1630
+ const content = readFileSync6(fullPath, "utf-8");
1597
1631
  for (const pat of patterns) {
1598
1632
  pat.lastIndex = 0;
1599
1633
  let match;
@@ -1622,7 +1656,7 @@ function generateEnvExample(cwd, vars) {
1622
1656
  const envExamplePath = join6(cwd, ".env.example");
1623
1657
  if (existsSync5(envExamplePath)) return false;
1624
1658
  const content = vars.map((v) => `${v}=`).join("\n") + "\n";
1625
- writeFileSync2(envExamplePath, content, "utf-8");
1659
+ writeFileSync3(envExamplePath, content, "utf-8");
1626
1660
  return true;
1627
1661
  }
1628
1662
  function detectFrameworkConfigFixes(errorOutput, cwd, framework) {
@@ -1631,7 +1665,7 @@ function detectFrameworkConfigFixes(errorOutput, cwd, framework) {
1631
1665
  const nextConfigPath = existsSync5(join6(cwd, "next.config.mjs")) ? join6(cwd, "next.config.mjs") : existsSync5(join6(cwd, "next.config.js")) ? join6(cwd, "next.config.js") : null;
1632
1666
  if (nextConfigPath && errorOutput.includes("output")) {
1633
1667
  try {
1634
- const content = readFileSync5(nextConfigPath, "utf-8");
1668
+ const content = readFileSync6(nextConfigPath, "utf-8");
1635
1669
  if (!content.includes("output")) {
1636
1670
  fixes.push(`Add output: 'export' to ${nextConfigPath.split("/").pop()}`);
1637
1671
  }
@@ -1642,7 +1676,7 @@ function detectFrameworkConfigFixes(errorOutput, cwd, framework) {
1642
1676
  if ((framework === "vite-react" || framework === "vite-vue" || framework === "vite-svelte") && errorOutput.includes("plugin")) {
1643
1677
  const pkgPath = join6(cwd, "package.json");
1644
1678
  try {
1645
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
1679
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
1646
1680
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1647
1681
  if (framework === "vite-react" && !deps["@vitejs/plugin-react"]) {
1648
1682
  fixes.push("Install @vitejs/plugin-react");
@@ -1715,7 +1749,7 @@ async function runFixHeuristics(errorOutput, cwd, config) {
1715
1749
  }
1716
1750
 
1717
1751
  // src/commands/env.ts
1718
- import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync4, writeFileSync as writeFileSync3 } from "fs";
1752
+ import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4, writeFileSync as writeFileSync4 } from "fs";
1719
1753
  import { join as join7 } from "path";
1720
1754
  function scanSourceForEnvVars(cwd) {
1721
1755
  const found = /* @__PURE__ */ new Set();
@@ -1735,7 +1769,7 @@ function scanSourceForEnvVars(cwd) {
1735
1769
  scanDir(fullPath, depth + 1);
1736
1770
  } else if (/\.(ts|tsx|js|jsx|mjs|mts|vue|svelte|astro)$/.test(entry.name)) {
1737
1771
  try {
1738
- const content = readFileSync6(fullPath, "utf-8");
1772
+ const content = readFileSync7(fullPath, "utf-8");
1739
1773
  for (const pat of patterns) {
1740
1774
  pat.lastIndex = 0;
1741
1775
  let match;
@@ -1774,7 +1808,7 @@ function readEnvFile(cwd) {
1774
1808
  }
1775
1809
  function readFile2(path) {
1776
1810
  try {
1777
- return readFileSync6(path, "utf-8");
1811
+ return readFileSync7(path, "utf-8");
1778
1812
  } catch {
1779
1813
  return null;
1780
1814
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ship-em",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "One-command deployment for apps built by AI coding tools",
5
5
  "type": "module",
6
6
  "bin": {