ship-em 0.1.0 → 0.2.1
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 +8 -8
- package/dist/index.js +2541 -745
- package/dist/lib.d.ts +252 -0
- package/dist/lib.js +1923 -0
- package/package.json +23 -4
package/dist/index.js
CHANGED
|
@@ -9,14 +9,15 @@ 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
|
|
13
|
-
import { join as
|
|
12
|
+
import { rmSync, existsSync as existsSync6, appendFileSync, writeFileSync as writeFileSync4, readFileSync as readFileSync7, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
|
|
13
|
+
import { join as join8 } from "path";
|
|
14
14
|
import { tmpdir } from "os";
|
|
15
15
|
import { create as tarCreate } from "tar";
|
|
16
16
|
|
|
17
17
|
// src/ui/terminal.ts
|
|
18
18
|
import chalk from "chalk";
|
|
19
19
|
import ora from "ora";
|
|
20
|
+
import { execSync } from "child_process";
|
|
20
21
|
var brand = {
|
|
21
22
|
blue: chalk.hex("#3B82F6"),
|
|
22
23
|
brightBlue: chalk.hex("#60A5FA"),
|
|
@@ -197,8 +198,31 @@ var ui = {
|
|
|
197
198
|
);
|
|
198
199
|
console.log("");
|
|
199
200
|
},
|
|
201
|
+
// Copy text to clipboard (best-effort)
|
|
202
|
+
copyToClipboard(text) {
|
|
203
|
+
try {
|
|
204
|
+
if (process.platform === "darwin") {
|
|
205
|
+
execSync("pbcopy", { input: text, stdio: ["pipe", "ignore", "ignore"] });
|
|
206
|
+
return true;
|
|
207
|
+
} else if (process.platform === "linux") {
|
|
208
|
+
try {
|
|
209
|
+
execSync("xclip -selection clipboard", { input: text, stdio: ["pipe", "ignore", "ignore"] });
|
|
210
|
+
return true;
|
|
211
|
+
} catch {
|
|
212
|
+
try {
|
|
213
|
+
execSync("xsel --clipboard --input", { input: text, stdio: ["pipe", "ignore", "ignore"] });
|
|
214
|
+
return true;
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
},
|
|
200
224
|
// Deploy celebration box — shown on success
|
|
201
|
-
deployBox(appName, url, elapsedSec, fileCount, totalBytes) {
|
|
225
|
+
deployBox(appName, url, elapsedSec, fileCount, totalBytes, isAnonymous = false) {
|
|
202
226
|
const statsLine = `Deployed in ${elapsedSec.toFixed(1)}s \xB7 ${fileCount} files \xB7 ${ui.formatBytes(totalBytes)}`;
|
|
203
227
|
const content = [
|
|
204
228
|
["\u{1F680} Shipped!", brand.green.bold("\u{1F680} Shipped!")],
|
|
@@ -229,13 +253,24 @@ var ui = {
|
|
|
229
253
|
}
|
|
230
254
|
console.log(` ${brand.blue("\u2502")}${" ".repeat(innerWidth)}${brand.blue("\u2502")}`);
|
|
231
255
|
console.log(` ${brand.blue("\u2570" + "\u2500".repeat(innerWidth) + "\u256F")}`);
|
|
256
|
+
const copied = ui.copyToClipboard(url);
|
|
232
257
|
console.log("");
|
|
233
|
-
console.log(` ${brand.bold("
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
258
|
+
console.log(` ${brand.bold("Share it:")}`);
|
|
259
|
+
const tweetText = encodeURIComponent(`I just shipped ${appName} live in ${elapsedSec.toFixed(0)}s with @shipem_dev
|
|
260
|
+
|
|
261
|
+
${url}`);
|
|
262
|
+
console.log(` ${brand.gray("Twitter")} \u2192 ${brand.brightBlue.underline(`https://twitter.com/intent/tweet?text=${tweetText}`)}`);
|
|
263
|
+
console.log(` ${brand.gray("Copy URL")} \u2192 ${copied ? brand.green("Copied to clipboard!") : brand.dim(url)}`);
|
|
237
264
|
console.log("");
|
|
238
|
-
|
|
265
|
+
if (isAnonymous) {
|
|
266
|
+
console.log(` ${brand.bold("Keep it:")} Run ${chalk.cyan("shipem login")} to claim this deploy`);
|
|
267
|
+
console.log(` and get a custom domain.`);
|
|
268
|
+
console.log("");
|
|
269
|
+
}
|
|
270
|
+
console.log(` ${brand.bold("Next steps:")}`);
|
|
271
|
+
console.log(` ${brand.gray("Update")} \u2192 ${chalk.cyan("npx ship-em")}`);
|
|
272
|
+
console.log(` ${brand.gray("Status")} \u2192 ${chalk.cyan("npx ship-em status")}`);
|
|
273
|
+
console.log(` ${brand.gray("Remove")} \u2192 ${chalk.cyan("npx ship-em down")}`);
|
|
239
274
|
console.log("");
|
|
240
275
|
},
|
|
241
276
|
// Format bytes
|
|
@@ -244,6 +279,97 @@ var ui = {
|
|
|
244
279
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
245
280
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
246
281
|
},
|
|
282
|
+
// Progress bar
|
|
283
|
+
progressBar(current, total, width = 30) {
|
|
284
|
+
const ratio = Math.min(current / total, 1);
|
|
285
|
+
const filled = Math.round(width * ratio);
|
|
286
|
+
const empty = width - filled;
|
|
287
|
+
const bar = brand.blue("\u2588".repeat(filled)) + brand.gray("\u2591".repeat(empty));
|
|
288
|
+
const pct = Math.round(ratio * 100);
|
|
289
|
+
return `${bar} ${pct}%`;
|
|
290
|
+
},
|
|
291
|
+
// Phase timing display
|
|
292
|
+
phaseTimeline(phases) {
|
|
293
|
+
const parts = [];
|
|
294
|
+
for (const phase of phases) {
|
|
295
|
+
const sec = (phase.durationMs / 1e3).toFixed(1);
|
|
296
|
+
parts.push(`${phase.name} (${sec}s)`);
|
|
297
|
+
}
|
|
298
|
+
console.log(` ${brand.dim(parts.join(" \u2192 "))}`);
|
|
299
|
+
},
|
|
300
|
+
// Deploy celebration box — enhanced with timing
|
|
301
|
+
deployBoxEnhanced(appName, url, phases, fileCount, totalBytes, totalElapsedSec, isAnonymous = false) {
|
|
302
|
+
const statsLine = `Deployed in ${totalElapsedSec.toFixed(1)}s \xB7 ${fileCount} files \xB7 ${ui.formatBytes(totalBytes)}`;
|
|
303
|
+
const content = [
|
|
304
|
+
["\u{1F680} Shipped!", brand.green.bold("\u{1F680} Shipped!")],
|
|
305
|
+
null,
|
|
306
|
+
[`${appName} is live`, `${brand.white.bold(appName)} is live`],
|
|
307
|
+
null,
|
|
308
|
+
[`\u2192 ${url}`, `${brand.blue("\u2192")} ${brand.brightBlue.underline(url)}`],
|
|
309
|
+
null,
|
|
310
|
+
[statsLine, brand.dim(statsLine)]
|
|
311
|
+
];
|
|
312
|
+
const maxPlain = Math.max(
|
|
313
|
+
...content.filter((item) => item !== null).map(([p]) => visibleLen(p))
|
|
314
|
+
);
|
|
315
|
+
const innerWidth = maxPlain + 6;
|
|
316
|
+
console.log("");
|
|
317
|
+
console.log(` ${brand.blue("\u256D" + "\u2500".repeat(innerWidth) + "\u256E")}`);
|
|
318
|
+
console.log(` ${brand.blue("\u2502")}${" ".repeat(innerWidth)}${brand.blue("\u2502")}`);
|
|
319
|
+
for (const item of content) {
|
|
320
|
+
if (!item) {
|
|
321
|
+
console.log(` ${brand.blue("\u2502")}${" ".repeat(innerWidth)}${brand.blue("\u2502")}`);
|
|
322
|
+
} else {
|
|
323
|
+
const [plain, colored] = item;
|
|
324
|
+
const rightPad = innerWidth - 3 - visibleLen(plain);
|
|
325
|
+
console.log(
|
|
326
|
+
` ${brand.blue("\u2502")} ${colored}${" ".repeat(Math.max(0, rightPad))}${brand.blue("\u2502")}`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
console.log(` ${brand.blue("\u2502")}${" ".repeat(innerWidth)}${brand.blue("\u2502")}`);
|
|
331
|
+
console.log(` ${brand.blue("\u2570" + "\u2500".repeat(innerWidth) + "\u256F")}`);
|
|
332
|
+
if (phases.length > 0) {
|
|
333
|
+
console.log("");
|
|
334
|
+
ui.phaseTimeline(phases);
|
|
335
|
+
}
|
|
336
|
+
const copied = ui.copyToClipboard(url);
|
|
337
|
+
console.log("");
|
|
338
|
+
console.log(` ${brand.bold("Share it:")}`);
|
|
339
|
+
const tweetText = encodeURIComponent(`I just shipped ${appName} live in ${totalElapsedSec.toFixed(0)}s with @shipem_dev
|
|
340
|
+
|
|
341
|
+
${url}`);
|
|
342
|
+
console.log(` ${brand.gray("Twitter")} \u2192 ${brand.brightBlue.underline(`https://twitter.com/intent/tweet?text=${tweetText}`)}`);
|
|
343
|
+
console.log(` ${brand.gray("Copy URL")} \u2192 ${copied ? brand.green("Copied to clipboard!") : brand.dim(url)}`);
|
|
344
|
+
console.log("");
|
|
345
|
+
if (isAnonymous) {
|
|
346
|
+
console.log(` ${brand.bold("Keep it:")} Run ${chalk.cyan("shipem login")} to claim this deploy`);
|
|
347
|
+
console.log(` and get a custom domain.`);
|
|
348
|
+
console.log("");
|
|
349
|
+
}
|
|
350
|
+
console.log(` ${brand.bold("Next steps:")}`);
|
|
351
|
+
console.log(` ${brand.gray("Update")} \u2192 ${chalk.cyan("npx ship-em")}`);
|
|
352
|
+
console.log(` ${brand.gray("Status")} \u2192 ${chalk.cyan("npx ship-em status")}`);
|
|
353
|
+
console.log(` ${brand.gray("Remove")} \u2192 ${chalk.cyan("npx ship-em down")}`);
|
|
354
|
+
console.log("");
|
|
355
|
+
},
|
|
356
|
+
// Rich status display for deploy history
|
|
357
|
+
deployHistory(deploys) {
|
|
358
|
+
if (deploys.length === 0) return;
|
|
359
|
+
console.log(` ${brand.bold("Recent deploys:")}`);
|
|
360
|
+
const show = deploys.slice(0, 5);
|
|
361
|
+
for (let i = 0; i < show.length; i++) {
|
|
362
|
+
const d = show[i];
|
|
363
|
+
const date = new Date(d.timestamp);
|
|
364
|
+
const dateStr = date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
365
|
+
const timeStr = date.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
|
|
366
|
+
const connector = i === show.length - 1 ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
|
|
367
|
+
console.log(
|
|
368
|
+
` ${brand.gray(connector)} ${dateStr}, ${timeStr} ${brand.green("\u2713")} ${d.duration.toFixed(1)}s ${d.files} files ${d.size}`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
console.log("");
|
|
372
|
+
},
|
|
247
373
|
// Colorize framework name
|
|
248
374
|
framework(name) {
|
|
249
375
|
const colors = {
|
|
@@ -256,14 +382,28 @@ var ui = {
|
|
|
256
382
|
remix: "#818cf8",
|
|
257
383
|
nuxt: "#00DC82",
|
|
258
384
|
gatsby: "#663399",
|
|
259
|
-
flask: "#22c55e",
|
|
260
|
-
fastapi: "#009688",
|
|
261
|
-
django: "#22c55e",
|
|
262
|
-
express: "#fbbf24",
|
|
263
385
|
hono: "#E36002"
|
|
264
386
|
};
|
|
265
387
|
const color = colors[name] ?? "#3B82F6";
|
|
266
388
|
return chalk.hex(color).bold(name);
|
|
389
|
+
},
|
|
390
|
+
// Framework icon
|
|
391
|
+
frameworkIcon(name) {
|
|
392
|
+
const icons = {
|
|
393
|
+
nextjs: "\u25B2",
|
|
394
|
+
"vite-react": "\u269B",
|
|
395
|
+
"vite-vue": "\u{1F7E2}",
|
|
396
|
+
"vite-svelte": "\u{1F525}",
|
|
397
|
+
astro: "\u{1F680}",
|
|
398
|
+
sveltekit: "\u{1F525}",
|
|
399
|
+
remix: "\u{1F4BF}",
|
|
400
|
+
nuxt: "\u{1F49A}",
|
|
401
|
+
gatsby: "\u{1F7E3}",
|
|
402
|
+
hono: "\u{1F525}",
|
|
403
|
+
"create-react-app": "\u269B",
|
|
404
|
+
"static-html": "\u{1F4C4}"
|
|
405
|
+
};
|
|
406
|
+
return icons[name] ?? "\u{1F4E6}";
|
|
267
407
|
}
|
|
268
408
|
};
|
|
269
409
|
|
|
@@ -358,6 +498,12 @@ function getNodeVersion(pkg, cwd) {
|
|
|
358
498
|
return "20";
|
|
359
499
|
}
|
|
360
500
|
function scanProject(cwd = process.cwd()) {
|
|
501
|
+
const result = scanProjectInternal(cwd);
|
|
502
|
+
const monorepo = detectMonorepo(cwd);
|
|
503
|
+
if (monorepo) result.monorepo = monorepo;
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
function scanProjectInternal(cwd) {
|
|
361
507
|
const files = listTopLevelFiles(cwd);
|
|
362
508
|
const dirs = listTopLevelDirs(cwd);
|
|
363
509
|
const allEntries = [...files, ...dirs];
|
|
@@ -534,21 +680,6 @@ function scanProject(cwd = process.cwd()) {
|
|
|
534
680
|
files: allEntries
|
|
535
681
|
};
|
|
536
682
|
}
|
|
537
|
-
if (deps["express"]) {
|
|
538
|
-
return {
|
|
539
|
-
framework: "express",
|
|
540
|
-
buildCommand: pkg.scripts?.build ?? "npm run build",
|
|
541
|
-
outputDirectory: "dist",
|
|
542
|
-
installCommand: detectPackageManager(cwd),
|
|
543
|
-
serverType: "server",
|
|
544
|
-
deployTarget: "flyio",
|
|
545
|
-
nodeVersion,
|
|
546
|
-
envVars,
|
|
547
|
-
projectName,
|
|
548
|
-
confidence: 0.88,
|
|
549
|
-
files: allEntries
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
683
|
if (pkg.scripts?.build) {
|
|
553
684
|
return {
|
|
554
685
|
framework: "unknown",
|
|
@@ -565,41 +696,9 @@ function scanProject(cwd = process.cwd()) {
|
|
|
565
696
|
};
|
|
566
697
|
}
|
|
567
698
|
}
|
|
568
|
-
if (hasFile("requirements.txt") || hasFile("pyproject.toml") || hasFile("Pipfile")) {
|
|
569
|
-
const projectName = cwd.split("/").pop() ?? "my-app";
|
|
570
|
-
const envVars = detectEnvVarsFromFiles(cwd);
|
|
571
|
-
let framework = "unknown";
|
|
572
|
-
let buildCommand = "pip install -r requirements.txt";
|
|
573
|
-
const requirements = readFile(join(cwd, "requirements.txt")) ?? "";
|
|
574
|
-
const pyproject = readFile(join(cwd, "pyproject.toml")) ?? "";
|
|
575
|
-
const combined = requirements + pyproject;
|
|
576
|
-
if (combined.includes("fastapi")) {
|
|
577
|
-
framework = "fastapi";
|
|
578
|
-
buildCommand = "pip install -r requirements.txt";
|
|
579
|
-
} else if (combined.includes("flask")) {
|
|
580
|
-
framework = "flask";
|
|
581
|
-
buildCommand = "pip install -r requirements.txt";
|
|
582
|
-
} else if (combined.includes("django")) {
|
|
583
|
-
framework = "django";
|
|
584
|
-
buildCommand = "pip install -r requirements.txt && python manage.py collectstatic --noinput";
|
|
585
|
-
}
|
|
586
|
-
return {
|
|
587
|
-
framework,
|
|
588
|
-
buildCommand,
|
|
589
|
-
outputDirectory: ".",
|
|
590
|
-
installCommand: "pip install -r requirements.txt",
|
|
591
|
-
serverType: "server",
|
|
592
|
-
deployTarget: "flyio",
|
|
593
|
-
pythonVersion: detectPythonVersion(cwd),
|
|
594
|
-
envVars,
|
|
595
|
-
projectName,
|
|
596
|
-
confidence: 0.85,
|
|
597
|
-
files: allEntries
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
699
|
if (hasFile("index.html")) {
|
|
601
700
|
const projectName = cwd.split("/").pop() ?? "my-app";
|
|
602
|
-
|
|
701
|
+
const result = {
|
|
603
702
|
framework: "static-html",
|
|
604
703
|
buildCommand: "",
|
|
605
704
|
outputDirectory: ".",
|
|
@@ -611,6 +710,7 @@ function scanProject(cwd = process.cwd()) {
|
|
|
611
710
|
confidence: 0.9,
|
|
612
711
|
files: allEntries
|
|
613
712
|
};
|
|
713
|
+
return result;
|
|
614
714
|
}
|
|
615
715
|
return buildUnknown(cwd, files);
|
|
616
716
|
}
|
|
@@ -634,16 +734,6 @@ function detectPackageManager(cwd) {
|
|
|
634
734
|
if (existsSync(join(cwd, "bun.lockb"))) return "bun install";
|
|
635
735
|
return "npm install";
|
|
636
736
|
}
|
|
637
|
-
function detectPythonVersion(cwd) {
|
|
638
|
-
const pythonVersion = readFile(join(cwd, ".python-version"));
|
|
639
|
-
if (pythonVersion) return pythonVersion.trim();
|
|
640
|
-
const pyproject = readFile(join(cwd, "pyproject.toml"));
|
|
641
|
-
if (pyproject) {
|
|
642
|
-
const match = pyproject.match(/python\s*=\s*["']([^"']+)["']/);
|
|
643
|
-
if (match) return match[1].replace(/[^0-9.]/g, "");
|
|
644
|
-
}
|
|
645
|
-
return "3.11";
|
|
646
|
-
}
|
|
647
737
|
function mergeEnvVars(base, extra) {
|
|
648
738
|
const seen = new Set(base.map((v) => v.name));
|
|
649
739
|
const merged = [...base];
|
|
@@ -655,325 +745,57 @@ function mergeEnvVars(base, extra) {
|
|
|
655
745
|
}
|
|
656
746
|
return merged;
|
|
657
747
|
}
|
|
658
|
-
function
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
"
|
|
663
|
-
"vite.config.ts",
|
|
664
|
-
"vite.config.js",
|
|
665
|
-
"next.config.js",
|
|
666
|
-
"next.config.ts",
|
|
667
|
-
"astro.config.mjs",
|
|
668
|
-
"svelte.config.js",
|
|
669
|
-
"nuxt.config.ts",
|
|
670
|
-
"remix.config.js",
|
|
671
|
-
"requirements.txt",
|
|
672
|
-
"pyproject.toml",
|
|
673
|
-
"Pipfile",
|
|
674
|
-
"Dockerfile",
|
|
675
|
-
"docker-compose.yml",
|
|
676
|
-
".env.example",
|
|
677
|
-
".env.local.example",
|
|
678
|
-
"wrangler.toml",
|
|
679
|
-
"fly.toml"
|
|
680
|
-
];
|
|
681
|
-
for (const file of importantFiles) {
|
|
682
|
-
const content = readFile(join(cwd, file));
|
|
683
|
-
if (content) {
|
|
684
|
-
samples[file] = content.slice(0, 2e3);
|
|
685
|
-
if (Object.keys(samples).length >= maxFiles) break;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
const topLevel = readdirSync(cwd).slice(0, 30);
|
|
689
|
-
samples["[directory listing]"] = topLevel.join("\n");
|
|
690
|
-
return samples;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// src/detect/ai-detect.ts
|
|
694
|
-
import Anthropic from "@anthropic-ai/sdk";
|
|
695
|
-
import { z } from "zod";
|
|
696
|
-
|
|
697
|
-
// src/config.ts
|
|
698
|
-
import Conf from "conf";
|
|
699
|
-
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
700
|
-
import { join as join2 } from "path";
|
|
701
|
-
var SHIPEM_API_URL = process.env.SHIPEM_API_URL ?? "https://api.shipem.dev";
|
|
702
|
-
var globalConf = new Conf({
|
|
703
|
-
projectName: "shipem",
|
|
704
|
-
schema: {
|
|
705
|
-
cloudflare: {
|
|
706
|
-
type: "object",
|
|
707
|
-
properties: {
|
|
708
|
-
apiToken: { type: "string" },
|
|
709
|
-
accountId: { type: "string" }
|
|
710
|
-
}
|
|
711
|
-
},
|
|
712
|
-
anthropicApiKey: { type: "string" },
|
|
713
|
-
sessionToken: { type: "string" }
|
|
748
|
+
function detectMonorepo(cwd) {
|
|
749
|
+
const hasFile = (name) => existsSync(join(cwd, name));
|
|
750
|
+
if (hasFile("turbo.json")) {
|
|
751
|
+
const packages = findWorkspacePackages(cwd);
|
|
752
|
+
return { type: "turbo", packages };
|
|
714
753
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
const token = process.env.CLOUDFLARE_API_TOKEN ?? creds?.apiToken;
|
|
719
|
-
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID ?? creds?.accountId;
|
|
720
|
-
if (token && accountId) {
|
|
721
|
-
return { apiToken: token, accountId };
|
|
754
|
+
if (hasFile("nx.json")) {
|
|
755
|
+
const packages = findWorkspacePackages(cwd);
|
|
756
|
+
return { type: "nx", packages };
|
|
722
757
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
return process.env.ANTHROPIC_API_KEY ?? globalConf.get("anthropicApiKey") ?? null;
|
|
727
|
-
}
|
|
728
|
-
function getSessionToken() {
|
|
729
|
-
return globalConf.get("sessionToken") ?? null;
|
|
730
|
-
}
|
|
731
|
-
function setSessionToken(token) {
|
|
732
|
-
globalConf.set("sessionToken", token);
|
|
733
|
-
}
|
|
734
|
-
function clearSessionToken() {
|
|
735
|
-
globalConf.delete("sessionToken");
|
|
736
|
-
}
|
|
737
|
-
var SHIPIT_CONFIG_FILE = "shipem.json";
|
|
738
|
-
function readProjectConfig(cwd = process.cwd()) {
|
|
739
|
-
const configPath = join2(cwd, SHIPIT_CONFIG_FILE);
|
|
740
|
-
if (!existsSync2(configPath)) {
|
|
741
|
-
return {};
|
|
758
|
+
if (hasFile("pnpm-workspace.yaml")) {
|
|
759
|
+
const packages = findWorkspacePackages(cwd);
|
|
760
|
+
return { type: "pnpm-workspaces", packages };
|
|
742
761
|
}
|
|
743
762
|
try {
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
763
|
+
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
|
|
764
|
+
if (pkg.workspaces) {
|
|
765
|
+
const packages = findWorkspacePackages(cwd);
|
|
766
|
+
if (hasFile("yarn.lock")) {
|
|
767
|
+
return { type: "yarn-workspaces", packages };
|
|
768
|
+
}
|
|
769
|
+
return { type: "npm-workspaces", packages };
|
|
770
|
+
}
|
|
748
771
|
} catch {
|
|
749
|
-
return {};
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
function writeProjectConfig(config, cwd = process.cwd()) {
|
|
753
|
-
const configPath = join2(cwd, SHIPIT_CONFIG_FILE);
|
|
754
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
755
|
-
}
|
|
756
|
-
var SECRET_KEY_PATTERN = /token|secret|key|password|credential|api_?key/i;
|
|
757
|
-
function warnIfConfigContainsSecrets(config, configPath) {
|
|
758
|
-
const envVars = config.project?.envVars ?? [];
|
|
759
|
-
const populated = envVars.filter((v) => v.value && SECRET_KEY_PATTERN.test(v.name));
|
|
760
|
-
if (populated.length > 0) {
|
|
761
|
-
const names = populated.map((v) => v.name).join(", ");
|
|
762
|
-
process.stderr.write(
|
|
763
|
-
`
|
|
764
|
-
[Shipem] Warning: ${configPath} contains values for sensitive env vars (${names}).
|
|
765
|
-
Ensure shipem.json is listed in .gitignore to avoid committing credentials.
|
|
766
|
-
|
|
767
|
-
`
|
|
768
|
-
);
|
|
769
772
|
}
|
|
773
|
+
return void 0;
|
|
770
774
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
"nuxt",
|
|
784
|
-
"gatsby",
|
|
785
|
-
"create-react-app",
|
|
786
|
-
"flask",
|
|
787
|
-
"fastapi",
|
|
788
|
-
"django",
|
|
789
|
-
"express",
|
|
790
|
-
"hono",
|
|
791
|
-
"static-html",
|
|
792
|
-
"unknown"
|
|
793
|
-
]),
|
|
794
|
-
buildCommand: z.string().describe("Command to build the project"),
|
|
795
|
-
outputDirectory: z.string().describe("Directory containing built output"),
|
|
796
|
-
installCommand: z.string().describe("Command to install dependencies"),
|
|
797
|
-
serverType: z.enum(["static", "serverless", "server"]),
|
|
798
|
-
deployTarget: z.enum(["cloudflare-pages", "cloudflare-workers", "flyio"]),
|
|
799
|
-
nodeVersion: z.string().optional(),
|
|
800
|
-
pythonVersion: z.string().optional(),
|
|
801
|
-
envVars: z.array(z.object({
|
|
802
|
-
name: z.string(),
|
|
803
|
-
description: z.string(),
|
|
804
|
-
required: z.boolean(),
|
|
805
|
-
autoProvision: z.boolean().optional()
|
|
806
|
-
})),
|
|
807
|
-
notes: z.string().describe("Brief explanation of detection decisions"),
|
|
808
|
-
confidence: z.number().min(0).max(1)
|
|
809
|
-
});
|
|
810
|
-
async function aiDetectProject(fileSamples, heuristicResult) {
|
|
811
|
-
const apiKey = getAnthropicApiKey();
|
|
812
|
-
if (!apiKey) {
|
|
813
|
-
return null;
|
|
814
|
-
}
|
|
815
|
-
const client = new Anthropic({ apiKey });
|
|
816
|
-
const fileContent = Object.entries(fileSamples).map(([name, content]) => `### ${name}
|
|
817
|
-
\`\`\`
|
|
818
|
-
${content}
|
|
819
|
-
\`\`\``).join("\n\n");
|
|
820
|
-
const heuristicContext = heuristicResult ? `
|
|
821
|
-
Heuristic detection suggests: ${JSON.stringify(heuristicResult, null, 2)}
|
|
822
|
-
` : "";
|
|
823
|
-
const prompt = `You are analyzing a software project to determine how to deploy it.
|
|
824
|
-
|
|
825
|
-
${heuristicContext}
|
|
826
|
-
|
|
827
|
-
Here are the key project files:
|
|
828
|
-
|
|
829
|
-
${fileContent}
|
|
830
|
-
|
|
831
|
-
Based on these files, determine:
|
|
832
|
-
1. The exact framework/stack being used
|
|
833
|
-
2. The correct build command
|
|
834
|
-
3. The output directory after build
|
|
835
|
-
4. Required environment variables (look carefully at the code for API keys, database URLs, etc.)
|
|
836
|
-
5. Whether this is a static site, serverless, or needs a persistent server
|
|
837
|
-
6. The best deploy target (cloudflare-pages for static/JAMstack, cloudflare-workers for edge/serverless, flyio for full-stack servers)
|
|
838
|
-
|
|
839
|
-
Be thorough about environment variables - check imports, API clients, and configuration files for any external services being used.
|
|
840
|
-
|
|
841
|
-
Call the analyze_project tool with your findings.`;
|
|
842
|
-
const toolInputSchema = {
|
|
843
|
-
type: "object",
|
|
844
|
-
properties: {
|
|
845
|
-
framework: { type: "string", description: "The detected framework or stack" },
|
|
846
|
-
frameworkId: {
|
|
847
|
-
type: "string",
|
|
848
|
-
enum: [
|
|
849
|
-
"nextjs",
|
|
850
|
-
"vite-react",
|
|
851
|
-
"vite-vue",
|
|
852
|
-
"vite-svelte",
|
|
853
|
-
"astro",
|
|
854
|
-
"sveltekit",
|
|
855
|
-
"remix",
|
|
856
|
-
"nuxt",
|
|
857
|
-
"gatsby",
|
|
858
|
-
"create-react-app",
|
|
859
|
-
"flask",
|
|
860
|
-
"fastapi",
|
|
861
|
-
"django",
|
|
862
|
-
"express",
|
|
863
|
-
"hono",
|
|
864
|
-
"static-html",
|
|
865
|
-
"unknown"
|
|
866
|
-
]
|
|
867
|
-
},
|
|
868
|
-
buildCommand: { type: "string", description: "Command to build the project" },
|
|
869
|
-
outputDirectory: { type: "string", description: "Directory containing built output" },
|
|
870
|
-
installCommand: { type: "string", description: "Command to install dependencies" },
|
|
871
|
-
serverType: { type: "string", enum: ["static", "serverless", "server"] },
|
|
872
|
-
deployTarget: { type: "string", enum: ["cloudflare-pages", "cloudflare-workers", "flyio"] },
|
|
873
|
-
nodeVersion: { type: "string" },
|
|
874
|
-
pythonVersion: { type: "string" },
|
|
875
|
-
envVars: {
|
|
876
|
-
type: "array",
|
|
877
|
-
items: {
|
|
878
|
-
type: "object",
|
|
879
|
-
properties: {
|
|
880
|
-
name: { type: "string" },
|
|
881
|
-
description: { type: "string" },
|
|
882
|
-
required: { type: "boolean" },
|
|
883
|
-
autoProvision: { type: "boolean" }
|
|
884
|
-
},
|
|
885
|
-
required: ["name", "description", "required"]
|
|
886
|
-
}
|
|
887
|
-
},
|
|
888
|
-
notes: { type: "string", description: "Brief explanation of detection decisions" },
|
|
889
|
-
confidence: { type: "number", minimum: 0, maximum: 1 }
|
|
890
|
-
},
|
|
891
|
-
required: [
|
|
892
|
-
"framework",
|
|
893
|
-
"frameworkId",
|
|
894
|
-
"buildCommand",
|
|
895
|
-
"outputDirectory",
|
|
896
|
-
"installCommand",
|
|
897
|
-
"serverType",
|
|
898
|
-
"deployTarget",
|
|
899
|
-
"envVars",
|
|
900
|
-
"notes",
|
|
901
|
-
"confidence"
|
|
902
|
-
]
|
|
903
|
-
};
|
|
904
|
-
try {
|
|
905
|
-
const response = await client.messages.create({
|
|
906
|
-
model: "claude-sonnet-4-20250514",
|
|
907
|
-
max_tokens: 4096,
|
|
908
|
-
tools: [
|
|
909
|
-
{
|
|
910
|
-
name: "analyze_project",
|
|
911
|
-
description: "Analyze a software project to determine its framework, build config, and deployment configuration",
|
|
912
|
-
input_schema: toolInputSchema
|
|
775
|
+
function findWorkspacePackages(cwd) {
|
|
776
|
+
const packages = [];
|
|
777
|
+
const commonDirs = ["packages", "apps", "projects", "services"];
|
|
778
|
+
for (const dir of commonDirs) {
|
|
779
|
+
const dirPath = join(cwd, dir);
|
|
780
|
+
if (existsSync(dirPath)) {
|
|
781
|
+
try {
|
|
782
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
783
|
+
for (const entry of entries) {
|
|
784
|
+
if (entry.isDirectory() && existsSync(join(dirPath, entry.name, "package.json"))) {
|
|
785
|
+
packages.push(entry.name);
|
|
786
|
+
}
|
|
913
787
|
}
|
|
914
|
-
|
|
915
|
-
tool_choice: { type: "tool", name: "analyze_project" },
|
|
916
|
-
messages: [{ role: "user", content: prompt }]
|
|
917
|
-
});
|
|
918
|
-
let toolInput = null;
|
|
919
|
-
for (const block of response.content) {
|
|
920
|
-
if (block.type === "tool_use" && block.name === "analyze_project") {
|
|
921
|
-
toolInput = block.input;
|
|
922
|
-
break;
|
|
788
|
+
} catch {
|
|
923
789
|
}
|
|
924
790
|
}
|
|
925
|
-
if (!toolInput) {
|
|
926
|
-
return null;
|
|
927
|
-
}
|
|
928
|
-
const validated = AIDetectionSchema.parse(toolInput);
|
|
929
|
-
return {
|
|
930
|
-
framework: validated.frameworkId,
|
|
931
|
-
buildCommand: validated.buildCommand,
|
|
932
|
-
outputDirectory: validated.outputDirectory,
|
|
933
|
-
installCommand: validated.installCommand,
|
|
934
|
-
serverType: validated.serverType,
|
|
935
|
-
deployTarget: validated.deployTarget,
|
|
936
|
-
nodeVersion: validated.nodeVersion,
|
|
937
|
-
pythonVersion: validated.pythonVersion,
|
|
938
|
-
envVars: validated.envVars,
|
|
939
|
-
notes: validated.notes,
|
|
940
|
-
confidence: validated.confidence
|
|
941
|
-
};
|
|
942
|
-
} catch (err) {
|
|
943
|
-
return null;
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
function mergeDetectionResults(heuristic, aiResult) {
|
|
947
|
-
if (!aiResult || aiResult.confidence < 0.7) {
|
|
948
|
-
return heuristic;
|
|
949
|
-
}
|
|
950
|
-
const seen = new Set(heuristic.envVars.map((v) => v.name));
|
|
951
|
-
const mergedEnvVars = [...heuristic.envVars];
|
|
952
|
-
for (const v of aiResult.envVars) {
|
|
953
|
-
if (!seen.has(v.name)) {
|
|
954
|
-
seen.add(v.name);
|
|
955
|
-
mergedEnvVars.push(v);
|
|
956
|
-
}
|
|
957
791
|
}
|
|
958
|
-
return
|
|
959
|
-
framework: aiResult.framework !== "unknown" ? aiResult.framework : heuristic.framework,
|
|
960
|
-
buildCommand: aiResult.buildCommand || heuristic.buildCommand,
|
|
961
|
-
outputDirectory: aiResult.outputDirectory || heuristic.outputDirectory,
|
|
962
|
-
installCommand: aiResult.installCommand || heuristic.installCommand,
|
|
963
|
-
serverType: aiResult.serverType,
|
|
964
|
-
deployTarget: aiResult.deployTarget,
|
|
965
|
-
nodeVersion: aiResult.nodeVersion ?? heuristic.nodeVersion,
|
|
966
|
-
pythonVersion: aiResult.pythonVersion ?? heuristic.pythonVersion,
|
|
967
|
-
envVars: mergedEnvVars,
|
|
968
|
-
notes: aiResult.notes,
|
|
969
|
-
confidence: Math.max(heuristic.confidence, aiResult.confidence)
|
|
970
|
-
};
|
|
792
|
+
return packages;
|
|
971
793
|
}
|
|
972
794
|
|
|
973
795
|
// src/build/builder.ts
|
|
974
796
|
import { execa } from "execa";
|
|
975
|
-
import { existsSync as
|
|
976
|
-
import { join as
|
|
797
|
+
import { existsSync as existsSync2 } from "fs";
|
|
798
|
+
import { join as join2 } from "path";
|
|
977
799
|
import chalk2 from "chalk";
|
|
978
800
|
async function buildProject(config, cwd = process.cwd()) {
|
|
979
801
|
const start = Date.now();
|
|
@@ -1091,8 +913,8 @@ async function buildProject(config, cwd = process.cwd()) {
|
|
|
1091
913
|
}
|
|
1092
914
|
console.log("");
|
|
1093
915
|
}
|
|
1094
|
-
const outputPath =
|
|
1095
|
-
if (config.buildCommand && !
|
|
916
|
+
const outputPath = join2(cwd, config.outputDirectory);
|
|
917
|
+
if (config.buildCommand && !existsSync2(outputPath)) {
|
|
1096
918
|
return {
|
|
1097
919
|
success: false,
|
|
1098
920
|
outputDirectory: config.outputDirectory,
|
|
@@ -1120,66 +942,178 @@ function parseErrorMessage(raw, outputLines) {
|
|
|
1120
942
|
return lines.slice(-5).join("\n");
|
|
1121
943
|
}
|
|
1122
944
|
|
|
1123
|
-
// src/
|
|
1124
|
-
import
|
|
1125
|
-
import {
|
|
1126
|
-
import
|
|
1127
|
-
var
|
|
1128
|
-
var
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
945
|
+
// src/config.ts
|
|
946
|
+
import Conf from "conf";
|
|
947
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3 } from "fs";
|
|
948
|
+
import { join as join3 } from "path";
|
|
949
|
+
var SHIPEM_API_URL = process.env.SHIPEM_API_URL ?? "https://api.shipem.dev";
|
|
950
|
+
var globalConf = new Conf({
|
|
951
|
+
projectName: "shipem",
|
|
952
|
+
schema: {
|
|
953
|
+
cloudflare: {
|
|
954
|
+
type: "object",
|
|
955
|
+
properties: {
|
|
956
|
+
apiToken: { type: "string" },
|
|
957
|
+
accountId: { type: "string" }
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
sessionToken: { type: "string" }
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
function getCloudflareCredentials() {
|
|
964
|
+
const creds = globalConf.get("cloudflare");
|
|
965
|
+
const token = process.env.CLOUDFLARE_API_TOKEN ?? creds?.apiToken;
|
|
966
|
+
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID ?? creds?.accountId;
|
|
967
|
+
if (token && accountId) {
|
|
968
|
+
return { apiToken: token, accountId };
|
|
969
|
+
}
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
function getSessionToken() {
|
|
973
|
+
return globalConf.get("sessionToken") ?? null;
|
|
974
|
+
}
|
|
975
|
+
function setSessionToken(token) {
|
|
976
|
+
globalConf.set("sessionToken", token);
|
|
977
|
+
}
|
|
978
|
+
function clearSessionToken() {
|
|
979
|
+
globalConf.delete("sessionToken");
|
|
980
|
+
}
|
|
981
|
+
var SHIPIT_CONFIG_FILE = "shipem.json";
|
|
982
|
+
function readProjectConfig(cwd = process.cwd()) {
|
|
983
|
+
const configPath = join3(cwd, SHIPIT_CONFIG_FILE);
|
|
984
|
+
if (!existsSync3(configPath)) {
|
|
985
|
+
return {};
|
|
986
|
+
}
|
|
987
|
+
try {
|
|
988
|
+
const raw = readFileSync2(configPath, "utf-8");
|
|
989
|
+
const config = JSON.parse(raw);
|
|
990
|
+
warnIfConfigContainsSecrets(config, configPath);
|
|
991
|
+
return config;
|
|
992
|
+
} catch {
|
|
993
|
+
return {};
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
function writeProjectConfig(config, cwd = process.cwd()) {
|
|
997
|
+
const configPath = join3(cwd, SHIPIT_CONFIG_FILE);
|
|
998
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
999
|
+
}
|
|
1000
|
+
var SECRET_KEY_PATTERN = /token|secret|key|password|credential|api_?key/i;
|
|
1001
|
+
function warnIfConfigContainsSecrets(config, configPath) {
|
|
1002
|
+
const envVars = config.project?.envVars ?? [];
|
|
1003
|
+
const populated = envVars.filter((v) => v.value && SECRET_KEY_PATTERN.test(v.name));
|
|
1004
|
+
if (populated.length > 0) {
|
|
1005
|
+
const names = populated.map((v) => v.name).join(", ");
|
|
1006
|
+
process.stderr.write(
|
|
1007
|
+
`
|
|
1008
|
+
[Shipem] Warning: ${configPath} contains values for sensitive env vars (${names}).
|
|
1009
|
+
Ensure shipem.json is listed in .gitignore to avoid committing credentials.
|
|
1010
|
+
|
|
1011
|
+
`
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// src/commands/login.ts
|
|
1017
|
+
import { createServer } from "http";
|
|
1018
|
+
import { randomBytes } from "crypto";
|
|
1019
|
+
import open from "open";
|
|
1020
|
+
|
|
1021
|
+
// src/errors.ts
|
|
1022
|
+
var ShipemError = class extends Error {
|
|
1023
|
+
/** Machine-readable error code for programmatic consumers (--json mode). */
|
|
1024
|
+
code;
|
|
1025
|
+
/** Suggested exit code: 1 = user error, 2 = system/infra error. */
|
|
1026
|
+
exitCode;
|
|
1027
|
+
constructor(message, opts = { code: "ERR_UNKNOWN" }) {
|
|
1028
|
+
super(message, { cause: opts.cause });
|
|
1029
|
+
this.name = "ShipemError";
|
|
1030
|
+
this.code = opts.code;
|
|
1031
|
+
this.exitCode = opts.exitCode ?? 1;
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
var NetworkError = class extends ShipemError {
|
|
1035
|
+
constructor(message, opts) {
|
|
1036
|
+
super(message, { code: "ERR_NETWORK", exitCode: 2, cause: opts?.cause });
|
|
1037
|
+
this.name = "NetworkError";
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
var AuthError = class extends ShipemError {
|
|
1041
|
+
constructor(message, opts) {
|
|
1042
|
+
super(message, { code: "ERR_AUTH", exitCode: 1, cause: opts?.cause });
|
|
1043
|
+
this.name = "AuthError";
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
var DeployError = class extends ShipemError {
|
|
1047
|
+
constructor(message, opts) {
|
|
1048
|
+
super(message, { code: "ERR_DEPLOY", exitCode: 2, cause: opts?.cause });
|
|
1049
|
+
this.name = "DeployError";
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
var ConfigError = class extends ShipemError {
|
|
1053
|
+
constructor(message, opts) {
|
|
1054
|
+
super(message, { code: "ERR_CONFIG", exitCode: 1, cause: opts?.cause });
|
|
1055
|
+
this.name = "ConfigError";
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
// src/commands/login.ts
|
|
1060
|
+
var CALLBACK_PORT_START = 9999;
|
|
1061
|
+
var MAX_PORT_ATTEMPTS = 10;
|
|
1062
|
+
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1063
|
+
async function loginCommand(opts = {}) {
|
|
1064
|
+
if (!opts.skipBanner) ui.banner();
|
|
1065
|
+
ui.section("Logging in to Shipem...");
|
|
1066
|
+
ui.br();
|
|
1067
|
+
const cliState = randomBytes(16).toString("hex");
|
|
1068
|
+
let resolveToken;
|
|
1069
|
+
let rejectToken;
|
|
1070
|
+
const tokenPromise = new Promise((resolve, reject) => {
|
|
1071
|
+
resolveToken = resolve;
|
|
1072
|
+
rejectToken = reject;
|
|
1073
|
+
});
|
|
1074
|
+
let hasHandled = false;
|
|
1075
|
+
const server = createServer((req, res) => {
|
|
1076
|
+
if (!req.url) return;
|
|
1077
|
+
if (hasHandled) {
|
|
1078
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1079
|
+
res.end('<html><body style="font-family:sans-serif;padding:40px">Login already completed. You can close this tab.</body></html>');
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
const url = new URL(req.url, `http://127.0.0.1:${actualPort}`);
|
|
1083
|
+
const token2 = url.searchParams.get("token");
|
|
1084
|
+
const login = url.searchParams.get("login");
|
|
1085
|
+
const returnedState = url.searchParams.get("state");
|
|
1086
|
+
if (returnedState !== cliState) {
|
|
1087
|
+
hasHandled = true;
|
|
1088
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
1089
|
+
res.end('<html><body style="font-family:sans-serif;padding:40px;color:red">State mismatch \u2014 possible CSRF. Please try logging in again.</body></html>');
|
|
1090
|
+
rejectToken(new AuthError("State mismatch in OAuth callback \u2014 possible CSRF attack"));
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
if (token2) {
|
|
1094
|
+
hasHandled = true;
|
|
1095
|
+
const html = `<!DOCTYPE html>
|
|
1096
|
+
<html>
|
|
1097
|
+
<head><title>Shipem \u2014 Logged in!</title>
|
|
1098
|
+
<style>
|
|
1099
|
+
body { font-family: -apple-system, sans-serif; text-align: center; padding: 60px; background: #0f172a; color: #e2e8f0; }
|
|
1100
|
+
h2 { color: #22c55e; font-size: 2rem; margin-bottom: 12px; }
|
|
1101
|
+
p { color: #94a3b8; font-size: 1.1rem; }
|
|
1102
|
+
</style>
|
|
1103
|
+
</head>
|
|
1104
|
+
<body>
|
|
1105
|
+
<h2>\u2705 Logged in${login ? ` as @${login}` : ""}!</h2>
|
|
1106
|
+
<p>You can close this tab and return to your terminal.</p>
|
|
1107
|
+
</body>
|
|
1108
|
+
</html>`;
|
|
1109
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1110
|
+
res.end(html);
|
|
1111
|
+
resolveToken(token2);
|
|
1112
|
+
} else {
|
|
1113
|
+
hasHandled = true;
|
|
1114
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
1115
|
+
res.end('<html><body style="font-family:sans-serif;padding:40px">Authentication failed. Please try again.</body></html>');
|
|
1116
|
+
rejectToken(new AuthError("Authentication failed \u2014 no token received"));
|
|
1183
1117
|
}
|
|
1184
1118
|
});
|
|
1185
1119
|
let actualPort = CALLBACK_PORT_START;
|
|
@@ -1201,7 +1135,7 @@ async function loginCommand(opts = {}) {
|
|
|
1201
1135
|
}
|
|
1202
1136
|
}
|
|
1203
1137
|
if (!bound) {
|
|
1204
|
-
throw new
|
|
1138
|
+
throw new NetworkError(`Could not bind to any port in range ${CALLBACK_PORT_START}\u2013${CALLBACK_PORT_START + MAX_PORT_ATTEMPTS - 1}. Please free up a port and try again.`);
|
|
1205
1139
|
}
|
|
1206
1140
|
const callbackUrl = `http://localhost:${actualPort}`;
|
|
1207
1141
|
const loginUrl = `${SHIPEM_API_URL}/auth/github?cli_callback=${encodeURIComponent(callbackUrl)}&cli_state=${cliState}`;
|
|
@@ -1210,13 +1144,12 @@ async function loginCommand(opts = {}) {
|
|
|
1210
1144
|
ui.dim(loginUrl);
|
|
1211
1145
|
ui.br();
|
|
1212
1146
|
await open(loginUrl);
|
|
1213
|
-
const timeoutMs = 5 * 60 * 1e3;
|
|
1214
1147
|
let token;
|
|
1215
1148
|
try {
|
|
1216
1149
|
token = await Promise.race([
|
|
1217
1150
|
tokenPromise,
|
|
1218
1151
|
new Promise(
|
|
1219
|
-
(_, reject) => setTimeout(() => reject(new
|
|
1152
|
+
(_, reject) => setTimeout(() => reject(new AuthError("Login timed out after 5 minutes. Please try again.")), LOGIN_TIMEOUT_MS)
|
|
1220
1153
|
)
|
|
1221
1154
|
]);
|
|
1222
1155
|
} finally {
|
|
@@ -1228,15 +1161,339 @@ async function loginCommand(opts = {}) {
|
|
|
1228
1161
|
ui.br();
|
|
1229
1162
|
}
|
|
1230
1163
|
|
|
1164
|
+
// src/commands/fix.ts
|
|
1165
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
1166
|
+
import { join as join4 } from "path";
|
|
1167
|
+
import { execa as execa2 } from "execa";
|
|
1168
|
+
function findMissingModules(errorOutput) {
|
|
1169
|
+
const modules = /* @__PURE__ */ new Set();
|
|
1170
|
+
const cannotFind = errorOutput.matchAll(/Cannot find module ['"]([^'"]+)['"]/g);
|
|
1171
|
+
for (const m of cannotFind) {
|
|
1172
|
+
modules.add(extractPkgName(m[1]));
|
|
1173
|
+
}
|
|
1174
|
+
const moduleNotFound = errorOutput.matchAll(/Module not found.*?['"]([^'"]+)['"]/g);
|
|
1175
|
+
for (const m of moduleNotFound) {
|
|
1176
|
+
modules.add(extractPkgName(m[1]));
|
|
1177
|
+
}
|
|
1178
|
+
const cannotFindPkg = errorOutput.matchAll(/Cannot find package ['"]([^'"]+)['"]/g);
|
|
1179
|
+
for (const m of cannotFindPkg) {
|
|
1180
|
+
modules.add(extractPkgName(m[1]));
|
|
1181
|
+
}
|
|
1182
|
+
const couldNotResolve = errorOutput.matchAll(/Could not resolve ["']([^"']+)["']/g);
|
|
1183
|
+
for (const m of couldNotResolve) {
|
|
1184
|
+
if (!m[1].startsWith(".") && !m[1].startsWith("/")) {
|
|
1185
|
+
modules.add(extractPkgName(m[1]));
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return [...modules].filter((m) => m && !m.startsWith(".") && !m.startsWith("/"));
|
|
1189
|
+
}
|
|
1190
|
+
function extractPkgName(importPath) {
|
|
1191
|
+
if (importPath.startsWith("@")) {
|
|
1192
|
+
const parts = importPath.split("/");
|
|
1193
|
+
return parts.slice(0, 2).join("/");
|
|
1194
|
+
}
|
|
1195
|
+
return importPath.split("/")[0];
|
|
1196
|
+
}
|
|
1197
|
+
function findMissingPeerDeps(errorOutput) {
|
|
1198
|
+
const peers = /* @__PURE__ */ new Set();
|
|
1199
|
+
const peerMatches = errorOutput.matchAll(
|
|
1200
|
+
/(?:requires|missing) (?:a )?peer (?:dependency|dep)[^"']*['"]([^'"]+)['"]/gi
|
|
1201
|
+
);
|
|
1202
|
+
for (const m of peerMatches) {
|
|
1203
|
+
peers.add(extractPkgName(m[1]));
|
|
1204
|
+
}
|
|
1205
|
+
const warnMatches = errorOutput.matchAll(
|
|
1206
|
+
/peerDepend[^"']*['"]([^'"]+)['"].*should be/gi
|
|
1207
|
+
);
|
|
1208
|
+
for (const m of warnMatches) {
|
|
1209
|
+
peers.add(extractPkgName(m[1]));
|
|
1210
|
+
}
|
|
1211
|
+
return [...peers];
|
|
1212
|
+
}
|
|
1213
|
+
function detectTsConfigFixes(errorOutput, cwd) {
|
|
1214
|
+
const tsErrors = errorOutput.match(/TS\d+/g) ?? [];
|
|
1215
|
+
if (tsErrors.length === 0) return null;
|
|
1216
|
+
const fixes = {};
|
|
1217
|
+
const descriptions = [];
|
|
1218
|
+
if (tsErrors.includes("TS7006") || tsErrors.includes("TS7005")) {
|
|
1219
|
+
fixes.noImplicitAny = false;
|
|
1220
|
+
descriptions.push("Disable noImplicitAny");
|
|
1221
|
+
}
|
|
1222
|
+
if (tsErrors.includes("TS2307")) {
|
|
1223
|
+
fixes.moduleResolution = "bundler";
|
|
1224
|
+
descriptions.push("Set moduleResolution to bundler");
|
|
1225
|
+
}
|
|
1226
|
+
if (tsErrors.includes("TS2769") || tsErrors.includes("TS2345") || tsErrors.includes("TS2322")) {
|
|
1227
|
+
fixes.strict = false;
|
|
1228
|
+
descriptions.push("Disable strict mode");
|
|
1229
|
+
}
|
|
1230
|
+
if (tsErrors.includes("TS18028") || tsErrors.includes("TS1259") || tsErrors.includes("TS1479")) {
|
|
1231
|
+
fixes.esModuleInterop = true;
|
|
1232
|
+
fixes.allowSyntheticDefaultImports = true;
|
|
1233
|
+
descriptions.push("Enable ESM interop");
|
|
1234
|
+
}
|
|
1235
|
+
if (descriptions.length === 0) return null;
|
|
1236
|
+
return { fixes, description: descriptions.join(", ") };
|
|
1237
|
+
}
|
|
1238
|
+
function applyTsConfigFixes(cwd, fixes) {
|
|
1239
|
+
const tsconfigPath = join4(cwd, "tsconfig.json");
|
|
1240
|
+
if (!existsSync4(tsconfigPath)) return false;
|
|
1241
|
+
try {
|
|
1242
|
+
const content = readFileSync3(tsconfigPath, "utf-8");
|
|
1243
|
+
const stripped = content.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
1244
|
+
const tsconfig = JSON.parse(stripped);
|
|
1245
|
+
if (!tsconfig.compilerOptions) tsconfig.compilerOptions = {};
|
|
1246
|
+
Object.assign(tsconfig.compilerOptions, fixes);
|
|
1247
|
+
writeFileSync2(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n", "utf-8");
|
|
1248
|
+
return true;
|
|
1249
|
+
} catch {
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
function findMissingEnvVars(cwd) {
|
|
1254
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
1255
|
+
const patterns = [
|
|
1256
|
+
/process\.env\.([A-Z_][A-Z0-9_]*)/g,
|
|
1257
|
+
/import\.meta\.env\.([A-Z_][A-Z0-9_]*)/g
|
|
1258
|
+
];
|
|
1259
|
+
function scanDir(dir, depth) {
|
|
1260
|
+
if (depth > 4) return;
|
|
1261
|
+
try {
|
|
1262
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
1263
|
+
for (const entry of entries) {
|
|
1264
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist" || entry.name === ".next") continue;
|
|
1265
|
+
const fullPath = join4(dir, entry.name);
|
|
1266
|
+
if (entry.isDirectory()) {
|
|
1267
|
+
scanDir(fullPath, depth + 1);
|
|
1268
|
+
} else if (/\.(ts|tsx|js|jsx|mjs|mts|vue|svelte|astro)$/.test(entry.name)) {
|
|
1269
|
+
try {
|
|
1270
|
+
const content = readFileSync3(fullPath, "utf-8");
|
|
1271
|
+
for (const pat of patterns) {
|
|
1272
|
+
pat.lastIndex = 0;
|
|
1273
|
+
let match;
|
|
1274
|
+
while ((match = pat.exec(content)) !== null) {
|
|
1275
|
+
referenced.add(match[1]);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
} catch {
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
} catch {
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
scanDir(cwd, 0);
|
|
1286
|
+
const ignoredVars = /* @__PURE__ */ new Set(["NODE_ENV", "PORT", "HOST", "CI", "HOME", "PWD", "PATH", "TERM"]);
|
|
1287
|
+
const missing = [];
|
|
1288
|
+
for (const v of referenced) {
|
|
1289
|
+
if (ignoredVars.has(v)) continue;
|
|
1290
|
+
if (!process.env[v]) missing.push(v);
|
|
1291
|
+
}
|
|
1292
|
+
return missing;
|
|
1293
|
+
}
|
|
1294
|
+
function generateEnvExample(cwd, vars) {
|
|
1295
|
+
if (vars.length === 0) return false;
|
|
1296
|
+
const envExamplePath = join4(cwd, ".env.example");
|
|
1297
|
+
if (existsSync4(envExamplePath)) return false;
|
|
1298
|
+
const content = vars.map((v) => `${v}=`).join("\n") + "\n";
|
|
1299
|
+
writeFileSync2(envExamplePath, content, "utf-8");
|
|
1300
|
+
return true;
|
|
1301
|
+
}
|
|
1302
|
+
function detectFrameworkConfigFixes(errorOutput, cwd, framework) {
|
|
1303
|
+
const fixes = [];
|
|
1304
|
+
if (framework === "nextjs") {
|
|
1305
|
+
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
|
+
if (nextConfigPath && errorOutput.includes("output")) {
|
|
1307
|
+
try {
|
|
1308
|
+
const content = readFileSync3(nextConfigPath, "utf-8");
|
|
1309
|
+
if (!content.includes("output")) {
|
|
1310
|
+
fixes.push(`Add output: 'export' to ${nextConfigPath.split("/").pop()}`);
|
|
1311
|
+
}
|
|
1312
|
+
} catch {
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
if ((framework === "vite-react" || framework === "vite-vue" || framework === "vite-svelte") && errorOutput.includes("plugin")) {
|
|
1317
|
+
const pkgPath = join4(cwd, "package.json");
|
|
1318
|
+
try {
|
|
1319
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
1320
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1321
|
+
if (framework === "vite-react" && !deps["@vitejs/plugin-react"]) {
|
|
1322
|
+
fixes.push("Install @vitejs/plugin-react");
|
|
1323
|
+
}
|
|
1324
|
+
if (framework === "vite-vue" && !deps["@vitejs/plugin-vue"]) {
|
|
1325
|
+
fixes.push("Install @vitejs/plugin-vue");
|
|
1326
|
+
}
|
|
1327
|
+
} catch {
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return fixes;
|
|
1331
|
+
}
|
|
1332
|
+
async function runFixHeuristics(errorOutput, cwd, config) {
|
|
1333
|
+
const actions = [];
|
|
1334
|
+
let anyFixApplied = false;
|
|
1335
|
+
const missingModules = findMissingModules(errorOutput);
|
|
1336
|
+
if (missingModules.length > 0) {
|
|
1337
|
+
const installCmd = config.installCommand.split(" ")[0] ?? "npm";
|
|
1338
|
+
for (const mod of missingModules) {
|
|
1339
|
+
const spinner = ui.spinner(`Installing missing dependency: ${mod}`);
|
|
1340
|
+
try {
|
|
1341
|
+
await execa2(installCmd, ["add", mod], { cwd, timeout: 6e4 });
|
|
1342
|
+
spinner.succeed(`Installed ${mod}`);
|
|
1343
|
+
actions.push(`Installed missing dependency: ${mod}`);
|
|
1344
|
+
anyFixApplied = true;
|
|
1345
|
+
} catch {
|
|
1346
|
+
spinner.fail(`Failed to install ${mod}`);
|
|
1347
|
+
actions.push(`Failed to install ${mod}`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
const missingPeers = findMissingPeerDeps(errorOutput);
|
|
1352
|
+
if (missingPeers.length > 0) {
|
|
1353
|
+
const installCmd = config.installCommand.split(" ")[0] ?? "npm";
|
|
1354
|
+
for (const peer of missingPeers) {
|
|
1355
|
+
if (missingModules.includes(peer)) continue;
|
|
1356
|
+
const spinner = ui.spinner(`Installing peer dependency: ${peer}`);
|
|
1357
|
+
try {
|
|
1358
|
+
await execa2(installCmd, ["add", peer], { cwd, timeout: 6e4 });
|
|
1359
|
+
spinner.succeed(`Installed peer dep: ${peer}`);
|
|
1360
|
+
actions.push(`Installed peer dependency: ${peer}`);
|
|
1361
|
+
anyFixApplied = true;
|
|
1362
|
+
} catch {
|
|
1363
|
+
spinner.fail(`Failed to install peer dep: ${peer}`);
|
|
1364
|
+
actions.push(`Failed to install ${peer}`);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
const tsConfigFixes = detectTsConfigFixes(errorOutput, cwd);
|
|
1369
|
+
if (tsConfigFixes) {
|
|
1370
|
+
const applied = applyTsConfigFixes(cwd, tsConfigFixes.fixes);
|
|
1371
|
+
if (applied) {
|
|
1372
|
+
actions.push(`Updated tsconfig.json: ${tsConfigFixes.description}`);
|
|
1373
|
+
anyFixApplied = true;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
const missingEnvVars = findMissingEnvVars(cwd);
|
|
1377
|
+
if (missingEnvVars.length > 0) {
|
|
1378
|
+
const generated = generateEnvExample(cwd, missingEnvVars);
|
|
1379
|
+
if (generated) {
|
|
1380
|
+
actions.push(`Generated .env.example with ${missingEnvVars.length} vars: ${missingEnvVars.join(", ")}`);
|
|
1381
|
+
anyFixApplied = true;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
const frameworkFixes = detectFrameworkConfigFixes(errorOutput, cwd, config.framework);
|
|
1385
|
+
for (const fix of frameworkFixes) {
|
|
1386
|
+
actions.push(`Suggestion: ${fix}`);
|
|
1387
|
+
}
|
|
1388
|
+
return { actions, fixed: anyFixApplied };
|
|
1389
|
+
}
|
|
1390
|
+
async function fixCommand(options = {}) {
|
|
1391
|
+
const cwd = process.cwd();
|
|
1392
|
+
ui.section("Shipem Fix \u2014 Auto-repair build issues");
|
|
1393
|
+
ui.br();
|
|
1394
|
+
const scanSpinner = ui.spinner("Scanning project...");
|
|
1395
|
+
const detection = scanProject(cwd);
|
|
1396
|
+
scanSpinner.succeed(`Detected: ${ui.framework(detection.framework)}`);
|
|
1397
|
+
if (!detection.buildCommand) {
|
|
1398
|
+
ui.warn("No build command detected. Nothing to fix.");
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
const projectConfig = {
|
|
1402
|
+
name: detection.projectName,
|
|
1403
|
+
framework: detection.framework,
|
|
1404
|
+
buildCommand: detection.buildCommand,
|
|
1405
|
+
outputDirectory: detection.outputDirectory,
|
|
1406
|
+
installCommand: detection.installCommand,
|
|
1407
|
+
serverType: detection.serverType,
|
|
1408
|
+
nodeVersion: detection.nodeVersion,
|
|
1409
|
+
envVars: detection.envVars,
|
|
1410
|
+
deployTarget: detection.deployTarget,
|
|
1411
|
+
detectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1412
|
+
};
|
|
1413
|
+
ui.br();
|
|
1414
|
+
ui.section("Running build...");
|
|
1415
|
+
ui.br();
|
|
1416
|
+
let buildResult = await buildProject(projectConfig, cwd);
|
|
1417
|
+
if (buildResult.success) {
|
|
1418
|
+
ui.br();
|
|
1419
|
+
ui.success("Build already passes! No fixes needed.");
|
|
1420
|
+
ui.br();
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
ui.br();
|
|
1424
|
+
ui.section("Analyzing build errors...");
|
|
1425
|
+
ui.br();
|
|
1426
|
+
const errorOutput = buildResult.error ?? "";
|
|
1427
|
+
const { actions, fixed } = await runFixHeuristics(errorOutput, cwd, projectConfig);
|
|
1428
|
+
ui.br();
|
|
1429
|
+
if (actions.length > 0) {
|
|
1430
|
+
ui.section("Actions taken:");
|
|
1431
|
+
for (const action of actions) {
|
|
1432
|
+
ui.info(action);
|
|
1433
|
+
}
|
|
1434
|
+
ui.br();
|
|
1435
|
+
}
|
|
1436
|
+
if (!fixed) {
|
|
1437
|
+
ui.warn("Could not automatically fix the build errors.");
|
|
1438
|
+
ui.br();
|
|
1439
|
+
ui.dim("Suggestions:");
|
|
1440
|
+
ui.dim(" 1. Paste the error into your AI coding tool for a fix");
|
|
1441
|
+
ui.dim(" 2. Check the error output above for clues");
|
|
1442
|
+
ui.dim(" 3. Run `npx ship-em --verbose` for detailed output");
|
|
1443
|
+
ui.br();
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
ui.section("Re-running build after fixes...");
|
|
1447
|
+
ui.br();
|
|
1448
|
+
buildResult = await buildProject(projectConfig, cwd);
|
|
1449
|
+
if (buildResult.success) {
|
|
1450
|
+
ui.br();
|
|
1451
|
+
ui.success("Fixed! Build passes now.");
|
|
1452
|
+
ui.dim("Run `npx ship-em` to deploy.");
|
|
1453
|
+
ui.br();
|
|
1454
|
+
} else {
|
|
1455
|
+
ui.br();
|
|
1456
|
+
ui.warn("Build still failing after fixes.");
|
|
1457
|
+
ui.br();
|
|
1458
|
+
ui.dim("What was tried:");
|
|
1459
|
+
for (const action of actions) {
|
|
1460
|
+
ui.dim(` - ${action}`);
|
|
1461
|
+
}
|
|
1462
|
+
ui.br();
|
|
1463
|
+
ui.dim("Tip: Paste the error into your AI coding tool \u2014 it can usually fix it.");
|
|
1464
|
+
ui.br();
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
async function tryInlineFix(errorOutput, cwd, config) {
|
|
1468
|
+
ui.br();
|
|
1469
|
+
ui.section("Attempting auto-fix...");
|
|
1470
|
+
ui.br();
|
|
1471
|
+
const { actions, fixed } = await runFixHeuristics(errorOutput, cwd, config);
|
|
1472
|
+
if (actions.length > 0) {
|
|
1473
|
+
for (const action of actions) {
|
|
1474
|
+
ui.info(action);
|
|
1475
|
+
}
|
|
1476
|
+
ui.br();
|
|
1477
|
+
}
|
|
1478
|
+
if (!fixed) {
|
|
1479
|
+
ui.warn("Could not automatically fix the issue.");
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
ui.section("Re-building after fix...");
|
|
1483
|
+
ui.br();
|
|
1484
|
+
const buildResult = await buildProject(config, cwd);
|
|
1485
|
+
return buildResult;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1231
1488
|
// src/deploy/cloudflare.ts
|
|
1232
1489
|
import axios from "axios";
|
|
1233
1490
|
import { createHash } from "crypto";
|
|
1234
|
-
import { readdirSync as
|
|
1235
|
-
import { join as
|
|
1491
|
+
import { readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync5 } from "fs";
|
|
1492
|
+
import { join as join6, relative } from "path";
|
|
1236
1493
|
|
|
1237
1494
|
// src/deploy/exclude.ts
|
|
1238
|
-
import { existsSync as
|
|
1239
|
-
import { join as
|
|
1495
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
1496
|
+
import { join as join5 } from "path";
|
|
1240
1497
|
import chalk3 from "chalk";
|
|
1241
1498
|
var DEFAULT_PATTERNS = [
|
|
1242
1499
|
".git",
|
|
@@ -1288,9 +1545,9 @@ function matchesIgnoreLine(relPath, line) {
|
|
|
1288
1545
|
return false;
|
|
1289
1546
|
}
|
|
1290
1547
|
function loadIgnoreLines(filePath) {
|
|
1291
|
-
if (!
|
|
1548
|
+
if (!existsSync5(filePath)) return [];
|
|
1292
1549
|
try {
|
|
1293
|
-
return
|
|
1550
|
+
return readFileSync4(filePath, "utf-8").split("\n");
|
|
1294
1551
|
} catch {
|
|
1295
1552
|
return [];
|
|
1296
1553
|
}
|
|
@@ -1303,11 +1560,11 @@ function shouldExclude(filePath, projectRoot) {
|
|
|
1303
1560
|
}
|
|
1304
1561
|
if (projectRoot) {
|
|
1305
1562
|
const relPath = normalized;
|
|
1306
|
-
const shipemIgnoreLines = loadIgnoreLines(
|
|
1563
|
+
const shipemIgnoreLines = loadIgnoreLines(join5(projectRoot, ".shipemignore"));
|
|
1307
1564
|
for (const line of shipemIgnoreLines) {
|
|
1308
1565
|
if (matchesIgnoreLine(relPath, line)) return true;
|
|
1309
1566
|
}
|
|
1310
|
-
const gitIgnoreLines = loadIgnoreLines(
|
|
1567
|
+
const gitIgnoreLines = loadIgnoreLines(join5(projectRoot, ".gitignore"));
|
|
1311
1568
|
for (const line of gitIgnoreLines) {
|
|
1312
1569
|
if (matchesIgnoreLine(relPath, line)) return true;
|
|
1313
1570
|
}
|
|
@@ -1317,7 +1574,7 @@ function shouldExclude(filePath, projectRoot) {
|
|
|
1317
1574
|
function warnIfEnvFilesInOutputDir(outputDir) {
|
|
1318
1575
|
const envFiles = [".env", ".env.local", ".env.production"];
|
|
1319
1576
|
for (const name of envFiles) {
|
|
1320
|
-
if (
|
|
1577
|
+
if (existsSync5(join5(outputDir, name))) {
|
|
1321
1578
|
process.stderr.write(
|
|
1322
1579
|
chalk3.yellow(`
|
|
1323
1580
|
\u26A0 Warning: ${name} found in output directory \u2014 it will be excluded from deployment.
|
|
@@ -1329,6 +1586,11 @@ function warnIfEnvFilesInOutputDir(outputDir) {
|
|
|
1329
1586
|
|
|
1330
1587
|
// src/deploy/cloudflare.ts
|
|
1331
1588
|
var CF_API_BASE = "https://api.cloudflare.com/client/v4";
|
|
1589
|
+
var CF_UPLOAD_ENDPOINT = "https://upload.storageapi.cloudflare.com/pages-uploads";
|
|
1590
|
+
var CF_PROJECT_NAME_MAX_LENGTH = 28;
|
|
1591
|
+
var CF_REQUEST_TIMEOUT_MS = 6e4;
|
|
1592
|
+
var DEPLOY_POLL_INTERVAL_MS = 3e3;
|
|
1593
|
+
var DEPLOY_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
1332
1594
|
var CloudflarePages = class {
|
|
1333
1595
|
client;
|
|
1334
1596
|
accountId;
|
|
@@ -1340,7 +1602,7 @@ var CloudflarePages = class {
|
|
|
1340
1602
|
Authorization: `Bearer ${apiToken}`,
|
|
1341
1603
|
"Content-Type": "application/json"
|
|
1342
1604
|
},
|
|
1343
|
-
timeout:
|
|
1605
|
+
timeout: CF_REQUEST_TIMEOUT_MS
|
|
1344
1606
|
});
|
|
1345
1607
|
}
|
|
1346
1608
|
async getOrCreateProject(projectName, config) {
|
|
@@ -1354,7 +1616,7 @@ var CloudflarePages = class {
|
|
|
1354
1616
|
} catch (err) {
|
|
1355
1617
|
const status = err.response?.status;
|
|
1356
1618
|
if (status !== 404) {
|
|
1357
|
-
throw new
|
|
1619
|
+
throw new DeployError(`Failed to check project: ${extractCFError(err)}`);
|
|
1358
1620
|
}
|
|
1359
1621
|
}
|
|
1360
1622
|
try {
|
|
@@ -1381,11 +1643,11 @@ var CloudflarePages = class {
|
|
|
1381
1643
|
}
|
|
1382
1644
|
);
|
|
1383
1645
|
if (!res.data.success) {
|
|
1384
|
-
throw new
|
|
1646
|
+
throw new DeployError(res.data.errors[0]?.message ?? "Unknown error creating project");
|
|
1385
1647
|
}
|
|
1386
1648
|
return res.data.result;
|
|
1387
1649
|
} catch (err) {
|
|
1388
|
-
throw new
|
|
1650
|
+
throw new DeployError(`Failed to create Pages project: ${extractCFError(err)}`, { cause: err });
|
|
1389
1651
|
}
|
|
1390
1652
|
}
|
|
1391
1653
|
async setEnvironmentVariables(projectName, envVars) {
|
|
@@ -1410,16 +1672,16 @@ var CloudflarePages = class {
|
|
|
1410
1672
|
}
|
|
1411
1673
|
);
|
|
1412
1674
|
} catch (err) {
|
|
1413
|
-
throw new
|
|
1675
|
+
throw new DeployError(`Failed to set environment variables: ${extractCFError(err)}`, { cause: err });
|
|
1414
1676
|
}
|
|
1415
1677
|
}
|
|
1416
1678
|
async deployDirectory(projectName, outputDir, cwd = process.cwd()) {
|
|
1417
|
-
const fullOutputPath =
|
|
1679
|
+
const fullOutputPath = join6(cwd, outputDir);
|
|
1418
1680
|
const filePaths = collectFiles(fullOutputPath);
|
|
1419
1681
|
let totalBytes = 0;
|
|
1420
1682
|
const fileMap = /* @__PURE__ */ new Map();
|
|
1421
1683
|
for (const filePath of filePaths) {
|
|
1422
|
-
const content =
|
|
1684
|
+
const content = readFileSync5(filePath);
|
|
1423
1685
|
const hash = createHash("sha256").update(content).digest("hex");
|
|
1424
1686
|
const urlPath = "/" + relative(fullOutputPath, filePath).replace(/\\/g, "/");
|
|
1425
1687
|
fileMap.set(urlPath, { hash, content });
|
|
@@ -1442,11 +1704,11 @@ var CloudflarePages = class {
|
|
|
1442
1704
|
const res = await this.client.post(
|
|
1443
1705
|
`/accounts/${this.accountId}/pages/projects/${projectName}/deployments`,
|
|
1444
1706
|
{ files: manifest, branch: "main" },
|
|
1445
|
-
{ timeout:
|
|
1707
|
+
{ timeout: CF_REQUEST_TIMEOUT_MS }
|
|
1446
1708
|
);
|
|
1447
1709
|
if (!res.data.success) {
|
|
1448
1710
|
deploySpinner.fail("Deployment creation failed");
|
|
1449
|
-
throw new
|
|
1711
|
+
throw new DeployError(res.data.errors[0]?.message ?? "Deployment failed");
|
|
1450
1712
|
}
|
|
1451
1713
|
jwt = res.data.result.jwt;
|
|
1452
1714
|
requiredFiles = res.data.result.required_files ?? [];
|
|
@@ -1454,7 +1716,7 @@ var CloudflarePages = class {
|
|
|
1454
1716
|
deploySpinner.succeed("Deployment created");
|
|
1455
1717
|
} catch (err) {
|
|
1456
1718
|
deploySpinner.fail("Deployment creation failed");
|
|
1457
|
-
throw new
|
|
1719
|
+
throw new DeployError(`Deployment failed: ${extractCFError(err)}`, { cause: err });
|
|
1458
1720
|
}
|
|
1459
1721
|
if (requiredFiles.length > 0) {
|
|
1460
1722
|
const uploadSpinner = ui.spinner(
|
|
@@ -1467,19 +1729,19 @@ var CloudflarePages = class {
|
|
|
1467
1729
|
try {
|
|
1468
1730
|
const formData = new FormData();
|
|
1469
1731
|
formData.append(hash, new Blob([content], { type: "application/octet-stream" }), hash);
|
|
1470
|
-
await fetch(
|
|
1732
|
+
await fetch(CF_UPLOAD_ENDPOINT, {
|
|
1471
1733
|
method: "POST",
|
|
1472
1734
|
headers: { Authorization: `Bearer ${jwt}` },
|
|
1473
1735
|
body: formData
|
|
1474
1736
|
}).then(async (res) => {
|
|
1475
1737
|
if (!res.ok) {
|
|
1476
1738
|
const text = await res.text().catch(() => res.statusText);
|
|
1477
|
-
throw new
|
|
1739
|
+
throw new DeployError(`Upload failed (${res.status}): ${text}`);
|
|
1478
1740
|
}
|
|
1479
1741
|
});
|
|
1480
1742
|
} catch (err) {
|
|
1481
1743
|
uploadSpinner.fail("Upload failed");
|
|
1482
|
-
throw new
|
|
1744
|
+
throw new DeployError(`Failed to upload file (hash ${hash}): ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1483
1745
|
}
|
|
1484
1746
|
uploaded++;
|
|
1485
1747
|
uploadSpinner.text = `Uploading files... ${uploaded}/${requiredFiles.length}`;
|
|
@@ -1489,16 +1751,92 @@ var CloudflarePages = class {
|
|
|
1489
1751
|
ui.success("Live!");
|
|
1490
1752
|
return { deployment, fileCount, totalBytes };
|
|
1491
1753
|
}
|
|
1492
|
-
async
|
|
1493
|
-
const
|
|
1494
|
-
const
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1754
|
+
async deployBranch(projectName, outputDir, branch, cwd = process.cwd()) {
|
|
1755
|
+
const fullOutputPath = join6(cwd, outputDir);
|
|
1756
|
+
const filePaths = collectFiles(fullOutputPath);
|
|
1757
|
+
let totalBytes = 0;
|
|
1758
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
1759
|
+
for (const filePath of filePaths) {
|
|
1760
|
+
const content = readFileSync5(filePath);
|
|
1761
|
+
const hash = createHash("sha256").update(content).digest("hex");
|
|
1762
|
+
const urlPath = "/" + relative(fullOutputPath, filePath).replace(/\\/g, "/");
|
|
1763
|
+
fileMap.set(urlPath, { hash, content });
|
|
1764
|
+
totalBytes += content.length;
|
|
1765
|
+
}
|
|
1766
|
+
const hashToContent = /* @__PURE__ */ new Map();
|
|
1767
|
+
for (const { hash, content } of fileMap.values()) {
|
|
1768
|
+
if (!hashToContent.has(hash)) hashToContent.set(hash, content);
|
|
1769
|
+
}
|
|
1770
|
+
const fileCount = filePaths.length;
|
|
1771
|
+
const manifest = {};
|
|
1772
|
+
for (const [urlPath, { hash }] of fileMap) {
|
|
1773
|
+
manifest[urlPath] = hash;
|
|
1774
|
+
}
|
|
1775
|
+
const deploySpinner = ui.spinner(`Creating preview deployment (branch: ${branch})...`);
|
|
1776
|
+
let jwt;
|
|
1777
|
+
let requiredFiles;
|
|
1778
|
+
let deployment;
|
|
1779
|
+
try {
|
|
1780
|
+
const res = await this.client.post(
|
|
1781
|
+
`/accounts/${this.accountId}/pages/projects/${projectName}/deployments`,
|
|
1782
|
+
{ files: manifest, branch },
|
|
1783
|
+
{ timeout: CF_REQUEST_TIMEOUT_MS }
|
|
1784
|
+
);
|
|
1785
|
+
if (!res.data.success) {
|
|
1786
|
+
deploySpinner.fail("Preview deployment creation failed");
|
|
1787
|
+
throw new DeployError(res.data.errors[0]?.message ?? "Preview deployment failed");
|
|
1788
|
+
}
|
|
1789
|
+
jwt = res.data.result.jwt;
|
|
1790
|
+
requiredFiles = res.data.result.required_files ?? [];
|
|
1791
|
+
deployment = res.data.result;
|
|
1792
|
+
deploySpinner.succeed("Preview deployment created");
|
|
1793
|
+
} catch (err) {
|
|
1794
|
+
deploySpinner.fail("Preview deployment creation failed");
|
|
1795
|
+
throw new DeployError(`Preview deployment failed: ${extractCFError(err)}`, { cause: err });
|
|
1796
|
+
}
|
|
1797
|
+
if (requiredFiles.length > 0) {
|
|
1798
|
+
const uploadSpinner = ui.spinner(
|
|
1799
|
+
`Uploading ${requiredFiles.length} files (${ui.formatBytes(totalBytes)})`
|
|
1800
|
+
);
|
|
1801
|
+
let uploaded = 0;
|
|
1802
|
+
for (const hash of requiredFiles) {
|
|
1803
|
+
const content = hashToContent.get(hash);
|
|
1804
|
+
if (!content) continue;
|
|
1805
|
+
try {
|
|
1806
|
+
const formData = new FormData();
|
|
1807
|
+
formData.append(hash, new Blob([content], { type: "application/octet-stream" }), hash);
|
|
1808
|
+
await fetch(CF_UPLOAD_ENDPOINT, {
|
|
1809
|
+
method: "POST",
|
|
1810
|
+
headers: { Authorization: `Bearer ${jwt}` },
|
|
1811
|
+
body: formData
|
|
1812
|
+
}).then(async (res) => {
|
|
1813
|
+
if (!res.ok) {
|
|
1814
|
+
const text = await res.text().catch(() => res.statusText);
|
|
1815
|
+
throw new DeployError(`Upload failed (${res.status}): ${text}`);
|
|
1816
|
+
}
|
|
1817
|
+
});
|
|
1818
|
+
} catch (err) {
|
|
1819
|
+
uploadSpinner.fail("Upload failed");
|
|
1820
|
+
throw new DeployError(`Failed to upload file (hash ${hash}): ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1821
|
+
}
|
|
1822
|
+
uploaded++;
|
|
1823
|
+
uploadSpinner.text = `Uploading files... ${uploaded}/${requiredFiles.length}`;
|
|
1824
|
+
}
|
|
1825
|
+
uploadSpinner.succeed(`Upload complete \u2014 ${fileCount} files (${ui.formatBytes(totalBytes)})`);
|
|
1826
|
+
}
|
|
1827
|
+
ui.success("Preview deployed!");
|
|
1828
|
+
return { deployment, fileCount, totalBytes };
|
|
1829
|
+
}
|
|
1830
|
+
async waitForDeployment(projectName, deploymentId, timeoutMs = DEPLOY_TIMEOUT_MS) {
|
|
1831
|
+
const deploySpinner = ui.spinner("Deploying to Cloudflare Pages");
|
|
1832
|
+
const start = Date.now();
|
|
1833
|
+
while (Date.now() - start < timeoutMs) {
|
|
1834
|
+
await sleep(DEPLOY_POLL_INTERVAL_MS);
|
|
1835
|
+
try {
|
|
1836
|
+
const res = await this.client.get(
|
|
1837
|
+
`/accounts/${this.accountId}/pages/projects/${projectName}/deployments/${deploymentId}`
|
|
1838
|
+
);
|
|
1839
|
+
if (!res.data.success) continue;
|
|
1502
1840
|
const deployment = res.data.result;
|
|
1503
1841
|
const stage = deployment.latest_stage;
|
|
1504
1842
|
deploySpinner.text = `Deploying... (${stage?.name ?? "initializing"})`;
|
|
@@ -1508,18 +1846,18 @@ var CloudflarePages = class {
|
|
|
1508
1846
|
}
|
|
1509
1847
|
if (stage?.status === "failure") {
|
|
1510
1848
|
deploySpinner.fail("Deployment failed");
|
|
1511
|
-
throw new
|
|
1849
|
+
throw new DeployError(
|
|
1512
1850
|
`Deployment failed at stage '${stage.name}'. Check the Cloudflare Pages dashboard for details.`
|
|
1513
1851
|
);
|
|
1514
1852
|
}
|
|
1515
1853
|
} catch (err) {
|
|
1516
|
-
if (err instanceof
|
|
1854
|
+
if (err instanceof DeployError) {
|
|
1517
1855
|
throw err;
|
|
1518
1856
|
}
|
|
1519
1857
|
}
|
|
1520
1858
|
}
|
|
1521
1859
|
deploySpinner.fail("Deployment timed out");
|
|
1522
|
-
throw new
|
|
1860
|
+
throw new DeployError("Deployment timed out. Check the Cloudflare Pages dashboard for status.");
|
|
1523
1861
|
}
|
|
1524
1862
|
async getDeploymentStatus(projectName, deploymentId) {
|
|
1525
1863
|
try {
|
|
@@ -1548,7 +1886,7 @@ var CloudflarePages = class {
|
|
|
1548
1886
|
`/accounts/${this.accountId}/pages/projects/${projectName}`
|
|
1549
1887
|
);
|
|
1550
1888
|
} catch (err) {
|
|
1551
|
-
throw new
|
|
1889
|
+
throw new DeployError(`Failed to delete project: ${extractCFError(err)}`, { cause: err });
|
|
1552
1890
|
}
|
|
1553
1891
|
}
|
|
1554
1892
|
getProjectUrl(projectName) {
|
|
@@ -1572,9 +1910,9 @@ var CloudflarePages = class {
|
|
|
1572
1910
|
function collectFiles(dir) {
|
|
1573
1911
|
const results = [];
|
|
1574
1912
|
function walk(currentDir) {
|
|
1575
|
-
const entries =
|
|
1913
|
+
const entries = readdirSync3(currentDir);
|
|
1576
1914
|
for (const entry of entries) {
|
|
1577
|
-
const fullPath =
|
|
1915
|
+
const fullPath = join6(currentDir, entry);
|
|
1578
1916
|
if (shouldExclude(fullPath)) continue;
|
|
1579
1917
|
if (entry.startsWith(".") && entry !== ".well-known") continue;
|
|
1580
1918
|
const stat = statSync2(fullPath);
|
|
@@ -1601,7 +1939,35 @@ function sleep(ms) {
|
|
|
1601
1939
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1602
1940
|
}
|
|
1603
1941
|
function sanitizeProjectName(name) {
|
|
1604
|
-
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0,
|
|
1942
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, CF_PROJECT_NAME_MAX_LENGTH);
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// src/deploy/badge.ts
|
|
1946
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
1947
|
+
import { join as join7 } from "path";
|
|
1948
|
+
var BADGE_HTML = `<!-- Shipped with Shipem -->
|
|
1949
|
+
<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>`;
|
|
1950
|
+
function injectBadge(outputDir) {
|
|
1951
|
+
let injected = 0;
|
|
1952
|
+
try {
|
|
1953
|
+
const files = readdirSync4(outputDir).filter((f) => f.endsWith(".html"));
|
|
1954
|
+
for (const file of files) {
|
|
1955
|
+
const filePath = join7(outputDir, file);
|
|
1956
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
1957
|
+
if (content.includes("shipem-badge")) continue;
|
|
1958
|
+
if (content.includes("</body>")) {
|
|
1959
|
+
writeFileSync3(filePath, content.replace("</body>", `${BADGE_HTML}
|
|
1960
|
+
</body>`), "utf-8");
|
|
1961
|
+
injected++;
|
|
1962
|
+
} else if (content.includes("</html>")) {
|
|
1963
|
+
writeFileSync3(filePath, content.replace("</html>", `${BADGE_HTML}
|
|
1964
|
+
</html>`), "utf-8");
|
|
1965
|
+
injected++;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
} catch {
|
|
1969
|
+
}
|
|
1970
|
+
return injected;
|
|
1605
1971
|
}
|
|
1606
1972
|
|
|
1607
1973
|
// src/commands/deploy.ts
|
|
@@ -1619,6 +1985,10 @@ var ENV_VAR_HELP = {
|
|
|
1619
1985
|
STRIPE_PUBLISHABLE_KEY: { desc: "Stripe publishable key", url: "https://dashboard.stripe.com/apikeys" },
|
|
1620
1986
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: { desc: "Stripe publishable key (public)", url: "https://dashboard.stripe.com/apikeys" }
|
|
1621
1987
|
};
|
|
1988
|
+
var SOURCE_FILE_SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".next", "dist", "build", "out", ".turbo", "coverage"]);
|
|
1989
|
+
var DEPLOY_TIMEOUT_MS2 = 5 * 60 * 1e3;
|
|
1990
|
+
var LOW_CONFIDENCE_THRESHOLD = 0.7;
|
|
1991
|
+
var NOT_A_PROJECT_THRESHOLD = 0.4;
|
|
1622
1992
|
async function apiRequest(config) {
|
|
1623
1993
|
try {
|
|
1624
1994
|
return await axios2(config);
|
|
@@ -1626,26 +1996,25 @@ async function apiRequest(config) {
|
|
|
1626
1996
|
if (axios2.isAxiosError(err)) {
|
|
1627
1997
|
const code = err.code;
|
|
1628
1998
|
if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT") {
|
|
1629
|
-
throw new
|
|
1999
|
+
throw new NetworkError("Cannot reach Shipem servers. Check your internet connection and try again.", { cause: err });
|
|
1630
2000
|
}
|
|
1631
2001
|
const status = err.response?.status;
|
|
1632
2002
|
if (status === 401) throw err;
|
|
1633
|
-
if (status === 500) throw new
|
|
1634
|
-
if (status === 429) throw new
|
|
1635
|
-
if (status === 413) throw new
|
|
2003
|
+
if (status === 500) throw new DeployError("Shipem servers are having issues. Try again in a moment.", { cause: err });
|
|
2004
|
+
if (status === 429) throw new DeployError("Too many requests. Please wait before deploying again.", { cause: err });
|
|
2005
|
+
if (status === 413) throw new DeployError("Project is too large to deploy (max 100 MB).", { cause: err });
|
|
1636
2006
|
}
|
|
1637
2007
|
throw err;
|
|
1638
2008
|
}
|
|
1639
2009
|
}
|
|
1640
2010
|
function countSourceFiles(dir) {
|
|
1641
|
-
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".next", "dist", "build", "out", ".turbo", "coverage"]);
|
|
1642
2011
|
let count = 0;
|
|
1643
2012
|
function walk(d) {
|
|
1644
2013
|
try {
|
|
1645
|
-
const entries =
|
|
2014
|
+
const entries = readdirSync5(d, { withFileTypes: true });
|
|
1646
2015
|
for (const entry of entries) {
|
|
1647
|
-
if (
|
|
1648
|
-
if (entry.isDirectory()) walk(
|
|
2016
|
+
if (SOURCE_FILE_SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
2017
|
+
if (entry.isDirectory()) walk(join8(d, entry.name));
|
|
1649
2018
|
else count++;
|
|
1650
2019
|
}
|
|
1651
2020
|
} catch {
|
|
@@ -1659,9 +2028,9 @@ function countOutputFiles(dir) {
|
|
|
1659
2028
|
let totalBytes = 0;
|
|
1660
2029
|
function walk(d) {
|
|
1661
2030
|
try {
|
|
1662
|
-
const entries =
|
|
2031
|
+
const entries = readdirSync5(d, { withFileTypes: true });
|
|
1663
2032
|
for (const entry of entries) {
|
|
1664
|
-
const fullPath =
|
|
2033
|
+
const fullPath = join8(d, entry.name);
|
|
1665
2034
|
if (entry.isDirectory()) walk(fullPath);
|
|
1666
2035
|
else {
|
|
1667
2036
|
fileCount++;
|
|
@@ -1677,8 +2046,55 @@ function countOutputFiles(dir) {
|
|
|
1677
2046
|
walk(dir);
|
|
1678
2047
|
return { fileCount, totalBytes };
|
|
1679
2048
|
}
|
|
2049
|
+
function showBuildErrorHelp(errMsg) {
|
|
2050
|
+
const moduleMatch = errMsg.match(/Cannot find module ['"]([^'"]+)['"]/);
|
|
2051
|
+
const missingDepMatch = errMsg.match(/Module not found.*['"]([^'"]+)['"]/);
|
|
2052
|
+
const tsErrorMatch = errMsg.match(/TS\d+:/);
|
|
2053
|
+
const syntaxMatch = errMsg.match(/SyntaxError/);
|
|
2054
|
+
const envMatch = errMsg.match(/process\.env\.([A-Z_][A-Z0-9_]*)/);
|
|
2055
|
+
if (moduleMatch || missingDepMatch) {
|
|
2056
|
+
const modName = moduleMatch?.[1] ?? missingDepMatch?.[1] ?? "unknown";
|
|
2057
|
+
const pkgName = modName.startsWith("@") ? modName.split("/").slice(0, 2).join("/") : modName.split("/")[0];
|
|
2058
|
+
ui.friendlyError(
|
|
2059
|
+
"Missing dependency",
|
|
2060
|
+
`Module '${modName}' not found`,
|
|
2061
|
+
`Run: npm install ${pkgName}`,
|
|
2062
|
+
"AI tools sometimes reference packages that aren't installed. Install the missing package and try again."
|
|
2063
|
+
);
|
|
2064
|
+
} else if (tsErrorMatch) {
|
|
2065
|
+
ui.friendlyError(
|
|
2066
|
+
"TypeScript error",
|
|
2067
|
+
errMsg.split("\n")[0] ?? errMsg,
|
|
2068
|
+
"Run: npx tsc --noEmit to see all type errors",
|
|
2069
|
+
"AI-generated code sometimes has type errors. Check the error above and fix the types."
|
|
2070
|
+
);
|
|
2071
|
+
} else if (syntaxMatch) {
|
|
2072
|
+
ui.friendlyError(
|
|
2073
|
+
"Syntax error in your code",
|
|
2074
|
+
errMsg.split("\n")[0] ?? errMsg,
|
|
2075
|
+
"Check the file mentioned above for syntax issues",
|
|
2076
|
+
"AI tools occasionally generate invalid syntax. Look for missing brackets, commas, or quotes."
|
|
2077
|
+
);
|
|
2078
|
+
} else if (envMatch) {
|
|
2079
|
+
ui.friendlyError(
|
|
2080
|
+
"Missing environment variable",
|
|
2081
|
+
`Your code references ${envMatch[1]} but it's not set`,
|
|
2082
|
+
`Set it: export ${envMatch[1]}=your_value`,
|
|
2083
|
+
"Or add it to a .env file in your project root."
|
|
2084
|
+
);
|
|
2085
|
+
} else {
|
|
2086
|
+
ui.friendlyError(
|
|
2087
|
+
"Your app failed to build",
|
|
2088
|
+
errMsg.split("\n")[0] ?? errMsg,
|
|
2089
|
+
"Fix the error above, then run `npx ship-em` again",
|
|
2090
|
+
"Tip: If your AI tool generated this code, paste the error back to it for a fix."
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
1680
2094
|
async function deployCommand(options) {
|
|
1681
2095
|
const startTime = Date.now();
|
|
2096
|
+
const phases = [];
|
|
2097
|
+
let phaseStart = Date.now();
|
|
1682
2098
|
const cwd = process.cwd();
|
|
1683
2099
|
const existingConfig = readProjectConfig(cwd);
|
|
1684
2100
|
const isRedeploy = !!existingConfig.project && !!existingConfig.deployment;
|
|
@@ -1692,27 +2108,47 @@ async function deployCommand(options) {
|
|
|
1692
2108
|
} else {
|
|
1693
2109
|
ui.banner();
|
|
1694
2110
|
}
|
|
2111
|
+
const isAnonymous = !getSessionToken() && !getCloudflareCredentials();
|
|
2112
|
+
let sessionToken = getSessionToken();
|
|
1695
2113
|
if (isFirstRun) {
|
|
1696
2114
|
console.log(` ${chalk4.white.bold("Welcome to Shipem!")} \u2728`);
|
|
1697
2115
|
console.log("");
|
|
1698
|
-
console.log(` ${chalk4.dim("One-time setup (takes 30 seconds):")}`);
|
|
1699
|
-
console.log(` ${chalk4.dim("1. Sign in with GitHub")}`);
|
|
1700
|
-
console.log(` ${chalk4.dim("2. We handle everything else")}`);
|
|
1701
|
-
console.log("");
|
|
1702
2116
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
2117
|
+
if (isAnonymous) {
|
|
2118
|
+
console.log(` ${chalk4.dim("To deploy, you need either Cloudflare credentials or a Shipem login.")}`);
|
|
2119
|
+
console.log("");
|
|
2120
|
+
console.log(` ${chalk4.cyan("Option 1:")} Set Cloudflare credentials (free account)`);
|
|
2121
|
+
console.log(` ${chalk4.dim("export CLOUDFLARE_API_TOKEN=your_token")}`);
|
|
2122
|
+
console.log(` ${chalk4.dim("export CLOUDFLARE_ACCOUNT_ID=your_account_id")}`);
|
|
2123
|
+
console.log(` ${chalk4.dim("Get these from: https://dash.cloudflare.com/profile/api-tokens")}`);
|
|
2124
|
+
console.log("");
|
|
2125
|
+
console.log(` ${chalk4.cyan("Option 2:")} Log in to Shipem`);
|
|
2126
|
+
console.log(` ${chalk4.dim("npx ship-em login")}`);
|
|
2127
|
+
console.log("");
|
|
2128
|
+
const { action } = await inquirer.prompt([
|
|
2129
|
+
{
|
|
2130
|
+
type: "list",
|
|
2131
|
+
name: "action",
|
|
2132
|
+
message: "What would you like to do?",
|
|
2133
|
+
choices: [
|
|
2134
|
+
{ name: "Log in with GitHub (sets up deployment)", value: "login" },
|
|
2135
|
+
{ name: "Quit (I'll set up Cloudflare credentials)", value: "quit" }
|
|
2136
|
+
]
|
|
2137
|
+
}
|
|
2138
|
+
]);
|
|
2139
|
+
if (action === "quit") {
|
|
2140
|
+
process.exit(0);
|
|
2141
|
+
}
|
|
1705
2142
|
await loginCommand({ skipBanner: true });
|
|
1706
2143
|
sessionToken = getSessionToken();
|
|
1707
2144
|
if (!sessionToken) {
|
|
1708
|
-
|
|
2145
|
+
throw new AuthError("Login failed. Please try again with: npx ship-em login");
|
|
1709
2146
|
}
|
|
1710
2147
|
console.log("");
|
|
1711
|
-
console.log(` ${chalk4.hex("#22C55E")("\u2713")} ${chalk4.bold("Signed in!")}`);
|
|
1712
|
-
console.log("");
|
|
1713
|
-
console.log(` Now deploying your app...`);
|
|
2148
|
+
console.log(` ${chalk4.hex("#22C55E")("\u2713")} ${chalk4.bold("Signed in! Continuing deploy...")}`);
|
|
1714
2149
|
console.log("");
|
|
1715
2150
|
}
|
|
2151
|
+
phaseStart = Date.now();
|
|
1716
2152
|
ui.section("Scanning project...");
|
|
1717
2153
|
ui.br();
|
|
1718
2154
|
let projectConfig;
|
|
@@ -1721,41 +2157,40 @@ async function deployCommand(options) {
|
|
|
1721
2157
|
ui.success(`Using saved config: ${ui.framework(projectConfig.framework)}`);
|
|
1722
2158
|
} else {
|
|
1723
2159
|
const scanSpinner = ui.spinner("Scanning project files");
|
|
1724
|
-
|
|
2160
|
+
let heuristicResult = scanProject(cwd);
|
|
1725
2161
|
scanSpinner.succeed("Project files scanned");
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
const
|
|
1746
|
-
const
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
2162
|
+
if (heuristicResult.monorepo && heuristicResult.monorepo.packages.length > 0) {
|
|
2163
|
+
const mono = heuristicResult.monorepo;
|
|
2164
|
+
ui.info(`Monorepo detected (${mono.type}) \u2014 ${mono.packages.length} packages found`);
|
|
2165
|
+
ui.br();
|
|
2166
|
+
let packageName = options.package;
|
|
2167
|
+
if (!packageName && !options.yes) {
|
|
2168
|
+
const { selected } = await inquirer.prompt([
|
|
2169
|
+
{
|
|
2170
|
+
type: "list",
|
|
2171
|
+
name: "selected",
|
|
2172
|
+
message: "Which package to deploy?",
|
|
2173
|
+
choices: mono.packages.map((p) => ({ name: p, value: p }))
|
|
2174
|
+
}
|
|
2175
|
+
]);
|
|
2176
|
+
packageName = selected;
|
|
2177
|
+
} else if (!packageName) {
|
|
2178
|
+
packageName = mono.packages[0];
|
|
2179
|
+
}
|
|
2180
|
+
if (packageName) {
|
|
2181
|
+
const commonDirs = ["packages", "apps", "projects", "services"];
|
|
2182
|
+
for (const dir of commonDirs) {
|
|
2183
|
+
const pkgDir = join8(cwd, dir, packageName);
|
|
2184
|
+
if (existsSync6(pkgDir)) {
|
|
2185
|
+
ui.info(`Deploying package: ${chalk4.cyan(packageName)} (${dir}/${packageName})`);
|
|
2186
|
+
heuristicResult = scanProject(pkgDir);
|
|
2187
|
+
break;
|
|
2188
|
+
}
|
|
1751
2189
|
}
|
|
1752
|
-
} catch {
|
|
1753
|
-
aiSpinner.warn("AI analysis failed, using heuristic detection");
|
|
1754
|
-
finalDetection = heuristicAsAnalysis;
|
|
1755
2190
|
}
|
|
1756
|
-
} else {
|
|
1757
|
-
finalDetection = heuristicAsAnalysis;
|
|
1758
2191
|
}
|
|
2192
|
+
const sourceFileCount = countSourceFiles(cwd);
|
|
2193
|
+
const finalDetection = heuristicResult;
|
|
1759
2194
|
const projectName = options.name ?? heuristicResult.projectName;
|
|
1760
2195
|
ui.projectAnalysis({
|
|
1761
2196
|
name: projectName,
|
|
@@ -1766,11 +2201,11 @@ async function deployCommand(options) {
|
|
|
1766
2201
|
deployTarget: finalDetection.deployTarget,
|
|
1767
2202
|
envVarCount: finalDetection.envVars.length
|
|
1768
2203
|
});
|
|
1769
|
-
if (finalDetection.confidence <
|
|
2204
|
+
if (finalDetection.confidence < LOW_CONFIDENCE_THRESHOLD && !options.yes) {
|
|
1770
2205
|
ui.warn(`Low confidence detection (${Math.round(finalDetection.confidence * 100)}%). Please verify the settings.`);
|
|
1771
2206
|
ui.br();
|
|
1772
2207
|
}
|
|
1773
|
-
if (finalDetection.confidence <
|
|
2208
|
+
if (finalDetection.confidence < NOT_A_PROJECT_THRESHOLD && !existsSync6(join8(cwd, "package.json")) && !existsSync6(join8(cwd, "requirements.txt")) && !existsSync6(join8(cwd, "index.html"))) {
|
|
1774
2209
|
ui.friendlyError(
|
|
1775
2210
|
"This does not look like a project",
|
|
1776
2211
|
"No package.json, requirements.txt, or index.html found",
|
|
@@ -1858,66 +2293,80 @@ async function deployCommand(options) {
|
|
|
1858
2293
|
}
|
|
1859
2294
|
}
|
|
1860
2295
|
}
|
|
1861
|
-
|
|
2296
|
+
phases.push({ name: "Scan", durationMs: Date.now() - phaseStart });
|
|
2297
|
+
if (!options.skipBuild && !options.turbo) {
|
|
2298
|
+
phaseStart = Date.now();
|
|
1862
2299
|
ui.section("Building...");
|
|
1863
2300
|
ui.br();
|
|
1864
2301
|
let buildResult = await buildProject(projectConfig, cwd);
|
|
1865
2302
|
if (!buildResult.success) {
|
|
1866
2303
|
ui.br();
|
|
1867
2304
|
const errMsg = buildResult.error ?? "Unknown build error";
|
|
1868
|
-
|
|
1869
|
-
if (moduleMatch) {
|
|
1870
|
-
ui.friendlyError(
|
|
1871
|
-
"Your app failed to build",
|
|
1872
|
-
`Module '${moduleMatch[1]}' not found`,
|
|
1873
|
-
'Run "npm install" in your project, then try again'
|
|
1874
|
-
);
|
|
1875
|
-
} else {
|
|
1876
|
-
ui.friendlyError(
|
|
1877
|
-
"Your app failed to build",
|
|
1878
|
-
errMsg.split("\n")[0] ?? errMsg,
|
|
1879
|
-
"Fix the error above, then run `npx shipem` again"
|
|
1880
|
-
);
|
|
1881
|
-
}
|
|
2305
|
+
showBuildErrorHelp(errMsg);
|
|
1882
2306
|
if (options.yes) {
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
message: "Build failed. What would you like to do?",
|
|
1890
|
-
choices: [
|
|
1891
|
-
{ name: "Try again", value: "retry" },
|
|
1892
|
-
{ name: "Deploy without building (use existing output directory)", value: "skip" },
|
|
1893
|
-
{ name: "Quit", value: "quit" }
|
|
1894
|
-
]
|
|
2307
|
+
const fixResult = await tryInlineFix(errMsg, cwd, projectConfig);
|
|
2308
|
+
if (fixResult?.success) {
|
|
2309
|
+
ui.success("Fixed! Continuing deploy...");
|
|
2310
|
+
ui.br();
|
|
2311
|
+
} else {
|
|
2312
|
+
process.exit(1);
|
|
1895
2313
|
}
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
2314
|
+
} else {
|
|
2315
|
+
const { action } = await inquirer.prompt([
|
|
2316
|
+
{
|
|
2317
|
+
type: "list",
|
|
2318
|
+
name: "action",
|
|
2319
|
+
message: "Build failed. What would you like to do?",
|
|
2320
|
+
choices: [
|
|
2321
|
+
{ name: "Auto-fix and retry (recommended)", value: "fix" },
|
|
2322
|
+
{ name: "Try again without fixing", value: "retry" },
|
|
2323
|
+
{ name: "Deploy without building (use existing output directory)", value: "skip" },
|
|
2324
|
+
{ name: "Quit", value: "quit" }
|
|
2325
|
+
]
|
|
2326
|
+
}
|
|
2327
|
+
]);
|
|
2328
|
+
if (action === "quit") {
|
|
1907
2329
|
ui.br();
|
|
1908
|
-
ui.friendlyError(
|
|
1909
|
-
"Your app failed to build again",
|
|
1910
|
-
buildResult.error?.split("\n")[0] ?? "Unknown build error",
|
|
1911
|
-
"Fix the error above, then run `npx shipem` again"
|
|
1912
|
-
);
|
|
1913
2330
|
process.exit(1);
|
|
1914
2331
|
}
|
|
2332
|
+
if (action === "fix") {
|
|
2333
|
+
const fixResult = await tryInlineFix(errMsg, cwd, projectConfig);
|
|
2334
|
+
if (fixResult?.success) {
|
|
2335
|
+
ui.success("Fixed! Continuing deploy...");
|
|
2336
|
+
ui.br();
|
|
2337
|
+
} else {
|
|
2338
|
+
ui.br();
|
|
2339
|
+
ui.dim("Tip: Paste this error into your AI coding tool \u2014 it can usually fix it.");
|
|
2340
|
+
process.exit(1);
|
|
2341
|
+
}
|
|
2342
|
+
} else if (action === "retry") {
|
|
2343
|
+
ui.br();
|
|
2344
|
+
ui.section("Building (retry)...");
|
|
2345
|
+
ui.br();
|
|
2346
|
+
buildResult = await buildProject(projectConfig, cwd);
|
|
2347
|
+
if (!buildResult.success) {
|
|
2348
|
+
ui.br();
|
|
2349
|
+
showBuildErrorHelp(buildResult.error ?? "Unknown build error");
|
|
2350
|
+
ui.dim("Tip: Paste this error into your AI coding tool \u2014 it can usually fix it.");
|
|
2351
|
+
process.exit(1);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
1915
2354
|
}
|
|
1916
2355
|
ui.br();
|
|
1917
2356
|
} else {
|
|
1918
2357
|
ui.br();
|
|
1919
2358
|
}
|
|
2359
|
+
phases.push({ name: "Build", durationMs: Date.now() - phaseStart });
|
|
2360
|
+
}
|
|
2361
|
+
if (isAnonymous) {
|
|
2362
|
+
const outputPath = join8(cwd, projectConfig.outputDirectory);
|
|
2363
|
+
const badgeCount = injectBadge(outputPath);
|
|
2364
|
+
if (badgeCount > 0) {
|
|
2365
|
+
ui.dim(`Added "Shipped with Shipem" badge to ${badgeCount} HTML file${badgeCount > 1 ? "s" : ""}`);
|
|
2366
|
+
ui.br();
|
|
2367
|
+
}
|
|
1920
2368
|
}
|
|
2369
|
+
phaseStart = Date.now();
|
|
1921
2370
|
ui.section("Deploying...");
|
|
1922
2371
|
ui.br();
|
|
1923
2372
|
let liveUrl;
|
|
@@ -1928,12 +2377,9 @@ async function deployCommand(options) {
|
|
|
1928
2377
|
const useDirectDeploy = options.direct === true || cfCreds !== null;
|
|
1929
2378
|
if (useDirectDeploy) {
|
|
1930
2379
|
if (!cfCreds) {
|
|
1931
|
-
|
|
1932
|
-
"Direct deploy requires Cloudflare credentials"
|
|
1933
|
-
"CLOUDFLARE_API_TOKEN or CLOUDFLARE_ACCOUNT_ID is not set",
|
|
1934
|
-
"Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables, then try again"
|
|
2380
|
+
throw new ConfigError(
|
|
2381
|
+
"Direct deploy requires Cloudflare credentials. Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables."
|
|
1935
2382
|
);
|
|
1936
|
-
process.exit(1);
|
|
1937
2383
|
}
|
|
1938
2384
|
ui.dim("Direct Cloudflare deployment (using your credentials)...");
|
|
1939
2385
|
ui.br();
|
|
@@ -1951,18 +2397,13 @@ async function deployCommand(options) {
|
|
|
1951
2397
|
liveUrl = finalDeployment.url || cf.getProjectUrl(cfProjectName);
|
|
1952
2398
|
projectId = cfProjectName;
|
|
1953
2399
|
} catch (err) {
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
"Deployment failed",
|
|
1957
|
-
msg,
|
|
1958
|
-
"Check your Cloudflare credentials and try again"
|
|
1959
|
-
);
|
|
1960
|
-
process.exit(1);
|
|
2400
|
+
if (err instanceof DeployError) throw err;
|
|
2401
|
+
throw new DeployError(err instanceof Error ? err.message : String(err), { cause: err });
|
|
1961
2402
|
}
|
|
1962
|
-
} else {
|
|
1963
|
-
const tarPath =
|
|
2403
|
+
} else if (sessionToken) {
|
|
2404
|
+
const tarPath = join8(tmpdir(), `shipem-${Date.now()}.tar.gz`);
|
|
1964
2405
|
try {
|
|
1965
|
-
const outputPath =
|
|
2406
|
+
const outputPath = join8(cwd, projectConfig.outputDirectory);
|
|
1966
2407
|
warnIfEnvFilesInOutputDir(outputPath);
|
|
1967
2408
|
const stats = countOutputFiles(outputPath);
|
|
1968
2409
|
deployFileCount = stats.fileCount;
|
|
@@ -2003,7 +2444,7 @@ async function deployCommand(options) {
|
|
|
2003
2444
|
Authorization: `Bearer ${sessionToken}`
|
|
2004
2445
|
},
|
|
2005
2446
|
maxBodyLength: Infinity,
|
|
2006
|
-
timeout:
|
|
2447
|
+
timeout: DEPLOY_TIMEOUT_MS2,
|
|
2007
2448
|
onUploadProgress: (evt) => {
|
|
2008
2449
|
if (!activatingSpinner && evt.total && evt.loaded >= evt.total) {
|
|
2009
2450
|
const uploadSec = ((Date.now() - uploadStart) / 1e3).toFixed(1);
|
|
@@ -2025,7 +2466,7 @@ async function deployCommand(options) {
|
|
|
2025
2466
|
await loginCommand({ skipBanner: true });
|
|
2026
2467
|
sessionToken = getSessionToken();
|
|
2027
2468
|
if (!sessionToken) {
|
|
2028
|
-
throw new
|
|
2469
|
+
throw new AuthError("Login failed after session expiry. Please run: shipem login");
|
|
2029
2470
|
}
|
|
2030
2471
|
const retryForm = new FormData2();
|
|
2031
2472
|
retryForm.append("config", JSON.stringify({
|
|
@@ -2047,7 +2488,7 @@ async function deployCommand(options) {
|
|
|
2047
2488
|
response = await apiRequest(deployConfig);
|
|
2048
2489
|
} catch (retryErr) {
|
|
2049
2490
|
if (axios2.isAxiosError(retryErr) && retryErr.response?.status === 401) {
|
|
2050
|
-
throw new
|
|
2491
|
+
throw new AuthError("Authentication failed after re-login. Please run: shipem login");
|
|
2051
2492
|
}
|
|
2052
2493
|
throw retryErr;
|
|
2053
2494
|
}
|
|
@@ -2063,28 +2504,22 @@ async function deployCommand(options) {
|
|
|
2063
2504
|
liveUrl = response.data.url;
|
|
2064
2505
|
projectId = response.data.projectId;
|
|
2065
2506
|
} catch (err) {
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
ui.friendlyError(
|
|
2069
|
-
"Cannot reach Shipem servers",
|
|
2070
|
-
"Network connection failed",
|
|
2071
|
-
"Check your internet and try again"
|
|
2072
|
-
);
|
|
2073
|
-
} else {
|
|
2074
|
-
ui.friendlyError(
|
|
2075
|
-
"Deployment failed",
|
|
2076
|
-
msg,
|
|
2077
|
-
"Check your connection and try again with: npx shipem"
|
|
2078
|
-
);
|
|
2507
|
+
if (err instanceof AuthError || err instanceof NetworkError || err instanceof DeployError) {
|
|
2508
|
+
throw err;
|
|
2079
2509
|
}
|
|
2080
|
-
|
|
2510
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2511
|
+
throw new DeployError(`Deployment failed: ${msg}`, { cause: err });
|
|
2081
2512
|
} finally {
|
|
2082
2513
|
try {
|
|
2083
2514
|
rmSync(tarPath);
|
|
2084
2515
|
} catch {
|
|
2085
2516
|
}
|
|
2086
2517
|
}
|
|
2518
|
+
} else {
|
|
2519
|
+
throw new AuthError("No Cloudflare credentials found and not logged in. Run: npx ship-em login");
|
|
2087
2520
|
}
|
|
2521
|
+
phases.push({ name: "Upload", durationMs: Date.now() - phaseStart });
|
|
2522
|
+
const elapsedSec = (Date.now() - startTime) / 1e3;
|
|
2088
2523
|
const configToSave = {
|
|
2089
2524
|
...projectConfig,
|
|
2090
2525
|
envVars: projectConfig.envVars.map(({ value: _stripped, ...rest }) => rest)
|
|
@@ -2097,119 +2532,801 @@ async function deployCommand(options) {
|
|
|
2097
2532
|
deployedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2098
2533
|
status: "success"
|
|
2099
2534
|
};
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2535
|
+
const deployRecord = {
|
|
2536
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2537
|
+
url: liveUrl,
|
|
2538
|
+
duration: elapsedSec,
|
|
2539
|
+
files: deployFileCount,
|
|
2540
|
+
size: ui.formatBytes(deployTotalBytes),
|
|
2541
|
+
framework: projectConfig.framework
|
|
2542
|
+
};
|
|
2543
|
+
const existingHistory = existingConfig.deployments ?? [];
|
|
2544
|
+
const updatedHistory = [deployRecord, ...existingHistory].slice(0, 20);
|
|
2545
|
+
writeProjectConfig({
|
|
2546
|
+
project: configToSave,
|
|
2547
|
+
deployment: deploymentState,
|
|
2548
|
+
deployments: updatedHistory
|
|
2549
|
+
}, cwd);
|
|
2550
|
+
const gitignorePath = join8(cwd, ".gitignore");
|
|
2551
|
+
if (existsSync6(gitignorePath)) {
|
|
2552
|
+
const gitignoreContent = readFileSync7(gitignorePath, "utf-8");
|
|
2104
2553
|
const lines = gitignoreContent.split("\n").map((l) => l.trim());
|
|
2105
2554
|
if (!lines.includes("shipem.json")) {
|
|
2106
2555
|
appendFileSync(gitignorePath, "\n# Shipem config\nshipem.json\n");
|
|
2107
2556
|
}
|
|
2108
2557
|
} else {
|
|
2109
|
-
|
|
2558
|
+
writeFileSync4(gitignorePath, "# Shipem config\nshipem.json\n");
|
|
2110
2559
|
}
|
|
2111
|
-
|
|
2112
|
-
|
|
2560
|
+
ui.deployBoxEnhanced(
|
|
2561
|
+
projectConfig.name,
|
|
2562
|
+
liveUrl,
|
|
2563
|
+
phases,
|
|
2564
|
+
deployFileCount,
|
|
2565
|
+
deployTotalBytes,
|
|
2566
|
+
elapsedSec,
|
|
2567
|
+
isAnonymous
|
|
2568
|
+
);
|
|
2113
2569
|
}
|
|
2114
2570
|
|
|
2115
|
-
// src/commands/
|
|
2116
|
-
import
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
}
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2571
|
+
// src/commands/env.ts
|
|
2572
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, readdirSync as readdirSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
2573
|
+
import { join as join9 } from "path";
|
|
2574
|
+
var SERVICE_LINKS = {
|
|
2575
|
+
SUPABASE_URL: { name: "Supabase", url: "https://app.supabase.com/project/_/settings/api" },
|
|
2576
|
+
SUPABASE_ANON_KEY: { name: "Supabase", url: "https://app.supabase.com/project/_/settings/api" },
|
|
2577
|
+
NEXT_PUBLIC_SUPABASE_URL: { name: "Supabase", url: "https://app.supabase.com/project/_/settings/api" },
|
|
2578
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY: { name: "Supabase", url: "https://app.supabase.com/project/_/settings/api" },
|
|
2579
|
+
VITE_SUPABASE_URL: { name: "Supabase", url: "https://app.supabase.com/project/_/settings/api" },
|
|
2580
|
+
VITE_SUPABASE_ANON_KEY: { name: "Supabase", url: "https://app.supabase.com/project/_/settings/api" },
|
|
2581
|
+
OPENAI_API_KEY: { name: "OpenAI", url: "https://platform.openai.com/api-keys" },
|
|
2582
|
+
ANTHROPIC_API_KEY: { name: "Anthropic", url: "https://console.anthropic.com/settings/keys" },
|
|
2583
|
+
STRIPE_SECRET_KEY: { name: "Stripe", url: "https://dashboard.stripe.com/apikeys" },
|
|
2584
|
+
STRIPE_PUBLISHABLE_KEY: { name: "Stripe", url: "https://dashboard.stripe.com/apikeys" },
|
|
2585
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: { name: "Stripe", url: "https://dashboard.stripe.com/apikeys" },
|
|
2586
|
+
FIREBASE_API_KEY: { name: "Firebase", url: "https://console.firebase.google.com/project/_/settings/general" },
|
|
2587
|
+
NEXT_PUBLIC_FIREBASE_API_KEY: { name: "Firebase", url: "https://console.firebase.google.com/project/_/settings/general" },
|
|
2588
|
+
FIREBASE_PROJECT_ID: { name: "Firebase", url: "https://console.firebase.google.com/project/_/settings/general" },
|
|
2589
|
+
DATABASE_URL: { name: "Database", url: "" },
|
|
2590
|
+
RESEND_API_KEY: { name: "Resend", url: "https://resend.com/api-keys" },
|
|
2591
|
+
CLERK_SECRET_KEY: { name: "Clerk", url: "https://dashboard.clerk.com/last-active?path=api-keys" },
|
|
2592
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: { name: "Clerk", url: "https://dashboard.clerk.com/last-active?path=api-keys" }
|
|
2593
|
+
};
|
|
2594
|
+
function scanSourceForEnvVars(cwd) {
|
|
2595
|
+
const found = /* @__PURE__ */ new Set();
|
|
2596
|
+
const patterns = [
|
|
2597
|
+
/process\.env\.([A-Z_][A-Z0-9_]*)/g,
|
|
2598
|
+
/import\.meta\.env\.([A-Z_][A-Z0-9_]*)/g,
|
|
2599
|
+
/(?:NEXT_PUBLIC_|VITE_|NUXT_|GATSBY_)([A-Z0-9_]*)/g
|
|
2600
|
+
];
|
|
2601
|
+
function scanDir(dir, depth) {
|
|
2602
|
+
if (depth > 5) return;
|
|
2603
|
+
try {
|
|
2604
|
+
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
2605
|
+
for (const entry of entries) {
|
|
2606
|
+
if (["node_modules", ".git", "dist", ".next", "build", ".svelte-kit", ".output", ".turbo", "coverage"].includes(entry.name)) continue;
|
|
2607
|
+
const fullPath = join9(dir, entry.name);
|
|
2608
|
+
if (entry.isDirectory()) {
|
|
2609
|
+
scanDir(fullPath, depth + 1);
|
|
2610
|
+
} else if (/\.(ts|tsx|js|jsx|mjs|mts|vue|svelte|astro)$/.test(entry.name)) {
|
|
2611
|
+
try {
|
|
2612
|
+
const content = readFileSync8(fullPath, "utf-8");
|
|
2613
|
+
for (const pat of patterns) {
|
|
2614
|
+
pat.lastIndex = 0;
|
|
2615
|
+
let match;
|
|
2616
|
+
while ((match = pat.exec(content)) !== null) {
|
|
2617
|
+
if (pat.source.includes("NEXT_PUBLIC_") && !match[0].includes("process.env") && !match[0].includes("import.meta.env")) {
|
|
2618
|
+
found.add(match[0]);
|
|
2619
|
+
} else {
|
|
2620
|
+
found.add(match[1]);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2156
2623
|
}
|
|
2157
|
-
|
|
2624
|
+
} catch {
|
|
2158
2625
|
}
|
|
2159
|
-
} else {
|
|
2160
|
-
spinner.warn("Could not fetch live status");
|
|
2161
2626
|
}
|
|
2162
|
-
} catch {
|
|
2163
|
-
spinner.warn("Could not fetch live status from Cloudflare");
|
|
2164
2627
|
}
|
|
2628
|
+
} catch {
|
|
2165
2629
|
}
|
|
2166
2630
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2631
|
+
scanDir(cwd, 0);
|
|
2632
|
+
const ignore = /* @__PURE__ */ new Set(["NODE_ENV", "PORT", "HOST", "CI", "HOME", "PWD", "PATH", "TERM", "USER", "SHELL", "LANG", "TZ"]);
|
|
2633
|
+
return [...found].filter((v) => !ignore.has(v)).sort();
|
|
2634
|
+
}
|
|
2635
|
+
function readEnvFile(cwd) {
|
|
2636
|
+
const vars = /* @__PURE__ */ new Set();
|
|
2637
|
+
const envFiles = [".env", ".env.local", ".env.development", ".env.production"];
|
|
2638
|
+
for (const file of envFiles) {
|
|
2639
|
+
const content = readFile2(join9(cwd, file));
|
|
2640
|
+
if (content) {
|
|
2641
|
+
for (const line of content.split("\n")) {
|
|
2642
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
|
|
2643
|
+
if (match) vars.add(match[1]);
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2173
2646
|
}
|
|
2174
|
-
|
|
2647
|
+
return vars;
|
|
2175
2648
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2649
|
+
function readFile2(path) {
|
|
2650
|
+
try {
|
|
2651
|
+
return readFileSync8(path, "utf-8");
|
|
2652
|
+
} catch {
|
|
2653
|
+
return null;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
async function envCommand() {
|
|
2180
2657
|
const cwd = process.cwd();
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2658
|
+
console.log("");
|
|
2659
|
+
console.log(` ${brand.blue.bold("\u{1F511} Environment Variable Intelligence")}`);
|
|
2660
|
+
console.log("");
|
|
2661
|
+
const spinner = ui.spinner("Scanning source code for environment variables...");
|
|
2662
|
+
const sourceVars = scanSourceForEnvVars(cwd);
|
|
2663
|
+
const envFileVars = readEnvFile(cwd);
|
|
2664
|
+
spinner.succeed(`Found ${sourceVars.length} env vars referenced in code`);
|
|
2665
|
+
console.log("");
|
|
2666
|
+
if (sourceVars.length === 0) {
|
|
2667
|
+
ui.success("No environment variables detected in source code.");
|
|
2668
|
+
ui.br();
|
|
2669
|
+
return;
|
|
2186
2670
|
}
|
|
2187
|
-
const
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
process.
|
|
2671
|
+
const present = [];
|
|
2672
|
+
const missing = [];
|
|
2673
|
+
const inEnv = [];
|
|
2674
|
+
for (const v of sourceVars) {
|
|
2675
|
+
if (process.env[v]) {
|
|
2676
|
+
inEnv.push(v);
|
|
2677
|
+
} else if (envFileVars.has(v)) {
|
|
2678
|
+
present.push(v);
|
|
2679
|
+
} else {
|
|
2680
|
+
missing.push(v);
|
|
2681
|
+
}
|
|
2192
2682
|
}
|
|
2193
|
-
if (
|
|
2194
|
-
|
|
2683
|
+
if (present.length > 0) {
|
|
2684
|
+
console.log(` ${brand.green("\u2713")} ${brand.bold("Set in .env file:")}`);
|
|
2685
|
+
for (const v of present) {
|
|
2686
|
+
console.log(` ${brand.green("\xB7")} ${v}`);
|
|
2687
|
+
}
|
|
2688
|
+
console.log("");
|
|
2195
2689
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
"
|
|
2200
|
-
|
|
2201
|
-
);
|
|
2690
|
+
if (inEnv.length > 0) {
|
|
2691
|
+
console.log(` ${brand.green("\u2713")} ${brand.bold("Set in environment:")}`);
|
|
2692
|
+
for (const v of inEnv) {
|
|
2693
|
+
console.log(` ${brand.green("\xB7")} ${v}`);
|
|
2694
|
+
}
|
|
2695
|
+
console.log("");
|
|
2202
2696
|
}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2697
|
+
if (missing.length > 0) {
|
|
2698
|
+
console.log(` ${brand.yellow("\u26A0")} ${brand.bold("Missing (not set):")}`);
|
|
2699
|
+
for (const v of missing) {
|
|
2700
|
+
const service = SERVICE_LINKS[v];
|
|
2701
|
+
if (service && service.url) {
|
|
2702
|
+
console.log(` ${brand.red("\xB7")} ${v} ${brand.dim("\u2192")} ${brand.brightBlue.underline(service.url)}`);
|
|
2703
|
+
} else {
|
|
2704
|
+
console.log(` ${brand.red("\xB7")} ${v}`);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
console.log("");
|
|
2708
|
+
}
|
|
2709
|
+
const envExamplePath = join9(cwd, ".env.example");
|
|
2710
|
+
if (!existsSync7(envExamplePath) && sourceVars.length > 0) {
|
|
2711
|
+
const content = sourceVars.map((v) => {
|
|
2712
|
+
const service = SERVICE_LINKS[v];
|
|
2713
|
+
const comment = service ? ` # ${service.name}` : "";
|
|
2714
|
+
return `${v}=${comment}`;
|
|
2715
|
+
}).join("\n") + "\n";
|
|
2716
|
+
writeFileSync5(envExamplePath, content, "utf-8");
|
|
2717
|
+
ui.success("Generated .env.example from detected variables");
|
|
2718
|
+
console.log("");
|
|
2719
|
+
} else if (existsSync7(envExamplePath)) {
|
|
2720
|
+
ui.dim(".env.example already exists");
|
|
2721
|
+
console.log("");
|
|
2722
|
+
}
|
|
2723
|
+
const total = sourceVars.length;
|
|
2724
|
+
const setCount = present.length + inEnv.length;
|
|
2725
|
+
if (missing.length === 0) {
|
|
2726
|
+
ui.success(`All ${total} environment variables are set.`);
|
|
2727
|
+
} else {
|
|
2728
|
+
ui.warn(`${missing.length} of ${total} env vars are missing. Your build may fail.`);
|
|
2729
|
+
ui.dim("Set them in a .env file or export them in your shell.");
|
|
2730
|
+
}
|
|
2731
|
+
console.log("");
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
// src/commands/init.ts
|
|
2735
|
+
import inquirer2 from "inquirer";
|
|
2736
|
+
import { execa as execa3 } from "execa";
|
|
2737
|
+
import { existsSync as existsSync8, writeFileSync as writeFileSync6, mkdirSync } from "fs";
|
|
2738
|
+
import { join as join10 } from "path";
|
|
2739
|
+
import chalk6 from "chalk";
|
|
2740
|
+
|
|
2741
|
+
// src/commands/templates.ts
|
|
2742
|
+
import chalk5 from "chalk";
|
|
2743
|
+
var TEMPLATES = [
|
|
2744
|
+
{
|
|
2745
|
+
name: "astro-blog",
|
|
2746
|
+
description: "Astro blog with Markdown/MDX support and RSS feed",
|
|
2747
|
+
framework: "astro",
|
|
2748
|
+
createCmd: ["npm", "create", "astro@latest", "--", "--template", "blog"],
|
|
2749
|
+
postScaffold: ["npx", "astro", "add", "mdx", "--yes"]
|
|
2750
|
+
},
|
|
2751
|
+
{
|
|
2752
|
+
name: "nextjs-saas",
|
|
2753
|
+
description: "Next.js SaaS starter with App Router, Tailwind CSS, and TypeScript",
|
|
2754
|
+
framework: "nextjs",
|
|
2755
|
+
createCmd: ["npx", "create-next-app@latest", "--", "--ts", "--app", "--tailwind", "--eslint", "--src-dir", "--no-import-alias"]
|
|
2756
|
+
},
|
|
2757
|
+
{
|
|
2758
|
+
name: "vite-react",
|
|
2759
|
+
description: "Vite + React + TypeScript \u2014 fast SPA starter",
|
|
2760
|
+
framework: "vite-react",
|
|
2761
|
+
createCmd: ["npm", "create", "vite@latest", "--", "--template", "react-ts"]
|
|
2762
|
+
},
|
|
2763
|
+
{
|
|
2764
|
+
name: "sveltekit-app",
|
|
2765
|
+
description: "SvelteKit full-stack app with TypeScript",
|
|
2766
|
+
framework: "sveltekit",
|
|
2767
|
+
createCmd: ["npm", "create", "svelte@latest"]
|
|
2768
|
+
},
|
|
2769
|
+
{
|
|
2770
|
+
name: "static-portfolio",
|
|
2771
|
+
description: "Minimal static HTML/CSS portfolio \u2014 no build step required",
|
|
2772
|
+
framework: "static-html",
|
|
2773
|
+
createCmd: []
|
|
2774
|
+
// No create tool — we scaffold manually
|
|
2775
|
+
}
|
|
2776
|
+
];
|
|
2777
|
+
function getTemplate(name) {
|
|
2778
|
+
return TEMPLATES.find((t) => t.name.toLowerCase() === name.toLowerCase());
|
|
2779
|
+
}
|
|
2780
|
+
function getTemplateNames() {
|
|
2781
|
+
return TEMPLATES.map((t) => t.name);
|
|
2782
|
+
}
|
|
2783
|
+
async function templatesCommand() {
|
|
2784
|
+
ui.banner();
|
|
2785
|
+
console.log(` ${brand.bold("Available templates")}`);
|
|
2786
|
+
console.log(` ${brand.dim("Use with: shipem init --template <name>")}`);
|
|
2787
|
+
console.log("");
|
|
2788
|
+
const maxNameLen = Math.max(...TEMPLATES.map((t) => t.name.length));
|
|
2789
|
+
for (const tpl of TEMPLATES) {
|
|
2790
|
+
const padding = " ".repeat(maxNameLen - tpl.name.length);
|
|
2791
|
+
console.log(
|
|
2792
|
+
` ${chalk5.cyan(tpl.name)}${padding} ${brand.dim("\u2014")} ${tpl.description}`
|
|
2793
|
+
);
|
|
2794
|
+
console.log(
|
|
2795
|
+
` ${" ".repeat(maxNameLen)} ${brand.dim(`Framework: ${tpl.framework}`)}`
|
|
2796
|
+
);
|
|
2797
|
+
console.log("");
|
|
2798
|
+
}
|
|
2799
|
+
console.log(` ${brand.bold("Quick start:")}`);
|
|
2800
|
+
console.log(` ${chalk5.cyan("npx ship-em init --template astro-blog")}`);
|
|
2801
|
+
console.log(` ${chalk5.cyan("npx ship-em init --template nextjs-saas")}`);
|
|
2802
|
+
console.log("");
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// src/commands/init.ts
|
|
2806
|
+
var FRAMEWORKS = [
|
|
2807
|
+
{
|
|
2808
|
+
name: "Astro (static sites, blogs, portfolios)",
|
|
2809
|
+
value: "astro",
|
|
2810
|
+
createCmd: ["npm", "create", "astro@latest", "--", "--template", "basics"],
|
|
2811
|
+
keywords: ["portfolio", "blog", "personal", "marketing", "docs", "documentation", "static"]
|
|
2812
|
+
},
|
|
2813
|
+
{
|
|
2814
|
+
name: "Next.js (full-stack React, dashboards, SaaS)",
|
|
2815
|
+
value: "nextjs",
|
|
2816
|
+
createCmd: ["npx", "create-next-app@latest", "--", "--ts", "--app", "--tailwind", "--eslint", "--src-dir", "--no-import-alias"],
|
|
2817
|
+
keywords: ["dashboard", "saas", "app", "application", "admin", "full-stack", "fullstack", "e-commerce", "ecommerce"]
|
|
2818
|
+
},
|
|
2819
|
+
{
|
|
2820
|
+
name: "Vite + React (SPAs, landing pages)",
|
|
2821
|
+
value: "vite-react",
|
|
2822
|
+
createCmd: ["npm", "create", "vite@latest", "--", "--template", "react-ts"],
|
|
2823
|
+
keywords: ["landing", "page", "spa", "single", "react", "frontend", "form", "calculator"]
|
|
2824
|
+
},
|
|
2825
|
+
{
|
|
2826
|
+
name: "Vite + Vue (SPAs with Vue)",
|
|
2827
|
+
value: "vite-vue",
|
|
2828
|
+
createCmd: ["npm", "create", "vite@latest", "--", "--template", "vue-ts"],
|
|
2829
|
+
keywords: ["vue"]
|
|
2830
|
+
},
|
|
2831
|
+
{
|
|
2832
|
+
name: "SvelteKit (Svelte full-stack)",
|
|
2833
|
+
value: "sveltekit",
|
|
2834
|
+
createCmd: ["npm", "create", "svelte@latest"],
|
|
2835
|
+
keywords: ["svelte"]
|
|
2836
|
+
}
|
|
2837
|
+
];
|
|
2838
|
+
function matchFramework(description) {
|
|
2839
|
+
const lower = description.toLowerCase();
|
|
2840
|
+
let bestScore = 0;
|
|
2841
|
+
let best = FRAMEWORKS[0];
|
|
2842
|
+
for (const fw of FRAMEWORKS) {
|
|
2843
|
+
let score = 0;
|
|
2844
|
+
for (const keyword of fw.keywords) {
|
|
2845
|
+
if (lower.includes(keyword)) score += 1;
|
|
2846
|
+
}
|
|
2847
|
+
if (score > bestScore) {
|
|
2848
|
+
bestScore = score;
|
|
2849
|
+
best = fw;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
return best;
|
|
2853
|
+
}
|
|
2854
|
+
function generateProjectName(description) {
|
|
2855
|
+
return description.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 3).join("-").slice(0, 30) || "my-app";
|
|
2856
|
+
}
|
|
2857
|
+
async function initCommand(options = {}) {
|
|
2858
|
+
ui.banner();
|
|
2859
|
+
console.log(` ${brand.bold("Create a new project")}`);
|
|
2860
|
+
console.log(` ${brand.dim("From idea to live website in 2 commands")}`);
|
|
2861
|
+
console.log("");
|
|
2862
|
+
if (options.template) {
|
|
2863
|
+
const tpl = getTemplate(options.template);
|
|
2864
|
+
if (!tpl) {
|
|
2865
|
+
ui.error(`Unknown template: ${options.template}`);
|
|
2866
|
+
ui.br();
|
|
2867
|
+
ui.info("Available templates:");
|
|
2868
|
+
for (const name of getTemplateNames()) {
|
|
2869
|
+
ui.dim(` ${name}`);
|
|
2870
|
+
}
|
|
2871
|
+
ui.br();
|
|
2872
|
+
ui.dim("Run `shipem templates` to see descriptions.");
|
|
2873
|
+
ui.br();
|
|
2874
|
+
process.exit(1);
|
|
2875
|
+
}
|
|
2876
|
+
await scaffoldFromTemplate(tpl, options);
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
const { description } = await inquirer2.prompt([
|
|
2880
|
+
{
|
|
2881
|
+
type: "input",
|
|
2882
|
+
name: "description",
|
|
2883
|
+
message: "What are you building? (describe in plain English)",
|
|
2884
|
+
validate: (v) => v.trim().length > 0 || "Please describe your project"
|
|
2885
|
+
}
|
|
2886
|
+
]);
|
|
2887
|
+
const matched = matchFramework(description);
|
|
2888
|
+
const projectName = generateProjectName(description);
|
|
2889
|
+
console.log("");
|
|
2890
|
+
ui.info(`Matched: ${ui.framework(matched.value)} \u2014 ${matched.name.split("(")[0].trim()}`);
|
|
2891
|
+
if (!options.yes) {
|
|
2892
|
+
const { framework } = await inquirer2.prompt([
|
|
2893
|
+
{
|
|
2894
|
+
type: "list",
|
|
2895
|
+
name: "framework",
|
|
2896
|
+
message: "Use this framework?",
|
|
2897
|
+
choices: [
|
|
2898
|
+
{ name: `${matched.name} (recommended)`, value: matched.value },
|
|
2899
|
+
...FRAMEWORKS.filter((f) => f.value !== matched.value).map((f) => ({
|
|
2900
|
+
name: f.name,
|
|
2901
|
+
value: f.value
|
|
2902
|
+
}))
|
|
2903
|
+
]
|
|
2904
|
+
}
|
|
2905
|
+
]);
|
|
2906
|
+
const selected = FRAMEWORKS.find((f) => f.value === framework) ?? matched;
|
|
2907
|
+
if (selected.value !== matched.value) {
|
|
2908
|
+
Object.assign(matched, selected);
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
console.log("");
|
|
2912
|
+
const scaffoldSpinner = ui.spinner(`Scaffolding with ${matched.value}...`);
|
|
2913
|
+
try {
|
|
2914
|
+
const cmd = [...matched.createCmd];
|
|
2915
|
+
if (matched.value === "astro") {
|
|
2916
|
+
cmd.push(projectName);
|
|
2917
|
+
} else if (matched.value === "nextjs") {
|
|
2918
|
+
const dashIdx = cmd.indexOf("--");
|
|
2919
|
+
cmd.splice(dashIdx, 0, projectName);
|
|
2920
|
+
} else {
|
|
2921
|
+
const dashIdx = cmd.indexOf("--");
|
|
2922
|
+
if (dashIdx >= 0) {
|
|
2923
|
+
cmd.splice(dashIdx, 0, projectName);
|
|
2924
|
+
} else {
|
|
2925
|
+
cmd.push(projectName);
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
await execa3(cmd[0], cmd.slice(1), {
|
|
2929
|
+
cwd: process.cwd(),
|
|
2930
|
+
timeout: 12e4,
|
|
2931
|
+
stdio: "pipe"
|
|
2932
|
+
});
|
|
2933
|
+
scaffoldSpinner.succeed(`Created ${projectName}/`);
|
|
2934
|
+
} catch (err) {
|
|
2935
|
+
scaffoldSpinner.fail("Scaffolding failed");
|
|
2936
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2937
|
+
ui.dim(msg);
|
|
2938
|
+
ui.br();
|
|
2939
|
+
ui.info("Creating minimal project instead...");
|
|
2940
|
+
createMinimalProject(projectName, description);
|
|
2941
|
+
}
|
|
2942
|
+
const projectDir = join10(process.cwd(), projectName);
|
|
2943
|
+
if (existsSync8(projectDir)) {
|
|
2944
|
+
writeFileSync6(
|
|
2945
|
+
join10(projectDir, "shipem.json"),
|
|
2946
|
+
JSON.stringify({ project: { name: projectName, framework: matched.value } }, null, 2) + "\n",
|
|
2947
|
+
"utf-8"
|
|
2948
|
+
);
|
|
2949
|
+
}
|
|
2950
|
+
console.log("");
|
|
2951
|
+
console.log(` ${brand.green.bold("Ready!")} Now run:`);
|
|
2952
|
+
console.log("");
|
|
2953
|
+
console.log(` ${chalk6.cyan(`cd ${projectName} && npx ship-em`)}`);
|
|
2954
|
+
console.log("");
|
|
2955
|
+
}
|
|
2956
|
+
async function scaffoldFromTemplate(tpl, options) {
|
|
2957
|
+
let projectName;
|
|
2958
|
+
if (options.yes) {
|
|
2959
|
+
projectName = `my-${tpl.name}`;
|
|
2960
|
+
} else {
|
|
2961
|
+
const { name } = await inquirer2.prompt([
|
|
2962
|
+
{
|
|
2963
|
+
type: "input",
|
|
2964
|
+
name: "name",
|
|
2965
|
+
message: "Project name?",
|
|
2966
|
+
default: `my-${tpl.name}`,
|
|
2967
|
+
validate: (v) => v.trim().length > 0 || "Please enter a project name"
|
|
2968
|
+
}
|
|
2969
|
+
]);
|
|
2970
|
+
projectName = name.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").slice(0, 30) || `my-${tpl.name}`;
|
|
2971
|
+
}
|
|
2972
|
+
ui.info(`Template: ${chalk6.cyan(tpl.name)} \u2014 ${tpl.description}`);
|
|
2973
|
+
ui.br();
|
|
2974
|
+
if (tpl.createCmd.length === 0) {
|
|
2975
|
+
const scaffoldSpinner = ui.spinner(`Creating ${projectName}/...`);
|
|
2976
|
+
createStaticPortfolio(projectName);
|
|
2977
|
+
scaffoldSpinner.succeed(`Created ${projectName}/`);
|
|
2978
|
+
} else {
|
|
2979
|
+
const scaffoldSpinner = ui.spinner(`Scaffolding ${tpl.name}...`);
|
|
2980
|
+
try {
|
|
2981
|
+
const cmd = [...tpl.createCmd];
|
|
2982
|
+
if (tpl.framework === "astro") {
|
|
2983
|
+
cmd.push(projectName);
|
|
2984
|
+
} else if (tpl.framework === "nextjs") {
|
|
2985
|
+
const dashIdx = cmd.indexOf("--");
|
|
2986
|
+
cmd.splice(dashIdx, 0, projectName);
|
|
2987
|
+
} else {
|
|
2988
|
+
const dashIdx = cmd.indexOf("--");
|
|
2989
|
+
if (dashIdx >= 0) {
|
|
2990
|
+
cmd.splice(dashIdx, 0, projectName);
|
|
2991
|
+
} else {
|
|
2992
|
+
cmd.push(projectName);
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
await execa3(cmd[0], cmd.slice(1), {
|
|
2996
|
+
cwd: process.cwd(),
|
|
2997
|
+
timeout: 12e4,
|
|
2998
|
+
stdio: "pipe"
|
|
2999
|
+
});
|
|
3000
|
+
scaffoldSpinner.succeed(`Created ${projectName}/`);
|
|
3001
|
+
if (tpl.postScaffold && tpl.postScaffold.length > 0) {
|
|
3002
|
+
const postSpinner = ui.spinner("Running post-scaffold setup...");
|
|
3003
|
+
try {
|
|
3004
|
+
await execa3(tpl.postScaffold[0], tpl.postScaffold.slice(1), {
|
|
3005
|
+
cwd: join10(process.cwd(), projectName),
|
|
3006
|
+
timeout: 12e4,
|
|
3007
|
+
stdio: "pipe"
|
|
3008
|
+
});
|
|
3009
|
+
postSpinner.succeed("Post-scaffold setup complete");
|
|
3010
|
+
} catch {
|
|
3011
|
+
postSpinner.fail("Post-scaffold setup failed (non-critical)");
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
} catch (err) {
|
|
3015
|
+
scaffoldSpinner.fail("Scaffolding failed");
|
|
3016
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3017
|
+
ui.dim(msg);
|
|
3018
|
+
ui.br();
|
|
3019
|
+
ui.info("Creating minimal project instead...");
|
|
3020
|
+
createMinimalProject(projectName, tpl.description);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
const projectDir = join10(process.cwd(), projectName);
|
|
3024
|
+
if (existsSync8(projectDir)) {
|
|
3025
|
+
writeFileSync6(
|
|
3026
|
+
join10(projectDir, "shipem.json"),
|
|
3027
|
+
JSON.stringify({ project: { name: projectName, framework: tpl.framework } }, null, 2) + "\n",
|
|
3028
|
+
"utf-8"
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
console.log("");
|
|
3032
|
+
console.log(` ${brand.green.bold("Ready!")} Now run:`);
|
|
3033
|
+
console.log("");
|
|
3034
|
+
console.log(` ${chalk6.cyan(`cd ${projectName} && npx ship-em`)}`);
|
|
3035
|
+
console.log("");
|
|
3036
|
+
}
|
|
3037
|
+
function createMinimalProject(projectName, description) {
|
|
3038
|
+
const dir = join10(process.cwd(), projectName);
|
|
3039
|
+
if (!existsSync8(dir)) {
|
|
3040
|
+
mkdirSync(dir, { recursive: true });
|
|
3041
|
+
writeFileSync6(join10(dir, "index.html"), `<!DOCTYPE html>
|
|
3042
|
+
<html lang="en">
|
|
3043
|
+
<head>
|
|
3044
|
+
<meta charset="UTF-8">
|
|
3045
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3046
|
+
<title>${description}</title>
|
|
3047
|
+
<style>
|
|
3048
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
3049
|
+
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; background: #0a0a0a; color: #ededed; }
|
|
3050
|
+
h1 { font-size: 2.5rem; margin-bottom: 1rem; }
|
|
3051
|
+
p { color: #888; }
|
|
3052
|
+
</style>
|
|
3053
|
+
</head>
|
|
3054
|
+
<body>
|
|
3055
|
+
<div style="text-align: center">
|
|
3056
|
+
<h1>${description}</h1>
|
|
3057
|
+
<p>Built with Shipem</p>
|
|
3058
|
+
</div>
|
|
3059
|
+
</body>
|
|
3060
|
+
</html>`, "utf-8");
|
|
3061
|
+
ui.success(`Created ${projectName}/ with minimal HTML`);
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
function createStaticPortfolio(projectName) {
|
|
3065
|
+
const dir = join10(process.cwd(), projectName);
|
|
3066
|
+
mkdirSync(dir, { recursive: true });
|
|
3067
|
+
writeFileSync6(join10(dir, "index.html"), `<!DOCTYPE html>
|
|
3068
|
+
<html lang="en">
|
|
3069
|
+
<head>
|
|
3070
|
+
<meta charset="UTF-8">
|
|
3071
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3072
|
+
<title>My Portfolio</title>
|
|
3073
|
+
<link rel="stylesheet" href="style.css">
|
|
3074
|
+
</head>
|
|
3075
|
+
<body>
|
|
3076
|
+
<header>
|
|
3077
|
+
<h1>Hello, I'm <span class="accent">Your Name</span></h1>
|
|
3078
|
+
<p>Developer · Designer · Creator</p>
|
|
3079
|
+
</header>
|
|
3080
|
+
<main>
|
|
3081
|
+
<section class="projects">
|
|
3082
|
+
<h2>Projects</h2>
|
|
3083
|
+
<div class="grid">
|
|
3084
|
+
<div class="card">
|
|
3085
|
+
<h3>Project One</h3>
|
|
3086
|
+
<p>A brief description of this project.</p>
|
|
3087
|
+
</div>
|
|
3088
|
+
<div class="card">
|
|
3089
|
+
<h3>Project Two</h3>
|
|
3090
|
+
<p>A brief description of this project.</p>
|
|
3091
|
+
</div>
|
|
3092
|
+
<div class="card">
|
|
3093
|
+
<h3>Project Three</h3>
|
|
3094
|
+
<p>A brief description of this project.</p>
|
|
3095
|
+
</div>
|
|
3096
|
+
</div>
|
|
3097
|
+
</section>
|
|
3098
|
+
</main>
|
|
3099
|
+
<footer>
|
|
3100
|
+
<p>Shipped with Shipem</p>
|
|
3101
|
+
</footer>
|
|
3102
|
+
</body>
|
|
3103
|
+
</html>`, "utf-8");
|
|
3104
|
+
writeFileSync6(join10(dir, "style.css"), `* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
3105
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0a0a; color: #ededed; min-height: 100vh; }
|
|
3106
|
+
header { text-align: center; padding: 4rem 1rem 2rem; }
|
|
3107
|
+
header h1 { font-size: 2.5rem; margin-bottom: 0.5rem; }
|
|
3108
|
+
.accent { color: #3B82F6; }
|
|
3109
|
+
header p { color: #888; font-size: 1.1rem; }
|
|
3110
|
+
main { max-width: 900px; margin: 0 auto; padding: 2rem 1rem; }
|
|
3111
|
+
h2 { margin-bottom: 1.5rem; color: #ccc; }
|
|
3112
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; }
|
|
3113
|
+
.card { background: #1a1a1a; border: 1px solid #333; border-radius: 8px; padding: 1.5rem; }
|
|
3114
|
+
.card h3 { margin-bottom: 0.5rem; }
|
|
3115
|
+
.card p { color: #888; font-size: 0.9rem; }
|
|
3116
|
+
footer { text-align: center; padding: 3rem 1rem; color: #555; font-size: 0.85rem; }
|
|
3117
|
+
`, "utf-8");
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
// src/commands/dev.ts
|
|
3121
|
+
import { execa as execa4 } from "execa";
|
|
3122
|
+
import chalk7 from "chalk";
|
|
3123
|
+
var DEV_COMMANDS = {
|
|
3124
|
+
nextjs: ["npx", "next", "dev"],
|
|
3125
|
+
astro: ["npx", "astro", "dev"],
|
|
3126
|
+
sveltekit: ["npx", "vite", "dev"],
|
|
3127
|
+
nuxt: ["npx", "nuxi", "dev"],
|
|
3128
|
+
remix: ["npx", "remix", "dev"],
|
|
3129
|
+
gatsby: ["npx", "gatsby", "develop"],
|
|
3130
|
+
"vite-react": ["npx", "vite"],
|
|
3131
|
+
"vite-vue": ["npx", "vite"],
|
|
3132
|
+
"vite-svelte": ["npx", "vite"],
|
|
3133
|
+
"create-react-app": ["npx", "react-scripts", "start"]
|
|
3134
|
+
};
|
|
3135
|
+
async function devCommand() {
|
|
3136
|
+
const cwd = process.cwd();
|
|
3137
|
+
ui.section("Shipem Dev");
|
|
3138
|
+
ui.br();
|
|
3139
|
+
const detection = scanProject(cwd);
|
|
3140
|
+
const devCmd = DEV_COMMANDS[detection.framework];
|
|
3141
|
+
if (!devCmd) {
|
|
3142
|
+
ui.warn(`No dev server known for framework: ${detection.framework}`);
|
|
3143
|
+
ui.dim("Start your dev server manually, then press [d] to deploy.");
|
|
3144
|
+
ui.br();
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
ui.success(`Detected: ${ui.framework(detection.framework)}`);
|
|
3148
|
+
ui.info(`Starting: ${devCmd.join(" ")}`);
|
|
3149
|
+
ui.br();
|
|
3150
|
+
let devProcess = null;
|
|
3151
|
+
try {
|
|
3152
|
+
devProcess = execa4(devCmd[0], devCmd.slice(1), {
|
|
3153
|
+
cwd,
|
|
3154
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3155
|
+
env: { ...process.env, FORCE_COLOR: "1" }
|
|
3156
|
+
});
|
|
3157
|
+
devProcess.stdout?.on("data", (chunk) => {
|
|
3158
|
+
process.stdout.write(chunk);
|
|
3159
|
+
});
|
|
3160
|
+
devProcess.stderr?.on("data", (chunk) => {
|
|
3161
|
+
process.stderr.write(chunk);
|
|
3162
|
+
});
|
|
3163
|
+
console.log("");
|
|
3164
|
+
console.log(` ${brand.blue("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}`);
|
|
3165
|
+
console.log(` ${brand.bold("Shipem Dev")} Press ${chalk7.cyan("[d]")} to deploy Press ${chalk7.cyan("[q]")} to quit`);
|
|
3166
|
+
console.log(` ${brand.blue("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}`);
|
|
3167
|
+
console.log("");
|
|
3168
|
+
if (process.stdin.isTTY) {
|
|
3169
|
+
process.stdin.setRawMode(true);
|
|
3170
|
+
process.stdin.resume();
|
|
3171
|
+
process.stdin.setEncoding("utf-8");
|
|
3172
|
+
process.stdin.on("data", async (key) => {
|
|
3173
|
+
if (key === "d" || key === "D") {
|
|
3174
|
+
console.log("");
|
|
3175
|
+
ui.section("Deploying...");
|
|
3176
|
+
ui.br();
|
|
3177
|
+
if (devProcess) {
|
|
3178
|
+
devProcess.kill("SIGTERM");
|
|
3179
|
+
}
|
|
3180
|
+
try {
|
|
3181
|
+
await deployCommand({ yes: true, skipBuild: false });
|
|
3182
|
+
} catch (err) {
|
|
3183
|
+
ui.error(err instanceof Error ? err.message : "Deploy failed");
|
|
3184
|
+
}
|
|
3185
|
+
console.log("");
|
|
3186
|
+
ui.dim("Dev server stopped after deploy. Run `shipem dev` again to restart.");
|
|
3187
|
+
process.exit(0);
|
|
3188
|
+
}
|
|
3189
|
+
if (key === "q" || key === "Q" || key === "") {
|
|
3190
|
+
console.log("");
|
|
3191
|
+
ui.info("Shutting down dev server...");
|
|
3192
|
+
if (devProcess) {
|
|
3193
|
+
devProcess.kill("SIGTERM");
|
|
3194
|
+
}
|
|
3195
|
+
process.exit(0);
|
|
3196
|
+
}
|
|
3197
|
+
});
|
|
3198
|
+
}
|
|
3199
|
+
await devProcess;
|
|
3200
|
+
} catch (err) {
|
|
3201
|
+
if (devProcess?.killed) return;
|
|
3202
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3203
|
+
if (!msg.includes("SIGTERM")) {
|
|
3204
|
+
ui.error(`Dev server failed: ${msg}`);
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
// src/commands/status.ts
|
|
3210
|
+
import chalk8 from "chalk";
|
|
3211
|
+
function timeAgo2(isoStr) {
|
|
3212
|
+
const diff = Date.now() - new Date(isoStr).getTime();
|
|
3213
|
+
const mins = Math.floor(diff / 6e4);
|
|
3214
|
+
if (mins < 1) return "just now";
|
|
3215
|
+
if (mins === 1) return "1 minute ago";
|
|
3216
|
+
if (mins < 60) return `${mins} minutes ago`;
|
|
3217
|
+
const hrs = Math.floor(mins / 60);
|
|
3218
|
+
if (hrs === 1) return "1 hour ago";
|
|
3219
|
+
if (hrs < 24) return `${hrs} hours ago`;
|
|
3220
|
+
const days = Math.floor(hrs / 24);
|
|
3221
|
+
return days === 1 ? "yesterday" : `${days} days ago`;
|
|
3222
|
+
}
|
|
3223
|
+
async function statusCommand() {
|
|
3224
|
+
const cwd = process.cwd();
|
|
3225
|
+
const config = readProjectConfig(cwd);
|
|
3226
|
+
if (!config.deployment) {
|
|
3227
|
+
ui.warn("No deployment found in this directory.");
|
|
3228
|
+
ui.dim("Run `shipem` to deploy your app.");
|
|
3229
|
+
process.exit(0);
|
|
3230
|
+
}
|
|
3231
|
+
const { deployment } = config;
|
|
3232
|
+
const project = config.project;
|
|
3233
|
+
const deploys = config.deployments ?? [];
|
|
3234
|
+
console.log("");
|
|
3235
|
+
console.log(` ${brand.blue.bold("\u26A1 Shipem Status")}`);
|
|
3236
|
+
console.log("");
|
|
3237
|
+
ui.kv("Project", chalk8.bold(deployment.projectName));
|
|
3238
|
+
if (project) {
|
|
3239
|
+
ui.kv("Framework", `${ui.frameworkIcon(project.framework)} ${ui.framework(project.framework)}`);
|
|
3240
|
+
}
|
|
3241
|
+
const ago = timeAgo2(deployment.deployedAt);
|
|
3242
|
+
const lastDeploy = deploys[0];
|
|
3243
|
+
if (lastDeploy) {
|
|
3244
|
+
ui.kv("Last deploy", `${ago} (${lastDeploy.duration.toFixed(1)}s)`);
|
|
3245
|
+
} else {
|
|
3246
|
+
ui.kv("Last deploy", ago);
|
|
3247
|
+
}
|
|
3248
|
+
ui.kv("URL", chalk8.blue.underline(deployment.url));
|
|
3249
|
+
ui.kv("Deploys", `${deploys.length} total`);
|
|
3250
|
+
console.log("");
|
|
3251
|
+
if (deploys.length > 0) {
|
|
3252
|
+
ui.deployHistory(deploys);
|
|
3253
|
+
}
|
|
3254
|
+
if ((deployment.deployTarget === "cloudflare-pages" || deployment.deployTarget === "cloudflare-workers") && deployment.cloudflareProjectName) {
|
|
3255
|
+
const cfCreds = getCloudflareCredentials();
|
|
3256
|
+
if (cfCreds) {
|
|
3257
|
+
const spinner = ui.spinner("Fetching live status from Cloudflare");
|
|
3258
|
+
const cf = new CloudflarePages(cfCreds.apiToken, cfCreds.accountId);
|
|
3259
|
+
try {
|
|
3260
|
+
const status = await cf.getDeploymentStatus(
|
|
3261
|
+
deployment.cloudflareProjectName,
|
|
3262
|
+
deployment.deploymentId
|
|
3263
|
+
);
|
|
3264
|
+
if (status) {
|
|
3265
|
+
spinner.succeed("Status fetched");
|
|
3266
|
+
ui.kv("Deployment ID", status.id);
|
|
3267
|
+
ui.kv(
|
|
3268
|
+
"Status",
|
|
3269
|
+
status.latest_stage?.status === "success" ? chalk8.green("\u2713 Active") : chalk8.yellow(status.latest_stage?.status ?? "unknown")
|
|
3270
|
+
);
|
|
3271
|
+
ui.br();
|
|
3272
|
+
if (status.stages && status.stages.length > 0) {
|
|
3273
|
+
ui.section("Build stages:");
|
|
3274
|
+
for (const stage of status.stages) {
|
|
3275
|
+
const icon = stage.status === "success" ? chalk8.green("\u2713") : stage.status === "failure" ? chalk8.red("\u2717") : chalk8.gray("\xB7");
|
|
3276
|
+
ui.info(`${icon} ${stage.name}`);
|
|
3277
|
+
}
|
|
3278
|
+
ui.br();
|
|
3279
|
+
}
|
|
3280
|
+
} else {
|
|
3281
|
+
spinner.warn("Could not fetch live status");
|
|
3282
|
+
}
|
|
3283
|
+
} catch {
|
|
3284
|
+
spinner.warn("Could not fetch live status from Cloudflare");
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
ui.url("Live URL", deployment.url);
|
|
3289
|
+
if (deployment.cloudflareProjectName) {
|
|
3290
|
+
ui.url(
|
|
3291
|
+
"Dashboard",
|
|
3292
|
+
`https://dash.cloudflare.com/${deployment.cloudflareAccountId}/pages/view/${deployment.cloudflareProjectName}`
|
|
3293
|
+
);
|
|
3294
|
+
}
|
|
3295
|
+
ui.br();
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
// src/commands/logs.ts
|
|
3299
|
+
import chalk9 from "chalk";
|
|
3300
|
+
async function logsCommand(options) {
|
|
3301
|
+
const cwd = process.cwd();
|
|
3302
|
+
const config = readProjectConfig(cwd);
|
|
3303
|
+
if (!config.deployment) {
|
|
3304
|
+
ui.warn("No deployment found in this directory.");
|
|
3305
|
+
ui.dim("Run `shipem` to deploy your app.");
|
|
3306
|
+
process.exit(0);
|
|
3307
|
+
}
|
|
3308
|
+
const { deployment } = config;
|
|
3309
|
+
if (deployment.deployTarget !== "cloudflare-pages" && deployment.deployTarget !== "cloudflare-workers") {
|
|
3310
|
+
ui.warn("Log streaming is only available for Cloudflare Pages deployments.");
|
|
3311
|
+
process.exit(0);
|
|
3312
|
+
}
|
|
3313
|
+
if (!deployment.cloudflareProjectName) {
|
|
3314
|
+
throw new ConfigError("Missing Cloudflare project name in deployment config.");
|
|
3315
|
+
}
|
|
3316
|
+
const cfCreds = getCloudflareCredentials();
|
|
3317
|
+
if (!cfCreds) {
|
|
3318
|
+
throw new ConfigError("Cloudflare credentials not found. Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables.");
|
|
3319
|
+
}
|
|
3320
|
+
ui.section("Deployment Logs");
|
|
3321
|
+
ui.br();
|
|
3322
|
+
ui.kv("App", deployment.projectName);
|
|
3323
|
+
ui.kv("Deployment", deployment.deploymentId);
|
|
3324
|
+
ui.br();
|
|
3325
|
+
const spinner = ui.spinner("Fetching logs");
|
|
3326
|
+
const cf = new CloudflarePages(cfCreds.apiToken, cfCreds.accountId);
|
|
3327
|
+
try {
|
|
3328
|
+
const logs = await cf.getDeploymentLogs(
|
|
3329
|
+
deployment.cloudflareProjectName,
|
|
2213
3330
|
deployment.deploymentId
|
|
2214
3331
|
);
|
|
2215
3332
|
spinner.stop();
|
|
@@ -2220,11 +3337,11 @@ async function logsCommand(options) {
|
|
|
2220
3337
|
const displayLogs = logs.slice(-linesToShow);
|
|
2221
3338
|
for (const line of displayLogs) {
|
|
2222
3339
|
if (line.includes("ERROR") || line.includes("error") || line.includes("FAIL")) {
|
|
2223
|
-
console.log(
|
|
3340
|
+
console.log(chalk9.red(` ${line}`));
|
|
2224
3341
|
} else if (line.includes("WARN") || line.includes("warn")) {
|
|
2225
|
-
console.log(
|
|
3342
|
+
console.log(chalk9.yellow(` ${line}`));
|
|
2226
3343
|
} else {
|
|
2227
|
-
console.log(
|
|
3344
|
+
console.log(chalk9.gray(` ${line}`));
|
|
2228
3345
|
}
|
|
2229
3346
|
}
|
|
2230
3347
|
}
|
|
@@ -2236,7 +3353,7 @@ async function logsCommand(options) {
|
|
|
2236
3353
|
}
|
|
2237
3354
|
|
|
2238
3355
|
// src/commands/down.ts
|
|
2239
|
-
import
|
|
3356
|
+
import inquirer3 from "inquirer";
|
|
2240
3357
|
import axios3 from "axios";
|
|
2241
3358
|
async function downCommand(options) {
|
|
2242
3359
|
const cwd = process.cwd();
|
|
@@ -2253,7 +3370,7 @@ async function downCommand(options) {
|
|
|
2253
3370
|
ui.kv("URL", deployment.url);
|
|
2254
3371
|
ui.br();
|
|
2255
3372
|
if (!options.yes) {
|
|
2256
|
-
const { confirmed } = await
|
|
3373
|
+
const { confirmed } = await inquirer3.prompt([
|
|
2257
3374
|
{
|
|
2258
3375
|
type: "confirm",
|
|
2259
3376
|
name: "confirmed",
|
|
@@ -2270,10 +3387,7 @@ async function downCommand(options) {
|
|
|
2270
3387
|
if ((deployment.deployTarget === "cloudflare-pages" || deployment.deployTarget === "cloudflare-workers") && deployment.cloudflareProjectName) {
|
|
2271
3388
|
const cfCreds = getCloudflareCredentials();
|
|
2272
3389
|
if (!cfCreds) {
|
|
2273
|
-
|
|
2274
|
-
"Cloudflare credentials not found.",
|
|
2275
|
-
"Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables."
|
|
2276
|
-
);
|
|
3390
|
+
throw new ConfigError("Cloudflare credentials not found. Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables.");
|
|
2277
3391
|
}
|
|
2278
3392
|
const spinner = ui.spinner(`Deleting ${deployment.projectName} from Cloudflare Pages`);
|
|
2279
3393
|
const cf = new CloudflarePages(cfCreds.apiToken, cfCreds.accountId);
|
|
@@ -2282,13 +3396,12 @@ async function downCommand(options) {
|
|
|
2282
3396
|
spinner.succeed("Deployment deleted from Cloudflare Pages");
|
|
2283
3397
|
} catch (err) {
|
|
2284
3398
|
spinner.fail("Failed to delete deployment");
|
|
2285
|
-
|
|
2286
|
-
process.exit(1);
|
|
3399
|
+
throw err instanceof DeployError ? err : new DeployError(err instanceof Error ? err.message : "Unknown error", { cause: err });
|
|
2287
3400
|
}
|
|
2288
3401
|
} else if ((deployment.deployTarget === "cloudflare-pages" || deployment.deployTarget === "cloudflare-workers") && !deployment.cloudflareProjectName) {
|
|
2289
3402
|
const token = getSessionToken();
|
|
2290
3403
|
if (!token) {
|
|
2291
|
-
|
|
3404
|
+
throw new AuthError("Not logged in. Run `shipem login` first.");
|
|
2292
3405
|
}
|
|
2293
3406
|
const spinner = ui.spinner(`Deleting ${deployment.projectName} from Shipem`);
|
|
2294
3407
|
try {
|
|
@@ -2298,16 +3411,11 @@ async function downCommand(options) {
|
|
|
2298
3411
|
spinner.succeed("Deployment deleted");
|
|
2299
3412
|
} catch (err) {
|
|
2300
3413
|
spinner.fail("Failed to delete deployment");
|
|
2301
|
-
|
|
2302
|
-
|
|
3414
|
+
if (axios3.isAxiosError(err) && err.code && ["ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT"].includes(err.code)) {
|
|
3415
|
+
throw new NetworkError("Cannot reach Shipem servers. Check your internet connection.", { cause: err });
|
|
3416
|
+
}
|
|
3417
|
+
throw new DeployError(err instanceof Error ? err.message : "Unknown error", { cause: err });
|
|
2303
3418
|
}
|
|
2304
|
-
} else if (deployment.deployTarget === "flyio") {
|
|
2305
|
-
ui.warn("Fly.io apps cannot be deleted automatically. Manual cleanup required:");
|
|
2306
|
-
ui.info(`flyctl apps destroy ${deployment.projectName} --yes`);
|
|
2307
|
-
ui.br();
|
|
2308
|
-
ui.warn("Local deployment config has NOT been cleared \u2014 re-run after manual deletion.");
|
|
2309
|
-
ui.br();
|
|
2310
|
-
return;
|
|
2311
3419
|
}
|
|
2312
3420
|
writeProjectConfig({ project: config.project }, cwd);
|
|
2313
3421
|
ui.br();
|
|
@@ -2316,8 +3424,481 @@ async function downCommand(options) {
|
|
|
2316
3424
|
ui.br();
|
|
2317
3425
|
}
|
|
2318
3426
|
|
|
2319
|
-
// src/commands/
|
|
3427
|
+
// src/commands/watch.ts
|
|
3428
|
+
import { watch as fsWatch } from "fs";
|
|
3429
|
+
import { join as join11, relative as relative2 } from "path";
|
|
3430
|
+
import { readdirSync as readdirSync7 } from "fs";
|
|
3431
|
+
import chalk10 from "chalk";
|
|
3432
|
+
var IGNORE_PATTERNS = /* @__PURE__ */ new Set([
|
|
3433
|
+
"node_modules",
|
|
3434
|
+
".git",
|
|
3435
|
+
"dist",
|
|
3436
|
+
"build",
|
|
3437
|
+
".shipem",
|
|
3438
|
+
"shipem.json",
|
|
3439
|
+
".next",
|
|
3440
|
+
".nuxt",
|
|
3441
|
+
".output",
|
|
3442
|
+
".svelte-kit",
|
|
3443
|
+
".turbo",
|
|
3444
|
+
"coverage",
|
|
3445
|
+
".DS_Store",
|
|
3446
|
+
".env",
|
|
3447
|
+
".env.local"
|
|
3448
|
+
]);
|
|
3449
|
+
var DEBOUNCE_MS = 2e3;
|
|
3450
|
+
function timestamp() {
|
|
3451
|
+
const d = /* @__PURE__ */ new Date();
|
|
3452
|
+
return d.toLocaleTimeString("en-US", { hour12: false });
|
|
3453
|
+
}
|
|
3454
|
+
function collectDirs(dir) {
|
|
3455
|
+
const dirs = [dir];
|
|
3456
|
+
try {
|
|
3457
|
+
const entries = readdirSync7(dir, { withFileTypes: true });
|
|
3458
|
+
for (const entry of entries) {
|
|
3459
|
+
if (!entry.isDirectory()) continue;
|
|
3460
|
+
if (IGNORE_PATTERNS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
3461
|
+
dirs.push(...collectDirs(join11(dir, entry.name)));
|
|
3462
|
+
}
|
|
3463
|
+
} catch {
|
|
3464
|
+
}
|
|
3465
|
+
return dirs;
|
|
3466
|
+
}
|
|
3467
|
+
async function watchCommand() {
|
|
3468
|
+
const cwd = process.cwd();
|
|
3469
|
+
ui.banner();
|
|
3470
|
+
console.log(` ${brand.blue.bold("\u26A1")} ${brand.bold("Watching")} for changes...`);
|
|
3471
|
+
console.log(` Auto-deploy on save. Press ${chalk10.cyan("[q]")} to stop, ${chalk10.cyan("[d]")} to force deploy.`);
|
|
3472
|
+
console.log("");
|
|
3473
|
+
let debounceTimer = null;
|
|
3474
|
+
let isDeploying = false;
|
|
3475
|
+
let lastChangedFile = "";
|
|
3476
|
+
const watchers = [];
|
|
3477
|
+
const triggerDeploy = async (file) => {
|
|
3478
|
+
if (isDeploying) return;
|
|
3479
|
+
isDeploying = true;
|
|
3480
|
+
console.log(` ${brand.dim(`[${timestamp()}]`)} ${brand.blue("Deploying...")}`);
|
|
3481
|
+
const startTime = Date.now();
|
|
3482
|
+
try {
|
|
3483
|
+
await deployCommand({ yes: true, skipBuild: false });
|
|
3484
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
3485
|
+
console.log(` ${brand.dim(`[${timestamp()}]`)} ${brand.green("\u2713")} Deployed (${elapsed}s)`);
|
|
3486
|
+
} catch (err) {
|
|
3487
|
+
console.log(` ${brand.dim(`[${timestamp()}]`)} ${brand.red("\u2717")} Deploy failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3488
|
+
}
|
|
3489
|
+
isDeploying = false;
|
|
3490
|
+
console.log("");
|
|
3491
|
+
console.log(` Watching for changes... Press ${chalk10.cyan("[q]")} to stop.`);
|
|
3492
|
+
console.log("");
|
|
3493
|
+
};
|
|
3494
|
+
const onFileChange = (dir, filename) => {
|
|
3495
|
+
if (!filename) return;
|
|
3496
|
+
if (IGNORE_PATTERNS.has(filename)) return;
|
|
3497
|
+
if (isDeploying) return;
|
|
3498
|
+
const relPath = relative2(cwd, join11(dir, filename));
|
|
3499
|
+
lastChangedFile = relPath;
|
|
3500
|
+
console.log(` ${brand.dim(`[${timestamp()}]`)} File changed: ${chalk10.cyan(relPath)}`);
|
|
3501
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
3502
|
+
debounceTimer = setTimeout(() => {
|
|
3503
|
+
void triggerDeploy(lastChangedFile);
|
|
3504
|
+
}, DEBOUNCE_MS);
|
|
3505
|
+
};
|
|
3506
|
+
const dirs = collectDirs(cwd);
|
|
3507
|
+
for (const dir of dirs) {
|
|
3508
|
+
try {
|
|
3509
|
+
const watcher = fsWatch(dir, { persistent: true }, (_event, filename) => {
|
|
3510
|
+
onFileChange(dir, filename);
|
|
3511
|
+
});
|
|
3512
|
+
watchers.push(watcher);
|
|
3513
|
+
} catch {
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
if (process.stdin.isTTY) {
|
|
3517
|
+
process.stdin.setRawMode(true);
|
|
3518
|
+
process.stdin.resume();
|
|
3519
|
+
process.stdin.setEncoding("utf-8");
|
|
3520
|
+
process.stdin.on("data", (key) => {
|
|
3521
|
+
if (key === "q" || key === "Q" || key === "") {
|
|
3522
|
+
console.log("");
|
|
3523
|
+
ui.info("Stopped watching.");
|
|
3524
|
+
for (const w of watchers) w.close();
|
|
3525
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
3526
|
+
process.exit(0);
|
|
3527
|
+
}
|
|
3528
|
+
if (key === "d" || key === "D") {
|
|
3529
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
3530
|
+
console.log(` ${brand.dim(`[${timestamp()}]`)} Force deploy triggered`);
|
|
3531
|
+
void triggerDeploy("manual");
|
|
3532
|
+
}
|
|
3533
|
+
});
|
|
3534
|
+
}
|
|
3535
|
+
await new Promise(() => {
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
// src/commands/history.ts
|
|
3540
|
+
import chalk11 from "chalk";
|
|
3541
|
+
|
|
3542
|
+
// src/memory/index.ts
|
|
3543
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync2, existsSync as existsSync9 } from "fs";
|
|
3544
|
+
import { join as join12 } from "path";
|
|
3545
|
+
import { homedir } from "os";
|
|
3546
|
+
var SHIPEM_HOME = join12(homedir(), ".shipem");
|
|
3547
|
+
var MEMORY_PATH = join12(SHIPEM_HOME, "memory.json");
|
|
3548
|
+
var PROJECTS_PATH = join12(SHIPEM_HOME, "projects.json");
|
|
3549
|
+
var FIXES_PATH = join12(SHIPEM_HOME, "fixes.json");
|
|
3550
|
+
var CONFIG_PATH = join12(SHIPEM_HOME, "config.json");
|
|
3551
|
+
function ensureDir() {
|
|
3552
|
+
if (!existsSync9(SHIPEM_HOME)) {
|
|
3553
|
+
mkdirSync2(SHIPEM_HOME, { recursive: true });
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
function readJson2(path, fallback) {
|
|
3557
|
+
try {
|
|
3558
|
+
if (!existsSync9(path)) return fallback;
|
|
3559
|
+
return JSON.parse(readFileSync9(path, "utf-8"));
|
|
3560
|
+
} catch {
|
|
3561
|
+
return fallback;
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
function writeJson(path, data) {
|
|
3565
|
+
ensureDir();
|
|
3566
|
+
writeFileSync7(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
3567
|
+
}
|
|
3568
|
+
function getMemory() {
|
|
3569
|
+
return readJson2(MEMORY_PATH, {
|
|
3570
|
+
totalDeploys: 0,
|
|
3571
|
+
totalFixes: 0,
|
|
3572
|
+
firstUsed: (/* @__PURE__ */ new Date()).toISOString()
|
|
3573
|
+
});
|
|
3574
|
+
}
|
|
3575
|
+
function getProjects() {
|
|
3576
|
+
return readJson2(PROJECTS_PATH, []);
|
|
3577
|
+
}
|
|
3578
|
+
function getGlobalConfig() {
|
|
3579
|
+
return readJson2(CONFIG_PATH, {});
|
|
3580
|
+
}
|
|
3581
|
+
function setWebhook(url) {
|
|
3582
|
+
const config = getGlobalConfig();
|
|
3583
|
+
if (url) {
|
|
3584
|
+
config.notifications = { ...config.notifications, webhook: url };
|
|
3585
|
+
} else {
|
|
3586
|
+
if (config.notifications) delete config.notifications.webhook;
|
|
3587
|
+
}
|
|
3588
|
+
writeJson(CONFIG_PATH, config);
|
|
3589
|
+
}
|
|
3590
|
+
function getWebhook() {
|
|
3591
|
+
return getGlobalConfig().notifications?.webhook;
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
// src/commands/history.ts
|
|
3595
|
+
async function historyCommand() {
|
|
3596
|
+
const projects = getProjects();
|
|
3597
|
+
const memory = getMemory();
|
|
3598
|
+
ui.section("Shipem History \u2014 All Projects");
|
|
3599
|
+
ui.br();
|
|
3600
|
+
if (projects.length === 0) {
|
|
3601
|
+
ui.dim("No projects deployed yet. Run `shipem deploy` to get started.");
|
|
3602
|
+
ui.br();
|
|
3603
|
+
return;
|
|
3604
|
+
}
|
|
3605
|
+
console.log(` ${brand.dim("Total deploys:")} ${chalk11.cyan(String(memory.totalDeploys))} ${brand.dim("Total fixes:")} ${chalk11.cyan(String(memory.totalFixes))} ${brand.dim("Since:")} ${chalk11.cyan(new Date(memory.firstUsed).toLocaleDateString())}`);
|
|
3606
|
+
ui.br();
|
|
3607
|
+
for (const project of projects) {
|
|
3608
|
+
const avgTime = project.deployCount > 0 ? (project.totalDeployTime / project.deployCount).toFixed(1) : "0.0";
|
|
3609
|
+
const lastDeploy = new Date(project.lastDeploy);
|
|
3610
|
+
const dateStr = lastDeploy.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
3611
|
+
const timeStr = lastDeploy.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
|
|
3612
|
+
console.log(` ${brand.bold(project.name)} ${brand.dim(`(${project.framework})`)}`);
|
|
3613
|
+
console.log(` ${brand.dim("URL:")} ${brand.brightBlue.underline(project.url)}`);
|
|
3614
|
+
console.log(` ${brand.dim("Deploys:")} ${chalk11.cyan(String(project.deployCount))} ${brand.dim("Avg:")} ${chalk11.cyan(avgTime + "s")} ${brand.dim("Last:")} ${dateStr} ${timeStr}`);
|
|
3615
|
+
console.log(` ${brand.dim("Path:")} ${project.path}`);
|
|
3616
|
+
ui.br();
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
// src/commands/config.ts
|
|
3621
|
+
async function configCommand(action, key, value) {
|
|
3622
|
+
if (!action || action === "show") {
|
|
3623
|
+
const config = getGlobalConfig();
|
|
3624
|
+
ui.section("Shipem Global Config");
|
|
3625
|
+
ui.br();
|
|
3626
|
+
ui.kv("Webhook", config.notifications?.webhook ?? "(not set)");
|
|
3627
|
+
ui.br();
|
|
3628
|
+
return;
|
|
3629
|
+
}
|
|
3630
|
+
if (action === "set") {
|
|
3631
|
+
if (!key) {
|
|
3632
|
+
ui.error("Usage: shipem config set <key> <value>");
|
|
3633
|
+
ui.dim("Available keys: webhook");
|
|
3634
|
+
ui.br();
|
|
3635
|
+
return;
|
|
3636
|
+
}
|
|
3637
|
+
if (key === "webhook") {
|
|
3638
|
+
if (!value || value === "off") {
|
|
3639
|
+
setWebhook(null);
|
|
3640
|
+
ui.success("Webhook notifications disabled.");
|
|
3641
|
+
} else {
|
|
3642
|
+
setWebhook(value);
|
|
3643
|
+
ui.success(`Webhook set to: ${value}`);
|
|
3644
|
+
}
|
|
3645
|
+
ui.br();
|
|
3646
|
+
return;
|
|
3647
|
+
}
|
|
3648
|
+
ui.error(`Unknown config key: ${key}`);
|
|
3649
|
+
ui.dim("Available keys: webhook");
|
|
3650
|
+
ui.br();
|
|
3651
|
+
return;
|
|
3652
|
+
}
|
|
3653
|
+
ui.error(`Unknown action: ${action}`);
|
|
3654
|
+
ui.dim("Usage: shipem config [show|set] [key] [value]");
|
|
3655
|
+
ui.br();
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
// src/commands/monitor.ts
|
|
3659
|
+
import axios5 from "axios";
|
|
3660
|
+
import chalk12 from "chalk";
|
|
3661
|
+
import { writeFileSync as writeFileSync8, readFileSync as readFileSync10, existsSync as existsSync10, unlinkSync } from "fs";
|
|
3662
|
+
import { join as join13 } from "path";
|
|
3663
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
3664
|
+
|
|
3665
|
+
// src/notifications/index.ts
|
|
2320
3666
|
import axios4 from "axios";
|
|
3667
|
+
async function sendStatusNotification(message, webhookOverride) {
|
|
3668
|
+
const webhook = webhookOverride ?? getWebhook();
|
|
3669
|
+
if (!webhook) return false;
|
|
3670
|
+
try {
|
|
3671
|
+
await axios4.post(webhook, { text: message }, { timeout: 5e3 });
|
|
3672
|
+
return true;
|
|
3673
|
+
} catch {
|
|
3674
|
+
return false;
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
// src/commands/monitor.ts
|
|
3679
|
+
var PID_FILE = join13(tmpdir2(), "shipem-monitor.pid");
|
|
3680
|
+
var DEFAULT_INTERVAL_MS = 5 * 60 * 1e3;
|
|
3681
|
+
async function checkSite(name, url) {
|
|
3682
|
+
const start = Date.now();
|
|
3683
|
+
try {
|
|
3684
|
+
const resp = await axios5.head(url, { timeout: 1e4, maxRedirects: 5 });
|
|
3685
|
+
return {
|
|
3686
|
+
name,
|
|
3687
|
+
url,
|
|
3688
|
+
status: resp.status,
|
|
3689
|
+
responseTime: Date.now() - start,
|
|
3690
|
+
up: resp.status >= 200 && resp.status < 400
|
|
3691
|
+
};
|
|
3692
|
+
} catch (err) {
|
|
3693
|
+
const elapsed = Date.now() - start;
|
|
3694
|
+
if (axios5.isAxiosError(err) && err.response) {
|
|
3695
|
+
return {
|
|
3696
|
+
name,
|
|
3697
|
+
url,
|
|
3698
|
+
status: err.response.status,
|
|
3699
|
+
responseTime: elapsed,
|
|
3700
|
+
up: false,
|
|
3701
|
+
error: `HTTP ${err.response.status}`
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
return {
|
|
3705
|
+
name,
|
|
3706
|
+
url,
|
|
3707
|
+
status: null,
|
|
3708
|
+
responseTime: elapsed,
|
|
3709
|
+
up: false,
|
|
3710
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
function displayResults(results) {
|
|
3715
|
+
console.clear();
|
|
3716
|
+
ui.section("Shipem Monitor");
|
|
3717
|
+
ui.br();
|
|
3718
|
+
if (results.length === 0) {
|
|
3719
|
+
ui.dim("No projects to monitor. Deploy a project first.");
|
|
3720
|
+
ui.br();
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
3723
|
+
const maxName = Math.max(...results.map((r) => r.name.length));
|
|
3724
|
+
for (const r of results) {
|
|
3725
|
+
const nameCol = r.name.padEnd(maxName + 2);
|
|
3726
|
+
const timeCol = `${r.responseTime}ms`.padStart(7);
|
|
3727
|
+
if (r.up) {
|
|
3728
|
+
console.log(` ${brand.green("\u2713")} ${brand.bold(nameCol)} ${brand.dim(r.url.padEnd(40))} ${brand.green(`${r.status} OK`)} ${brand.dim(`(${timeCol})`)}`);
|
|
3729
|
+
} else {
|
|
3730
|
+
const statusText = r.error ?? `${r.status}`;
|
|
3731
|
+
console.log(` ${brand.red("\u2717")} ${brand.bold(nameCol)} ${brand.dim(r.url.padEnd(40))} ${brand.red(statusText)} ${brand.red("\u2190 DOWN!")}`);
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
ui.br();
|
|
3735
|
+
}
|
|
3736
|
+
async function monitorCommand(options = {}) {
|
|
3737
|
+
if (options.stop) {
|
|
3738
|
+
if (existsSync10(PID_FILE)) {
|
|
3739
|
+
try {
|
|
3740
|
+
const pid = parseInt(readFileSync10(PID_FILE, "utf-8").trim(), 10);
|
|
3741
|
+
process.kill(pid, "SIGTERM");
|
|
3742
|
+
unlinkSync(PID_FILE);
|
|
3743
|
+
ui.success("Monitor daemon stopped.");
|
|
3744
|
+
} catch {
|
|
3745
|
+
ui.warn("Could not stop monitor daemon. It may have already exited.");
|
|
3746
|
+
try {
|
|
3747
|
+
unlinkSync(PID_FILE);
|
|
3748
|
+
} catch {
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
} else {
|
|
3752
|
+
ui.info("No monitor daemon running.");
|
|
3753
|
+
}
|
|
3754
|
+
ui.br();
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3757
|
+
const projects = getProjects();
|
|
3758
|
+
if (projects.length === 0) {
|
|
3759
|
+
ui.section("Shipem Monitor");
|
|
3760
|
+
ui.br();
|
|
3761
|
+
ui.dim("No projects found. Deploy a project first, then run `shipem monitor`.");
|
|
3762
|
+
ui.br();
|
|
3763
|
+
return;
|
|
3764
|
+
}
|
|
3765
|
+
const intervalMs = options.interval ? parseInt(options.interval, 10) * 60 * 1e3 : DEFAULT_INTERVAL_MS;
|
|
3766
|
+
if (options.daemon) {
|
|
3767
|
+
writePidFile();
|
|
3768
|
+
ui.success(`Monitor daemon started (PID ${process.pid}). Checking every ${intervalMs / 6e4} minutes.`);
|
|
3769
|
+
ui.dim(`Stop with: shipem monitor stop`);
|
|
3770
|
+
ui.br();
|
|
3771
|
+
}
|
|
3772
|
+
const previousUp = /* @__PURE__ */ new Map();
|
|
3773
|
+
const runCheck = async () => {
|
|
3774
|
+
const results = await Promise.all(
|
|
3775
|
+
projects.map((p) => checkSite(p.name, p.url))
|
|
3776
|
+
);
|
|
3777
|
+
if (!options.daemon) {
|
|
3778
|
+
displayResults(results);
|
|
3779
|
+
const intervalMin = intervalMs / 6e4;
|
|
3780
|
+
console.log(` ${brand.dim(`Checking every ${intervalMin} minutes. Press ${chalk12.cyan("[q]")} to stop.`)}`);
|
|
3781
|
+
ui.br();
|
|
3782
|
+
}
|
|
3783
|
+
for (const r of results) {
|
|
3784
|
+
const wasUp = previousUp.get(r.name);
|
|
3785
|
+
if (wasUp !== void 0) {
|
|
3786
|
+
if (wasUp && !r.up) {
|
|
3787
|
+
await sendStatusNotification(`\u{1F534} ${r.name} is DOWN! ${r.url} \u2014 ${r.error ?? "unreachable"}`);
|
|
3788
|
+
} else if (!wasUp && r.up) {
|
|
3789
|
+
await sendStatusNotification(`\u{1F7E2} ${r.name} is back UP! ${r.url} \u2014 ${r.responseTime}ms`);
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
previousUp.set(r.name, r.up);
|
|
3793
|
+
}
|
|
3794
|
+
};
|
|
3795
|
+
await runCheck();
|
|
3796
|
+
const timer = setInterval(() => void runCheck(), intervalMs);
|
|
3797
|
+
if (!options.daemon && process.stdin.isTTY) {
|
|
3798
|
+
process.stdin.setRawMode(true);
|
|
3799
|
+
process.stdin.resume();
|
|
3800
|
+
process.stdin.setEncoding("utf-8");
|
|
3801
|
+
process.stdin.on("data", (key) => {
|
|
3802
|
+
if (key === "q" || key === "Q" || key === "") {
|
|
3803
|
+
clearInterval(timer);
|
|
3804
|
+
console.log("");
|
|
3805
|
+
ui.info("Monitor stopped.");
|
|
3806
|
+
process.exit(0);
|
|
3807
|
+
}
|
|
3808
|
+
});
|
|
3809
|
+
}
|
|
3810
|
+
await new Promise(() => {
|
|
3811
|
+
});
|
|
3812
|
+
}
|
|
3813
|
+
function writePidFile() {
|
|
3814
|
+
writeFileSync8(PID_FILE, String(process.pid), "utf-8");
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
// src/commands/hooks.ts
|
|
3818
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync2, chmodSync, mkdirSync as mkdirSync3 } from "fs";
|
|
3819
|
+
import { join as join14 } from "path";
|
|
3820
|
+
var HOOK_MARKER = "# shipem-auto-deploy";
|
|
3821
|
+
var HOOK_CONTENT = `#!/bin/sh
|
|
3822
|
+
${HOOK_MARKER}
|
|
3823
|
+
# Auto-deploy on commit \u2014 installed by shipem hooks install
|
|
3824
|
+
# Run shipem deploy in background so it doesn't block the commit
|
|
3825
|
+
nohup shipem deploy --yes --quiet > /dev/null 2>&1 &
|
|
3826
|
+
`;
|
|
3827
|
+
function getHookPath(cwd) {
|
|
3828
|
+
return join14(cwd, ".git", "hooks", "post-commit");
|
|
3829
|
+
}
|
|
3830
|
+
function isShipemHook(path) {
|
|
3831
|
+
if (!existsSync11(path)) return false;
|
|
3832
|
+
const content = readFileSync11(path, "utf-8");
|
|
3833
|
+
return content.includes(HOOK_MARKER);
|
|
3834
|
+
}
|
|
3835
|
+
async function hooksCommand(action) {
|
|
3836
|
+
const cwd = process.cwd();
|
|
3837
|
+
const gitDir = join14(cwd, ".git");
|
|
3838
|
+
if (!existsSync11(gitDir)) {
|
|
3839
|
+
ui.error("Not a git repository. Run `git init` first.");
|
|
3840
|
+
ui.br();
|
|
3841
|
+
return;
|
|
3842
|
+
}
|
|
3843
|
+
const hooksDir = join14(gitDir, "hooks");
|
|
3844
|
+
const hookPath = getHookPath(cwd);
|
|
3845
|
+
if (!action || action === "status") {
|
|
3846
|
+
ui.section("Git Hooks");
|
|
3847
|
+
ui.br();
|
|
3848
|
+
if (isShipemHook(hookPath)) {
|
|
3849
|
+
ui.success("post-commit hook is installed (auto-deploy on commit)");
|
|
3850
|
+
ui.dim(`Remove with: shipem hooks remove`);
|
|
3851
|
+
} else if (existsSync11(hookPath)) {
|
|
3852
|
+
ui.info("A post-commit hook exists but was not installed by Shipem.");
|
|
3853
|
+
ui.dim(`Path: ${hookPath}`);
|
|
3854
|
+
} else {
|
|
3855
|
+
ui.info("No post-commit hook installed.");
|
|
3856
|
+
ui.dim(`Install with: shipem hooks install`);
|
|
3857
|
+
}
|
|
3858
|
+
ui.br();
|
|
3859
|
+
return;
|
|
3860
|
+
}
|
|
3861
|
+
if (action === "install") {
|
|
3862
|
+
if (existsSync11(hookPath) && !isShipemHook(hookPath)) {
|
|
3863
|
+
ui.warn("A post-commit hook already exists and was not created by Shipem.");
|
|
3864
|
+
ui.dim(`Path: ${hookPath}`);
|
|
3865
|
+
ui.dim("Remove it manually or back it up before installing.");
|
|
3866
|
+
ui.br();
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
3869
|
+
if (!existsSync11(hooksDir)) {
|
|
3870
|
+
mkdirSync3(hooksDir, { recursive: true });
|
|
3871
|
+
}
|
|
3872
|
+
writeFileSync9(hookPath, HOOK_CONTENT, "utf-8");
|
|
3873
|
+
chmodSync(hookPath, 493);
|
|
3874
|
+
ui.success("Installed post-commit hook \u2192 auto-deploy on commit");
|
|
3875
|
+
ui.dim(`Remove with: shipem hooks remove`);
|
|
3876
|
+
ui.br();
|
|
3877
|
+
return;
|
|
3878
|
+
}
|
|
3879
|
+
if (action === "remove") {
|
|
3880
|
+
if (!existsSync11(hookPath)) {
|
|
3881
|
+
ui.info("No post-commit hook to remove.");
|
|
3882
|
+
ui.br();
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3885
|
+
if (!isShipemHook(hookPath)) {
|
|
3886
|
+
ui.warn("The post-commit hook was not installed by Shipem. Not removing.");
|
|
3887
|
+
ui.br();
|
|
3888
|
+
return;
|
|
3889
|
+
}
|
|
3890
|
+
unlinkSync2(hookPath);
|
|
3891
|
+
ui.success("Removed post-commit hook.");
|
|
3892
|
+
ui.br();
|
|
3893
|
+
return;
|
|
3894
|
+
}
|
|
3895
|
+
ui.error(`Unknown action: ${action}`);
|
|
3896
|
+
ui.dim("Usage: shipem hooks [install|remove|status]");
|
|
3897
|
+
ui.br();
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
// src/commands/logout.ts
|
|
3901
|
+
import axios6 from "axios";
|
|
2321
3902
|
async function logoutCommand() {
|
|
2322
3903
|
const token = getSessionToken();
|
|
2323
3904
|
if (!token) {
|
|
@@ -2326,7 +3907,7 @@ async function logoutCommand() {
|
|
|
2326
3907
|
return;
|
|
2327
3908
|
}
|
|
2328
3909
|
try {
|
|
2329
|
-
await
|
|
3910
|
+
await axios6.post(`${SHIPEM_API_URL}/auth/logout`, { token }, { timeout: 5e3 });
|
|
2330
3911
|
} catch {
|
|
2331
3912
|
}
|
|
2332
3913
|
clearSessionToken();
|
|
@@ -2334,20 +3915,145 @@ async function logoutCommand() {
|
|
|
2334
3915
|
ui.br();
|
|
2335
3916
|
}
|
|
2336
3917
|
|
|
3918
|
+
// src/commands/preview.ts
|
|
3919
|
+
import { execa as execa5 } from "execa";
|
|
3920
|
+
import chalk13 from "chalk";
|
|
3921
|
+
async function getCurrentBranch() {
|
|
3922
|
+
try {
|
|
3923
|
+
const { stdout } = await execa5("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
3924
|
+
return stdout.trim();
|
|
3925
|
+
} catch {
|
|
3926
|
+
throw new ConfigError(
|
|
3927
|
+
"Could not detect git branch. Make sure you are inside a git repository."
|
|
3928
|
+
);
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
function sanitizeBranchName(branch) {
|
|
3932
|
+
return branch.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 28);
|
|
3933
|
+
}
|
|
3934
|
+
async function previewCommand(options = {}) {
|
|
3935
|
+
const startTime = Date.now();
|
|
3936
|
+
const cwd = process.cwd();
|
|
3937
|
+
ui.banner();
|
|
3938
|
+
console.log(` ${brand.bold("Preview deployment")}`);
|
|
3939
|
+
console.log(` ${brand.dim("Deploy a preview from your current branch")}`);
|
|
3940
|
+
console.log("");
|
|
3941
|
+
const branch = await getCurrentBranch();
|
|
3942
|
+
if (branch === "main" || branch === "master") {
|
|
3943
|
+
ui.warn(`You are on '${branch}'. Previews are meant for feature branches.`);
|
|
3944
|
+
ui.dim("Use `shipem deploy` for production deployments.");
|
|
3945
|
+
ui.br();
|
|
3946
|
+
}
|
|
3947
|
+
ui.info(`Branch: ${chalk13.cyan(branch)}`);
|
|
3948
|
+
ui.br();
|
|
3949
|
+
const existingConfig = readProjectConfig(cwd);
|
|
3950
|
+
if (!existingConfig.project) {
|
|
3951
|
+
throw new ConfigError(
|
|
3952
|
+
"No shipem.json found. Run `shipem deploy` first to detect and configure your project."
|
|
3953
|
+
);
|
|
3954
|
+
}
|
|
3955
|
+
const projectConfig = existingConfig.project;
|
|
3956
|
+
const cfCreds = getCloudflareCredentials();
|
|
3957
|
+
if (!cfCreds) {
|
|
3958
|
+
throw new ConfigError(
|
|
3959
|
+
"Preview deployments require Cloudflare credentials.\n Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables.\n Get these from: https://dash.cloudflare.com/profile/api-tokens"
|
|
3960
|
+
);
|
|
3961
|
+
}
|
|
3962
|
+
if (!options.skipBuild) {
|
|
3963
|
+
ui.section("Building...");
|
|
3964
|
+
ui.br();
|
|
3965
|
+
const buildResult = await buildProject(projectConfig, cwd);
|
|
3966
|
+
if (!buildResult.success) {
|
|
3967
|
+
throw new DeployError(
|
|
3968
|
+
`Build failed: ${buildResult.error ?? "Unknown error"}. Fix build errors and try again.`
|
|
3969
|
+
);
|
|
3970
|
+
}
|
|
3971
|
+
ui.br();
|
|
3972
|
+
}
|
|
3973
|
+
ui.section("Deploying preview...");
|
|
3974
|
+
ui.br();
|
|
3975
|
+
const cf = new CloudflarePages(cfCreds.apiToken, cfCreds.accountId);
|
|
3976
|
+
const cfProjectName = sanitizeProjectName(projectConfig.name);
|
|
3977
|
+
await cf.getOrCreateProject(cfProjectName, projectConfig);
|
|
3978
|
+
const result = await cf.deployBranch(
|
|
3979
|
+
cfProjectName,
|
|
3980
|
+
projectConfig.outputDirectory,
|
|
3981
|
+
branch,
|
|
3982
|
+
cwd
|
|
3983
|
+
);
|
|
3984
|
+
const finalDeployment = await cf.waitForDeployment(cfProjectName, result.deployment.id);
|
|
3985
|
+
const branchSlug = sanitizeBranchName(branch);
|
|
3986
|
+
const previewUrl = finalDeployment.url || `https://${branchSlug}.${cfProjectName}.pages.dev`;
|
|
3987
|
+
const previewRecord = {
|
|
3988
|
+
branch,
|
|
3989
|
+
url: previewUrl,
|
|
3990
|
+
deploymentId: result.deployment.id,
|
|
3991
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3992
|
+
status: "success"
|
|
3993
|
+
};
|
|
3994
|
+
const existingPreviews = existingConfig.previews ?? [];
|
|
3995
|
+
const updatedPreviews = [
|
|
3996
|
+
previewRecord,
|
|
3997
|
+
...existingPreviews.filter((p) => p.branch !== branch)
|
|
3998
|
+
].slice(0, 20);
|
|
3999
|
+
writeProjectConfig(
|
|
4000
|
+
{ ...existingConfig, previews: updatedPreviews },
|
|
4001
|
+
cwd
|
|
4002
|
+
);
|
|
4003
|
+
const elapsedSec = (Date.now() - startTime) / 1e3;
|
|
4004
|
+
console.log("");
|
|
4005
|
+
console.log(` ${brand.green.bold("Preview live!")}`);
|
|
4006
|
+
console.log("");
|
|
4007
|
+
console.log(` ${brand.gray("Branch:")} ${chalk13.cyan(branch)}`);
|
|
4008
|
+
console.log(` ${brand.gray("URL:")} ${brand.brightBlue.underline(previewUrl)}`);
|
|
4009
|
+
console.log(` ${brand.gray("Files:")} ${result.fileCount} files (${ui.formatBytes(result.totalBytes)})`);
|
|
4010
|
+
console.log(` ${brand.gray("Time:")} ${elapsedSec.toFixed(1)}s`);
|
|
4011
|
+
console.log("");
|
|
4012
|
+
const copied = ui.copyToClipboard(previewUrl);
|
|
4013
|
+
if (copied) {
|
|
4014
|
+
ui.success("Preview URL copied to clipboard!");
|
|
4015
|
+
}
|
|
4016
|
+
console.log("");
|
|
4017
|
+
console.log(` ${brand.bold("Next steps:")}`);
|
|
4018
|
+
console.log(` ${brand.gray("Share")} \u2192 Send the preview URL to teammates`);
|
|
4019
|
+
console.log(` ${brand.gray("Update")} \u2192 ${chalk13.cyan("npx ship-em preview")} (re-deploy this branch)`);
|
|
4020
|
+
console.log(` ${brand.gray("Promote")} \u2192 Merge to main and ${chalk13.cyan("npx ship-em deploy")}`);
|
|
4021
|
+
console.log("");
|
|
4022
|
+
}
|
|
4023
|
+
|
|
2337
4024
|
// src/index.ts
|
|
2338
|
-
import { readFileSync as
|
|
4025
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
2339
4026
|
import { fileURLToPath } from "url";
|
|
2340
|
-
import { dirname, join as
|
|
4027
|
+
import { dirname, join as join15 } from "path";
|
|
2341
4028
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
2342
4029
|
var __dirname2 = dirname(__filename2);
|
|
2343
4030
|
var version = "0.1.0";
|
|
2344
4031
|
try {
|
|
2345
4032
|
const pkg = JSON.parse(
|
|
2346
|
-
|
|
4033
|
+
readFileSync12(join15(__dirname2, "../package.json"), "utf-8")
|
|
2347
4034
|
);
|
|
2348
4035
|
version = pkg.version;
|
|
2349
4036
|
} catch {
|
|
2350
4037
|
}
|
|
4038
|
+
var globalVerbose = false;
|
|
4039
|
+
var globalJson = false;
|
|
4040
|
+
function handleError(err, fallbackMsg) {
|
|
4041
|
+
if (globalJson) {
|
|
4042
|
+
const code = err instanceof ShipemError ? err.code : "ERR_UNKNOWN";
|
|
4043
|
+
const message = err instanceof Error ? err.message : fallbackMsg;
|
|
4044
|
+
console.log(JSON.stringify({ success: false, error: message, code }));
|
|
4045
|
+
process.exit(err instanceof ShipemError ? err.exitCode : 2);
|
|
4046
|
+
}
|
|
4047
|
+
ui.br();
|
|
4048
|
+
if (err instanceof ShipemError) {
|
|
4049
|
+
ui.error(err.message);
|
|
4050
|
+
if (globalVerbose || process.env.DEBUG) console.error(err);
|
|
4051
|
+
process.exit(err.exitCode);
|
|
4052
|
+
}
|
|
4053
|
+
ui.error(err instanceof Error ? err.message : fallbackMsg);
|
|
4054
|
+
if (globalVerbose || process.env.DEBUG) console.error(err);
|
|
4055
|
+
process.exit(2);
|
|
4056
|
+
}
|
|
2351
4057
|
var program = new Command();
|
|
2352
4058
|
program.name("shipem").description(
|
|
2353
4059
|
"One-command deployment for apps built by AI coding tools.\n\nYour AI built it. We'll ship it."
|
|
@@ -2356,61 +4062,136 @@ program.name("shipem").description(
|
|
|
2356
4062
|
return "";
|
|
2357
4063
|
}).addHelpText("after", `
|
|
2358
4064
|
Quick start:
|
|
2359
|
-
npx
|
|
2360
|
-
npx
|
|
2361
|
-
npx
|
|
2362
|
-
npx
|
|
4065
|
+
npx ship-em Deploy your app
|
|
4066
|
+
npx ship-em preview Deploy a preview from your branch
|
|
4067
|
+
npx ship-em watch Watch for changes and auto-deploy
|
|
4068
|
+
npx ship-em init Bootstrap a new project
|
|
4069
|
+
npx ship-em init -t <t> Init from a template
|
|
4070
|
+
npx ship-em templates List available templates
|
|
4071
|
+
npx ship-em fix Auto-fix build errors
|
|
4072
|
+
npx ship-em env Check environment variables
|
|
4073
|
+
npx ship-em dev Start dev server with deploy shortcut
|
|
4074
|
+
npx ship-em status Check deployment status
|
|
4075
|
+
npx ship-em history Show deploy history across projects
|
|
4076
|
+
npx ship-em monitor Monitor site uptime
|
|
4077
|
+
npx ship-em hooks Manage git deploy hooks
|
|
4078
|
+
npx ship-em config Manage global settings
|
|
4079
|
+
npx ship-em login Authenticate with GitHub
|
|
4080
|
+
npx ship-em down Take your app offline
|
|
2363
4081
|
`);
|
|
2364
|
-
program.command("deploy", { isDefault: true }).description("Detect and deploy your app (default command)").option("-y, --yes", "Skip all prompts and use defaults").option("-n, --name <name>", "Override app name").option("--skip-build", "Skip the build step (deploy existing output directory)").option("--direct", "Deploy directly using your Cloudflare credentials (bypass Shipem API)").action(async (options) => {
|
|
4082
|
+
program.command("deploy", { isDefault: true }).description("Detect and deploy your app (default command)").option("-y, --yes", "Skip all prompts and use defaults").option("-n, --name <name>", "Override app name").option("--skip-build", "Skip the build step (deploy existing output directory)").option("--turbo", "Skip the build step (alias for --skip-build)").option("--package <name>", "Deploy a specific package from a monorepo").option("--direct", "Deploy directly using your Cloudflare credentials (bypass Shipem API)").option("--verbose", "Show detailed debug output").option("--json", "Output machine-readable JSON instead of human-friendly text").option("--notify [url]", "Send deploy notification to webhook").option("--quiet", "Minimal output (just URL on success)").action(async (options) => {
|
|
4083
|
+
globalVerbose = options.verbose ?? false;
|
|
4084
|
+
globalJson = options.json ?? false;
|
|
2365
4085
|
try {
|
|
2366
4086
|
await deployCommand(options);
|
|
2367
4087
|
} catch (err) {
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
4088
|
+
handleError(err, "An unexpected error occurred.");
|
|
4089
|
+
}
|
|
4090
|
+
});
|
|
4091
|
+
program.command("fix").description("Auto-fix common build errors (missing deps, TypeScript issues, etc.)").option("--verbose", "Show detailed debug output").action(async (options) => {
|
|
4092
|
+
globalVerbose = options.verbose ?? false;
|
|
4093
|
+
try {
|
|
4094
|
+
await fixCommand(options);
|
|
4095
|
+
} catch (err) {
|
|
4096
|
+
handleError(err, "Fix command failed.");
|
|
4097
|
+
}
|
|
4098
|
+
});
|
|
4099
|
+
program.command("env").description("Scan and manage environment variables").action(async () => {
|
|
4100
|
+
try {
|
|
4101
|
+
await envCommand();
|
|
4102
|
+
} catch (err) {
|
|
4103
|
+
handleError(err, "Env command failed.");
|
|
4104
|
+
}
|
|
4105
|
+
});
|
|
4106
|
+
program.command("init").description("Bootstrap a new project from a description or template").option("-y, --yes", "Skip prompts and use defaults").option("-t, --template <name>", "Use a built-in template (run `shipem templates` to list)").action(async (options) => {
|
|
4107
|
+
try {
|
|
4108
|
+
await initCommand(options);
|
|
4109
|
+
} catch (err) {
|
|
4110
|
+
handleError(err, "Init command failed.");
|
|
4111
|
+
}
|
|
4112
|
+
});
|
|
4113
|
+
program.command("dev").description("Start dev server with deploy shortcut").action(async () => {
|
|
4114
|
+
try {
|
|
4115
|
+
await devCommand();
|
|
4116
|
+
} catch (err) {
|
|
4117
|
+
handleError(err, "Dev command failed.");
|
|
2374
4118
|
}
|
|
2375
4119
|
});
|
|
2376
4120
|
program.command("login").description("Log in to Shipem via GitHub").action(async () => {
|
|
2377
4121
|
try {
|
|
2378
4122
|
await loginCommand();
|
|
2379
4123
|
} catch (err) {
|
|
2380
|
-
|
|
2381
|
-
process.exit(1);
|
|
4124
|
+
handleError(err, "Login failed.");
|
|
2382
4125
|
}
|
|
2383
4126
|
});
|
|
2384
4127
|
program.command("logout").description("Log out of Shipem").action(async () => {
|
|
2385
4128
|
try {
|
|
2386
4129
|
await logoutCommand();
|
|
2387
4130
|
} catch (err) {
|
|
2388
|
-
|
|
2389
|
-
process.exit(1);
|
|
4131
|
+
handleError(err, "Logout failed.");
|
|
2390
4132
|
}
|
|
2391
4133
|
});
|
|
2392
4134
|
program.command("status").description("Check deployment status").action(async () => {
|
|
2393
4135
|
try {
|
|
2394
4136
|
await statusCommand();
|
|
2395
4137
|
} catch (err) {
|
|
2396
|
-
|
|
2397
|
-
process.exit(1);
|
|
4138
|
+
handleError(err, "An unexpected error occurred.");
|
|
2398
4139
|
}
|
|
2399
4140
|
});
|
|
2400
4141
|
program.command("logs").description("View recent deployment logs").option("-n, --lines <number>", "Number of log lines to show", "100").action(async (options) => {
|
|
2401
4142
|
try {
|
|
2402
4143
|
await logsCommand({ lines: options.lines ? parseInt(options.lines, 10) : void 0 });
|
|
2403
4144
|
} catch (err) {
|
|
2404
|
-
|
|
2405
|
-
process.exit(1);
|
|
4145
|
+
handleError(err, "An unexpected error occurred.");
|
|
2406
4146
|
}
|
|
2407
4147
|
});
|
|
2408
4148
|
program.command("down").description("Take down a deployment").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
|
|
2409
4149
|
try {
|
|
2410
4150
|
await downCommand(options);
|
|
2411
4151
|
} catch (err) {
|
|
2412
|
-
|
|
2413
|
-
|
|
4152
|
+
handleError(err, "An unexpected error occurred.");
|
|
4153
|
+
}
|
|
4154
|
+
});
|
|
4155
|
+
program.command("history").description("Show deploy history across all projects").action(async () => {
|
|
4156
|
+
try {
|
|
4157
|
+
await historyCommand();
|
|
4158
|
+
} catch (err) {
|
|
4159
|
+
handleError(err, "History command failed.");
|
|
4160
|
+
}
|
|
4161
|
+
});
|
|
4162
|
+
program.command("config [action] [key] [value]").description("Manage global Shipem settings").action(async (action, key, value) => {
|
|
4163
|
+
try {
|
|
4164
|
+
await configCommand(action, key, value);
|
|
4165
|
+
} catch (err) {
|
|
4166
|
+
handleError(err, "Config command failed.");
|
|
4167
|
+
}
|
|
4168
|
+
});
|
|
4169
|
+
program.command("watch").description("Watch for file changes and auto-deploy").action(async () => {
|
|
4170
|
+
try {
|
|
4171
|
+
await watchCommand();
|
|
4172
|
+
} catch (err) {
|
|
4173
|
+
handleError(err, "Watch command failed.");
|
|
4174
|
+
}
|
|
4175
|
+
});
|
|
4176
|
+
program.command("hooks [action]").description("Manage git hooks for auto-deploy (install|remove|status)").action(async (action) => {
|
|
4177
|
+
try {
|
|
4178
|
+
await hooksCommand(action);
|
|
4179
|
+
} catch (err) {
|
|
4180
|
+
handleError(err, "Hooks command failed.");
|
|
4181
|
+
}
|
|
4182
|
+
});
|
|
4183
|
+
var monitorCmd = program.command("monitor").description("Monitor deployed sites for uptime").option("--interval <minutes>", "Check interval in minutes (default: 5)").option("--daemon", "Run as background daemon").action(async (options) => {
|
|
4184
|
+
try {
|
|
4185
|
+
await monitorCommand(options);
|
|
4186
|
+
} catch (err) {
|
|
4187
|
+
handleError(err, "Monitor command failed.");
|
|
4188
|
+
}
|
|
4189
|
+
});
|
|
4190
|
+
monitorCmd.command("stop").description("Stop the monitor daemon").action(async () => {
|
|
4191
|
+
try {
|
|
4192
|
+
await monitorCommand({ stop: true });
|
|
4193
|
+
} catch (err) {
|
|
4194
|
+
handleError(err, "Monitor stop failed.");
|
|
2414
4195
|
}
|
|
2415
4196
|
});
|
|
2416
4197
|
var domainsCmd = program.command("domains").description("Manage custom domains (coming soon)");
|
|
@@ -2419,6 +4200,21 @@ domainsCmd.command("add <domain>").description("Add a custom domain to your depl
|
|
|
2419
4200
|
ui.dim("Follow our changelog at https://shipem.dev/changelog for updates.");
|
|
2420
4201
|
ui.br();
|
|
2421
4202
|
});
|
|
4203
|
+
program.command("preview").description("Deploy a preview URL from the current git branch").option("--skip-build", "Skip the build step").option("--verbose", "Show detailed debug output").action(async (options) => {
|
|
4204
|
+
globalVerbose = options.verbose ?? false;
|
|
4205
|
+
try {
|
|
4206
|
+
await previewCommand(options);
|
|
4207
|
+
} catch (err) {
|
|
4208
|
+
handleError(err, "Preview command failed.");
|
|
4209
|
+
}
|
|
4210
|
+
});
|
|
4211
|
+
program.command("templates").description("List available project templates for `shipem init --template`").action(async () => {
|
|
4212
|
+
try {
|
|
4213
|
+
await templatesCommand();
|
|
4214
|
+
} catch (err) {
|
|
4215
|
+
handleError(err, "Templates command failed.");
|
|
4216
|
+
}
|
|
4217
|
+
});
|
|
2422
4218
|
program.on("command:*", (cmds) => {
|
|
2423
4219
|
ui.error(`Unknown command: ${cmds[0]}`);
|
|
2424
4220
|
ui.dim("Run `shipem --help` to see available commands.");
|