ship-em 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2552 -696
- 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,14 +253,25 @@ 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);
|
|
257
|
+
console.log("");
|
|
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)}`);
|
|
232
264
|
console.log("");
|
|
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
|
+
}
|
|
233
270
|
console.log(` ${brand.bold("Next steps:")}`);
|
|
234
271
|
console.log(` ${brand.gray("Update")} \u2192 ${chalk.cyan("npx shipem")}`);
|
|
235
272
|
console.log(` ${brand.gray("Status")} \u2192 ${chalk.cyan("npx shipem status")}`);
|
|
236
273
|
console.log(` ${brand.gray("Remove")} \u2192 ${chalk.cyan("npx shipem down")}`);
|
|
237
274
|
console.log("");
|
|
238
|
-
console.log(` ${brand.dim("Tip: Share your app! Just send the URL.")}`);
|
|
239
|
-
console.log("");
|
|
240
275
|
},
|
|
241
276
|
// Format bytes
|
|
242
277
|
formatBytes(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 shipem")}`);
|
|
352
|
+
console.log(` ${brand.gray("Status")} \u2192 ${chalk.cyan("npx shipem status")}`);
|
|
353
|
+
console.log(` ${brand.gray("Remove")} \u2192 ${chalk.cyan("npx shipem 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
|
-
|
|
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 {
|
|
1179
1113
|
hasHandled = true;
|
|
1180
1114
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
1181
1115
|
res.end('<html><body style="font-family:sans-serif;padding:40px">Authentication failed. Please try again.</body></html>');
|
|
1182
|
-
rejectToken(new
|
|
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 shipem --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 shipem` 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,37 +1751,113 @@ 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
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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;
|
|
1840
|
+
const deployment = res.data.result;
|
|
1841
|
+
const stage = deployment.latest_stage;
|
|
1842
|
+
deploySpinner.text = `Deploying... (${stage?.name ?? "initializing"})`;
|
|
1505
1843
|
if (stage?.status === "success") {
|
|
1506
1844
|
deploySpinner.succeed("Deployed successfully");
|
|
1507
1845
|
return deployment;
|
|
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 shipem` 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,15 @@ async function deployCommand(options) {
|
|
|
1692
2108
|
} else {
|
|
1693
2109
|
ui.banner();
|
|
1694
2110
|
}
|
|
2111
|
+
const isAnonymous = !getSessionToken() && !getCloudflareCredentials();
|
|
1695
2112
|
if (isFirstRun) {
|
|
1696
2113
|
console.log(` ${chalk4.white.bold("Welcome to Shipem!")} \u2728`);
|
|
1697
2114
|
console.log("");
|
|
1698
|
-
console.log(` ${chalk4.dim("
|
|
1699
|
-
console.log(` ${chalk4.dim("1. Sign in with GitHub")}`);
|
|
1700
|
-
console.log(` ${chalk4.dim("2. We handle everything else")}`);
|
|
2115
|
+
console.log(` ${chalk4.dim("No account needed. Deploying your app now...")}`);
|
|
1701
2116
|
console.log("");
|
|
1702
2117
|
}
|
|
1703
2118
|
let sessionToken = getSessionToken();
|
|
1704
|
-
|
|
1705
|
-
await loginCommand({ skipBanner: true });
|
|
1706
|
-
sessionToken = getSessionToken();
|
|
1707
|
-
if (!sessionToken) {
|
|
1708
|
-
ui.fatal("Login failed. Please try again with: shipem login");
|
|
1709
|
-
}
|
|
1710
|
-
console.log("");
|
|
1711
|
-
console.log(` ${chalk4.hex("#22C55E")("\u2713")} ${chalk4.bold("Signed in!")}`);
|
|
1712
|
-
console.log("");
|
|
1713
|
-
console.log(` Now deploying your app...`);
|
|
1714
|
-
console.log("");
|
|
1715
|
-
}
|
|
2119
|
+
phaseStart = Date.now();
|
|
1716
2120
|
ui.section("Scanning project...");
|
|
1717
2121
|
ui.br();
|
|
1718
2122
|
let projectConfig;
|
|
@@ -1721,41 +2125,40 @@ async function deployCommand(options) {
|
|
|
1721
2125
|
ui.success(`Using saved config: ${ui.framework(projectConfig.framework)}`);
|
|
1722
2126
|
} else {
|
|
1723
2127
|
const scanSpinner = ui.spinner("Scanning project files");
|
|
1724
|
-
|
|
2128
|
+
let heuristicResult = scanProject(cwd);
|
|
1725
2129
|
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
|
-
|
|
2130
|
+
if (heuristicResult.monorepo && heuristicResult.monorepo.packages.length > 0) {
|
|
2131
|
+
const mono = heuristicResult.monorepo;
|
|
2132
|
+
ui.info(`Monorepo detected (${mono.type}) \u2014 ${mono.packages.length} packages found`);
|
|
2133
|
+
ui.br();
|
|
2134
|
+
let packageName = options.package;
|
|
2135
|
+
if (!packageName && !options.yes) {
|
|
2136
|
+
const { selected } = await inquirer.prompt([
|
|
2137
|
+
{
|
|
2138
|
+
type: "list",
|
|
2139
|
+
name: "selected",
|
|
2140
|
+
message: "Which package to deploy?",
|
|
2141
|
+
choices: mono.packages.map((p) => ({ name: p, value: p }))
|
|
2142
|
+
}
|
|
2143
|
+
]);
|
|
2144
|
+
packageName = selected;
|
|
2145
|
+
} else if (!packageName) {
|
|
2146
|
+
packageName = mono.packages[0];
|
|
2147
|
+
}
|
|
2148
|
+
if (packageName) {
|
|
2149
|
+
const commonDirs = ["packages", "apps", "projects", "services"];
|
|
2150
|
+
for (const dir of commonDirs) {
|
|
2151
|
+
const pkgDir = join8(cwd, dir, packageName);
|
|
2152
|
+
if (existsSync6(pkgDir)) {
|
|
2153
|
+
ui.info(`Deploying package: ${chalk4.cyan(packageName)} (${dir}/${packageName})`);
|
|
2154
|
+
heuristicResult = scanProject(pkgDir);
|
|
2155
|
+
break;
|
|
2156
|
+
}
|
|
1751
2157
|
}
|
|
1752
|
-
} catch {
|
|
1753
|
-
aiSpinner.warn("AI analysis failed, using heuristic detection");
|
|
1754
|
-
finalDetection = heuristicAsAnalysis;
|
|
1755
2158
|
}
|
|
1756
|
-
} else {
|
|
1757
|
-
finalDetection = heuristicAsAnalysis;
|
|
1758
2159
|
}
|
|
2160
|
+
const sourceFileCount = countSourceFiles(cwd);
|
|
2161
|
+
const finalDetection = heuristicResult;
|
|
1759
2162
|
const projectName = options.name ?? heuristicResult.projectName;
|
|
1760
2163
|
ui.projectAnalysis({
|
|
1761
2164
|
name: projectName,
|
|
@@ -1766,11 +2169,11 @@ async function deployCommand(options) {
|
|
|
1766
2169
|
deployTarget: finalDetection.deployTarget,
|
|
1767
2170
|
envVarCount: finalDetection.envVars.length
|
|
1768
2171
|
});
|
|
1769
|
-
if (finalDetection.confidence <
|
|
2172
|
+
if (finalDetection.confidence < LOW_CONFIDENCE_THRESHOLD && !options.yes) {
|
|
1770
2173
|
ui.warn(`Low confidence detection (${Math.round(finalDetection.confidence * 100)}%). Please verify the settings.`);
|
|
1771
2174
|
ui.br();
|
|
1772
2175
|
}
|
|
1773
|
-
if (finalDetection.confidence <
|
|
2176
|
+
if (finalDetection.confidence < NOT_A_PROJECT_THRESHOLD && !existsSync6(join8(cwd, "package.json")) && !existsSync6(join8(cwd, "requirements.txt")) && !existsSync6(join8(cwd, "index.html"))) {
|
|
1774
2177
|
ui.friendlyError(
|
|
1775
2178
|
"This does not look like a project",
|
|
1776
2179
|
"No package.json, requirements.txt, or index.html found",
|
|
@@ -1858,66 +2261,80 @@ async function deployCommand(options) {
|
|
|
1858
2261
|
}
|
|
1859
2262
|
}
|
|
1860
2263
|
}
|
|
1861
|
-
|
|
2264
|
+
phases.push({ name: "Scan", durationMs: Date.now() - phaseStart });
|
|
2265
|
+
if (!options.skipBuild && !options.turbo) {
|
|
2266
|
+
phaseStart = Date.now();
|
|
1862
2267
|
ui.section("Building...");
|
|
1863
2268
|
ui.br();
|
|
1864
2269
|
let buildResult = await buildProject(projectConfig, cwd);
|
|
1865
2270
|
if (!buildResult.success) {
|
|
1866
2271
|
ui.br();
|
|
1867
2272
|
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
|
-
}
|
|
2273
|
+
showBuildErrorHelp(errMsg);
|
|
1882
2274
|
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
|
-
]
|
|
2275
|
+
const fixResult = await tryInlineFix(errMsg, cwd, projectConfig);
|
|
2276
|
+
if (fixResult?.success) {
|
|
2277
|
+
ui.success("Fixed! Continuing deploy...");
|
|
2278
|
+
ui.br();
|
|
2279
|
+
} else {
|
|
2280
|
+
process.exit(1);
|
|
1895
2281
|
}
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
2282
|
+
} else {
|
|
2283
|
+
const { action } = await inquirer.prompt([
|
|
2284
|
+
{
|
|
2285
|
+
type: "list",
|
|
2286
|
+
name: "action",
|
|
2287
|
+
message: "Build failed. What would you like to do?",
|
|
2288
|
+
choices: [
|
|
2289
|
+
{ name: "Auto-fix and retry (recommended)", value: "fix" },
|
|
2290
|
+
{ name: "Try again without fixing", value: "retry" },
|
|
2291
|
+
{ name: "Deploy without building (use existing output directory)", value: "skip" },
|
|
2292
|
+
{ name: "Quit", value: "quit" }
|
|
2293
|
+
]
|
|
2294
|
+
}
|
|
2295
|
+
]);
|
|
2296
|
+
if (action === "quit") {
|
|
1907
2297
|
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
2298
|
process.exit(1);
|
|
1914
2299
|
}
|
|
2300
|
+
if (action === "fix") {
|
|
2301
|
+
const fixResult = await tryInlineFix(errMsg, cwd, projectConfig);
|
|
2302
|
+
if (fixResult?.success) {
|
|
2303
|
+
ui.success("Fixed! Continuing deploy...");
|
|
2304
|
+
ui.br();
|
|
2305
|
+
} else {
|
|
2306
|
+
ui.br();
|
|
2307
|
+
ui.dim("Tip: Paste this error into your AI coding tool \u2014 it can usually fix it.");
|
|
2308
|
+
process.exit(1);
|
|
2309
|
+
}
|
|
2310
|
+
} else if (action === "retry") {
|
|
2311
|
+
ui.br();
|
|
2312
|
+
ui.section("Building (retry)...");
|
|
2313
|
+
ui.br();
|
|
2314
|
+
buildResult = await buildProject(projectConfig, cwd);
|
|
2315
|
+
if (!buildResult.success) {
|
|
2316
|
+
ui.br();
|
|
2317
|
+
showBuildErrorHelp(buildResult.error ?? "Unknown build error");
|
|
2318
|
+
ui.dim("Tip: Paste this error into your AI coding tool \u2014 it can usually fix it.");
|
|
2319
|
+
process.exit(1);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
1915
2322
|
}
|
|
1916
2323
|
ui.br();
|
|
1917
2324
|
} else {
|
|
1918
2325
|
ui.br();
|
|
1919
2326
|
}
|
|
2327
|
+
phases.push({ name: "Build", durationMs: Date.now() - phaseStart });
|
|
1920
2328
|
}
|
|
2329
|
+
if (isAnonymous) {
|
|
2330
|
+
const outputPath = join8(cwd, projectConfig.outputDirectory);
|
|
2331
|
+
const badgeCount = injectBadge(outputPath);
|
|
2332
|
+
if (badgeCount > 0) {
|
|
2333
|
+
ui.dim(`Added "Shipped with Shipem" badge to ${badgeCount} HTML file${badgeCount > 1 ? "s" : ""}`);
|
|
2334
|
+
ui.br();
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
phaseStart = Date.now();
|
|
1921
2338
|
ui.section("Deploying...");
|
|
1922
2339
|
ui.br();
|
|
1923
2340
|
let liveUrl;
|
|
@@ -1928,12 +2345,9 @@ async function deployCommand(options) {
|
|
|
1928
2345
|
const useDirectDeploy = options.direct === true || cfCreds !== null;
|
|
1929
2346
|
if (useDirectDeploy) {
|
|
1930
2347
|
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"
|
|
2348
|
+
throw new ConfigError(
|
|
2349
|
+
"Direct deploy requires Cloudflare credentials. Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables."
|
|
1935
2350
|
);
|
|
1936
|
-
process.exit(1);
|
|
1937
2351
|
}
|
|
1938
2352
|
ui.dim("Direct Cloudflare deployment (using your credentials)...");
|
|
1939
2353
|
ui.br();
|
|
@@ -1951,18 +2365,13 @@ async function deployCommand(options) {
|
|
|
1951
2365
|
liveUrl = finalDeployment.url || cf.getProjectUrl(cfProjectName);
|
|
1952
2366
|
projectId = cfProjectName;
|
|
1953
2367
|
} catch (err) {
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
"Deployment failed",
|
|
1957
|
-
msg,
|
|
1958
|
-
"Check your Cloudflare credentials and try again"
|
|
1959
|
-
);
|
|
1960
|
-
process.exit(1);
|
|
2368
|
+
if (err instanceof DeployError) throw err;
|
|
2369
|
+
throw new DeployError(err instanceof Error ? err.message : String(err), { cause: err });
|
|
1961
2370
|
}
|
|
1962
|
-
} else {
|
|
1963
|
-
const tarPath =
|
|
2371
|
+
} else if (sessionToken) {
|
|
2372
|
+
const tarPath = join8(tmpdir(), `shipem-${Date.now()}.tar.gz`);
|
|
1964
2373
|
try {
|
|
1965
|
-
const outputPath =
|
|
2374
|
+
const outputPath = join8(cwd, projectConfig.outputDirectory);
|
|
1966
2375
|
warnIfEnvFilesInOutputDir(outputPath);
|
|
1967
2376
|
const stats = countOutputFiles(outputPath);
|
|
1968
2377
|
deployFileCount = stats.fileCount;
|
|
@@ -2003,7 +2412,7 @@ async function deployCommand(options) {
|
|
|
2003
2412
|
Authorization: `Bearer ${sessionToken}`
|
|
2004
2413
|
},
|
|
2005
2414
|
maxBodyLength: Infinity,
|
|
2006
|
-
timeout:
|
|
2415
|
+
timeout: DEPLOY_TIMEOUT_MS2,
|
|
2007
2416
|
onUploadProgress: (evt) => {
|
|
2008
2417
|
if (!activatingSpinner && evt.total && evt.loaded >= evt.total) {
|
|
2009
2418
|
const uploadSec = ((Date.now() - uploadStart) / 1e3).toFixed(1);
|
|
@@ -2025,7 +2434,7 @@ async function deployCommand(options) {
|
|
|
2025
2434
|
await loginCommand({ skipBanner: true });
|
|
2026
2435
|
sessionToken = getSessionToken();
|
|
2027
2436
|
if (!sessionToken) {
|
|
2028
|
-
throw new
|
|
2437
|
+
throw new AuthError("Login failed after session expiry. Please run: shipem login");
|
|
2029
2438
|
}
|
|
2030
2439
|
const retryForm = new FormData2();
|
|
2031
2440
|
retryForm.append("config", JSON.stringify({
|
|
@@ -2047,7 +2456,7 @@ async function deployCommand(options) {
|
|
|
2047
2456
|
response = await apiRequest(deployConfig);
|
|
2048
2457
|
} catch (retryErr) {
|
|
2049
2458
|
if (axios2.isAxiosError(retryErr) && retryErr.response?.status === 401) {
|
|
2050
|
-
throw new
|
|
2459
|
+
throw new AuthError("Authentication failed after re-login. Please run: shipem login");
|
|
2051
2460
|
}
|
|
2052
2461
|
throw retryErr;
|
|
2053
2462
|
}
|
|
@@ -2063,21 +2472,105 @@ async function deployCommand(options) {
|
|
|
2063
2472
|
liveUrl = response.data.url;
|
|
2064
2473
|
projectId = response.data.projectId;
|
|
2065
2474
|
} catch (err) {
|
|
2475
|
+
if (err instanceof AuthError || err instanceof NetworkError || err instanceof DeployError) {
|
|
2476
|
+
throw err;
|
|
2477
|
+
}
|
|
2066
2478
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
);
|
|
2073
|
-
} else {
|
|
2074
|
-
ui.friendlyError(
|
|
2075
|
-
"Deployment failed",
|
|
2076
|
-
msg,
|
|
2077
|
-
"Check your connection and try again with: npx shipem"
|
|
2078
|
-
);
|
|
2479
|
+
throw new DeployError(`Deployment failed: ${msg}`, { cause: err });
|
|
2480
|
+
} finally {
|
|
2481
|
+
try {
|
|
2482
|
+
rmSync(tarPath);
|
|
2483
|
+
} catch {
|
|
2079
2484
|
}
|
|
2080
|
-
|
|
2485
|
+
}
|
|
2486
|
+
} else {
|
|
2487
|
+
ui.br();
|
|
2488
|
+
ui.warn("No Cloudflare credentials found and not logged in.");
|
|
2489
|
+
ui.br();
|
|
2490
|
+
console.log(` ${chalk4.bold("To deploy, choose one option:")}`);
|
|
2491
|
+
console.log("");
|
|
2492
|
+
console.log(` ${chalk4.cyan("Option 1:")} Set Cloudflare credentials (free account)`);
|
|
2493
|
+
console.log(` ${chalk4.dim("export CLOUDFLARE_API_TOKEN=your_token")}`);
|
|
2494
|
+
console.log(` ${chalk4.dim("export CLOUDFLARE_ACCOUNT_ID=your_account_id")}`);
|
|
2495
|
+
console.log(` ${chalk4.dim("Get these from: https://dash.cloudflare.com/profile/api-tokens")}`);
|
|
2496
|
+
console.log("");
|
|
2497
|
+
console.log(` ${chalk4.cyan("Option 2:")} Log in to Shipem`);
|
|
2498
|
+
console.log(` ${chalk4.dim("npx shipem login")}`);
|
|
2499
|
+
console.log("");
|
|
2500
|
+
const { action } = await inquirer.prompt([
|
|
2501
|
+
{
|
|
2502
|
+
type: "list",
|
|
2503
|
+
name: "action",
|
|
2504
|
+
message: "What would you like to do?",
|
|
2505
|
+
choices: [
|
|
2506
|
+
{ name: "Log in with GitHub (sets up deployment)", value: "login" },
|
|
2507
|
+
{ name: "Quit (I'll set up Cloudflare credentials)", value: "quit" }
|
|
2508
|
+
]
|
|
2509
|
+
}
|
|
2510
|
+
]);
|
|
2511
|
+
if (action === "quit") {
|
|
2512
|
+
process.exit(0);
|
|
2513
|
+
}
|
|
2514
|
+
await loginCommand({ skipBanner: true });
|
|
2515
|
+
sessionToken = getSessionToken();
|
|
2516
|
+
if (!sessionToken) {
|
|
2517
|
+
throw new AuthError("Login failed. Please try again with: shipem login");
|
|
2518
|
+
}
|
|
2519
|
+
console.log("");
|
|
2520
|
+
console.log(` ${chalk4.hex("#22C55E")("\u2713")} ${chalk4.bold("Signed in!")}`);
|
|
2521
|
+
console.log("");
|
|
2522
|
+
const tarPath = join8(tmpdir(), `shipem-${Date.now()}.tar.gz`);
|
|
2523
|
+
try {
|
|
2524
|
+
const outputPath = join8(cwd, projectConfig.outputDirectory);
|
|
2525
|
+
warnIfEnvFilesInOutputDir(outputPath);
|
|
2526
|
+
const stats = countOutputFiles(outputPath);
|
|
2527
|
+
deployFileCount = stats.fileCount;
|
|
2528
|
+
deployTotalBytes = stats.totalBytes;
|
|
2529
|
+
const packagingSpinner = ui.spinner("Packaging files...");
|
|
2530
|
+
await tarCreate(
|
|
2531
|
+
{
|
|
2532
|
+
gzip: true,
|
|
2533
|
+
file: tarPath,
|
|
2534
|
+
cwd: outputPath,
|
|
2535
|
+
filter: (filePath) => !shouldExclude(filePath, cwd)
|
|
2536
|
+
},
|
|
2537
|
+
["."]
|
|
2538
|
+
);
|
|
2539
|
+
packagingSpinner.succeed(
|
|
2540
|
+
`Packaged ${deployFileCount} files (${ui.formatBytes(deployTotalBytes)})`
|
|
2541
|
+
);
|
|
2542
|
+
const uploadSpinner = ui.spinner("Uploading to cloud...");
|
|
2543
|
+
const form = new FormData2();
|
|
2544
|
+
form.append("config", JSON.stringify({
|
|
2545
|
+
name: projectConfig.name,
|
|
2546
|
+
framework: projectConfig.framework,
|
|
2547
|
+
deployTarget: projectConfig.deployTarget,
|
|
2548
|
+
outputDirectory: projectConfig.outputDirectory
|
|
2549
|
+
}));
|
|
2550
|
+
form.append("artifacts", createReadStream(tarPath), {
|
|
2551
|
+
filename: "artifacts.tar.gz",
|
|
2552
|
+
contentType: "application/gzip"
|
|
2553
|
+
});
|
|
2554
|
+
const response = await apiRequest({
|
|
2555
|
+
method: "post",
|
|
2556
|
+
url: `${SHIPEM_API_URL}/projects/deploy`,
|
|
2557
|
+
data: form,
|
|
2558
|
+
headers: {
|
|
2559
|
+
...form.getHeaders(),
|
|
2560
|
+
Authorization: `Bearer ${sessionToken}`
|
|
2561
|
+
},
|
|
2562
|
+
maxBodyLength: Infinity,
|
|
2563
|
+
timeout: DEPLOY_TIMEOUT_MS2
|
|
2564
|
+
});
|
|
2565
|
+
uploadSpinner.succeed("Deployed successfully");
|
|
2566
|
+
liveUrl = response.data.url;
|
|
2567
|
+
projectId = response.data.projectId;
|
|
2568
|
+
} catch (err) {
|
|
2569
|
+
if (err instanceof AuthError || err instanceof NetworkError || err instanceof DeployError) {
|
|
2570
|
+
throw err;
|
|
2571
|
+
}
|
|
2572
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2573
|
+
throw new DeployError(`Deployment failed: ${msg}`, { cause: err });
|
|
2081
2574
|
} finally {
|
|
2082
2575
|
try {
|
|
2083
2576
|
rmSync(tarPath);
|
|
@@ -2085,6 +2578,8 @@ async function deployCommand(options) {
|
|
|
2085
2578
|
}
|
|
2086
2579
|
}
|
|
2087
2580
|
}
|
|
2581
|
+
phases.push({ name: "Upload", durationMs: Date.now() - phaseStart });
|
|
2582
|
+
const elapsedSec = (Date.now() - startTime) / 1e3;
|
|
2088
2583
|
const configToSave = {
|
|
2089
2584
|
...projectConfig,
|
|
2090
2585
|
envVars: projectConfig.envVars.map(({ value: _stripped, ...rest }) => rest)
|
|
@@ -2097,61 +2592,747 @@ async function deployCommand(options) {
|
|
|
2097
2592
|
deployedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2098
2593
|
status: "success"
|
|
2099
2594
|
};
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2595
|
+
const deployRecord = {
|
|
2596
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2597
|
+
url: liveUrl,
|
|
2598
|
+
duration: elapsedSec,
|
|
2599
|
+
files: deployFileCount,
|
|
2600
|
+
size: ui.formatBytes(deployTotalBytes),
|
|
2601
|
+
framework: projectConfig.framework
|
|
2602
|
+
};
|
|
2603
|
+
const existingHistory = existingConfig.deployments ?? [];
|
|
2604
|
+
const updatedHistory = [deployRecord, ...existingHistory].slice(0, 20);
|
|
2605
|
+
writeProjectConfig({
|
|
2606
|
+
project: configToSave,
|
|
2607
|
+
deployment: deploymentState,
|
|
2608
|
+
deployments: updatedHistory
|
|
2609
|
+
}, cwd);
|
|
2610
|
+
const gitignorePath = join8(cwd, ".gitignore");
|
|
2611
|
+
if (existsSync6(gitignorePath)) {
|
|
2612
|
+
const gitignoreContent = readFileSync7(gitignorePath, "utf-8");
|
|
2104
2613
|
const lines = gitignoreContent.split("\n").map((l) => l.trim());
|
|
2105
2614
|
if (!lines.includes("shipem.json")) {
|
|
2106
2615
|
appendFileSync(gitignorePath, "\n# Shipem config\nshipem.json\n");
|
|
2107
2616
|
}
|
|
2108
2617
|
} else {
|
|
2109
|
-
|
|
2618
|
+
writeFileSync4(gitignorePath, "# Shipem config\nshipem.json\n");
|
|
2110
2619
|
}
|
|
2111
|
-
|
|
2112
|
-
|
|
2620
|
+
ui.deployBoxEnhanced(
|
|
2621
|
+
projectConfig.name,
|
|
2622
|
+
liveUrl,
|
|
2623
|
+
phases,
|
|
2624
|
+
deployFileCount,
|
|
2625
|
+
deployTotalBytes,
|
|
2626
|
+
elapsedSec,
|
|
2627
|
+
isAnonymous
|
|
2628
|
+
);
|
|
2113
2629
|
}
|
|
2114
2630
|
|
|
2115
|
-
// src/commands/
|
|
2116
|
-
import
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2631
|
+
// src/commands/env.ts
|
|
2632
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, readdirSync as readdirSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
2633
|
+
import { join as join9 } from "path";
|
|
2634
|
+
var SERVICE_LINKS = {
|
|
2635
|
+
SUPABASE_URL: { name: "Supabase", url: "https://app.supabase.com/project/_/settings/api" },
|
|
2636
|
+
SUPABASE_ANON_KEY: { name: "Supabase", url: "https://app.supabase.com/project/_/settings/api" },
|
|
2637
|
+
NEXT_PUBLIC_SUPABASE_URL: { name: "Supabase", url: "https://app.supabase.com/project/_/settings/api" },
|
|
2638
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY: { name: "Supabase", url: "https://app.supabase.com/project/_/settings/api" },
|
|
2639
|
+
VITE_SUPABASE_URL: { name: "Supabase", url: "https://app.supabase.com/project/_/settings/api" },
|
|
2640
|
+
VITE_SUPABASE_ANON_KEY: { name: "Supabase", url: "https://app.supabase.com/project/_/settings/api" },
|
|
2641
|
+
OPENAI_API_KEY: { name: "OpenAI", url: "https://platform.openai.com/api-keys" },
|
|
2642
|
+
ANTHROPIC_API_KEY: { name: "Anthropic", url: "https://console.anthropic.com/settings/keys" },
|
|
2643
|
+
STRIPE_SECRET_KEY: { name: "Stripe", url: "https://dashboard.stripe.com/apikeys" },
|
|
2644
|
+
STRIPE_PUBLISHABLE_KEY: { name: "Stripe", url: "https://dashboard.stripe.com/apikeys" },
|
|
2645
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: { name: "Stripe", url: "https://dashboard.stripe.com/apikeys" },
|
|
2646
|
+
FIREBASE_API_KEY: { name: "Firebase", url: "https://console.firebase.google.com/project/_/settings/general" },
|
|
2647
|
+
NEXT_PUBLIC_FIREBASE_API_KEY: { name: "Firebase", url: "https://console.firebase.google.com/project/_/settings/general" },
|
|
2648
|
+
FIREBASE_PROJECT_ID: { name: "Firebase", url: "https://console.firebase.google.com/project/_/settings/general" },
|
|
2649
|
+
DATABASE_URL: { name: "Database", url: "" },
|
|
2650
|
+
RESEND_API_KEY: { name: "Resend", url: "https://resend.com/api-keys" },
|
|
2651
|
+
CLERK_SECRET_KEY: { name: "Clerk", url: "https://dashboard.clerk.com/last-active?path=api-keys" },
|
|
2652
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: { name: "Clerk", url: "https://dashboard.clerk.com/last-active?path=api-keys" }
|
|
2653
|
+
};
|
|
2654
|
+
function scanSourceForEnvVars(cwd) {
|
|
2655
|
+
const found = /* @__PURE__ */ new Set();
|
|
2656
|
+
const patterns = [
|
|
2657
|
+
/process\.env\.([A-Z_][A-Z0-9_]*)/g,
|
|
2658
|
+
/import\.meta\.env\.([A-Z_][A-Z0-9_]*)/g,
|
|
2659
|
+
/(?:NEXT_PUBLIC_|VITE_|NUXT_|GATSBY_)([A-Z0-9_]*)/g
|
|
2660
|
+
];
|
|
2661
|
+
function scanDir(dir, depth) {
|
|
2662
|
+
if (depth > 5) return;
|
|
2663
|
+
try {
|
|
2664
|
+
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
2665
|
+
for (const entry of entries) {
|
|
2666
|
+
if (["node_modules", ".git", "dist", ".next", "build", ".svelte-kit", ".output", ".turbo", "coverage"].includes(entry.name)) continue;
|
|
2667
|
+
const fullPath = join9(dir, entry.name);
|
|
2668
|
+
if (entry.isDirectory()) {
|
|
2669
|
+
scanDir(fullPath, depth + 1);
|
|
2670
|
+
} else if (/\.(ts|tsx|js|jsx|mjs|mts|vue|svelte|astro)$/.test(entry.name)) {
|
|
2671
|
+
try {
|
|
2672
|
+
const content = readFileSync8(fullPath, "utf-8");
|
|
2673
|
+
for (const pat of patterns) {
|
|
2674
|
+
pat.lastIndex = 0;
|
|
2675
|
+
let match;
|
|
2676
|
+
while ((match = pat.exec(content)) !== null) {
|
|
2677
|
+
if (pat.source.includes("NEXT_PUBLIC_") && !match[0].includes("process.env") && !match[0].includes("import.meta.env")) {
|
|
2678
|
+
found.add(match[0]);
|
|
2679
|
+
} else {
|
|
2680
|
+
found.add(match[1]);
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
} catch {
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
} catch {
|
|
2689
|
+
}
|
|
2124
2690
|
}
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2691
|
+
scanDir(cwd, 0);
|
|
2692
|
+
const ignore = /* @__PURE__ */ new Set(["NODE_ENV", "PORT", "HOST", "CI", "HOME", "PWD", "PATH", "TERM", "USER", "SHELL", "LANG", "TZ"]);
|
|
2693
|
+
return [...found].filter((v) => !ignore.has(v)).sort();
|
|
2694
|
+
}
|
|
2695
|
+
function readEnvFile(cwd) {
|
|
2696
|
+
const vars = /* @__PURE__ */ new Set();
|
|
2697
|
+
const envFiles = [".env", ".env.local", ".env.development", ".env.production"];
|
|
2698
|
+
for (const file of envFiles) {
|
|
2699
|
+
const content = readFile2(join9(cwd, file));
|
|
2700
|
+
if (content) {
|
|
2701
|
+
for (const line of content.split("\n")) {
|
|
2702
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
|
|
2703
|
+
if (match) vars.add(match[1]);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
return vars;
|
|
2708
|
+
}
|
|
2709
|
+
function readFile2(path) {
|
|
2710
|
+
try {
|
|
2711
|
+
return readFileSync8(path, "utf-8");
|
|
2712
|
+
} catch {
|
|
2713
|
+
return null;
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
async function envCommand() {
|
|
2717
|
+
const cwd = process.cwd();
|
|
2718
|
+
console.log("");
|
|
2719
|
+
console.log(` ${brand.blue.bold("\u{1F511} Environment Variable Intelligence")}`);
|
|
2720
|
+
console.log("");
|
|
2721
|
+
const spinner = ui.spinner("Scanning source code for environment variables...");
|
|
2722
|
+
const sourceVars = scanSourceForEnvVars(cwd);
|
|
2723
|
+
const envFileVars = readEnvFile(cwd);
|
|
2724
|
+
spinner.succeed(`Found ${sourceVars.length} env vars referenced in code`);
|
|
2725
|
+
console.log("");
|
|
2726
|
+
if (sourceVars.length === 0) {
|
|
2727
|
+
ui.success("No environment variables detected in source code.");
|
|
2728
|
+
ui.br();
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
const present = [];
|
|
2732
|
+
const missing = [];
|
|
2733
|
+
const inEnv = [];
|
|
2734
|
+
for (const v of sourceVars) {
|
|
2735
|
+
if (process.env[v]) {
|
|
2736
|
+
inEnv.push(v);
|
|
2737
|
+
} else if (envFileVars.has(v)) {
|
|
2738
|
+
present.push(v);
|
|
2739
|
+
} else {
|
|
2740
|
+
missing.push(v);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
if (present.length > 0) {
|
|
2744
|
+
console.log(` ${brand.green("\u2713")} ${brand.bold("Set in .env file:")}`);
|
|
2745
|
+
for (const v of present) {
|
|
2746
|
+
console.log(` ${brand.green("\xB7")} ${v}`);
|
|
2747
|
+
}
|
|
2748
|
+
console.log("");
|
|
2749
|
+
}
|
|
2750
|
+
if (inEnv.length > 0) {
|
|
2751
|
+
console.log(` ${brand.green("\u2713")} ${brand.bold("Set in environment:")}`);
|
|
2752
|
+
for (const v of inEnv) {
|
|
2753
|
+
console.log(` ${brand.green("\xB7")} ${v}`);
|
|
2754
|
+
}
|
|
2755
|
+
console.log("");
|
|
2756
|
+
}
|
|
2757
|
+
if (missing.length > 0) {
|
|
2758
|
+
console.log(` ${brand.yellow("\u26A0")} ${brand.bold("Missing (not set):")}`);
|
|
2759
|
+
for (const v of missing) {
|
|
2760
|
+
const service = SERVICE_LINKS[v];
|
|
2761
|
+
if (service && service.url) {
|
|
2762
|
+
console.log(` ${brand.red("\xB7")} ${v} ${brand.dim("\u2192")} ${brand.brightBlue.underline(service.url)}`);
|
|
2763
|
+
} else {
|
|
2764
|
+
console.log(` ${brand.red("\xB7")} ${v}`);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
console.log("");
|
|
2768
|
+
}
|
|
2769
|
+
const envExamplePath = join9(cwd, ".env.example");
|
|
2770
|
+
if (!existsSync7(envExamplePath) && sourceVars.length > 0) {
|
|
2771
|
+
const content = sourceVars.map((v) => {
|
|
2772
|
+
const service = SERVICE_LINKS[v];
|
|
2773
|
+
const comment = service ? ` # ${service.name}` : "";
|
|
2774
|
+
return `${v}=${comment}`;
|
|
2775
|
+
}).join("\n") + "\n";
|
|
2776
|
+
writeFileSync5(envExamplePath, content, "utf-8");
|
|
2777
|
+
ui.success("Generated .env.example from detected variables");
|
|
2778
|
+
console.log("");
|
|
2779
|
+
} else if (existsSync7(envExamplePath)) {
|
|
2780
|
+
ui.dim(".env.example already exists");
|
|
2781
|
+
console.log("");
|
|
2782
|
+
}
|
|
2783
|
+
const total = sourceVars.length;
|
|
2784
|
+
const setCount = present.length + inEnv.length;
|
|
2785
|
+
if (missing.length === 0) {
|
|
2786
|
+
ui.success(`All ${total} environment variables are set.`);
|
|
2787
|
+
} else {
|
|
2788
|
+
ui.warn(`${missing.length} of ${total} env vars are missing. Your build may fail.`);
|
|
2789
|
+
ui.dim("Set them in a .env file or export them in your shell.");
|
|
2790
|
+
}
|
|
2791
|
+
console.log("");
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
// src/commands/init.ts
|
|
2795
|
+
import inquirer2 from "inquirer";
|
|
2796
|
+
import { execa as execa3 } from "execa";
|
|
2797
|
+
import { existsSync as existsSync8, writeFileSync as writeFileSync6, mkdirSync } from "fs";
|
|
2798
|
+
import { join as join10 } from "path";
|
|
2799
|
+
import chalk6 from "chalk";
|
|
2800
|
+
|
|
2801
|
+
// src/commands/templates.ts
|
|
2802
|
+
import chalk5 from "chalk";
|
|
2803
|
+
var TEMPLATES = [
|
|
2804
|
+
{
|
|
2805
|
+
name: "astro-blog",
|
|
2806
|
+
description: "Astro blog with Markdown/MDX support and RSS feed",
|
|
2807
|
+
framework: "astro",
|
|
2808
|
+
createCmd: ["npm", "create", "astro@latest", "--", "--template", "blog"],
|
|
2809
|
+
postScaffold: ["npx", "astro", "add", "mdx", "--yes"]
|
|
2810
|
+
},
|
|
2811
|
+
{
|
|
2812
|
+
name: "nextjs-saas",
|
|
2813
|
+
description: "Next.js SaaS starter with App Router, Tailwind CSS, and TypeScript",
|
|
2814
|
+
framework: "nextjs",
|
|
2815
|
+
createCmd: ["npx", "create-next-app@latest", "--", "--ts", "--app", "--tailwind", "--eslint", "--src-dir", "--no-import-alias"]
|
|
2816
|
+
},
|
|
2817
|
+
{
|
|
2818
|
+
name: "vite-react",
|
|
2819
|
+
description: "Vite + React + TypeScript \u2014 fast SPA starter",
|
|
2820
|
+
framework: "vite-react",
|
|
2821
|
+
createCmd: ["npm", "create", "vite@latest", "--", "--template", "react-ts"]
|
|
2822
|
+
},
|
|
2823
|
+
{
|
|
2824
|
+
name: "sveltekit-app",
|
|
2825
|
+
description: "SvelteKit full-stack app with TypeScript",
|
|
2826
|
+
framework: "sveltekit",
|
|
2827
|
+
createCmd: ["npm", "create", "svelte@latest"]
|
|
2828
|
+
},
|
|
2829
|
+
{
|
|
2830
|
+
name: "static-portfolio",
|
|
2831
|
+
description: "Minimal static HTML/CSS portfolio \u2014 no build step required",
|
|
2832
|
+
framework: "static-html",
|
|
2833
|
+
createCmd: []
|
|
2834
|
+
// No create tool — we scaffold manually
|
|
2835
|
+
}
|
|
2836
|
+
];
|
|
2837
|
+
function getTemplate(name) {
|
|
2838
|
+
return TEMPLATES.find((t) => t.name.toLowerCase() === name.toLowerCase());
|
|
2839
|
+
}
|
|
2840
|
+
function getTemplateNames() {
|
|
2841
|
+
return TEMPLATES.map((t) => t.name);
|
|
2842
|
+
}
|
|
2843
|
+
async function templatesCommand() {
|
|
2844
|
+
ui.banner();
|
|
2845
|
+
console.log(` ${brand.bold("Available templates")}`);
|
|
2846
|
+
console.log(` ${brand.dim("Use with: shipem init --template <name>")}`);
|
|
2847
|
+
console.log("");
|
|
2848
|
+
const maxNameLen = Math.max(...TEMPLATES.map((t) => t.name.length));
|
|
2849
|
+
for (const tpl of TEMPLATES) {
|
|
2850
|
+
const padding = " ".repeat(maxNameLen - tpl.name.length);
|
|
2851
|
+
console.log(
|
|
2852
|
+
` ${chalk5.cyan(tpl.name)}${padding} ${brand.dim("\u2014")} ${tpl.description}`
|
|
2853
|
+
);
|
|
2854
|
+
console.log(
|
|
2855
|
+
` ${" ".repeat(maxNameLen)} ${brand.dim(`Framework: ${tpl.framework}`)}`
|
|
2856
|
+
);
|
|
2857
|
+
console.log("");
|
|
2858
|
+
}
|
|
2859
|
+
console.log(` ${brand.bold("Quick start:")}`);
|
|
2860
|
+
console.log(` ${chalk5.cyan("npx shipem init --template astro-blog")}`);
|
|
2861
|
+
console.log(` ${chalk5.cyan("npx shipem init --template nextjs-saas")}`);
|
|
2862
|
+
console.log("");
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
// src/commands/init.ts
|
|
2866
|
+
var FRAMEWORKS = [
|
|
2867
|
+
{
|
|
2868
|
+
name: "Astro (static sites, blogs, portfolios)",
|
|
2869
|
+
value: "astro",
|
|
2870
|
+
createCmd: ["npm", "create", "astro@latest", "--", "--template", "basics"],
|
|
2871
|
+
keywords: ["portfolio", "blog", "personal", "marketing", "docs", "documentation", "static"]
|
|
2872
|
+
},
|
|
2873
|
+
{
|
|
2874
|
+
name: "Next.js (full-stack React, dashboards, SaaS)",
|
|
2875
|
+
value: "nextjs",
|
|
2876
|
+
createCmd: ["npx", "create-next-app@latest", "--", "--ts", "--app", "--tailwind", "--eslint", "--src-dir", "--no-import-alias"],
|
|
2877
|
+
keywords: ["dashboard", "saas", "app", "application", "admin", "full-stack", "fullstack", "e-commerce", "ecommerce"]
|
|
2878
|
+
},
|
|
2879
|
+
{
|
|
2880
|
+
name: "Vite + React (SPAs, landing pages)",
|
|
2881
|
+
value: "vite-react",
|
|
2882
|
+
createCmd: ["npm", "create", "vite@latest", "--", "--template", "react-ts"],
|
|
2883
|
+
keywords: ["landing", "page", "spa", "single", "react", "frontend", "form", "calculator"]
|
|
2884
|
+
},
|
|
2885
|
+
{
|
|
2886
|
+
name: "Vite + Vue (SPAs with Vue)",
|
|
2887
|
+
value: "vite-vue",
|
|
2888
|
+
createCmd: ["npm", "create", "vite@latest", "--", "--template", "vue-ts"],
|
|
2889
|
+
keywords: ["vue"]
|
|
2890
|
+
},
|
|
2891
|
+
{
|
|
2892
|
+
name: "SvelteKit (Svelte full-stack)",
|
|
2893
|
+
value: "sveltekit",
|
|
2894
|
+
createCmd: ["npm", "create", "svelte@latest"],
|
|
2895
|
+
keywords: ["svelte"]
|
|
2896
|
+
}
|
|
2897
|
+
];
|
|
2898
|
+
function matchFramework(description) {
|
|
2899
|
+
const lower = description.toLowerCase();
|
|
2900
|
+
let bestScore = 0;
|
|
2901
|
+
let best = FRAMEWORKS[0];
|
|
2902
|
+
for (const fw of FRAMEWORKS) {
|
|
2903
|
+
let score = 0;
|
|
2904
|
+
for (const keyword of fw.keywords) {
|
|
2905
|
+
if (lower.includes(keyword)) score += 1;
|
|
2906
|
+
}
|
|
2907
|
+
if (score > bestScore) {
|
|
2908
|
+
bestScore = score;
|
|
2909
|
+
best = fw;
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
return best;
|
|
2913
|
+
}
|
|
2914
|
+
function generateProjectName(description) {
|
|
2915
|
+
return description.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 3).join("-").slice(0, 30) || "my-app";
|
|
2916
|
+
}
|
|
2917
|
+
async function initCommand(options = {}) {
|
|
2918
|
+
ui.banner();
|
|
2919
|
+
console.log(` ${brand.bold("Create a new project")}`);
|
|
2920
|
+
console.log(` ${brand.dim("From idea to live website in 2 commands")}`);
|
|
2921
|
+
console.log("");
|
|
2922
|
+
if (options.template) {
|
|
2923
|
+
const tpl = getTemplate(options.template);
|
|
2924
|
+
if (!tpl) {
|
|
2925
|
+
ui.error(`Unknown template: ${options.template}`);
|
|
2926
|
+
ui.br();
|
|
2927
|
+
ui.info("Available templates:");
|
|
2928
|
+
for (const name of getTemplateNames()) {
|
|
2929
|
+
ui.dim(` ${name}`);
|
|
2930
|
+
}
|
|
2931
|
+
ui.br();
|
|
2932
|
+
ui.dim("Run `shipem templates` to see descriptions.");
|
|
2933
|
+
ui.br();
|
|
2934
|
+
process.exit(1);
|
|
2935
|
+
}
|
|
2936
|
+
await scaffoldFromTemplate(tpl, options);
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2939
|
+
const { description } = await inquirer2.prompt([
|
|
2940
|
+
{
|
|
2941
|
+
type: "input",
|
|
2942
|
+
name: "description",
|
|
2943
|
+
message: "What are you building? (describe in plain English)",
|
|
2944
|
+
validate: (v) => v.trim().length > 0 || "Please describe your project"
|
|
2945
|
+
}
|
|
2946
|
+
]);
|
|
2947
|
+
const matched = matchFramework(description);
|
|
2948
|
+
const projectName = generateProjectName(description);
|
|
2949
|
+
console.log("");
|
|
2950
|
+
ui.info(`Matched: ${ui.framework(matched.value)} \u2014 ${matched.name.split("(")[0].trim()}`);
|
|
2951
|
+
if (!options.yes) {
|
|
2952
|
+
const { framework } = await inquirer2.prompt([
|
|
2953
|
+
{
|
|
2954
|
+
type: "list",
|
|
2955
|
+
name: "framework",
|
|
2956
|
+
message: "Use this framework?",
|
|
2957
|
+
choices: [
|
|
2958
|
+
{ name: `${matched.name} (recommended)`, value: matched.value },
|
|
2959
|
+
...FRAMEWORKS.filter((f) => f.value !== matched.value).map((f) => ({
|
|
2960
|
+
name: f.name,
|
|
2961
|
+
value: f.value
|
|
2962
|
+
}))
|
|
2963
|
+
]
|
|
2964
|
+
}
|
|
2965
|
+
]);
|
|
2966
|
+
const selected = FRAMEWORKS.find((f) => f.value === framework) ?? matched;
|
|
2967
|
+
if (selected.value !== matched.value) {
|
|
2968
|
+
Object.assign(matched, selected);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
console.log("");
|
|
2972
|
+
const scaffoldSpinner = ui.spinner(`Scaffolding with ${matched.value}...`);
|
|
2973
|
+
try {
|
|
2974
|
+
const cmd = [...matched.createCmd];
|
|
2975
|
+
if (matched.value === "astro") {
|
|
2976
|
+
cmd.push(projectName);
|
|
2977
|
+
} else if (matched.value === "nextjs") {
|
|
2978
|
+
const dashIdx = cmd.indexOf("--");
|
|
2979
|
+
cmd.splice(dashIdx, 0, projectName);
|
|
2980
|
+
} else {
|
|
2981
|
+
const dashIdx = cmd.indexOf("--");
|
|
2982
|
+
if (dashIdx >= 0) {
|
|
2983
|
+
cmd.splice(dashIdx, 0, projectName);
|
|
2984
|
+
} else {
|
|
2985
|
+
cmd.push(projectName);
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
await execa3(cmd[0], cmd.slice(1), {
|
|
2989
|
+
cwd: process.cwd(),
|
|
2990
|
+
timeout: 12e4,
|
|
2991
|
+
stdio: "pipe"
|
|
2992
|
+
});
|
|
2993
|
+
scaffoldSpinner.succeed(`Created ${projectName}/`);
|
|
2994
|
+
} catch (err) {
|
|
2995
|
+
scaffoldSpinner.fail("Scaffolding failed");
|
|
2996
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2997
|
+
ui.dim(msg);
|
|
2998
|
+
ui.br();
|
|
2999
|
+
ui.info("Creating minimal project instead...");
|
|
3000
|
+
createMinimalProject(projectName, description);
|
|
3001
|
+
}
|
|
3002
|
+
const projectDir = join10(process.cwd(), projectName);
|
|
3003
|
+
if (existsSync8(projectDir)) {
|
|
3004
|
+
writeFileSync6(
|
|
3005
|
+
join10(projectDir, "shipem.json"),
|
|
3006
|
+
JSON.stringify({ project: { name: projectName, framework: matched.value } }, null, 2) + "\n",
|
|
3007
|
+
"utf-8"
|
|
3008
|
+
);
|
|
3009
|
+
}
|
|
3010
|
+
console.log("");
|
|
3011
|
+
console.log(` ${brand.green.bold("Ready!")} Now run:`);
|
|
3012
|
+
console.log("");
|
|
3013
|
+
console.log(` ${chalk6.cyan(`cd ${projectName} && npx shipem`)}`);
|
|
3014
|
+
console.log("");
|
|
3015
|
+
}
|
|
3016
|
+
async function scaffoldFromTemplate(tpl, options) {
|
|
3017
|
+
let projectName;
|
|
3018
|
+
if (options.yes) {
|
|
3019
|
+
projectName = `my-${tpl.name}`;
|
|
3020
|
+
} else {
|
|
3021
|
+
const { name } = await inquirer2.prompt([
|
|
3022
|
+
{
|
|
3023
|
+
type: "input",
|
|
3024
|
+
name: "name",
|
|
3025
|
+
message: "Project name?",
|
|
3026
|
+
default: `my-${tpl.name}`,
|
|
3027
|
+
validate: (v) => v.trim().length > 0 || "Please enter a project name"
|
|
3028
|
+
}
|
|
3029
|
+
]);
|
|
3030
|
+
projectName = name.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").slice(0, 30) || `my-${tpl.name}`;
|
|
3031
|
+
}
|
|
3032
|
+
ui.info(`Template: ${chalk6.cyan(tpl.name)} \u2014 ${tpl.description}`);
|
|
3033
|
+
ui.br();
|
|
3034
|
+
if (tpl.createCmd.length === 0) {
|
|
3035
|
+
const scaffoldSpinner = ui.spinner(`Creating ${projectName}/...`);
|
|
3036
|
+
createStaticPortfolio(projectName);
|
|
3037
|
+
scaffoldSpinner.succeed(`Created ${projectName}/`);
|
|
3038
|
+
} else {
|
|
3039
|
+
const scaffoldSpinner = ui.spinner(`Scaffolding ${tpl.name}...`);
|
|
3040
|
+
try {
|
|
3041
|
+
const cmd = [...tpl.createCmd];
|
|
3042
|
+
if (tpl.framework === "astro") {
|
|
3043
|
+
cmd.push(projectName);
|
|
3044
|
+
} else if (tpl.framework === "nextjs") {
|
|
3045
|
+
const dashIdx = cmd.indexOf("--");
|
|
3046
|
+
cmd.splice(dashIdx, 0, projectName);
|
|
3047
|
+
} else {
|
|
3048
|
+
const dashIdx = cmd.indexOf("--");
|
|
3049
|
+
if (dashIdx >= 0) {
|
|
3050
|
+
cmd.splice(dashIdx, 0, projectName);
|
|
3051
|
+
} else {
|
|
3052
|
+
cmd.push(projectName);
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
await execa3(cmd[0], cmd.slice(1), {
|
|
3056
|
+
cwd: process.cwd(),
|
|
3057
|
+
timeout: 12e4,
|
|
3058
|
+
stdio: "pipe"
|
|
3059
|
+
});
|
|
3060
|
+
scaffoldSpinner.succeed(`Created ${projectName}/`);
|
|
3061
|
+
if (tpl.postScaffold && tpl.postScaffold.length > 0) {
|
|
3062
|
+
const postSpinner = ui.spinner("Running post-scaffold setup...");
|
|
3063
|
+
try {
|
|
3064
|
+
await execa3(tpl.postScaffold[0], tpl.postScaffold.slice(1), {
|
|
3065
|
+
cwd: join10(process.cwd(), projectName),
|
|
3066
|
+
timeout: 12e4,
|
|
3067
|
+
stdio: "pipe"
|
|
3068
|
+
});
|
|
3069
|
+
postSpinner.succeed("Post-scaffold setup complete");
|
|
3070
|
+
} catch {
|
|
3071
|
+
postSpinner.fail("Post-scaffold setup failed (non-critical)");
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
} catch (err) {
|
|
3075
|
+
scaffoldSpinner.fail("Scaffolding failed");
|
|
3076
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3077
|
+
ui.dim(msg);
|
|
3078
|
+
ui.br();
|
|
3079
|
+
ui.info("Creating minimal project instead...");
|
|
3080
|
+
createMinimalProject(projectName, tpl.description);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
const projectDir = join10(process.cwd(), projectName);
|
|
3084
|
+
if (existsSync8(projectDir)) {
|
|
3085
|
+
writeFileSync6(
|
|
3086
|
+
join10(projectDir, "shipem.json"),
|
|
3087
|
+
JSON.stringify({ project: { name: projectName, framework: tpl.framework } }, null, 2) + "\n",
|
|
3088
|
+
"utf-8"
|
|
3089
|
+
);
|
|
3090
|
+
}
|
|
3091
|
+
console.log("");
|
|
3092
|
+
console.log(` ${brand.green.bold("Ready!")} Now run:`);
|
|
3093
|
+
console.log("");
|
|
3094
|
+
console.log(` ${chalk6.cyan(`cd ${projectName} && npx shipem`)}`);
|
|
3095
|
+
console.log("");
|
|
3096
|
+
}
|
|
3097
|
+
function createMinimalProject(projectName, description) {
|
|
3098
|
+
const dir = join10(process.cwd(), projectName);
|
|
3099
|
+
if (!existsSync8(dir)) {
|
|
3100
|
+
mkdirSync(dir, { recursive: true });
|
|
3101
|
+
writeFileSync6(join10(dir, "index.html"), `<!DOCTYPE html>
|
|
3102
|
+
<html lang="en">
|
|
3103
|
+
<head>
|
|
3104
|
+
<meta charset="UTF-8">
|
|
3105
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3106
|
+
<title>${description}</title>
|
|
3107
|
+
<style>
|
|
3108
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
3109
|
+
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; background: #0a0a0a; color: #ededed; }
|
|
3110
|
+
h1 { font-size: 2.5rem; margin-bottom: 1rem; }
|
|
3111
|
+
p { color: #888; }
|
|
3112
|
+
</style>
|
|
3113
|
+
</head>
|
|
3114
|
+
<body>
|
|
3115
|
+
<div style="text-align: center">
|
|
3116
|
+
<h1>${description}</h1>
|
|
3117
|
+
<p>Built with Shipem</p>
|
|
3118
|
+
</div>
|
|
3119
|
+
</body>
|
|
3120
|
+
</html>`, "utf-8");
|
|
3121
|
+
ui.success(`Created ${projectName}/ with minimal HTML`);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
function createStaticPortfolio(projectName) {
|
|
3125
|
+
const dir = join10(process.cwd(), projectName);
|
|
3126
|
+
mkdirSync(dir, { recursive: true });
|
|
3127
|
+
writeFileSync6(join10(dir, "index.html"), `<!DOCTYPE html>
|
|
3128
|
+
<html lang="en">
|
|
3129
|
+
<head>
|
|
3130
|
+
<meta charset="UTF-8">
|
|
3131
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3132
|
+
<title>My Portfolio</title>
|
|
3133
|
+
<link rel="stylesheet" href="style.css">
|
|
3134
|
+
</head>
|
|
3135
|
+
<body>
|
|
3136
|
+
<header>
|
|
3137
|
+
<h1>Hello, I'm <span class="accent">Your Name</span></h1>
|
|
3138
|
+
<p>Developer · Designer · Creator</p>
|
|
3139
|
+
</header>
|
|
3140
|
+
<main>
|
|
3141
|
+
<section class="projects">
|
|
3142
|
+
<h2>Projects</h2>
|
|
3143
|
+
<div class="grid">
|
|
3144
|
+
<div class="card">
|
|
3145
|
+
<h3>Project One</h3>
|
|
3146
|
+
<p>A brief description of this project.</p>
|
|
3147
|
+
</div>
|
|
3148
|
+
<div class="card">
|
|
3149
|
+
<h3>Project Two</h3>
|
|
3150
|
+
<p>A brief description of this project.</p>
|
|
3151
|
+
</div>
|
|
3152
|
+
<div class="card">
|
|
3153
|
+
<h3>Project Three</h3>
|
|
3154
|
+
<p>A brief description of this project.</p>
|
|
3155
|
+
</div>
|
|
3156
|
+
</div>
|
|
3157
|
+
</section>
|
|
3158
|
+
</main>
|
|
3159
|
+
<footer>
|
|
3160
|
+
<p>Shipped with Shipem</p>
|
|
3161
|
+
</footer>
|
|
3162
|
+
</body>
|
|
3163
|
+
</html>`, "utf-8");
|
|
3164
|
+
writeFileSync6(join10(dir, "style.css"), `* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
3165
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0a0a; color: #ededed; min-height: 100vh; }
|
|
3166
|
+
header { text-align: center; padding: 4rem 1rem 2rem; }
|
|
3167
|
+
header h1 { font-size: 2.5rem; margin-bottom: 0.5rem; }
|
|
3168
|
+
.accent { color: #3B82F6; }
|
|
3169
|
+
header p { color: #888; font-size: 1.1rem; }
|
|
3170
|
+
main { max-width: 900px; margin: 0 auto; padding: 2rem 1rem; }
|
|
3171
|
+
h2 { margin-bottom: 1.5rem; color: #ccc; }
|
|
3172
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; }
|
|
3173
|
+
.card { background: #1a1a1a; border: 1px solid #333; border-radius: 8px; padding: 1.5rem; }
|
|
3174
|
+
.card h3 { margin-bottom: 0.5rem; }
|
|
3175
|
+
.card p { color: #888; font-size: 0.9rem; }
|
|
3176
|
+
footer { text-align: center; padding: 3rem 1rem; color: #555; font-size: 0.85rem; }
|
|
3177
|
+
`, "utf-8");
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
// src/commands/dev.ts
|
|
3181
|
+
import { execa as execa4 } from "execa";
|
|
3182
|
+
import chalk7 from "chalk";
|
|
3183
|
+
var DEV_COMMANDS = {
|
|
3184
|
+
nextjs: ["npx", "next", "dev"],
|
|
3185
|
+
astro: ["npx", "astro", "dev"],
|
|
3186
|
+
sveltekit: ["npx", "vite", "dev"],
|
|
3187
|
+
nuxt: ["npx", "nuxi", "dev"],
|
|
3188
|
+
remix: ["npx", "remix", "dev"],
|
|
3189
|
+
gatsby: ["npx", "gatsby", "develop"],
|
|
3190
|
+
"vite-react": ["npx", "vite"],
|
|
3191
|
+
"vite-vue": ["npx", "vite"],
|
|
3192
|
+
"vite-svelte": ["npx", "vite"],
|
|
3193
|
+
"create-react-app": ["npx", "react-scripts", "start"]
|
|
3194
|
+
};
|
|
3195
|
+
async function devCommand() {
|
|
3196
|
+
const cwd = process.cwd();
|
|
3197
|
+
ui.section("Shipem Dev");
|
|
3198
|
+
ui.br();
|
|
3199
|
+
const detection = scanProject(cwd);
|
|
3200
|
+
const devCmd = DEV_COMMANDS[detection.framework];
|
|
3201
|
+
if (!devCmd) {
|
|
3202
|
+
ui.warn(`No dev server known for framework: ${detection.framework}`);
|
|
3203
|
+
ui.dim("Start your dev server manually, then press [d] to deploy.");
|
|
3204
|
+
ui.br();
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
ui.success(`Detected: ${ui.framework(detection.framework)}`);
|
|
3208
|
+
ui.info(`Starting: ${devCmd.join(" ")}`);
|
|
3209
|
+
ui.br();
|
|
3210
|
+
let devProcess = null;
|
|
3211
|
+
try {
|
|
3212
|
+
devProcess = execa4(devCmd[0], devCmd.slice(1), {
|
|
3213
|
+
cwd,
|
|
3214
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3215
|
+
env: { ...process.env, FORCE_COLOR: "1" }
|
|
3216
|
+
});
|
|
3217
|
+
devProcess.stdout?.on("data", (chunk) => {
|
|
3218
|
+
process.stdout.write(chunk);
|
|
3219
|
+
});
|
|
3220
|
+
devProcess.stderr?.on("data", (chunk) => {
|
|
3221
|
+
process.stderr.write(chunk);
|
|
3222
|
+
});
|
|
3223
|
+
console.log("");
|
|
3224
|
+
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")}`);
|
|
3225
|
+
console.log(` ${brand.bold("Shipem Dev")} Press ${chalk7.cyan("[d]")} to deploy Press ${chalk7.cyan("[q]")} to quit`);
|
|
3226
|
+
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")}`);
|
|
3227
|
+
console.log("");
|
|
3228
|
+
if (process.stdin.isTTY) {
|
|
3229
|
+
process.stdin.setRawMode(true);
|
|
3230
|
+
process.stdin.resume();
|
|
3231
|
+
process.stdin.setEncoding("utf-8");
|
|
3232
|
+
process.stdin.on("data", async (key) => {
|
|
3233
|
+
if (key === "d" || key === "D") {
|
|
3234
|
+
console.log("");
|
|
3235
|
+
ui.section("Deploying...");
|
|
3236
|
+
ui.br();
|
|
3237
|
+
if (devProcess) {
|
|
3238
|
+
devProcess.kill("SIGTERM");
|
|
3239
|
+
}
|
|
3240
|
+
try {
|
|
3241
|
+
await deployCommand({ yes: true, skipBuild: false });
|
|
3242
|
+
} catch (err) {
|
|
3243
|
+
ui.error(err instanceof Error ? err.message : "Deploy failed");
|
|
3244
|
+
}
|
|
3245
|
+
console.log("");
|
|
3246
|
+
ui.dim("Dev server stopped after deploy. Run `shipem dev` again to restart.");
|
|
3247
|
+
process.exit(0);
|
|
3248
|
+
}
|
|
3249
|
+
if (key === "q" || key === "Q" || key === "") {
|
|
3250
|
+
console.log("");
|
|
3251
|
+
ui.info("Shutting down dev server...");
|
|
3252
|
+
if (devProcess) {
|
|
3253
|
+
devProcess.kill("SIGTERM");
|
|
3254
|
+
}
|
|
3255
|
+
process.exit(0);
|
|
3256
|
+
}
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3259
|
+
await devProcess;
|
|
3260
|
+
} catch (err) {
|
|
3261
|
+
if (devProcess?.killed) return;
|
|
3262
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3263
|
+
if (!msg.includes("SIGTERM")) {
|
|
3264
|
+
ui.error(`Dev server failed: ${msg}`);
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
// src/commands/status.ts
|
|
3270
|
+
import chalk8 from "chalk";
|
|
3271
|
+
function timeAgo2(isoStr) {
|
|
3272
|
+
const diff = Date.now() - new Date(isoStr).getTime();
|
|
3273
|
+
const mins = Math.floor(diff / 6e4);
|
|
3274
|
+
if (mins < 1) return "just now";
|
|
3275
|
+
if (mins === 1) return "1 minute ago";
|
|
3276
|
+
if (mins < 60) return `${mins} minutes ago`;
|
|
3277
|
+
const hrs = Math.floor(mins / 60);
|
|
3278
|
+
if (hrs === 1) return "1 hour ago";
|
|
3279
|
+
if (hrs < 24) return `${hrs} hours ago`;
|
|
3280
|
+
const days = Math.floor(hrs / 24);
|
|
3281
|
+
return days === 1 ? "yesterday" : `${days} days ago`;
|
|
3282
|
+
}
|
|
3283
|
+
async function statusCommand() {
|
|
3284
|
+
const cwd = process.cwd();
|
|
3285
|
+
const config = readProjectConfig(cwd);
|
|
3286
|
+
if (!config.deployment) {
|
|
3287
|
+
ui.warn("No deployment found in this directory.");
|
|
3288
|
+
ui.dim("Run `shipem` to deploy your app.");
|
|
3289
|
+
process.exit(0);
|
|
3290
|
+
}
|
|
3291
|
+
const { deployment } = config;
|
|
3292
|
+
const project = config.project;
|
|
3293
|
+
const deploys = config.deployments ?? [];
|
|
3294
|
+
console.log("");
|
|
3295
|
+
console.log(` ${brand.blue.bold("\u26A1 Shipem Status")}`);
|
|
3296
|
+
console.log("");
|
|
3297
|
+
ui.kv("Project", chalk8.bold(deployment.projectName));
|
|
3298
|
+
if (project) {
|
|
3299
|
+
ui.kv("Framework", `${ui.frameworkIcon(project.framework)} ${ui.framework(project.framework)}`);
|
|
3300
|
+
}
|
|
3301
|
+
const ago = timeAgo2(deployment.deployedAt);
|
|
3302
|
+
const lastDeploy = deploys[0];
|
|
3303
|
+
if (lastDeploy) {
|
|
3304
|
+
ui.kv("Last deploy", `${ago} (${lastDeploy.duration.toFixed(1)}s)`);
|
|
3305
|
+
} else {
|
|
3306
|
+
ui.kv("Last deploy", ago);
|
|
3307
|
+
}
|
|
3308
|
+
ui.kv("URL", chalk8.blue.underline(deployment.url));
|
|
3309
|
+
ui.kv("Deploys", `${deploys.length} total`);
|
|
3310
|
+
console.log("");
|
|
3311
|
+
if (deploys.length > 0) {
|
|
3312
|
+
ui.deployHistory(deploys);
|
|
3313
|
+
}
|
|
3314
|
+
if ((deployment.deployTarget === "cloudflare-pages" || deployment.deployTarget === "cloudflare-workers") && deployment.cloudflareProjectName) {
|
|
3315
|
+
const cfCreds = getCloudflareCredentials();
|
|
3316
|
+
if (cfCreds) {
|
|
3317
|
+
const spinner = ui.spinner("Fetching live status from Cloudflare");
|
|
3318
|
+
const cf = new CloudflarePages(cfCreds.apiToken, cfCreds.accountId);
|
|
3319
|
+
try {
|
|
3320
|
+
const status = await cf.getDeploymentStatus(
|
|
3321
|
+
deployment.cloudflareProjectName,
|
|
3322
|
+
deployment.deploymentId
|
|
2142
3323
|
);
|
|
2143
3324
|
if (status) {
|
|
2144
3325
|
spinner.succeed("Status fetched");
|
|
2145
3326
|
ui.kv("Deployment ID", status.id);
|
|
2146
3327
|
ui.kv(
|
|
2147
3328
|
"Status",
|
|
2148
|
-
status.latest_stage?.status === "success" ?
|
|
3329
|
+
status.latest_stage?.status === "success" ? chalk8.green("\u2713 Active") : chalk8.yellow(status.latest_stage?.status ?? "unknown")
|
|
2149
3330
|
);
|
|
2150
3331
|
ui.br();
|
|
2151
3332
|
if (status.stages && status.stages.length > 0) {
|
|
2152
3333
|
ui.section("Build stages:");
|
|
2153
3334
|
for (const stage of status.stages) {
|
|
2154
|
-
const icon = stage.status === "success" ?
|
|
3335
|
+
const icon = stage.status === "success" ? chalk8.green("\u2713") : stage.status === "failure" ? chalk8.red("\u2717") : chalk8.gray("\xB7");
|
|
2155
3336
|
ui.info(`${icon} ${stage.name}`);
|
|
2156
3337
|
}
|
|
2157
3338
|
ui.br();
|
|
@@ -2175,7 +3356,7 @@ async function statusCommand() {
|
|
|
2175
3356
|
}
|
|
2176
3357
|
|
|
2177
3358
|
// src/commands/logs.ts
|
|
2178
|
-
import
|
|
3359
|
+
import chalk9 from "chalk";
|
|
2179
3360
|
async function logsCommand(options) {
|
|
2180
3361
|
const cwd = process.cwd();
|
|
2181
3362
|
const config = readProjectConfig(cwd);
|
|
@@ -2187,18 +3368,14 @@ async function logsCommand(options) {
|
|
|
2187
3368
|
const { deployment } = config;
|
|
2188
3369
|
if (deployment.deployTarget !== "cloudflare-pages" && deployment.deployTarget !== "cloudflare-workers") {
|
|
2189
3370
|
ui.warn("Log streaming is only available for Cloudflare Pages deployments.");
|
|
2190
|
-
ui.dim(`For Fly.io apps, run: flyctl logs --app ${deployment.projectName}`);
|
|
2191
3371
|
process.exit(0);
|
|
2192
3372
|
}
|
|
2193
3373
|
if (!deployment.cloudflareProjectName) {
|
|
2194
|
-
|
|
3374
|
+
throw new ConfigError("Missing Cloudflare project name in deployment config.");
|
|
2195
3375
|
}
|
|
2196
3376
|
const cfCreds = getCloudflareCredentials();
|
|
2197
3377
|
if (!cfCreds) {
|
|
2198
|
-
|
|
2199
|
-
"Cloudflare credentials not found.",
|
|
2200
|
-
"Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables."
|
|
2201
|
-
);
|
|
3378
|
+
throw new ConfigError("Cloudflare credentials not found. Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables.");
|
|
2202
3379
|
}
|
|
2203
3380
|
ui.section("Deployment Logs");
|
|
2204
3381
|
ui.br();
|
|
@@ -2220,11 +3397,11 @@ async function logsCommand(options) {
|
|
|
2220
3397
|
const displayLogs = logs.slice(-linesToShow);
|
|
2221
3398
|
for (const line of displayLogs) {
|
|
2222
3399
|
if (line.includes("ERROR") || line.includes("error") || line.includes("FAIL")) {
|
|
2223
|
-
console.log(
|
|
3400
|
+
console.log(chalk9.red(` ${line}`));
|
|
2224
3401
|
} else if (line.includes("WARN") || line.includes("warn")) {
|
|
2225
|
-
console.log(
|
|
3402
|
+
console.log(chalk9.yellow(` ${line}`));
|
|
2226
3403
|
} else {
|
|
2227
|
-
console.log(
|
|
3404
|
+
console.log(chalk9.gray(` ${line}`));
|
|
2228
3405
|
}
|
|
2229
3406
|
}
|
|
2230
3407
|
}
|
|
@@ -2236,7 +3413,7 @@ async function logsCommand(options) {
|
|
|
2236
3413
|
}
|
|
2237
3414
|
|
|
2238
3415
|
// src/commands/down.ts
|
|
2239
|
-
import
|
|
3416
|
+
import inquirer3 from "inquirer";
|
|
2240
3417
|
import axios3 from "axios";
|
|
2241
3418
|
async function downCommand(options) {
|
|
2242
3419
|
const cwd = process.cwd();
|
|
@@ -2253,7 +3430,7 @@ async function downCommand(options) {
|
|
|
2253
3430
|
ui.kv("URL", deployment.url);
|
|
2254
3431
|
ui.br();
|
|
2255
3432
|
if (!options.yes) {
|
|
2256
|
-
const { confirmed } = await
|
|
3433
|
+
const { confirmed } = await inquirer3.prompt([
|
|
2257
3434
|
{
|
|
2258
3435
|
type: "confirm",
|
|
2259
3436
|
name: "confirmed",
|
|
@@ -2270,10 +3447,7 @@ async function downCommand(options) {
|
|
|
2270
3447
|
if ((deployment.deployTarget === "cloudflare-pages" || deployment.deployTarget === "cloudflare-workers") && deployment.cloudflareProjectName) {
|
|
2271
3448
|
const cfCreds = getCloudflareCredentials();
|
|
2272
3449
|
if (!cfCreds) {
|
|
2273
|
-
|
|
2274
|
-
"Cloudflare credentials not found.",
|
|
2275
|
-
"Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables."
|
|
2276
|
-
);
|
|
3450
|
+
throw new ConfigError("Cloudflare credentials not found. Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables.");
|
|
2277
3451
|
}
|
|
2278
3452
|
const spinner = ui.spinner(`Deleting ${deployment.projectName} from Cloudflare Pages`);
|
|
2279
3453
|
const cf = new CloudflarePages(cfCreds.apiToken, cfCreds.accountId);
|
|
@@ -2282,13 +3456,12 @@ async function downCommand(options) {
|
|
|
2282
3456
|
spinner.succeed("Deployment deleted from Cloudflare Pages");
|
|
2283
3457
|
} catch (err) {
|
|
2284
3458
|
spinner.fail("Failed to delete deployment");
|
|
2285
|
-
|
|
2286
|
-
process.exit(1);
|
|
3459
|
+
throw err instanceof DeployError ? err : new DeployError(err instanceof Error ? err.message : "Unknown error", { cause: err });
|
|
2287
3460
|
}
|
|
2288
3461
|
} else if ((deployment.deployTarget === "cloudflare-pages" || deployment.deployTarget === "cloudflare-workers") && !deployment.cloudflareProjectName) {
|
|
2289
3462
|
const token = getSessionToken();
|
|
2290
3463
|
if (!token) {
|
|
2291
|
-
|
|
3464
|
+
throw new AuthError("Not logged in. Run `shipem login` first.");
|
|
2292
3465
|
}
|
|
2293
3466
|
const spinner = ui.spinner(`Deleting ${deployment.projectName} from Shipem`);
|
|
2294
3467
|
try {
|
|
@@ -2298,16 +3471,11 @@ async function downCommand(options) {
|
|
|
2298
3471
|
spinner.succeed("Deployment deleted");
|
|
2299
3472
|
} catch (err) {
|
|
2300
3473
|
spinner.fail("Failed to delete deployment");
|
|
2301
|
-
|
|
2302
|
-
|
|
3474
|
+
if (axios3.isAxiosError(err) && err.code && ["ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT"].includes(err.code)) {
|
|
3475
|
+
throw new NetworkError("Cannot reach Shipem servers. Check your internet connection.", { cause: err });
|
|
3476
|
+
}
|
|
3477
|
+
throw new DeployError(err instanceof Error ? err.message : "Unknown error", { cause: err });
|
|
2303
3478
|
}
|
|
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
3479
|
}
|
|
2312
3480
|
writeProjectConfig({ project: config.project }, cwd);
|
|
2313
3481
|
ui.br();
|
|
@@ -2316,8 +3484,481 @@ async function downCommand(options) {
|
|
|
2316
3484
|
ui.br();
|
|
2317
3485
|
}
|
|
2318
3486
|
|
|
2319
|
-
// src/commands/
|
|
3487
|
+
// src/commands/watch.ts
|
|
3488
|
+
import { watch as fsWatch } from "fs";
|
|
3489
|
+
import { join as join11, relative as relative2 } from "path";
|
|
3490
|
+
import { readdirSync as readdirSync7 } from "fs";
|
|
3491
|
+
import chalk10 from "chalk";
|
|
3492
|
+
var IGNORE_PATTERNS = /* @__PURE__ */ new Set([
|
|
3493
|
+
"node_modules",
|
|
3494
|
+
".git",
|
|
3495
|
+
"dist",
|
|
3496
|
+
"build",
|
|
3497
|
+
".shipem",
|
|
3498
|
+
"shipem.json",
|
|
3499
|
+
".next",
|
|
3500
|
+
".nuxt",
|
|
3501
|
+
".output",
|
|
3502
|
+
".svelte-kit",
|
|
3503
|
+
".turbo",
|
|
3504
|
+
"coverage",
|
|
3505
|
+
".DS_Store",
|
|
3506
|
+
".env",
|
|
3507
|
+
".env.local"
|
|
3508
|
+
]);
|
|
3509
|
+
var DEBOUNCE_MS = 2e3;
|
|
3510
|
+
function timestamp() {
|
|
3511
|
+
const d = /* @__PURE__ */ new Date();
|
|
3512
|
+
return d.toLocaleTimeString("en-US", { hour12: false });
|
|
3513
|
+
}
|
|
3514
|
+
function collectDirs(dir) {
|
|
3515
|
+
const dirs = [dir];
|
|
3516
|
+
try {
|
|
3517
|
+
const entries = readdirSync7(dir, { withFileTypes: true });
|
|
3518
|
+
for (const entry of entries) {
|
|
3519
|
+
if (!entry.isDirectory()) continue;
|
|
3520
|
+
if (IGNORE_PATTERNS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
3521
|
+
dirs.push(...collectDirs(join11(dir, entry.name)));
|
|
3522
|
+
}
|
|
3523
|
+
} catch {
|
|
3524
|
+
}
|
|
3525
|
+
return dirs;
|
|
3526
|
+
}
|
|
3527
|
+
async function watchCommand() {
|
|
3528
|
+
const cwd = process.cwd();
|
|
3529
|
+
ui.banner();
|
|
3530
|
+
console.log(` ${brand.blue.bold("\u26A1")} ${brand.bold("Watching")} for changes...`);
|
|
3531
|
+
console.log(` Auto-deploy on save. Press ${chalk10.cyan("[q]")} to stop, ${chalk10.cyan("[d]")} to force deploy.`);
|
|
3532
|
+
console.log("");
|
|
3533
|
+
let debounceTimer = null;
|
|
3534
|
+
let isDeploying = false;
|
|
3535
|
+
let lastChangedFile = "";
|
|
3536
|
+
const watchers = [];
|
|
3537
|
+
const triggerDeploy = async (file) => {
|
|
3538
|
+
if (isDeploying) return;
|
|
3539
|
+
isDeploying = true;
|
|
3540
|
+
console.log(` ${brand.dim(`[${timestamp()}]`)} ${brand.blue("Deploying...")}`);
|
|
3541
|
+
const startTime = Date.now();
|
|
3542
|
+
try {
|
|
3543
|
+
await deployCommand({ yes: true, skipBuild: false });
|
|
3544
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
3545
|
+
console.log(` ${brand.dim(`[${timestamp()}]`)} ${brand.green("\u2713")} Deployed (${elapsed}s)`);
|
|
3546
|
+
} catch (err) {
|
|
3547
|
+
console.log(` ${brand.dim(`[${timestamp()}]`)} ${brand.red("\u2717")} Deploy failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3548
|
+
}
|
|
3549
|
+
isDeploying = false;
|
|
3550
|
+
console.log("");
|
|
3551
|
+
console.log(` Watching for changes... Press ${chalk10.cyan("[q]")} to stop.`);
|
|
3552
|
+
console.log("");
|
|
3553
|
+
};
|
|
3554
|
+
const onFileChange = (dir, filename) => {
|
|
3555
|
+
if (!filename) return;
|
|
3556
|
+
if (IGNORE_PATTERNS.has(filename)) return;
|
|
3557
|
+
if (isDeploying) return;
|
|
3558
|
+
const relPath = relative2(cwd, join11(dir, filename));
|
|
3559
|
+
lastChangedFile = relPath;
|
|
3560
|
+
console.log(` ${brand.dim(`[${timestamp()}]`)} File changed: ${chalk10.cyan(relPath)}`);
|
|
3561
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
3562
|
+
debounceTimer = setTimeout(() => {
|
|
3563
|
+
void triggerDeploy(lastChangedFile);
|
|
3564
|
+
}, DEBOUNCE_MS);
|
|
3565
|
+
};
|
|
3566
|
+
const dirs = collectDirs(cwd);
|
|
3567
|
+
for (const dir of dirs) {
|
|
3568
|
+
try {
|
|
3569
|
+
const watcher = fsWatch(dir, { persistent: true }, (_event, filename) => {
|
|
3570
|
+
onFileChange(dir, filename);
|
|
3571
|
+
});
|
|
3572
|
+
watchers.push(watcher);
|
|
3573
|
+
} catch {
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
if (process.stdin.isTTY) {
|
|
3577
|
+
process.stdin.setRawMode(true);
|
|
3578
|
+
process.stdin.resume();
|
|
3579
|
+
process.stdin.setEncoding("utf-8");
|
|
3580
|
+
process.stdin.on("data", (key) => {
|
|
3581
|
+
if (key === "q" || key === "Q" || key === "") {
|
|
3582
|
+
console.log("");
|
|
3583
|
+
ui.info("Stopped watching.");
|
|
3584
|
+
for (const w of watchers) w.close();
|
|
3585
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
3586
|
+
process.exit(0);
|
|
3587
|
+
}
|
|
3588
|
+
if (key === "d" || key === "D") {
|
|
3589
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
3590
|
+
console.log(` ${brand.dim(`[${timestamp()}]`)} Force deploy triggered`);
|
|
3591
|
+
void triggerDeploy("manual");
|
|
3592
|
+
}
|
|
3593
|
+
});
|
|
3594
|
+
}
|
|
3595
|
+
await new Promise(() => {
|
|
3596
|
+
});
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
// src/commands/history.ts
|
|
3600
|
+
import chalk11 from "chalk";
|
|
3601
|
+
|
|
3602
|
+
// src/memory/index.ts
|
|
3603
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync2, existsSync as existsSync9 } from "fs";
|
|
3604
|
+
import { join as join12 } from "path";
|
|
3605
|
+
import { homedir } from "os";
|
|
3606
|
+
var SHIPEM_HOME = join12(homedir(), ".shipem");
|
|
3607
|
+
var MEMORY_PATH = join12(SHIPEM_HOME, "memory.json");
|
|
3608
|
+
var PROJECTS_PATH = join12(SHIPEM_HOME, "projects.json");
|
|
3609
|
+
var FIXES_PATH = join12(SHIPEM_HOME, "fixes.json");
|
|
3610
|
+
var CONFIG_PATH = join12(SHIPEM_HOME, "config.json");
|
|
3611
|
+
function ensureDir() {
|
|
3612
|
+
if (!existsSync9(SHIPEM_HOME)) {
|
|
3613
|
+
mkdirSync2(SHIPEM_HOME, { recursive: true });
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
function readJson2(path, fallback) {
|
|
3617
|
+
try {
|
|
3618
|
+
if (!existsSync9(path)) return fallback;
|
|
3619
|
+
return JSON.parse(readFileSync9(path, "utf-8"));
|
|
3620
|
+
} catch {
|
|
3621
|
+
return fallback;
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
function writeJson(path, data) {
|
|
3625
|
+
ensureDir();
|
|
3626
|
+
writeFileSync7(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
3627
|
+
}
|
|
3628
|
+
function getMemory() {
|
|
3629
|
+
return readJson2(MEMORY_PATH, {
|
|
3630
|
+
totalDeploys: 0,
|
|
3631
|
+
totalFixes: 0,
|
|
3632
|
+
firstUsed: (/* @__PURE__ */ new Date()).toISOString()
|
|
3633
|
+
});
|
|
3634
|
+
}
|
|
3635
|
+
function getProjects() {
|
|
3636
|
+
return readJson2(PROJECTS_PATH, []);
|
|
3637
|
+
}
|
|
3638
|
+
function getGlobalConfig() {
|
|
3639
|
+
return readJson2(CONFIG_PATH, {});
|
|
3640
|
+
}
|
|
3641
|
+
function setWebhook(url) {
|
|
3642
|
+
const config = getGlobalConfig();
|
|
3643
|
+
if (url) {
|
|
3644
|
+
config.notifications = { ...config.notifications, webhook: url };
|
|
3645
|
+
} else {
|
|
3646
|
+
if (config.notifications) delete config.notifications.webhook;
|
|
3647
|
+
}
|
|
3648
|
+
writeJson(CONFIG_PATH, config);
|
|
3649
|
+
}
|
|
3650
|
+
function getWebhook() {
|
|
3651
|
+
return getGlobalConfig().notifications?.webhook;
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
// src/commands/history.ts
|
|
3655
|
+
async function historyCommand() {
|
|
3656
|
+
const projects = getProjects();
|
|
3657
|
+
const memory = getMemory();
|
|
3658
|
+
ui.section("Shipem History \u2014 All Projects");
|
|
3659
|
+
ui.br();
|
|
3660
|
+
if (projects.length === 0) {
|
|
3661
|
+
ui.dim("No projects deployed yet. Run `shipem deploy` to get started.");
|
|
3662
|
+
ui.br();
|
|
3663
|
+
return;
|
|
3664
|
+
}
|
|
3665
|
+
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())}`);
|
|
3666
|
+
ui.br();
|
|
3667
|
+
for (const project of projects) {
|
|
3668
|
+
const avgTime = project.deployCount > 0 ? (project.totalDeployTime / project.deployCount).toFixed(1) : "0.0";
|
|
3669
|
+
const lastDeploy = new Date(project.lastDeploy);
|
|
3670
|
+
const dateStr = lastDeploy.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
3671
|
+
const timeStr = lastDeploy.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
|
|
3672
|
+
console.log(` ${brand.bold(project.name)} ${brand.dim(`(${project.framework})`)}`);
|
|
3673
|
+
console.log(` ${brand.dim("URL:")} ${brand.brightBlue.underline(project.url)}`);
|
|
3674
|
+
console.log(` ${brand.dim("Deploys:")} ${chalk11.cyan(String(project.deployCount))} ${brand.dim("Avg:")} ${chalk11.cyan(avgTime + "s")} ${brand.dim("Last:")} ${dateStr} ${timeStr}`);
|
|
3675
|
+
console.log(` ${brand.dim("Path:")} ${project.path}`);
|
|
3676
|
+
ui.br();
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
// src/commands/config.ts
|
|
3681
|
+
async function configCommand(action, key, value) {
|
|
3682
|
+
if (!action || action === "show") {
|
|
3683
|
+
const config = getGlobalConfig();
|
|
3684
|
+
ui.section("Shipem Global Config");
|
|
3685
|
+
ui.br();
|
|
3686
|
+
ui.kv("Webhook", config.notifications?.webhook ?? "(not set)");
|
|
3687
|
+
ui.br();
|
|
3688
|
+
return;
|
|
3689
|
+
}
|
|
3690
|
+
if (action === "set") {
|
|
3691
|
+
if (!key) {
|
|
3692
|
+
ui.error("Usage: shipem config set <key> <value>");
|
|
3693
|
+
ui.dim("Available keys: webhook");
|
|
3694
|
+
ui.br();
|
|
3695
|
+
return;
|
|
3696
|
+
}
|
|
3697
|
+
if (key === "webhook") {
|
|
3698
|
+
if (!value || value === "off") {
|
|
3699
|
+
setWebhook(null);
|
|
3700
|
+
ui.success("Webhook notifications disabled.");
|
|
3701
|
+
} else {
|
|
3702
|
+
setWebhook(value);
|
|
3703
|
+
ui.success(`Webhook set to: ${value}`);
|
|
3704
|
+
}
|
|
3705
|
+
ui.br();
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
ui.error(`Unknown config key: ${key}`);
|
|
3709
|
+
ui.dim("Available keys: webhook");
|
|
3710
|
+
ui.br();
|
|
3711
|
+
return;
|
|
3712
|
+
}
|
|
3713
|
+
ui.error(`Unknown action: ${action}`);
|
|
3714
|
+
ui.dim("Usage: shipem config [show|set] [key] [value]");
|
|
3715
|
+
ui.br();
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
// src/commands/monitor.ts
|
|
3719
|
+
import axios5 from "axios";
|
|
3720
|
+
import chalk12 from "chalk";
|
|
3721
|
+
import { writeFileSync as writeFileSync8, readFileSync as readFileSync10, existsSync as existsSync10, unlinkSync } from "fs";
|
|
3722
|
+
import { join as join13 } from "path";
|
|
3723
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
3724
|
+
|
|
3725
|
+
// src/notifications/index.ts
|
|
2320
3726
|
import axios4 from "axios";
|
|
3727
|
+
async function sendStatusNotification(message, webhookOverride) {
|
|
3728
|
+
const webhook = webhookOverride ?? getWebhook();
|
|
3729
|
+
if (!webhook) return false;
|
|
3730
|
+
try {
|
|
3731
|
+
await axios4.post(webhook, { text: message }, { timeout: 5e3 });
|
|
3732
|
+
return true;
|
|
3733
|
+
} catch {
|
|
3734
|
+
return false;
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
// src/commands/monitor.ts
|
|
3739
|
+
var PID_FILE = join13(tmpdir2(), "shipem-monitor.pid");
|
|
3740
|
+
var DEFAULT_INTERVAL_MS = 5 * 60 * 1e3;
|
|
3741
|
+
async function checkSite(name, url) {
|
|
3742
|
+
const start = Date.now();
|
|
3743
|
+
try {
|
|
3744
|
+
const resp = await axios5.head(url, { timeout: 1e4, maxRedirects: 5 });
|
|
3745
|
+
return {
|
|
3746
|
+
name,
|
|
3747
|
+
url,
|
|
3748
|
+
status: resp.status,
|
|
3749
|
+
responseTime: Date.now() - start,
|
|
3750
|
+
up: resp.status >= 200 && resp.status < 400
|
|
3751
|
+
};
|
|
3752
|
+
} catch (err) {
|
|
3753
|
+
const elapsed = Date.now() - start;
|
|
3754
|
+
if (axios5.isAxiosError(err) && err.response) {
|
|
3755
|
+
return {
|
|
3756
|
+
name,
|
|
3757
|
+
url,
|
|
3758
|
+
status: err.response.status,
|
|
3759
|
+
responseTime: elapsed,
|
|
3760
|
+
up: false,
|
|
3761
|
+
error: `HTTP ${err.response.status}`
|
|
3762
|
+
};
|
|
3763
|
+
}
|
|
3764
|
+
return {
|
|
3765
|
+
name,
|
|
3766
|
+
url,
|
|
3767
|
+
status: null,
|
|
3768
|
+
responseTime: elapsed,
|
|
3769
|
+
up: false,
|
|
3770
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
function displayResults(results) {
|
|
3775
|
+
console.clear();
|
|
3776
|
+
ui.section("Shipem Monitor");
|
|
3777
|
+
ui.br();
|
|
3778
|
+
if (results.length === 0) {
|
|
3779
|
+
ui.dim("No projects to monitor. Deploy a project first.");
|
|
3780
|
+
ui.br();
|
|
3781
|
+
return;
|
|
3782
|
+
}
|
|
3783
|
+
const maxName = Math.max(...results.map((r) => r.name.length));
|
|
3784
|
+
for (const r of results) {
|
|
3785
|
+
const nameCol = r.name.padEnd(maxName + 2);
|
|
3786
|
+
const timeCol = `${r.responseTime}ms`.padStart(7);
|
|
3787
|
+
if (r.up) {
|
|
3788
|
+
console.log(` ${brand.green("\u2713")} ${brand.bold(nameCol)} ${brand.dim(r.url.padEnd(40))} ${brand.green(`${r.status} OK`)} ${brand.dim(`(${timeCol})`)}`);
|
|
3789
|
+
} else {
|
|
3790
|
+
const statusText = r.error ?? `${r.status}`;
|
|
3791
|
+
console.log(` ${brand.red("\u2717")} ${brand.bold(nameCol)} ${brand.dim(r.url.padEnd(40))} ${brand.red(statusText)} ${brand.red("\u2190 DOWN!")}`);
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
ui.br();
|
|
3795
|
+
}
|
|
3796
|
+
async function monitorCommand(options = {}) {
|
|
3797
|
+
if (options.stop) {
|
|
3798
|
+
if (existsSync10(PID_FILE)) {
|
|
3799
|
+
try {
|
|
3800
|
+
const pid = parseInt(readFileSync10(PID_FILE, "utf-8").trim(), 10);
|
|
3801
|
+
process.kill(pid, "SIGTERM");
|
|
3802
|
+
unlinkSync(PID_FILE);
|
|
3803
|
+
ui.success("Monitor daemon stopped.");
|
|
3804
|
+
} catch {
|
|
3805
|
+
ui.warn("Could not stop monitor daemon. It may have already exited.");
|
|
3806
|
+
try {
|
|
3807
|
+
unlinkSync(PID_FILE);
|
|
3808
|
+
} catch {
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
} else {
|
|
3812
|
+
ui.info("No monitor daemon running.");
|
|
3813
|
+
}
|
|
3814
|
+
ui.br();
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
const projects = getProjects();
|
|
3818
|
+
if (projects.length === 0) {
|
|
3819
|
+
ui.section("Shipem Monitor");
|
|
3820
|
+
ui.br();
|
|
3821
|
+
ui.dim("No projects found. Deploy a project first, then run `shipem monitor`.");
|
|
3822
|
+
ui.br();
|
|
3823
|
+
return;
|
|
3824
|
+
}
|
|
3825
|
+
const intervalMs = options.interval ? parseInt(options.interval, 10) * 60 * 1e3 : DEFAULT_INTERVAL_MS;
|
|
3826
|
+
if (options.daemon) {
|
|
3827
|
+
writePidFile();
|
|
3828
|
+
ui.success(`Monitor daemon started (PID ${process.pid}). Checking every ${intervalMs / 6e4} minutes.`);
|
|
3829
|
+
ui.dim(`Stop with: shipem monitor stop`);
|
|
3830
|
+
ui.br();
|
|
3831
|
+
}
|
|
3832
|
+
const previousUp = /* @__PURE__ */ new Map();
|
|
3833
|
+
const runCheck = async () => {
|
|
3834
|
+
const results = await Promise.all(
|
|
3835
|
+
projects.map((p) => checkSite(p.name, p.url))
|
|
3836
|
+
);
|
|
3837
|
+
if (!options.daemon) {
|
|
3838
|
+
displayResults(results);
|
|
3839
|
+
const intervalMin = intervalMs / 6e4;
|
|
3840
|
+
console.log(` ${brand.dim(`Checking every ${intervalMin} minutes. Press ${chalk12.cyan("[q]")} to stop.`)}`);
|
|
3841
|
+
ui.br();
|
|
3842
|
+
}
|
|
3843
|
+
for (const r of results) {
|
|
3844
|
+
const wasUp = previousUp.get(r.name);
|
|
3845
|
+
if (wasUp !== void 0) {
|
|
3846
|
+
if (wasUp && !r.up) {
|
|
3847
|
+
await sendStatusNotification(`\u{1F534} ${r.name} is DOWN! ${r.url} \u2014 ${r.error ?? "unreachable"}`);
|
|
3848
|
+
} else if (!wasUp && r.up) {
|
|
3849
|
+
await sendStatusNotification(`\u{1F7E2} ${r.name} is back UP! ${r.url} \u2014 ${r.responseTime}ms`);
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
previousUp.set(r.name, r.up);
|
|
3853
|
+
}
|
|
3854
|
+
};
|
|
3855
|
+
await runCheck();
|
|
3856
|
+
const timer = setInterval(() => void runCheck(), intervalMs);
|
|
3857
|
+
if (!options.daemon && process.stdin.isTTY) {
|
|
3858
|
+
process.stdin.setRawMode(true);
|
|
3859
|
+
process.stdin.resume();
|
|
3860
|
+
process.stdin.setEncoding("utf-8");
|
|
3861
|
+
process.stdin.on("data", (key) => {
|
|
3862
|
+
if (key === "q" || key === "Q" || key === "") {
|
|
3863
|
+
clearInterval(timer);
|
|
3864
|
+
console.log("");
|
|
3865
|
+
ui.info("Monitor stopped.");
|
|
3866
|
+
process.exit(0);
|
|
3867
|
+
}
|
|
3868
|
+
});
|
|
3869
|
+
}
|
|
3870
|
+
await new Promise(() => {
|
|
3871
|
+
});
|
|
3872
|
+
}
|
|
3873
|
+
function writePidFile() {
|
|
3874
|
+
writeFileSync8(PID_FILE, String(process.pid), "utf-8");
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
// src/commands/hooks.ts
|
|
3878
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync2, chmodSync, mkdirSync as mkdirSync3 } from "fs";
|
|
3879
|
+
import { join as join14 } from "path";
|
|
3880
|
+
var HOOK_MARKER = "# shipem-auto-deploy";
|
|
3881
|
+
var HOOK_CONTENT = `#!/bin/sh
|
|
3882
|
+
${HOOK_MARKER}
|
|
3883
|
+
# Auto-deploy on commit \u2014 installed by shipem hooks install
|
|
3884
|
+
# Run shipem deploy in background so it doesn't block the commit
|
|
3885
|
+
nohup shipem deploy --yes --quiet > /dev/null 2>&1 &
|
|
3886
|
+
`;
|
|
3887
|
+
function getHookPath(cwd) {
|
|
3888
|
+
return join14(cwd, ".git", "hooks", "post-commit");
|
|
3889
|
+
}
|
|
3890
|
+
function isShipemHook(path) {
|
|
3891
|
+
if (!existsSync11(path)) return false;
|
|
3892
|
+
const content = readFileSync11(path, "utf-8");
|
|
3893
|
+
return content.includes(HOOK_MARKER);
|
|
3894
|
+
}
|
|
3895
|
+
async function hooksCommand(action) {
|
|
3896
|
+
const cwd = process.cwd();
|
|
3897
|
+
const gitDir = join14(cwd, ".git");
|
|
3898
|
+
if (!existsSync11(gitDir)) {
|
|
3899
|
+
ui.error("Not a git repository. Run `git init` first.");
|
|
3900
|
+
ui.br();
|
|
3901
|
+
return;
|
|
3902
|
+
}
|
|
3903
|
+
const hooksDir = join14(gitDir, "hooks");
|
|
3904
|
+
const hookPath = getHookPath(cwd);
|
|
3905
|
+
if (!action || action === "status") {
|
|
3906
|
+
ui.section("Git Hooks");
|
|
3907
|
+
ui.br();
|
|
3908
|
+
if (isShipemHook(hookPath)) {
|
|
3909
|
+
ui.success("post-commit hook is installed (auto-deploy on commit)");
|
|
3910
|
+
ui.dim(`Remove with: shipem hooks remove`);
|
|
3911
|
+
} else if (existsSync11(hookPath)) {
|
|
3912
|
+
ui.info("A post-commit hook exists but was not installed by Shipem.");
|
|
3913
|
+
ui.dim(`Path: ${hookPath}`);
|
|
3914
|
+
} else {
|
|
3915
|
+
ui.info("No post-commit hook installed.");
|
|
3916
|
+
ui.dim(`Install with: shipem hooks install`);
|
|
3917
|
+
}
|
|
3918
|
+
ui.br();
|
|
3919
|
+
return;
|
|
3920
|
+
}
|
|
3921
|
+
if (action === "install") {
|
|
3922
|
+
if (existsSync11(hookPath) && !isShipemHook(hookPath)) {
|
|
3923
|
+
ui.warn("A post-commit hook already exists and was not created by Shipem.");
|
|
3924
|
+
ui.dim(`Path: ${hookPath}`);
|
|
3925
|
+
ui.dim("Remove it manually or back it up before installing.");
|
|
3926
|
+
ui.br();
|
|
3927
|
+
return;
|
|
3928
|
+
}
|
|
3929
|
+
if (!existsSync11(hooksDir)) {
|
|
3930
|
+
mkdirSync3(hooksDir, { recursive: true });
|
|
3931
|
+
}
|
|
3932
|
+
writeFileSync9(hookPath, HOOK_CONTENT, "utf-8");
|
|
3933
|
+
chmodSync(hookPath, 493);
|
|
3934
|
+
ui.success("Installed post-commit hook \u2192 auto-deploy on commit");
|
|
3935
|
+
ui.dim(`Remove with: shipem hooks remove`);
|
|
3936
|
+
ui.br();
|
|
3937
|
+
return;
|
|
3938
|
+
}
|
|
3939
|
+
if (action === "remove") {
|
|
3940
|
+
if (!existsSync11(hookPath)) {
|
|
3941
|
+
ui.info("No post-commit hook to remove.");
|
|
3942
|
+
ui.br();
|
|
3943
|
+
return;
|
|
3944
|
+
}
|
|
3945
|
+
if (!isShipemHook(hookPath)) {
|
|
3946
|
+
ui.warn("The post-commit hook was not installed by Shipem. Not removing.");
|
|
3947
|
+
ui.br();
|
|
3948
|
+
return;
|
|
3949
|
+
}
|
|
3950
|
+
unlinkSync2(hookPath);
|
|
3951
|
+
ui.success("Removed post-commit hook.");
|
|
3952
|
+
ui.br();
|
|
3953
|
+
return;
|
|
3954
|
+
}
|
|
3955
|
+
ui.error(`Unknown action: ${action}`);
|
|
3956
|
+
ui.dim("Usage: shipem hooks [install|remove|status]");
|
|
3957
|
+
ui.br();
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
// src/commands/logout.ts
|
|
3961
|
+
import axios6 from "axios";
|
|
2321
3962
|
async function logoutCommand() {
|
|
2322
3963
|
const token = getSessionToken();
|
|
2323
3964
|
if (!token) {
|
|
@@ -2326,7 +3967,7 @@ async function logoutCommand() {
|
|
|
2326
3967
|
return;
|
|
2327
3968
|
}
|
|
2328
3969
|
try {
|
|
2329
|
-
await
|
|
3970
|
+
await axios6.post(`${SHIPEM_API_URL}/auth/logout`, { token }, { timeout: 5e3 });
|
|
2330
3971
|
} catch {
|
|
2331
3972
|
}
|
|
2332
3973
|
clearSessionToken();
|
|
@@ -2334,20 +3975,145 @@ async function logoutCommand() {
|
|
|
2334
3975
|
ui.br();
|
|
2335
3976
|
}
|
|
2336
3977
|
|
|
3978
|
+
// src/commands/preview.ts
|
|
3979
|
+
import { execa as execa5 } from "execa";
|
|
3980
|
+
import chalk13 from "chalk";
|
|
3981
|
+
async function getCurrentBranch() {
|
|
3982
|
+
try {
|
|
3983
|
+
const { stdout } = await execa5("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
3984
|
+
return stdout.trim();
|
|
3985
|
+
} catch {
|
|
3986
|
+
throw new ConfigError(
|
|
3987
|
+
"Could not detect git branch. Make sure you are inside a git repository."
|
|
3988
|
+
);
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
function sanitizeBranchName(branch) {
|
|
3992
|
+
return branch.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 28);
|
|
3993
|
+
}
|
|
3994
|
+
async function previewCommand(options = {}) {
|
|
3995
|
+
const startTime = Date.now();
|
|
3996
|
+
const cwd = process.cwd();
|
|
3997
|
+
ui.banner();
|
|
3998
|
+
console.log(` ${brand.bold("Preview deployment")}`);
|
|
3999
|
+
console.log(` ${brand.dim("Deploy a preview from your current branch")}`);
|
|
4000
|
+
console.log("");
|
|
4001
|
+
const branch = await getCurrentBranch();
|
|
4002
|
+
if (branch === "main" || branch === "master") {
|
|
4003
|
+
ui.warn(`You are on '${branch}'. Previews are meant for feature branches.`);
|
|
4004
|
+
ui.dim("Use `shipem deploy` for production deployments.");
|
|
4005
|
+
ui.br();
|
|
4006
|
+
}
|
|
4007
|
+
ui.info(`Branch: ${chalk13.cyan(branch)}`);
|
|
4008
|
+
ui.br();
|
|
4009
|
+
const existingConfig = readProjectConfig(cwd);
|
|
4010
|
+
if (!existingConfig.project) {
|
|
4011
|
+
throw new ConfigError(
|
|
4012
|
+
"No shipem.json found. Run `shipem deploy` first to detect and configure your project."
|
|
4013
|
+
);
|
|
4014
|
+
}
|
|
4015
|
+
const projectConfig = existingConfig.project;
|
|
4016
|
+
const cfCreds = getCloudflareCredentials();
|
|
4017
|
+
if (!cfCreds) {
|
|
4018
|
+
throw new ConfigError(
|
|
4019
|
+
"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"
|
|
4020
|
+
);
|
|
4021
|
+
}
|
|
4022
|
+
if (!options.skipBuild) {
|
|
4023
|
+
ui.section("Building...");
|
|
4024
|
+
ui.br();
|
|
4025
|
+
const buildResult = await buildProject(projectConfig, cwd);
|
|
4026
|
+
if (!buildResult.success) {
|
|
4027
|
+
throw new DeployError(
|
|
4028
|
+
`Build failed: ${buildResult.error ?? "Unknown error"}. Fix build errors and try again.`
|
|
4029
|
+
);
|
|
4030
|
+
}
|
|
4031
|
+
ui.br();
|
|
4032
|
+
}
|
|
4033
|
+
ui.section("Deploying preview...");
|
|
4034
|
+
ui.br();
|
|
4035
|
+
const cf = new CloudflarePages(cfCreds.apiToken, cfCreds.accountId);
|
|
4036
|
+
const cfProjectName = sanitizeProjectName(projectConfig.name);
|
|
4037
|
+
await cf.getOrCreateProject(cfProjectName, projectConfig);
|
|
4038
|
+
const result = await cf.deployBranch(
|
|
4039
|
+
cfProjectName,
|
|
4040
|
+
projectConfig.outputDirectory,
|
|
4041
|
+
branch,
|
|
4042
|
+
cwd
|
|
4043
|
+
);
|
|
4044
|
+
const finalDeployment = await cf.waitForDeployment(cfProjectName, result.deployment.id);
|
|
4045
|
+
const branchSlug = sanitizeBranchName(branch);
|
|
4046
|
+
const previewUrl = finalDeployment.url || `https://${branchSlug}.${cfProjectName}.pages.dev`;
|
|
4047
|
+
const previewRecord = {
|
|
4048
|
+
branch,
|
|
4049
|
+
url: previewUrl,
|
|
4050
|
+
deploymentId: result.deployment.id,
|
|
4051
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4052
|
+
status: "success"
|
|
4053
|
+
};
|
|
4054
|
+
const existingPreviews = existingConfig.previews ?? [];
|
|
4055
|
+
const updatedPreviews = [
|
|
4056
|
+
previewRecord,
|
|
4057
|
+
...existingPreviews.filter((p) => p.branch !== branch)
|
|
4058
|
+
].slice(0, 20);
|
|
4059
|
+
writeProjectConfig(
|
|
4060
|
+
{ ...existingConfig, previews: updatedPreviews },
|
|
4061
|
+
cwd
|
|
4062
|
+
);
|
|
4063
|
+
const elapsedSec = (Date.now() - startTime) / 1e3;
|
|
4064
|
+
console.log("");
|
|
4065
|
+
console.log(` ${brand.green.bold("Preview live!")}`);
|
|
4066
|
+
console.log("");
|
|
4067
|
+
console.log(` ${brand.gray("Branch:")} ${chalk13.cyan(branch)}`);
|
|
4068
|
+
console.log(` ${brand.gray("URL:")} ${brand.brightBlue.underline(previewUrl)}`);
|
|
4069
|
+
console.log(` ${brand.gray("Files:")} ${result.fileCount} files (${ui.formatBytes(result.totalBytes)})`);
|
|
4070
|
+
console.log(` ${brand.gray("Time:")} ${elapsedSec.toFixed(1)}s`);
|
|
4071
|
+
console.log("");
|
|
4072
|
+
const copied = ui.copyToClipboard(previewUrl);
|
|
4073
|
+
if (copied) {
|
|
4074
|
+
ui.success("Preview URL copied to clipboard!");
|
|
4075
|
+
}
|
|
4076
|
+
console.log("");
|
|
4077
|
+
console.log(` ${brand.bold("Next steps:")}`);
|
|
4078
|
+
console.log(` ${brand.gray("Share")} \u2192 Send the preview URL to teammates`);
|
|
4079
|
+
console.log(` ${brand.gray("Update")} \u2192 ${chalk13.cyan("npx shipem preview")} (re-deploy this branch)`);
|
|
4080
|
+
console.log(` ${brand.gray("Promote")} \u2192 Merge to main and ${chalk13.cyan("npx shipem deploy")}`);
|
|
4081
|
+
console.log("");
|
|
4082
|
+
}
|
|
4083
|
+
|
|
2337
4084
|
// src/index.ts
|
|
2338
|
-
import { readFileSync as
|
|
4085
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
2339
4086
|
import { fileURLToPath } from "url";
|
|
2340
|
-
import { dirname, join as
|
|
4087
|
+
import { dirname, join as join15 } from "path";
|
|
2341
4088
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
2342
4089
|
var __dirname2 = dirname(__filename2);
|
|
2343
4090
|
var version = "0.1.0";
|
|
2344
4091
|
try {
|
|
2345
4092
|
const pkg = JSON.parse(
|
|
2346
|
-
|
|
4093
|
+
readFileSync12(join15(__dirname2, "../package.json"), "utf-8")
|
|
2347
4094
|
);
|
|
2348
4095
|
version = pkg.version;
|
|
2349
4096
|
} catch {
|
|
2350
4097
|
}
|
|
4098
|
+
var globalVerbose = false;
|
|
4099
|
+
var globalJson = false;
|
|
4100
|
+
function handleError(err, fallbackMsg) {
|
|
4101
|
+
if (globalJson) {
|
|
4102
|
+
const code = err instanceof ShipemError ? err.code : "ERR_UNKNOWN";
|
|
4103
|
+
const message = err instanceof Error ? err.message : fallbackMsg;
|
|
4104
|
+
console.log(JSON.stringify({ success: false, error: message, code }));
|
|
4105
|
+
process.exit(err instanceof ShipemError ? err.exitCode : 2);
|
|
4106
|
+
}
|
|
4107
|
+
ui.br();
|
|
4108
|
+
if (err instanceof ShipemError) {
|
|
4109
|
+
ui.error(err.message);
|
|
4110
|
+
if (globalVerbose || process.env.DEBUG) console.error(err);
|
|
4111
|
+
process.exit(err.exitCode);
|
|
4112
|
+
}
|
|
4113
|
+
ui.error(err instanceof Error ? err.message : fallbackMsg);
|
|
4114
|
+
if (globalVerbose || process.env.DEBUG) console.error(err);
|
|
4115
|
+
process.exit(2);
|
|
4116
|
+
}
|
|
2351
4117
|
var program = new Command();
|
|
2352
4118
|
program.name("shipem").description(
|
|
2353
4119
|
"One-command deployment for apps built by AI coding tools.\n\nYour AI built it. We'll ship it."
|
|
@@ -2356,61 +4122,136 @@ program.name("shipem").description(
|
|
|
2356
4122
|
return "";
|
|
2357
4123
|
}).addHelpText("after", `
|
|
2358
4124
|
Quick start:
|
|
2359
|
-
npx shipem
|
|
2360
|
-
npx shipem
|
|
2361
|
-
npx shipem
|
|
2362
|
-
npx shipem
|
|
4125
|
+
npx shipem Deploy your app
|
|
4126
|
+
npx shipem preview Deploy a preview from your branch
|
|
4127
|
+
npx shipem watch Watch for changes and auto-deploy
|
|
4128
|
+
npx shipem init Bootstrap a new project
|
|
4129
|
+
npx shipem init -t <t> Init from a template
|
|
4130
|
+
npx shipem templates List available templates
|
|
4131
|
+
npx shipem fix Auto-fix build errors
|
|
4132
|
+
npx shipem env Check environment variables
|
|
4133
|
+
npx shipem dev Start dev server with deploy shortcut
|
|
4134
|
+
npx shipem status Check deployment status
|
|
4135
|
+
npx shipem history Show deploy history across projects
|
|
4136
|
+
npx shipem monitor Monitor site uptime
|
|
4137
|
+
npx shipem hooks Manage git deploy hooks
|
|
4138
|
+
npx shipem config Manage global settings
|
|
4139
|
+
npx shipem login Authenticate with GitHub
|
|
4140
|
+
npx shipem down Take your app offline
|
|
2363
4141
|
`);
|
|
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) => {
|
|
4142
|
+
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) => {
|
|
4143
|
+
globalVerbose = options.verbose ?? false;
|
|
4144
|
+
globalJson = options.json ?? false;
|
|
2365
4145
|
try {
|
|
2366
4146
|
await deployCommand(options);
|
|
2367
4147
|
} catch (err) {
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
4148
|
+
handleError(err, "An unexpected error occurred.");
|
|
4149
|
+
}
|
|
4150
|
+
});
|
|
4151
|
+
program.command("fix").description("Auto-fix common build errors (missing deps, TypeScript issues, etc.)").option("--verbose", "Show detailed debug output").action(async (options) => {
|
|
4152
|
+
globalVerbose = options.verbose ?? false;
|
|
4153
|
+
try {
|
|
4154
|
+
await fixCommand(options);
|
|
4155
|
+
} catch (err) {
|
|
4156
|
+
handleError(err, "Fix command failed.");
|
|
4157
|
+
}
|
|
4158
|
+
});
|
|
4159
|
+
program.command("env").description("Scan and manage environment variables").action(async () => {
|
|
4160
|
+
try {
|
|
4161
|
+
await envCommand();
|
|
4162
|
+
} catch (err) {
|
|
4163
|
+
handleError(err, "Env command failed.");
|
|
4164
|
+
}
|
|
4165
|
+
});
|
|
4166
|
+
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) => {
|
|
4167
|
+
try {
|
|
4168
|
+
await initCommand(options);
|
|
4169
|
+
} catch (err) {
|
|
4170
|
+
handleError(err, "Init command failed.");
|
|
4171
|
+
}
|
|
4172
|
+
});
|
|
4173
|
+
program.command("dev").description("Start dev server with deploy shortcut").action(async () => {
|
|
4174
|
+
try {
|
|
4175
|
+
await devCommand();
|
|
4176
|
+
} catch (err) {
|
|
4177
|
+
handleError(err, "Dev command failed.");
|
|
2374
4178
|
}
|
|
2375
4179
|
});
|
|
2376
4180
|
program.command("login").description("Log in to Shipem via GitHub").action(async () => {
|
|
2377
4181
|
try {
|
|
2378
4182
|
await loginCommand();
|
|
2379
4183
|
} catch (err) {
|
|
2380
|
-
|
|
2381
|
-
process.exit(1);
|
|
4184
|
+
handleError(err, "Login failed.");
|
|
2382
4185
|
}
|
|
2383
4186
|
});
|
|
2384
4187
|
program.command("logout").description("Log out of Shipem").action(async () => {
|
|
2385
4188
|
try {
|
|
2386
4189
|
await logoutCommand();
|
|
2387
4190
|
} catch (err) {
|
|
2388
|
-
|
|
2389
|
-
process.exit(1);
|
|
4191
|
+
handleError(err, "Logout failed.");
|
|
2390
4192
|
}
|
|
2391
4193
|
});
|
|
2392
4194
|
program.command("status").description("Check deployment status").action(async () => {
|
|
2393
4195
|
try {
|
|
2394
4196
|
await statusCommand();
|
|
2395
4197
|
} catch (err) {
|
|
2396
|
-
|
|
2397
|
-
process.exit(1);
|
|
4198
|
+
handleError(err, "An unexpected error occurred.");
|
|
2398
4199
|
}
|
|
2399
4200
|
});
|
|
2400
4201
|
program.command("logs").description("View recent deployment logs").option("-n, --lines <number>", "Number of log lines to show", "100").action(async (options) => {
|
|
2401
4202
|
try {
|
|
2402
4203
|
await logsCommand({ lines: options.lines ? parseInt(options.lines, 10) : void 0 });
|
|
2403
4204
|
} catch (err) {
|
|
2404
|
-
|
|
2405
|
-
process.exit(1);
|
|
4205
|
+
handleError(err, "An unexpected error occurred.");
|
|
2406
4206
|
}
|
|
2407
4207
|
});
|
|
2408
4208
|
program.command("down").description("Take down a deployment").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
|
|
2409
4209
|
try {
|
|
2410
4210
|
await downCommand(options);
|
|
2411
4211
|
} catch (err) {
|
|
2412
|
-
|
|
2413
|
-
|
|
4212
|
+
handleError(err, "An unexpected error occurred.");
|
|
4213
|
+
}
|
|
4214
|
+
});
|
|
4215
|
+
program.command("history").description("Show deploy history across all projects").action(async () => {
|
|
4216
|
+
try {
|
|
4217
|
+
await historyCommand();
|
|
4218
|
+
} catch (err) {
|
|
4219
|
+
handleError(err, "History command failed.");
|
|
4220
|
+
}
|
|
4221
|
+
});
|
|
4222
|
+
program.command("config [action] [key] [value]").description("Manage global Shipem settings").action(async (action, key, value) => {
|
|
4223
|
+
try {
|
|
4224
|
+
await configCommand(action, key, value);
|
|
4225
|
+
} catch (err) {
|
|
4226
|
+
handleError(err, "Config command failed.");
|
|
4227
|
+
}
|
|
4228
|
+
});
|
|
4229
|
+
program.command("watch").description("Watch for file changes and auto-deploy").action(async () => {
|
|
4230
|
+
try {
|
|
4231
|
+
await watchCommand();
|
|
4232
|
+
} catch (err) {
|
|
4233
|
+
handleError(err, "Watch command failed.");
|
|
4234
|
+
}
|
|
4235
|
+
});
|
|
4236
|
+
program.command("hooks [action]").description("Manage git hooks for auto-deploy (install|remove|status)").action(async (action) => {
|
|
4237
|
+
try {
|
|
4238
|
+
await hooksCommand(action);
|
|
4239
|
+
} catch (err) {
|
|
4240
|
+
handleError(err, "Hooks command failed.");
|
|
4241
|
+
}
|
|
4242
|
+
});
|
|
4243
|
+
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) => {
|
|
4244
|
+
try {
|
|
4245
|
+
await monitorCommand(options);
|
|
4246
|
+
} catch (err) {
|
|
4247
|
+
handleError(err, "Monitor command failed.");
|
|
4248
|
+
}
|
|
4249
|
+
});
|
|
4250
|
+
monitorCmd.command("stop").description("Stop the monitor daemon").action(async () => {
|
|
4251
|
+
try {
|
|
4252
|
+
await monitorCommand({ stop: true });
|
|
4253
|
+
} catch (err) {
|
|
4254
|
+
handleError(err, "Monitor stop failed.");
|
|
2414
4255
|
}
|
|
2415
4256
|
});
|
|
2416
4257
|
var domainsCmd = program.command("domains").description("Manage custom domains (coming soon)");
|
|
@@ -2419,6 +4260,21 @@ domainsCmd.command("add <domain>").description("Add a custom domain to your depl
|
|
|
2419
4260
|
ui.dim("Follow our changelog at https://shipem.dev/changelog for updates.");
|
|
2420
4261
|
ui.br();
|
|
2421
4262
|
});
|
|
4263
|
+
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) => {
|
|
4264
|
+
globalVerbose = options.verbose ?? false;
|
|
4265
|
+
try {
|
|
4266
|
+
await previewCommand(options);
|
|
4267
|
+
} catch (err) {
|
|
4268
|
+
handleError(err, "Preview command failed.");
|
|
4269
|
+
}
|
|
4270
|
+
});
|
|
4271
|
+
program.command("templates").description("List available project templates for `shipem init --template`").action(async () => {
|
|
4272
|
+
try {
|
|
4273
|
+
await templatesCommand();
|
|
4274
|
+
} catch (err) {
|
|
4275
|
+
handleError(err, "Templates command failed.");
|
|
4276
|
+
}
|
|
4277
|
+
});
|
|
2422
4278
|
program.on("command:*", (cmds) => {
|
|
2423
4279
|
ui.error(`Unknown command: ${cmds[0]}`);
|
|
2424
4280
|
ui.dim("Run `shipem --help` to see available commands.");
|