ship-em 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +43 -0
  3. package/dist/index.js +2427 -0
  4. package/package.json +48 -0
package/dist/index.js ADDED
@@ -0,0 +1,2427 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/deploy.ts
7
+ import inquirer from "inquirer";
8
+ import chalk4 from "chalk";
9
+ import axios2 from "axios";
10
+ import FormData2 from "form-data";
11
+ import { createReadStream } from "fs";
12
+ import { rmSync, existsSync as existsSync5, appendFileSync, writeFileSync as writeFileSync2, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
13
+ import { join as join6 } from "path";
14
+ import { tmpdir } from "os";
15
+ import { create as tarCreate } from "tar";
16
+
17
+ // src/ui/terminal.ts
18
+ import chalk from "chalk";
19
+ import ora from "ora";
20
+ var brand = {
21
+ blue: chalk.hex("#3B82F6"),
22
+ brightBlue: chalk.hex("#60A5FA"),
23
+ green: chalk.hex("#22C55E"),
24
+ yellow: chalk.hex("#EAB308"),
25
+ red: chalk.hex("#EF4444"),
26
+ gray: chalk.hex("#6B7280"),
27
+ white: chalk.white,
28
+ bold: chalk.bold,
29
+ dim: chalk.dim
30
+ };
31
+ function visibleLen(str) {
32
+ return str.replace(/\x1B\[[0-9;]*[mGKHFJsu]/g, "").length;
33
+ }
34
+ function timeAgo(isoStr) {
35
+ const diff = Date.now() - new Date(isoStr).getTime();
36
+ const mins = Math.floor(diff / 6e4);
37
+ if (mins < 1) return "just now";
38
+ if (mins === 1) return "1 minute ago";
39
+ if (mins < 60) return `${mins} minutes ago`;
40
+ const hrs = Math.floor(mins / 60);
41
+ if (hrs === 1) return "1 hour ago";
42
+ if (hrs < 24) return `${hrs} hours ago`;
43
+ const days = Math.floor(hrs / 24);
44
+ return days === 1 ? "yesterday" : `${days} days ago`;
45
+ }
46
+ var ui = {
47
+ // Banner shown at startup
48
+ banner() {
49
+ console.log("");
50
+ console.log(
51
+ brand.blue.bold(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557")
52
+ );
53
+ console.log(
54
+ brand.blue.bold(" \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551")
55
+ );
56
+ console.log(
57
+ brand.brightBlue.bold(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551")
58
+ );
59
+ console.log(
60
+ brand.brightBlue.bold(" \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551")
61
+ );
62
+ console.log(
63
+ brand.blue.bold(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551")
64
+ );
65
+ console.log(
66
+ brand.blue.bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D")
67
+ );
68
+ console.log("");
69
+ console.log(
70
+ ` ${brand.gray("Your AI built it.")} ${brand.blue.bold("We'll ship it.")}`
71
+ );
72
+ console.log("");
73
+ },
74
+ // Section header
75
+ section(title) {
76
+ console.log(`${brand.blue("\u203A")} ${brand.bold(title)}`);
77
+ },
78
+ // Success item
79
+ success(message) {
80
+ console.log(` ${brand.green("\u2713")} ${message}`);
81
+ },
82
+ // Info item
83
+ info(message) {
84
+ console.log(` ${brand.blue("\xB7")} ${message}`);
85
+ },
86
+ // Warning
87
+ warn(message) {
88
+ console.log(` ${brand.yellow("\u26A0")} ${brand.yellow(message)}`);
89
+ },
90
+ // Error
91
+ error(message) {
92
+ console.log(` ${brand.red("\u2717")} ${brand.red(message)}`);
93
+ },
94
+ // Dim/secondary text
95
+ dim(message) {
96
+ console.log(` ${brand.dim(message)}`);
97
+ },
98
+ // Blank line
99
+ br() {
100
+ console.log("");
101
+ },
102
+ // Key-value display
103
+ kv(key, value) {
104
+ console.log(` ${brand.gray(key + ":")} ${value}`);
105
+ },
106
+ // Highlighted URL
107
+ url(label, url) {
108
+ console.log(` ${brand.green("\u2192")} ${brand.bold(label)}: ${brand.brightBlue.underline(url)}`);
109
+ },
110
+ // Box for important info
111
+ box(lines) {
112
+ const maxLen = Math.max(...lines.map((l) => l.length));
113
+ const border = brand.blue("\u2500".repeat(maxLen + 4));
114
+ console.log(` \u256D${border}\u256E`);
115
+ for (const line of lines) {
116
+ const padding = " ".repeat(maxLen - line.length);
117
+ console.log(` \u2502 ${line}${padding} \u2502`);
118
+ }
119
+ console.log(` \u2570${border}\u256F`);
120
+ },
121
+ // Spinner
122
+ spinner(text) {
123
+ return ora({
124
+ text,
125
+ spinner: "dots",
126
+ color: "blue",
127
+ prefixText: " "
128
+ }).start();
129
+ },
130
+ // Fatal error with exit
131
+ fatal(message, hint) {
132
+ console.log("");
133
+ console.log(` ${brand.red.bold("Error:")} ${message}`);
134
+ if (hint) {
135
+ console.log(` ${brand.gray("Hint:")} ${hint}`);
136
+ }
137
+ console.log("");
138
+ process.exit(1);
139
+ },
140
+ // Structured friendly error: what happened, why, and exactly what to do
141
+ friendlyError(title, why, fix, extra) {
142
+ console.log("");
143
+ console.log(` ${brand.red("\u2717")} ${brand.red.bold(title)}`);
144
+ console.log("");
145
+ console.log(` ${brand.dim("Why:")} ${brand.dim(why)}`);
146
+ console.log(` ${chalk.bold("Fix:")} ${fix}`);
147
+ if (extra) {
148
+ console.log("");
149
+ console.log(` ${brand.dim(extra)}`);
150
+ }
151
+ console.log("");
152
+ },
153
+ // Project analysis card — shown after detection
154
+ projectAnalysis(info) {
155
+ const buildLabel = info.buildCommand ? `${info.buildCommand} \u2192 ${info.outputDir}` : `No build step \u2192 ${info.outputDir}`;
156
+ const envLabel = info.envVarCount > 0 ? `${info.envVarCount} environment variable${info.envVarCount !== 1 ? "s" : ""} found` : "No environment variables";
157
+ const rows = [
158
+ [`\u{1F4E6} ${info.name}`, `\u{1F4E6} ${brand.bold(info.name)}`],
159
+ [`\u26A1 ${info.framework}`, `\u26A1 ${ui.framework(info.framework)}`],
160
+ [`\u{1F4C1} ${info.fileCount} source files`, `\u{1F4C1} ${chalk.cyan(String(info.fileCount))} source files`],
161
+ [
162
+ `\u{1F527} ${buildLabel}`,
163
+ info.buildCommand ? `\u{1F527} ${chalk.cyan(info.buildCommand)} \u2192 ${chalk.cyan(info.outputDir)}` : `\u{1F527} ${brand.dim("No build step")} \u2192 ${chalk.cyan(info.outputDir)}`
164
+ ],
165
+ [`\u{1F30D} Deploy target: ${info.deployTarget}`, `\u{1F30D} Deploy target: ${chalk.cyan(info.deployTarget)}`],
166
+ [
167
+ `\u{1F511} ${envLabel}`,
168
+ info.envVarCount > 0 ? `\u{1F511} ${chalk.cyan(envLabel)}` : `\u{1F511} ${brand.dim(envLabel)}`
169
+ ]
170
+ ];
171
+ const title = " Project Analysis ";
172
+ const maxPlain = Math.max(...rows.map(([p]) => visibleLen(p)));
173
+ const innerWidth = Math.max(maxPlain + 4, title.length + 2);
174
+ const topBorder = title + "\u2500".repeat(innerWidth - title.length);
175
+ const bottomBorder = "\u2500".repeat(innerWidth);
176
+ console.log("");
177
+ console.log(` ${brand.blue("\u250C" + topBorder + "\u2510")}`);
178
+ console.log(` ${brand.blue("\u2502")}${" ".repeat(innerWidth)}${brand.blue("\u2502")}`);
179
+ for (const [plain, colored] of rows) {
180
+ const rightPad = innerWidth - 2 - visibleLen(plain);
181
+ console.log(
182
+ ` ${brand.blue("\u2502")} ${colored}${" ".repeat(Math.max(0, rightPad))}${brand.blue("\u2502")}`
183
+ );
184
+ }
185
+ console.log(` ${brand.blue("\u2502")}${" ".repeat(innerWidth)}${brand.blue("\u2502")}`);
186
+ console.log(` ${brand.blue("\u2514" + bottomBorder + "\u2518")}`);
187
+ console.log("");
188
+ },
189
+ // Compact header for re-deploys — skips the banner
190
+ redeployHeader(projectName, lastUrl, lastDeployedAt) {
191
+ const ago = timeAgo(lastDeployedAt);
192
+ console.log("");
193
+ console.log(` \u{1F504} ${brand.bold("Re-deploying")} ${brand.blue.bold(projectName)}`);
194
+ console.log("");
195
+ console.log(
196
+ ` ${brand.dim("Last deploy:")} ${brand.dim(ago)} ${brand.dim("\u2192")} ${brand.brightBlue.underline(lastUrl)}`
197
+ );
198
+ console.log("");
199
+ },
200
+ // Deploy celebration box — shown on success
201
+ deployBox(appName, url, elapsedSec, fileCount, totalBytes) {
202
+ const statsLine = `Deployed in ${elapsedSec.toFixed(1)}s \xB7 ${fileCount} files \xB7 ${ui.formatBytes(totalBytes)}`;
203
+ const content = [
204
+ ["\u{1F680} Shipped!", brand.green.bold("\u{1F680} Shipped!")],
205
+ null,
206
+ [`${appName} is live`, `${brand.white.bold(appName)} is live`],
207
+ null,
208
+ [`\u2192 ${url}`, `${brand.blue("\u2192")} ${brand.brightBlue.underline(url)}`],
209
+ null,
210
+ [statsLine, brand.dim(statsLine)]
211
+ ];
212
+ const maxPlain = Math.max(
213
+ ...content.filter((item) => item !== null).map(([p]) => visibleLen(p))
214
+ );
215
+ const innerWidth = maxPlain + 6;
216
+ console.log("");
217
+ console.log(` ${brand.blue("\u256D" + "\u2500".repeat(innerWidth) + "\u256E")}`);
218
+ console.log(` ${brand.blue("\u2502")}${" ".repeat(innerWidth)}${brand.blue("\u2502")}`);
219
+ for (const item of content) {
220
+ if (!item) {
221
+ console.log(` ${brand.blue("\u2502")}${" ".repeat(innerWidth)}${brand.blue("\u2502")}`);
222
+ } else {
223
+ const [plain, colored] = item;
224
+ const rightPad = innerWidth - 3 - visibleLen(plain);
225
+ console.log(
226
+ ` ${brand.blue("\u2502")} ${colored}${" ".repeat(Math.max(0, rightPad))}${brand.blue("\u2502")}`
227
+ );
228
+ }
229
+ }
230
+ console.log(` ${brand.blue("\u2502")}${" ".repeat(innerWidth)}${brand.blue("\u2502")}`);
231
+ console.log(` ${brand.blue("\u2570" + "\u2500".repeat(innerWidth) + "\u256F")}`);
232
+ console.log("");
233
+ console.log(` ${brand.bold("Next steps:")}`);
234
+ console.log(` ${brand.gray("Update")} \u2192 ${chalk.cyan("npx shipem")}`);
235
+ console.log(` ${brand.gray("Status")} \u2192 ${chalk.cyan("npx shipem status")}`);
236
+ console.log(` ${brand.gray("Remove")} \u2192 ${chalk.cyan("npx shipem down")}`);
237
+ console.log("");
238
+ console.log(` ${brand.dim("Tip: Share your app! Just send the URL.")}`);
239
+ console.log("");
240
+ },
241
+ // Format bytes
242
+ formatBytes(bytes) {
243
+ if (bytes < 1024) return `${bytes} B`;
244
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
245
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
246
+ },
247
+ // Colorize framework name
248
+ framework(name) {
249
+ const colors = {
250
+ nextjs: "#ffffff",
251
+ "vite-react": "#61DAFB",
252
+ "vite-vue": "#42B883",
253
+ "vite-svelte": "#FF3E00",
254
+ astro: "#FF5D01",
255
+ sveltekit: "#FF3E00",
256
+ remix: "#818cf8",
257
+ nuxt: "#00DC82",
258
+ gatsby: "#663399",
259
+ flask: "#22c55e",
260
+ fastapi: "#009688",
261
+ django: "#22c55e",
262
+ express: "#fbbf24",
263
+ hono: "#E36002"
264
+ };
265
+ const color = colors[name] ?? "#3B82F6";
266
+ return chalk.hex(color).bold(name);
267
+ }
268
+ };
269
+
270
+ // src/detect/scanner.ts
271
+ import { readFileSync, existsSync, readdirSync, statSync } from "fs";
272
+ import { join } from "path";
273
+ function readJson(filePath) {
274
+ try {
275
+ return JSON.parse(readFileSync(filePath, "utf-8"));
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+ function readFile(filePath) {
281
+ try {
282
+ return readFileSync(filePath, "utf-8");
283
+ } catch {
284
+ return null;
285
+ }
286
+ }
287
+ function listTopLevelFiles(cwd) {
288
+ try {
289
+ return readdirSync(cwd).filter((f) => {
290
+ try {
291
+ return statSync(join(cwd, f)).isFile();
292
+ } catch {
293
+ return false;
294
+ }
295
+ });
296
+ } catch {
297
+ return [];
298
+ }
299
+ }
300
+ function listTopLevelDirs(cwd) {
301
+ try {
302
+ return readdirSync(cwd).filter((f) => {
303
+ try {
304
+ return statSync(join(cwd, f)).isDirectory();
305
+ } catch {
306
+ return false;
307
+ }
308
+ });
309
+ } catch {
310
+ return [];
311
+ }
312
+ }
313
+ function detectEnvVarsFromFiles(cwd) {
314
+ const envVars = [];
315
+ const seen = /* @__PURE__ */ new Set();
316
+ const envExampleFiles = [".env.example", ".env.local.example", ".env.sample", ".env.template"];
317
+ for (const file of envExampleFiles) {
318
+ const content = readFile(join(cwd, file));
319
+ if (content) {
320
+ for (const line of content.split("\n")) {
321
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
322
+ if (match && !seen.has(match[1])) {
323
+ seen.add(match[1]);
324
+ envVars.push({
325
+ name: match[1],
326
+ description: line.includes("#") ? line.split("#")[1].trim() : "",
327
+ required: true
328
+ });
329
+ }
330
+ }
331
+ }
332
+ }
333
+ const envContent = readFile(join(cwd, ".env"));
334
+ if (envContent) {
335
+ for (const line of envContent.split("\n")) {
336
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
337
+ if (match && !seen.has(match[1])) {
338
+ seen.add(match[1]);
339
+ envVars.push({
340
+ name: match[1],
341
+ description: "",
342
+ required: true
343
+ });
344
+ }
345
+ }
346
+ }
347
+ return envVars;
348
+ }
349
+ function getNodeVersion(pkg, cwd) {
350
+ const nvmrc = readFile(join(cwd, ".nvmrc"));
351
+ if (nvmrc) return nvmrc.trim().replace("v", "");
352
+ const nodeVersion = readFile(join(cwd, ".node-version"));
353
+ if (nodeVersion) return nodeVersion.trim().replace("v", "");
354
+ if (pkg.engines?.node) {
355
+ const match = pkg.engines.node.match(/(\d+)/);
356
+ if (match) return match[1];
357
+ }
358
+ return "20";
359
+ }
360
+ function scanProject(cwd = process.cwd()) {
361
+ const files = listTopLevelFiles(cwd);
362
+ const dirs = listTopLevelDirs(cwd);
363
+ const allEntries = [...files, ...dirs];
364
+ const hasFile = (name) => files.includes(name);
365
+ const hasDir = (name) => dirs.includes(name);
366
+ if (hasFile("package.json")) {
367
+ const pkg = readJson(join(cwd, "package.json"));
368
+ if (!pkg) {
369
+ return buildUnknown(cwd, files);
370
+ }
371
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
372
+ const projectName = pkg.name ?? cwd.split("/").pop() ?? "my-app";
373
+ const nodeVersion = getNodeVersion(pkg, cwd);
374
+ const envVars = detectEnvVarsFromFiles(cwd);
375
+ if (deps["next"]) {
376
+ const isAppRouter = hasDir("app") || existsSync(join(cwd, "src/app"));
377
+ const hasServerComponents = isAppRouter;
378
+ const hasApiRoutes = hasDir("pages") || existsSync(join(cwd, "pages/api")) || existsSync(join(cwd, "src/pages/api")) || existsSync(join(cwd, "app/api")) || existsSync(join(cwd, "src/app/api"));
379
+ const serverType = hasServerComponents || hasApiRoutes ? "serverless" : "static";
380
+ const commonNextEnvVars = [];
381
+ if (deps["@supabase/supabase-js"] || deps["@supabase/auth-helpers-nextjs"]) {
382
+ commonNextEnvVars.push(
383
+ { name: "NEXT_PUBLIC_SUPABASE_URL", description: "Supabase project URL", required: true, autoProvision: true },
384
+ { name: "NEXT_PUBLIC_SUPABASE_ANON_KEY", description: "Supabase anon key", required: true, autoProvision: true }
385
+ );
386
+ }
387
+ if (deps["openai"]) {
388
+ commonNextEnvVars.push({ name: "OPENAI_API_KEY", description: "OpenAI API key", required: true });
389
+ }
390
+ if (deps["@anthropic-ai/sdk"] || deps["@anthropic-ai/claude"]) {
391
+ commonNextEnvVars.push({ name: "ANTHROPIC_API_KEY", description: "Anthropic API key", required: true });
392
+ }
393
+ if (deps["stripe"]) {
394
+ commonNextEnvVars.push(
395
+ { name: "STRIPE_SECRET_KEY", description: "Stripe secret key", required: true },
396
+ { name: "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY", description: "Stripe publishable key", required: true }
397
+ );
398
+ }
399
+ const mergedEnvVars = mergeEnvVars(envVars, commonNextEnvVars);
400
+ return {
401
+ framework: "nextjs",
402
+ buildCommand: pkg.scripts?.build ?? "npm run build",
403
+ outputDirectory: ".next",
404
+ installCommand: detectPackageManager(cwd),
405
+ serverType,
406
+ deployTarget: "cloudflare-pages",
407
+ nodeVersion,
408
+ envVars: mergedEnvVars,
409
+ projectName,
410
+ confidence: 0.98,
411
+ files: allEntries
412
+ };
413
+ }
414
+ if (deps["astro"]) {
415
+ return {
416
+ framework: "astro",
417
+ buildCommand: pkg.scripts?.build ?? "npm run build",
418
+ outputDirectory: "dist",
419
+ installCommand: detectPackageManager(cwd),
420
+ serverType: "static",
421
+ deployTarget: "cloudflare-pages",
422
+ nodeVersion,
423
+ envVars,
424
+ projectName,
425
+ confidence: 0.97,
426
+ files: allEntries
427
+ };
428
+ }
429
+ if (deps["@sveltejs/kit"]) {
430
+ return {
431
+ framework: "sveltekit",
432
+ buildCommand: pkg.scripts?.build ?? "npm run build",
433
+ outputDirectory: ".svelte-kit",
434
+ installCommand: detectPackageManager(cwd),
435
+ serverType: "serverless",
436
+ deployTarget: "cloudflare-pages",
437
+ nodeVersion,
438
+ envVars,
439
+ projectName,
440
+ confidence: 0.97,
441
+ files: allEntries
442
+ };
443
+ }
444
+ if (deps["nuxt"] || deps["nuxt3"]) {
445
+ return {
446
+ framework: "nuxt",
447
+ buildCommand: pkg.scripts?.build ?? "npm run build",
448
+ outputDirectory: ".output",
449
+ installCommand: detectPackageManager(cwd),
450
+ serverType: "serverless",
451
+ deployTarget: "cloudflare-pages",
452
+ nodeVersion,
453
+ envVars,
454
+ projectName,
455
+ confidence: 0.97,
456
+ files: allEntries
457
+ };
458
+ }
459
+ if (deps["@remix-run/node"] || deps["@remix-run/react"]) {
460
+ return {
461
+ framework: "remix",
462
+ buildCommand: pkg.scripts?.build ?? "npm run build",
463
+ outputDirectory: "build",
464
+ installCommand: detectPackageManager(cwd),
465
+ serverType: "serverless",
466
+ deployTarget: "cloudflare-pages",
467
+ nodeVersion,
468
+ envVars,
469
+ projectName,
470
+ confidence: 0.97,
471
+ files: allEntries
472
+ };
473
+ }
474
+ if (deps["gatsby"]) {
475
+ return {
476
+ framework: "gatsby",
477
+ buildCommand: pkg.scripts?.build ?? "npm run build",
478
+ outputDirectory: "public",
479
+ installCommand: detectPackageManager(cwd),
480
+ serverType: "static",
481
+ deployTarget: "cloudflare-pages",
482
+ nodeVersion,
483
+ envVars,
484
+ projectName,
485
+ confidence: 0.97,
486
+ files: allEntries
487
+ };
488
+ }
489
+ if (deps["vite"]) {
490
+ let framework = "vite-react";
491
+ if (deps["vue"] || deps["@vue/core"]) framework = "vite-vue";
492
+ else if (deps["svelte"]) framework = "vite-svelte";
493
+ return {
494
+ framework,
495
+ buildCommand: pkg.scripts?.build ?? "npm run build",
496
+ outputDirectory: "dist",
497
+ installCommand: detectPackageManager(cwd),
498
+ serverType: "static",
499
+ deployTarget: "cloudflare-pages",
500
+ nodeVersion,
501
+ envVars,
502
+ projectName,
503
+ confidence: 0.95,
504
+ files: allEntries
505
+ };
506
+ }
507
+ if (deps["react-scripts"]) {
508
+ return {
509
+ framework: "create-react-app",
510
+ buildCommand: pkg.scripts?.build ?? "npm run build",
511
+ outputDirectory: "build",
512
+ installCommand: detectPackageManager(cwd),
513
+ serverType: "static",
514
+ deployTarget: "cloudflare-pages",
515
+ nodeVersion,
516
+ envVars,
517
+ projectName,
518
+ confidence: 0.95,
519
+ files: allEntries
520
+ };
521
+ }
522
+ if (deps["hono"]) {
523
+ return {
524
+ framework: "hono",
525
+ buildCommand: pkg.scripts?.build ?? "npm run build",
526
+ outputDirectory: "dist",
527
+ installCommand: detectPackageManager(cwd),
528
+ serverType: "serverless",
529
+ deployTarget: "cloudflare-workers",
530
+ nodeVersion,
531
+ envVars,
532
+ projectName,
533
+ confidence: 0.9,
534
+ files: allEntries
535
+ };
536
+ }
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
+ if (pkg.scripts?.build) {
553
+ return {
554
+ framework: "unknown",
555
+ buildCommand: pkg.scripts.build,
556
+ outputDirectory: "dist",
557
+ installCommand: detectPackageManager(cwd),
558
+ serverType: "server",
559
+ deployTarget: "cloudflare-pages",
560
+ nodeVersion,
561
+ envVars,
562
+ projectName,
563
+ confidence: 0.6,
564
+ files: allEntries
565
+ };
566
+ }
567
+ }
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
+ if (hasFile("index.html")) {
601
+ const projectName = cwd.split("/").pop() ?? "my-app";
602
+ return {
603
+ framework: "static-html",
604
+ buildCommand: "",
605
+ outputDirectory: ".",
606
+ installCommand: "",
607
+ serverType: "static",
608
+ deployTarget: "cloudflare-pages",
609
+ envVars: [],
610
+ projectName,
611
+ confidence: 0.9,
612
+ files: allEntries
613
+ };
614
+ }
615
+ return buildUnknown(cwd, files);
616
+ }
617
+ function buildUnknown(cwd, files) {
618
+ return {
619
+ framework: "unknown",
620
+ buildCommand: "",
621
+ outputDirectory: ".",
622
+ installCommand: "",
623
+ serverType: "static",
624
+ deployTarget: "cloudflare-pages",
625
+ envVars: [],
626
+ projectName: cwd.split("/").pop() ?? "my-app",
627
+ confidence: 0,
628
+ files
629
+ };
630
+ }
631
+ function detectPackageManager(cwd) {
632
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm install";
633
+ if (existsSync(join(cwd, "yarn.lock"))) return "yarn install";
634
+ if (existsSync(join(cwd, "bun.lockb"))) return "bun install";
635
+ return "npm install";
636
+ }
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
+ function mergeEnvVars(base, extra) {
648
+ const seen = new Set(base.map((v) => v.name));
649
+ const merged = [...base];
650
+ for (const v of extra) {
651
+ if (!seen.has(v.name)) {
652
+ seen.add(v.name);
653
+ merged.push(v);
654
+ }
655
+ }
656
+ return merged;
657
+ }
658
+ function collectFileSamples(cwd, maxFiles = 20) {
659
+ const samples = {};
660
+ const importantFiles = [
661
+ "package.json",
662
+ "tsconfig.json",
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" }
714
+ }
715
+ });
716
+ function getCloudflareCredentials() {
717
+ const creds = globalConf.get("cloudflare");
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 };
722
+ }
723
+ return null;
724
+ }
725
+ function getAnthropicApiKey() {
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 {};
742
+ }
743
+ try {
744
+ const raw = readFileSync2(configPath, "utf-8");
745
+ const config = JSON.parse(raw);
746
+ warnIfConfigContainsSecrets(config, configPath);
747
+ return config;
748
+ } 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
+ }
770
+ }
771
+
772
+ // src/detect/ai-detect.ts
773
+ var AIDetectionSchema = z.object({
774
+ framework: z.string().describe("The detected framework or stack"),
775
+ frameworkId: z.enum([
776
+ "nextjs",
777
+ "vite-react",
778
+ "vite-vue",
779
+ "vite-svelte",
780
+ "astro",
781
+ "sveltekit",
782
+ "remix",
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
913
+ }
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;
923
+ }
924
+ }
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
+ }
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
+ };
971
+ }
972
+
973
+ // src/build/builder.ts
974
+ import { execa } from "execa";
975
+ import { existsSync as existsSync3 } from "fs";
976
+ import { join as join3 } from "path";
977
+ import chalk2 from "chalk";
978
+ async function buildProject(config, cwd = process.cwd()) {
979
+ const start = Date.now();
980
+ if (config.installCommand) {
981
+ const installSpinner = ui.spinner(`Installing dependencies (${config.installCommand})`);
982
+ const outputLines = [];
983
+ let lineCount = 0;
984
+ try {
985
+ const [installBin, ...installArgs] = config.installCommand.split(" ");
986
+ const installProc = execa(installBin, installArgs, {
987
+ cwd,
988
+ env: { ...process.env, CI: "true" },
989
+ timeout: 5 * 60 * 1e3,
990
+ stdout: "pipe",
991
+ stderr: "pipe"
992
+ });
993
+ installProc.stdout?.on("data", (chunk) => {
994
+ for (const line of chunk.toString().split("\n")) {
995
+ if (line.trim()) {
996
+ outputLines.push(line);
997
+ lineCount++;
998
+ installSpinner.text = `Installing dependencies... (${lineCount} lines)`;
999
+ }
1000
+ }
1001
+ });
1002
+ installProc.stderr?.on("data", (chunk) => {
1003
+ for (const line of chunk.toString().split("\n")) {
1004
+ if (line.trim()) {
1005
+ outputLines.push(line);
1006
+ lineCount++;
1007
+ installSpinner.text = `Installing dependencies... (${lineCount} lines)`;
1008
+ }
1009
+ }
1010
+ });
1011
+ await installProc;
1012
+ installSpinner.succeed(`Dependencies installed (${lineCount} lines)`);
1013
+ } catch (err) {
1014
+ installSpinner.fail("Dependency installation failed");
1015
+ const tail = outputLines.slice(-20);
1016
+ if (tail.length > 0) {
1017
+ console.log("");
1018
+ for (const line of tail) {
1019
+ process.stdout.write(chalk2.dim(" " + line) + "\n");
1020
+ }
1021
+ }
1022
+ return {
1023
+ success: false,
1024
+ outputDirectory: config.outputDirectory,
1025
+ durationMs: Date.now() - start,
1026
+ error: err instanceof Error ? err.message : String(err)
1027
+ };
1028
+ }
1029
+ console.log("");
1030
+ }
1031
+ if (config.buildCommand) {
1032
+ const buildSpinner = ui.spinner("Building...");
1033
+ const outputLines = [];
1034
+ let lineCount = 0;
1035
+ const buildEnv = {
1036
+ ...process.env,
1037
+ NODE_ENV: "production",
1038
+ CI: "true"
1039
+ };
1040
+ for (const envVar of config.envVars) {
1041
+ if (envVar.value) {
1042
+ buildEnv[envVar.name] = envVar.value;
1043
+ }
1044
+ }
1045
+ try {
1046
+ const [finalBin, ...finalArgs] = config.buildCommand.split(" ");
1047
+ const buildProc = execa(finalBin, finalArgs, {
1048
+ cwd,
1049
+ env: buildEnv,
1050
+ timeout: 10 * 60 * 1e3,
1051
+ stdout: "pipe",
1052
+ stderr: "pipe"
1053
+ });
1054
+ buildProc.stdout?.on("data", (chunk) => {
1055
+ for (const line of chunk.toString().split("\n")) {
1056
+ if (line.trim()) {
1057
+ outputLines.push(line);
1058
+ lineCount++;
1059
+ buildSpinner.text = `Building... (${lineCount} lines)`;
1060
+ }
1061
+ }
1062
+ });
1063
+ buildProc.stderr?.on("data", (chunk) => {
1064
+ for (const line of chunk.toString().split("\n")) {
1065
+ if (line.trim()) {
1066
+ outputLines.push(line);
1067
+ lineCount++;
1068
+ buildSpinner.text = `Building... (${lineCount} lines)`;
1069
+ }
1070
+ }
1071
+ });
1072
+ await buildProc;
1073
+ const durationSec = ((Date.now() - start) / 1e3).toFixed(1);
1074
+ buildSpinner.succeed(`Build successful (${lineCount} lines, ${durationSec}s)`);
1075
+ } catch (err) {
1076
+ buildSpinner.fail("Build failed");
1077
+ const tail = outputLines.slice(-30);
1078
+ if (tail.length > 0) {
1079
+ console.log("");
1080
+ for (const line of tail) {
1081
+ process.stdout.write(chalk2.dim(" " + line) + "\n");
1082
+ }
1083
+ }
1084
+ const errMsg = err instanceof Error ? err.message : String(err);
1085
+ return {
1086
+ success: false,
1087
+ outputDirectory: config.outputDirectory,
1088
+ durationMs: Date.now() - start,
1089
+ error: parseErrorMessage(errMsg, outputLines)
1090
+ };
1091
+ }
1092
+ console.log("");
1093
+ }
1094
+ const outputPath = join3(cwd, config.outputDirectory);
1095
+ if (config.buildCommand && !existsSync3(outputPath)) {
1096
+ return {
1097
+ success: false,
1098
+ outputDirectory: config.outputDirectory,
1099
+ durationMs: Date.now() - start,
1100
+ error: `Build completed but output directory '${config.outputDirectory}' not found. Check your build configuration.`
1101
+ };
1102
+ }
1103
+ return {
1104
+ success: true,
1105
+ outputDirectory: config.outputDirectory,
1106
+ durationMs: Date.now() - start
1107
+ };
1108
+ }
1109
+ function parseErrorMessage(raw, outputLines) {
1110
+ const errorLines = outputLines.filter(
1111
+ (l) => l.includes("Error:") || l.includes("error:") || l.includes("ERROR") || l.includes("Failed") || l.includes("Cannot find") || l.includes("Module not found")
1112
+ );
1113
+ if (errorLines.length > 0) {
1114
+ return errorLines.slice(0, 3).join("\n");
1115
+ }
1116
+ if (outputLines.length > 0) {
1117
+ return outputLines.slice(-5).join("\n");
1118
+ }
1119
+ const lines = raw.split("\n").filter((l) => l.trim());
1120
+ return lines.slice(-5).join("\n");
1121
+ }
1122
+
1123
+ // src/commands/login.ts
1124
+ import { createServer } from "http";
1125
+ import { randomBytes } from "crypto";
1126
+ import open from "open";
1127
+ var CALLBACK_PORT_START = 9999;
1128
+ var MAX_PORT_ATTEMPTS = 10;
1129
+ async function loginCommand(opts = {}) {
1130
+ if (!opts.skipBanner) ui.banner();
1131
+ ui.section("Logging in to Shipem...");
1132
+ ui.br();
1133
+ const cliState = randomBytes(16).toString("hex");
1134
+ let resolveToken;
1135
+ let rejectToken;
1136
+ const tokenPromise = new Promise((resolve, reject) => {
1137
+ resolveToken = resolve;
1138
+ rejectToken = reject;
1139
+ });
1140
+ let hasHandled = false;
1141
+ const server = createServer((req, res) => {
1142
+ if (!req.url) return;
1143
+ if (hasHandled) {
1144
+ res.writeHead(200, { "Content-Type": "text/html" });
1145
+ res.end('<html><body style="font-family:sans-serif;padding:40px">Login already completed. You can close this tab.</body></html>');
1146
+ return;
1147
+ }
1148
+ const url = new URL(req.url, `http://127.0.0.1:${actualPort}`);
1149
+ const token2 = url.searchParams.get("token");
1150
+ const login = url.searchParams.get("login");
1151
+ const returnedState = url.searchParams.get("state");
1152
+ if (returnedState !== cliState) {
1153
+ hasHandled = true;
1154
+ res.writeHead(400, { "Content-Type": "text/html" });
1155
+ 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>');
1156
+ rejectToken(new Error("State mismatch in OAuth callback \u2014 possible CSRF attack"));
1157
+ return;
1158
+ }
1159
+ if (token2) {
1160
+ hasHandled = true;
1161
+ const html = `<!DOCTYPE html>
1162
+ <html>
1163
+ <head><title>Shipem \u2014 Logged in!</title>
1164
+ <style>
1165
+ body { font-family: -apple-system, sans-serif; text-align: center; padding: 60px; background: #0f172a; color: #e2e8f0; }
1166
+ h2 { color: #22c55e; font-size: 2rem; margin-bottom: 12px; }
1167
+ p { color: #94a3b8; font-size: 1.1rem; }
1168
+ </style>
1169
+ </head>
1170
+ <body>
1171
+ <h2>\u2705 Logged in${login ? ` as @${login}` : ""}!</h2>
1172
+ <p>You can close this tab and return to your terminal.</p>
1173
+ </body>
1174
+ </html>`;
1175
+ res.writeHead(200, { "Content-Type": "text/html" });
1176
+ res.end(html);
1177
+ resolveToken(token2);
1178
+ } else {
1179
+ hasHandled = true;
1180
+ res.writeHead(400, { "Content-Type": "text/html" });
1181
+ res.end('<html><body style="font-family:sans-serif;padding:40px">Authentication failed. Please try again.</body></html>');
1182
+ rejectToken(new Error("Authentication failed \u2014 no token received"));
1183
+ }
1184
+ });
1185
+ let actualPort = CALLBACK_PORT_START;
1186
+ let bound = false;
1187
+ for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt++) {
1188
+ try {
1189
+ await new Promise((resolve, reject) => {
1190
+ server.once("error", reject);
1191
+ server.listen(actualPort, "127.0.0.1", resolve);
1192
+ });
1193
+ bound = true;
1194
+ break;
1195
+ } catch (err) {
1196
+ if (err.code === "EADDRINUSE") {
1197
+ actualPort++;
1198
+ } else {
1199
+ throw err;
1200
+ }
1201
+ }
1202
+ }
1203
+ if (!bound) {
1204
+ throw new Error(`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
+ }
1206
+ const callbackUrl = `http://localhost:${actualPort}`;
1207
+ const loginUrl = `${SHIPEM_API_URL}/auth/github?cli_callback=${encodeURIComponent(callbackUrl)}&cli_state=${cliState}`;
1208
+ ui.info("Opening your browser for GitHub login...");
1209
+ ui.dim(`If the browser doesn't open, visit:`);
1210
+ ui.dim(loginUrl);
1211
+ ui.br();
1212
+ await open(loginUrl);
1213
+ const timeoutMs = 5 * 60 * 1e3;
1214
+ let token;
1215
+ try {
1216
+ token = await Promise.race([
1217
+ tokenPromise,
1218
+ new Promise(
1219
+ (_, reject) => setTimeout(() => reject(new Error("Login timed out after 5 minutes. Please try again.")), timeoutMs)
1220
+ )
1221
+ ]);
1222
+ } finally {
1223
+ server.close();
1224
+ }
1225
+ setSessionToken(token);
1226
+ ui.success("Logged in successfully!");
1227
+ ui.dim("Your session is saved \u2014 you won't need to log in again.");
1228
+ ui.br();
1229
+ }
1230
+
1231
+ // src/deploy/cloudflare.ts
1232
+ import axios from "axios";
1233
+ import { createHash } from "crypto";
1234
+ import { readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync4 } from "fs";
1235
+ import { join as join5, relative } from "path";
1236
+
1237
+ // src/deploy/exclude.ts
1238
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1239
+ import { join as join4 } from "path";
1240
+ import chalk3 from "chalk";
1241
+ var DEFAULT_PATTERNS = [
1242
+ ".git",
1243
+ "node_modules",
1244
+ ".env",
1245
+ ".env.local",
1246
+ ".env.production",
1247
+ ".DS_Store",
1248
+ "shipem.json",
1249
+ "__pycache__",
1250
+ ".pytest_cache",
1251
+ ".venv",
1252
+ "venv",
1253
+ ".idea",
1254
+ ".vscode"
1255
+ ];
1256
+ var DEFAULT_GLOB_PATTERNS = [
1257
+ ".env*.local",
1258
+ ".shipem*",
1259
+ "*.pem",
1260
+ "*.key",
1261
+ "*.p12"
1262
+ ];
1263
+ function matchesGlob(name, pattern) {
1264
+ const starIdx = pattern.indexOf("*");
1265
+ if (starIdx === -1) return name === pattern;
1266
+ const prefix = pattern.slice(0, starIdx);
1267
+ const suffix = pattern.slice(starIdx + 1);
1268
+ return name.startsWith(prefix) && name.endsWith(suffix) && name.length >= prefix.length + suffix.length;
1269
+ }
1270
+ function matchesAnyDefaultPattern(segment) {
1271
+ if (DEFAULT_PATTERNS.includes(segment)) return true;
1272
+ for (const pat of DEFAULT_GLOB_PATTERNS) {
1273
+ if (matchesGlob(segment, pat)) return true;
1274
+ }
1275
+ return false;
1276
+ }
1277
+ function matchesIgnoreLine(relPath, line) {
1278
+ const trimmed = line.trim();
1279
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("!")) return false;
1280
+ const pattern = trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
1281
+ const segments = relPath.replace(/\\/g, "/").split("/").filter(Boolean);
1282
+ const last = segments[segments.length - 1];
1283
+ if (last && matchesGlob(last, pattern)) return true;
1284
+ if (matchesGlob(relPath.replace(/\\/g, "/"), pattern)) return true;
1285
+ for (const seg of segments) {
1286
+ if (matchesGlob(seg, pattern)) return true;
1287
+ }
1288
+ return false;
1289
+ }
1290
+ function loadIgnoreLines(filePath) {
1291
+ if (!existsSync4(filePath)) return [];
1292
+ try {
1293
+ return readFileSync3(filePath, "utf-8").split("\n");
1294
+ } catch {
1295
+ return [];
1296
+ }
1297
+ }
1298
+ function shouldExclude(filePath, projectRoot) {
1299
+ const normalized = filePath.replace(/\\/g, "/");
1300
+ const segments = normalized.split("/").filter((s) => s && s !== ".");
1301
+ for (const seg of segments) {
1302
+ if (matchesAnyDefaultPattern(seg)) return true;
1303
+ }
1304
+ if (projectRoot) {
1305
+ const relPath = normalized;
1306
+ const shipemIgnoreLines = loadIgnoreLines(join4(projectRoot, ".shipemignore"));
1307
+ for (const line of shipemIgnoreLines) {
1308
+ if (matchesIgnoreLine(relPath, line)) return true;
1309
+ }
1310
+ const gitIgnoreLines = loadIgnoreLines(join4(projectRoot, ".gitignore"));
1311
+ for (const line of gitIgnoreLines) {
1312
+ if (matchesIgnoreLine(relPath, line)) return true;
1313
+ }
1314
+ }
1315
+ return false;
1316
+ }
1317
+ function warnIfEnvFilesInOutputDir(outputDir) {
1318
+ const envFiles = [".env", ".env.local", ".env.production"];
1319
+ for (const name of envFiles) {
1320
+ if (existsSync4(join4(outputDir, name))) {
1321
+ process.stderr.write(
1322
+ chalk3.yellow(`
1323
+ \u26A0 Warning: ${name} found in output directory \u2014 it will be excluded from deployment.
1324
+ `)
1325
+ );
1326
+ }
1327
+ }
1328
+ }
1329
+
1330
+ // src/deploy/cloudflare.ts
1331
+ var CF_API_BASE = "https://api.cloudflare.com/client/v4";
1332
+ var CloudflarePages = class {
1333
+ client;
1334
+ accountId;
1335
+ constructor(apiToken, accountId) {
1336
+ this.accountId = accountId;
1337
+ this.client = axios.create({
1338
+ baseURL: CF_API_BASE,
1339
+ headers: {
1340
+ Authorization: `Bearer ${apiToken}`,
1341
+ "Content-Type": "application/json"
1342
+ },
1343
+ timeout: 6e4
1344
+ });
1345
+ }
1346
+ async getOrCreateProject(projectName, config) {
1347
+ try {
1348
+ const res = await this.client.get(
1349
+ `/accounts/${this.accountId}/pages/projects/${projectName}`
1350
+ );
1351
+ if (res.data.success) {
1352
+ return res.data.result;
1353
+ }
1354
+ } catch (err) {
1355
+ const status = err.response?.status;
1356
+ if (status !== 404) {
1357
+ throw new Error(`Failed to check project: ${extractCFError(err)}`);
1358
+ }
1359
+ }
1360
+ try {
1361
+ const res = await this.client.post(
1362
+ `/accounts/${this.accountId}/pages/projects`,
1363
+ {
1364
+ name: projectName,
1365
+ production_branch: "main",
1366
+ build_config: {
1367
+ build_command: config.buildCommand || null,
1368
+ destination_dir: config.outputDirectory,
1369
+ root_dir: ""
1370
+ },
1371
+ deployment_configs: {
1372
+ production: {
1373
+ compatibility_date: "2024-01-01",
1374
+ environment_variables: {}
1375
+ },
1376
+ preview: {
1377
+ compatibility_date: "2024-01-01",
1378
+ environment_variables: {}
1379
+ }
1380
+ }
1381
+ }
1382
+ );
1383
+ if (!res.data.success) {
1384
+ throw new Error(res.data.errors[0]?.message ?? "Unknown error creating project");
1385
+ }
1386
+ return res.data.result;
1387
+ } catch (err) {
1388
+ throw new Error(`Failed to create Pages project: ${extractCFError(err)}`);
1389
+ }
1390
+ }
1391
+ async setEnvironmentVariables(projectName, envVars) {
1392
+ if (Object.keys(envVars).length === 0) return;
1393
+ const formattedVars = {};
1394
+ for (const [key, value] of Object.entries(envVars)) {
1395
+ const isSecret = key.toLowerCase().includes("secret") || key.toLowerCase().includes("key") || key.toLowerCase().includes("token") || key.toLowerCase().includes("password");
1396
+ formattedVars[key] = {
1397
+ value,
1398
+ type: isSecret ? "secret_text" : "plain_text"
1399
+ };
1400
+ }
1401
+ try {
1402
+ await this.client.patch(
1403
+ `/accounts/${this.accountId}/pages/projects/${projectName}`,
1404
+ {
1405
+ deployment_configs: {
1406
+ production: {
1407
+ environment_variables: formattedVars
1408
+ }
1409
+ }
1410
+ }
1411
+ );
1412
+ } catch (err) {
1413
+ throw new Error(`Failed to set environment variables: ${extractCFError(err)}`);
1414
+ }
1415
+ }
1416
+ async deployDirectory(projectName, outputDir, cwd = process.cwd()) {
1417
+ const fullOutputPath = join5(cwd, outputDir);
1418
+ const filePaths = collectFiles(fullOutputPath);
1419
+ let totalBytes = 0;
1420
+ const fileMap = /* @__PURE__ */ new Map();
1421
+ for (const filePath of filePaths) {
1422
+ const content = readFileSync4(filePath);
1423
+ const hash = createHash("sha256").update(content).digest("hex");
1424
+ const urlPath = "/" + relative(fullOutputPath, filePath).replace(/\\/g, "/");
1425
+ fileMap.set(urlPath, { hash, content });
1426
+ totalBytes += content.length;
1427
+ }
1428
+ const hashToContent = /* @__PURE__ */ new Map();
1429
+ for (const { hash, content } of fileMap.values()) {
1430
+ if (!hashToContent.has(hash)) hashToContent.set(hash, content);
1431
+ }
1432
+ const fileCount = filePaths.length;
1433
+ const manifest = {};
1434
+ for (const [urlPath, { hash }] of fileMap) {
1435
+ manifest[urlPath] = hash;
1436
+ }
1437
+ const deploySpinner = ui.spinner("Creating deployment...");
1438
+ let jwt;
1439
+ let requiredFiles;
1440
+ let deployment;
1441
+ try {
1442
+ const res = await this.client.post(
1443
+ `/accounts/${this.accountId}/pages/projects/${projectName}/deployments`,
1444
+ { files: manifest, branch: "main" },
1445
+ { timeout: 6e4 }
1446
+ );
1447
+ if (!res.data.success) {
1448
+ deploySpinner.fail("Deployment creation failed");
1449
+ throw new Error(res.data.errors[0]?.message ?? "Deployment failed");
1450
+ }
1451
+ jwt = res.data.result.jwt;
1452
+ requiredFiles = res.data.result.required_files ?? [];
1453
+ deployment = res.data.result;
1454
+ deploySpinner.succeed("Deployment created");
1455
+ } catch (err) {
1456
+ deploySpinner.fail("Deployment creation failed");
1457
+ throw new Error(`Deployment failed: ${extractCFError(err)}`);
1458
+ }
1459
+ if (requiredFiles.length > 0) {
1460
+ const uploadSpinner = ui.spinner(
1461
+ `Uploading ${requiredFiles.length} files (${ui.formatBytes(totalBytes)})`
1462
+ );
1463
+ let uploaded = 0;
1464
+ for (const hash of requiredFiles) {
1465
+ const content = hashToContent.get(hash);
1466
+ if (!content) continue;
1467
+ try {
1468
+ const formData = new FormData();
1469
+ formData.append(hash, new Blob([content], { type: "application/octet-stream" }), hash);
1470
+ await fetch("https://upload.storageapi.cloudflare.com/pages-uploads", {
1471
+ method: "POST",
1472
+ headers: { Authorization: `Bearer ${jwt}` },
1473
+ body: formData
1474
+ }).then(async (res) => {
1475
+ if (!res.ok) {
1476
+ const text = await res.text().catch(() => res.statusText);
1477
+ throw new Error(`Upload failed (${res.status}): ${text}`);
1478
+ }
1479
+ });
1480
+ } catch (err) {
1481
+ uploadSpinner.fail("Upload failed");
1482
+ throw new Error(`Failed to upload file (hash ${hash}): ${err instanceof Error ? err.message : String(err)}`);
1483
+ }
1484
+ uploaded++;
1485
+ uploadSpinner.text = `Uploading files... ${uploaded}/${requiredFiles.length}`;
1486
+ }
1487
+ uploadSpinner.succeed(`Upload complete \u2014 ${fileCount} files (${ui.formatBytes(totalBytes)})`);
1488
+ }
1489
+ ui.success("Live!");
1490
+ return { deployment, fileCount, totalBytes };
1491
+ }
1492
+ async waitForDeployment(projectName, deploymentId, timeoutMs = 3 * 60 * 1e3) {
1493
+ const deploySpinner = ui.spinner("Deploying to Cloudflare Pages");
1494
+ const start = Date.now();
1495
+ while (Date.now() - start < timeoutMs) {
1496
+ await sleep(3e3);
1497
+ try {
1498
+ const res = await this.client.get(
1499
+ `/accounts/${this.accountId}/pages/projects/${projectName}/deployments/${deploymentId}`
1500
+ );
1501
+ if (!res.data.success) continue;
1502
+ const deployment = res.data.result;
1503
+ const stage = deployment.latest_stage;
1504
+ deploySpinner.text = `Deploying... (${stage?.name ?? "initializing"})`;
1505
+ if (stage?.status === "success") {
1506
+ deploySpinner.succeed("Deployed successfully");
1507
+ return deployment;
1508
+ }
1509
+ if (stage?.status === "failure") {
1510
+ deploySpinner.fail("Deployment failed");
1511
+ throw new Error(
1512
+ `Deployment failed at stage '${stage.name}'. Check the Cloudflare Pages dashboard for details.`
1513
+ );
1514
+ }
1515
+ } catch (err) {
1516
+ if (err instanceof Error && err.message.includes("Deployment failed")) {
1517
+ throw err;
1518
+ }
1519
+ }
1520
+ }
1521
+ deploySpinner.fail("Deployment timed out");
1522
+ throw new Error("Deployment timed out. Check the Cloudflare Pages dashboard for status.");
1523
+ }
1524
+ async getDeploymentStatus(projectName, deploymentId) {
1525
+ try {
1526
+ const res = await this.client.get(
1527
+ `/accounts/${this.accountId}/pages/projects/${projectName}/deployments/${deploymentId}`
1528
+ );
1529
+ return res.data.success ? res.data.result : null;
1530
+ } catch {
1531
+ return null;
1532
+ }
1533
+ }
1534
+ async getDeploymentLogs(projectName, deploymentId) {
1535
+ try {
1536
+ const res = await this.client.get(
1537
+ `/accounts/${this.accountId}/pages/projects/${projectName}/deployments/${deploymentId}/history/logs`
1538
+ );
1539
+ if (!res.data.success) return [];
1540
+ return res.data.result.data.map((entry) => `[${entry.ts}] ${entry.message}`);
1541
+ } catch {
1542
+ return [];
1543
+ }
1544
+ }
1545
+ async deleteProject(projectName) {
1546
+ try {
1547
+ await this.client.delete(
1548
+ `/accounts/${this.accountId}/pages/projects/${projectName}`
1549
+ );
1550
+ } catch (err) {
1551
+ throw new Error(`Failed to delete project: ${extractCFError(err)}`);
1552
+ }
1553
+ }
1554
+ getProjectUrl(projectName) {
1555
+ return `https://${projectName}.pages.dev`;
1556
+ }
1557
+ getDashboardUrl(projectName) {
1558
+ return `https://dash.cloudflare.com/${this.accountId}/pages/view/${projectName}`;
1559
+ }
1560
+ // Validate credentials and get account info
1561
+ async validateCredentials() {
1562
+ try {
1563
+ const res = await this.client.get(
1564
+ `/accounts/${this.accountId}`
1565
+ );
1566
+ return { valid: res.data.success, accountName: res.data.result?.name };
1567
+ } catch {
1568
+ return { valid: false };
1569
+ }
1570
+ }
1571
+ };
1572
+ function collectFiles(dir) {
1573
+ const results = [];
1574
+ function walk(currentDir) {
1575
+ const entries = readdirSync2(currentDir);
1576
+ for (const entry of entries) {
1577
+ const fullPath = join5(currentDir, entry);
1578
+ if (shouldExclude(fullPath)) continue;
1579
+ if (entry.startsWith(".") && entry !== ".well-known") continue;
1580
+ const stat = statSync2(fullPath);
1581
+ if (stat.isDirectory()) {
1582
+ walk(fullPath);
1583
+ } else {
1584
+ results.push(fullPath);
1585
+ }
1586
+ }
1587
+ }
1588
+ walk(dir);
1589
+ return results;
1590
+ }
1591
+ function extractCFError(err) {
1592
+ if (err instanceof Error) {
1593
+ const cfErr = err;
1594
+ const msg = cfErr.response?.data?.errors?.[0]?.message;
1595
+ if (msg) return msg;
1596
+ return err.message;
1597
+ }
1598
+ return String(err);
1599
+ }
1600
+ function sleep(ms) {
1601
+ return new Promise((resolve) => setTimeout(resolve, ms));
1602
+ }
1603
+ function sanitizeProjectName(name) {
1604
+ return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 28);
1605
+ }
1606
+
1607
+ // src/commands/deploy.ts
1608
+ var ENV_VAR_HELP = {
1609
+ OPENAI_API_KEY: { desc: "OpenAI API key for AI features", url: "https://platform.openai.com/api-keys" },
1610
+ ANTHROPIC_API_KEY: { desc: "Anthropic API key for Claude AI", url: "https://console.anthropic.com/" },
1611
+ SUPABASE_URL: { desc: "Your Supabase project URL", url: "https://app.supabase.com" },
1612
+ SUPABASE_ANON_KEY: { desc: "Your Supabase anonymous key", url: "https://app.supabase.com" },
1613
+ NEXT_PUBLIC_SUPABASE_URL: { desc: "Supabase project URL (public)", url: "https://app.supabase.com" },
1614
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: { desc: "Supabase anon key (public)", url: "https://app.supabase.com" },
1615
+ VITE_SUPABASE_URL: { desc: "Supabase project URL for Vite apps", url: "https://app.supabase.com" },
1616
+ VITE_SUPABASE_ANON_KEY: { desc: "Supabase anon key for Vite apps", url: "https://app.supabase.com" },
1617
+ DATABASE_URL: { desc: "PostgreSQL database connection string" },
1618
+ STRIPE_SECRET_KEY: { desc: "Stripe secret key for payments", url: "https://dashboard.stripe.com/apikeys" },
1619
+ STRIPE_PUBLISHABLE_KEY: { desc: "Stripe publishable key", url: "https://dashboard.stripe.com/apikeys" },
1620
+ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: { desc: "Stripe publishable key (public)", url: "https://dashboard.stripe.com/apikeys" }
1621
+ };
1622
+ async function apiRequest(config) {
1623
+ try {
1624
+ return await axios2(config);
1625
+ } catch (err) {
1626
+ if (axios2.isAxiosError(err)) {
1627
+ const code = err.code;
1628
+ if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT") {
1629
+ throw new Error("Cannot reach Shipem servers. Check your internet connection and try again.");
1630
+ }
1631
+ const status = err.response?.status;
1632
+ if (status === 401) throw err;
1633
+ if (status === 500) throw new Error("Shipem servers are having issues. Try again in a moment.");
1634
+ if (status === 429) throw new Error("Too many requests. Please wait before deploying again.");
1635
+ if (status === 413) throw new Error("Project is too large to deploy (max 100 MB).");
1636
+ }
1637
+ throw err;
1638
+ }
1639
+ }
1640
+ function countSourceFiles(dir) {
1641
+ const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".next", "dist", "build", "out", ".turbo", "coverage"]);
1642
+ let count = 0;
1643
+ function walk(d) {
1644
+ try {
1645
+ const entries = readdirSync3(d, { withFileTypes: true });
1646
+ for (const entry of entries) {
1647
+ if (SKIP.has(entry.name) || entry.name.startsWith(".")) continue;
1648
+ if (entry.isDirectory()) walk(join6(d, entry.name));
1649
+ else count++;
1650
+ }
1651
+ } catch {
1652
+ }
1653
+ }
1654
+ walk(dir);
1655
+ return count;
1656
+ }
1657
+ function countOutputFiles(dir) {
1658
+ let fileCount = 0;
1659
+ let totalBytes = 0;
1660
+ function walk(d) {
1661
+ try {
1662
+ const entries = readdirSync3(d, { withFileTypes: true });
1663
+ for (const entry of entries) {
1664
+ const fullPath = join6(d, entry.name);
1665
+ if (entry.isDirectory()) walk(fullPath);
1666
+ else {
1667
+ fileCount++;
1668
+ try {
1669
+ totalBytes += statSync3(fullPath).size;
1670
+ } catch {
1671
+ }
1672
+ }
1673
+ }
1674
+ } catch {
1675
+ }
1676
+ }
1677
+ walk(dir);
1678
+ return { fileCount, totalBytes };
1679
+ }
1680
+ async function deployCommand(options) {
1681
+ const startTime = Date.now();
1682
+ const cwd = process.cwd();
1683
+ const existingConfig = readProjectConfig(cwd);
1684
+ const isRedeploy = !!existingConfig.project && !!existingConfig.deployment;
1685
+ const isFirstRun = !getSessionToken() && !existingConfig.project;
1686
+ if (isRedeploy) {
1687
+ ui.redeployHeader(
1688
+ existingConfig.deployment.projectName,
1689
+ existingConfig.deployment.url,
1690
+ existingConfig.deployment.deployedAt
1691
+ );
1692
+ } else {
1693
+ ui.banner();
1694
+ }
1695
+ if (isFirstRun) {
1696
+ console.log(` ${chalk4.white.bold("Welcome to Shipem!")} \u2728`);
1697
+ console.log("");
1698
+ console.log(` ${chalk4.dim("One-time setup (takes 30 seconds):")}`);
1699
+ console.log(` ${chalk4.dim("1. Sign in with GitHub")}`);
1700
+ console.log(` ${chalk4.dim("2. We handle everything else")}`);
1701
+ console.log("");
1702
+ }
1703
+ let sessionToken = getSessionToken();
1704
+ if (!sessionToken) {
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
+ }
1716
+ ui.section("Scanning project...");
1717
+ ui.br();
1718
+ let projectConfig;
1719
+ if (existingConfig.project && options.yes) {
1720
+ projectConfig = existingConfig.project;
1721
+ ui.success(`Using saved config: ${ui.framework(projectConfig.framework)}`);
1722
+ } else {
1723
+ const scanSpinner = ui.spinner("Scanning project files");
1724
+ const heuristicResult = scanProject(cwd);
1725
+ scanSpinner.succeed("Project files scanned");
1726
+ const sourceFileCount = countSourceFiles(cwd);
1727
+ let finalDetection;
1728
+ const anthropicKey = getAnthropicApiKey();
1729
+ const heuristicAsAnalysis = {
1730
+ framework: heuristicResult.framework,
1731
+ buildCommand: heuristicResult.buildCommand,
1732
+ outputDirectory: heuristicResult.outputDirectory,
1733
+ installCommand: heuristicResult.installCommand,
1734
+ serverType: heuristicResult.serverType,
1735
+ deployTarget: heuristicResult.deployTarget,
1736
+ nodeVersion: heuristicResult.nodeVersion,
1737
+ pythonVersion: heuristicResult.pythonVersion,
1738
+ envVars: heuristicResult.envVars,
1739
+ notes: "",
1740
+ confidence: heuristicResult.confidence
1741
+ };
1742
+ if (anthropicKey) {
1743
+ const aiSpinner = ui.spinner("Analyzing with Claude AI");
1744
+ try {
1745
+ const fileSamples = collectFileSamples(cwd);
1746
+ const aiResult = await aiDetectProject(fileSamples, heuristicAsAnalysis);
1747
+ finalDetection = mergeDetectionResults(heuristicAsAnalysis, aiResult);
1748
+ aiSpinner.succeed("AI analysis complete");
1749
+ if (aiResult?.notes) {
1750
+ ui.dim(aiResult.notes);
1751
+ }
1752
+ } catch {
1753
+ aiSpinner.warn("AI analysis failed, using heuristic detection");
1754
+ finalDetection = heuristicAsAnalysis;
1755
+ }
1756
+ } else {
1757
+ finalDetection = heuristicAsAnalysis;
1758
+ }
1759
+ const projectName = options.name ?? heuristicResult.projectName;
1760
+ ui.projectAnalysis({
1761
+ name: projectName,
1762
+ framework: finalDetection.framework,
1763
+ fileCount: sourceFileCount,
1764
+ buildCommand: finalDetection.buildCommand,
1765
+ outputDir: finalDetection.outputDirectory,
1766
+ deployTarget: finalDetection.deployTarget,
1767
+ envVarCount: finalDetection.envVars.length
1768
+ });
1769
+ if (finalDetection.confidence < 0.7 && !options.yes) {
1770
+ ui.warn(`Low confidence detection (${Math.round(finalDetection.confidence * 100)}%). Please verify the settings.`);
1771
+ ui.br();
1772
+ }
1773
+ if (finalDetection.confidence < 0.4 && !existsSync5(join6(cwd, "package.json")) && !existsSync5(join6(cwd, "requirements.txt")) && !existsSync5(join6(cwd, "index.html"))) {
1774
+ ui.friendlyError(
1775
+ "This does not look like a project",
1776
+ "No package.json, requirements.txt, or index.html found",
1777
+ "cd into your project directory first",
1778
+ `You are in: ${cwd}`
1779
+ );
1780
+ if (!options.yes) {
1781
+ const { proceed } = await inquirer.prompt([
1782
+ {
1783
+ type: "confirm",
1784
+ name: "proceed",
1785
+ message: "Continue anyway?",
1786
+ default: false
1787
+ }
1788
+ ]);
1789
+ if (!proceed) {
1790
+ ui.info("Cancelled. Navigate to your project directory and run `shipem` again.");
1791
+ ui.br();
1792
+ process.exit(0);
1793
+ }
1794
+ }
1795
+ }
1796
+ projectConfig = {
1797
+ name: projectName,
1798
+ framework: finalDetection.framework,
1799
+ buildCommand: finalDetection.buildCommand,
1800
+ outputDirectory: finalDetection.outputDirectory,
1801
+ installCommand: finalDetection.installCommand,
1802
+ serverType: finalDetection.serverType,
1803
+ nodeVersion: finalDetection.nodeVersion,
1804
+ pythonVersion: finalDetection.pythonVersion,
1805
+ envVars: finalDetection.envVars,
1806
+ deployTarget: finalDetection.deployTarget,
1807
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
1808
+ aiAnalysis: finalDetection.notes
1809
+ };
1810
+ }
1811
+ const envVarsToSet = {};
1812
+ if (projectConfig.envVars.length > 0 && !options.yes) {
1813
+ const unresolvedVars = projectConfig.envVars.filter(
1814
+ (v) => !v.autoProvision && !v.value && !process.env[v.name]
1815
+ );
1816
+ if (unresolvedVars.length > 0) {
1817
+ ui.section("Environment setup...");
1818
+ ui.br();
1819
+ ui.dim("(Stored locally only, never sent to Shipem servers)");
1820
+ ui.br();
1821
+ for (const envVar of unresolvedVars) {
1822
+ const existing = process.env[envVar.name];
1823
+ if (existing) {
1824
+ ui.success(`${envVar.name}: (from environment)`);
1825
+ envVarsToSet[envVar.name] = existing;
1826
+ continue;
1827
+ }
1828
+ const helpInfo = ENV_VAR_HELP[envVar.name];
1829
+ const desc = envVar.description || helpInfo?.desc || "";
1830
+ const helpUrl = helpInfo?.url;
1831
+ const messageParts = [envVar.name];
1832
+ if (desc) messageParts.push(chalk4.gray(` \u2014 ${desc}`));
1833
+ if (helpUrl) messageParts.push(chalk4.dim(` (${helpUrl})`));
1834
+ const message = messageParts.join("");
1835
+ const { value } = await inquirer.prompt([
1836
+ {
1837
+ type: "password",
1838
+ name: "value",
1839
+ message,
1840
+ mask: "*",
1841
+ validate: (v) => !envVar.required || v.trim().length > 0 || `${envVar.name} is required`
1842
+ }
1843
+ ]);
1844
+ if (value.trim()) {
1845
+ envVarsToSet[envVar.name] = value.trim();
1846
+ projectConfig.envVars = projectConfig.envVars.map(
1847
+ (v) => v.name === envVar.name ? { ...v, value: value.trim() } : v
1848
+ );
1849
+ }
1850
+ }
1851
+ ui.br();
1852
+ }
1853
+ } else {
1854
+ for (const envVar of projectConfig.envVars) {
1855
+ const existing = process.env[envVar.name];
1856
+ if (existing) {
1857
+ envVarsToSet[envVar.name] = existing;
1858
+ }
1859
+ }
1860
+ }
1861
+ if (!options.skipBuild) {
1862
+ ui.section("Building...");
1863
+ ui.br();
1864
+ let buildResult = await buildProject(projectConfig, cwd);
1865
+ if (!buildResult.success) {
1866
+ ui.br();
1867
+ const errMsg = buildResult.error ?? "Unknown build error";
1868
+ const moduleMatch = errMsg.match(/Cannot find module ['"]([^'"]+)['"]/);
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
+ }
1882
+ if (options.yes) {
1883
+ process.exit(1);
1884
+ }
1885
+ const { action } = await inquirer.prompt([
1886
+ {
1887
+ type: "list",
1888
+ name: "action",
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
+ ]
1895
+ }
1896
+ ]);
1897
+ if (action === "quit") {
1898
+ ui.br();
1899
+ process.exit(1);
1900
+ }
1901
+ if (action === "retry") {
1902
+ ui.br();
1903
+ ui.section("Building (retry)...");
1904
+ ui.br();
1905
+ buildResult = await buildProject(projectConfig, cwd);
1906
+ if (!buildResult.success) {
1907
+ 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
+ process.exit(1);
1914
+ }
1915
+ }
1916
+ ui.br();
1917
+ } else {
1918
+ ui.br();
1919
+ }
1920
+ }
1921
+ ui.section("Deploying...");
1922
+ ui.br();
1923
+ let liveUrl;
1924
+ let projectId;
1925
+ let deployFileCount = 0;
1926
+ let deployTotalBytes = 0;
1927
+ const cfCreds = getCloudflareCredentials();
1928
+ const useDirectDeploy = options.direct === true || cfCreds !== null;
1929
+ if (useDirectDeploy) {
1930
+ if (!cfCreds) {
1931
+ ui.friendlyError(
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"
1935
+ );
1936
+ process.exit(1);
1937
+ }
1938
+ ui.dim("Direct Cloudflare deployment (using your credentials)...");
1939
+ ui.br();
1940
+ try {
1941
+ const cf = new CloudflarePages(cfCreds.apiToken, cfCreds.accountId);
1942
+ const cfProjectName = sanitizeProjectName(projectConfig.name);
1943
+ await cf.getOrCreateProject(cfProjectName, projectConfig);
1944
+ if (Object.keys(envVarsToSet).length > 0) {
1945
+ await cf.setEnvironmentVariables(cfProjectName, envVarsToSet);
1946
+ }
1947
+ const result = await cf.deployDirectory(cfProjectName, projectConfig.outputDirectory, cwd);
1948
+ deployFileCount = result.fileCount;
1949
+ deployTotalBytes = result.totalBytes;
1950
+ const finalDeployment = await cf.waitForDeployment(cfProjectName, result.deployment.id);
1951
+ liveUrl = finalDeployment.url || cf.getProjectUrl(cfProjectName);
1952
+ projectId = cfProjectName;
1953
+ } catch (err) {
1954
+ const msg = err instanceof Error ? err.message : String(err);
1955
+ ui.friendlyError(
1956
+ "Deployment failed",
1957
+ msg,
1958
+ "Check your Cloudflare credentials and try again"
1959
+ );
1960
+ process.exit(1);
1961
+ }
1962
+ } else {
1963
+ const tarPath = join6(tmpdir(), `shipem-${Date.now()}.tar.gz`);
1964
+ try {
1965
+ const outputPath = join6(cwd, projectConfig.outputDirectory);
1966
+ warnIfEnvFilesInOutputDir(outputPath);
1967
+ const stats = countOutputFiles(outputPath);
1968
+ deployFileCount = stats.fileCount;
1969
+ deployTotalBytes = stats.totalBytes;
1970
+ const packagingSpinner = ui.spinner("Packaging files...");
1971
+ await tarCreate(
1972
+ {
1973
+ gzip: true,
1974
+ file: tarPath,
1975
+ cwd: outputPath,
1976
+ filter: (filePath) => !shouldExclude(filePath, cwd)
1977
+ },
1978
+ ["."]
1979
+ );
1980
+ packagingSpinner.succeed(
1981
+ `Packaged ${deployFileCount} files (${ui.formatBytes(deployTotalBytes)})`
1982
+ );
1983
+ const uploadStart = Date.now();
1984
+ const uploadSpinner = ui.spinner("Uploading to cloud...");
1985
+ let activatingSpinner = null;
1986
+ const form = new FormData2();
1987
+ form.append("config", JSON.stringify({
1988
+ name: projectConfig.name,
1989
+ framework: projectConfig.framework,
1990
+ deployTarget: projectConfig.deployTarget,
1991
+ outputDirectory: projectConfig.outputDirectory
1992
+ }));
1993
+ form.append("artifacts", createReadStream(tarPath), {
1994
+ filename: "artifacts.tar.gz",
1995
+ contentType: "application/gzip"
1996
+ });
1997
+ const deployConfig = {
1998
+ method: "post",
1999
+ url: `${SHIPEM_API_URL}/projects/deploy`,
2000
+ data: form,
2001
+ headers: {
2002
+ ...form.getHeaders(),
2003
+ Authorization: `Bearer ${sessionToken}`
2004
+ },
2005
+ maxBodyLength: Infinity,
2006
+ timeout: 5 * 60 * 1e3,
2007
+ onUploadProgress: (evt) => {
2008
+ if (!activatingSpinner && evt.total && evt.loaded >= evt.total) {
2009
+ const uploadSec = ((Date.now() - uploadStart) / 1e3).toFixed(1);
2010
+ uploadSpinner.succeed(`Upload complete (${uploadSec}s)`);
2011
+ activatingSpinner = ui.spinner("Activating deployment...");
2012
+ }
2013
+ }
2014
+ };
2015
+ let response;
2016
+ try {
2017
+ response = await apiRequest(deployConfig);
2018
+ } catch (err) {
2019
+ if (axios2.isAxiosError(err) && err.response?.status === 401) {
2020
+ if (activatingSpinner) {
2021
+ activatingSpinner.text = "Session expired. Logging in again...";
2022
+ } else {
2023
+ uploadSpinner.text = "Session expired. Logging in again...";
2024
+ }
2025
+ await loginCommand({ skipBanner: true });
2026
+ sessionToken = getSessionToken();
2027
+ if (!sessionToken) {
2028
+ throw new Error("Login failed after session expiry. Please run: shipem login");
2029
+ }
2030
+ const retryForm = new FormData2();
2031
+ retryForm.append("config", JSON.stringify({
2032
+ name: projectConfig.name,
2033
+ framework: projectConfig.framework,
2034
+ deployTarget: projectConfig.deployTarget,
2035
+ outputDirectory: projectConfig.outputDirectory
2036
+ }));
2037
+ retryForm.append("artifacts", createReadStream(tarPath), {
2038
+ filename: "artifacts.tar.gz",
2039
+ contentType: "application/gzip"
2040
+ });
2041
+ deployConfig.data = retryForm;
2042
+ deployConfig.headers = {
2043
+ ...retryForm.getHeaders(),
2044
+ Authorization: `Bearer ${sessionToken}`
2045
+ };
2046
+ try {
2047
+ response = await apiRequest(deployConfig);
2048
+ } catch (retryErr) {
2049
+ if (axios2.isAxiosError(retryErr) && retryErr.response?.status === 401) {
2050
+ throw new Error("Authentication failed after re-login. Please run: shipem login");
2051
+ }
2052
+ throw retryErr;
2053
+ }
2054
+ } else {
2055
+ throw err;
2056
+ }
2057
+ }
2058
+ if (activatingSpinner) {
2059
+ activatingSpinner.succeed("Live!");
2060
+ } else {
2061
+ uploadSpinner.succeed("Deployed successfully");
2062
+ }
2063
+ liveUrl = response.data.url;
2064
+ projectId = response.data.projectId;
2065
+ } catch (err) {
2066
+ const msg = err instanceof Error ? err.message : String(err);
2067
+ if (msg.includes("Cannot reach")) {
2068
+ ui.friendlyError(
2069
+ "Cannot reach Shipem servers",
2070
+ "Network connection failed",
2071
+ "Check your internet and try again"
2072
+ );
2073
+ } else {
2074
+ ui.friendlyError(
2075
+ "Deployment failed",
2076
+ msg,
2077
+ "Check your connection and try again with: npx shipem"
2078
+ );
2079
+ }
2080
+ process.exit(1);
2081
+ } finally {
2082
+ try {
2083
+ rmSync(tarPath);
2084
+ } catch {
2085
+ }
2086
+ }
2087
+ }
2088
+ const configToSave = {
2089
+ ...projectConfig,
2090
+ envVars: projectConfig.envVars.map(({ value: _stripped, ...rest }) => rest)
2091
+ };
2092
+ const deploymentState = {
2093
+ projectName: projectConfig.name,
2094
+ deploymentId: projectId,
2095
+ url: liveUrl,
2096
+ deployTarget: projectConfig.deployTarget,
2097
+ deployedAt: (/* @__PURE__ */ new Date()).toISOString(),
2098
+ status: "success"
2099
+ };
2100
+ writeProjectConfig({ project: configToSave, deployment: deploymentState }, cwd);
2101
+ const gitignorePath = join6(cwd, ".gitignore");
2102
+ if (existsSync5(gitignorePath)) {
2103
+ const gitignoreContent = readFileSync5(gitignorePath, "utf-8");
2104
+ const lines = gitignoreContent.split("\n").map((l) => l.trim());
2105
+ if (!lines.includes("shipem.json")) {
2106
+ appendFileSync(gitignorePath, "\n# Shipem config\nshipem.json\n");
2107
+ }
2108
+ } else {
2109
+ writeFileSync2(gitignorePath, "# Shipem config\nshipem.json\n");
2110
+ }
2111
+ const elapsedSec = (Date.now() - startTime) / 1e3;
2112
+ ui.deployBox(projectConfig.name, liveUrl, elapsedSec, deployFileCount, deployTotalBytes);
2113
+ }
2114
+
2115
+ // src/commands/status.ts
2116
+ import chalk5 from "chalk";
2117
+ async function statusCommand() {
2118
+ const cwd = process.cwd();
2119
+ const config = readProjectConfig(cwd);
2120
+ if (!config.deployment) {
2121
+ ui.warn("No deployment found in this directory.");
2122
+ ui.dim("Run `shipem` to deploy your app.");
2123
+ process.exit(0);
2124
+ }
2125
+ const { deployment, project } = config;
2126
+ ui.section("Deployment Status");
2127
+ ui.br();
2128
+ ui.kv("App", deployment.projectName);
2129
+ ui.kv("URL", chalk5.blue.underline(deployment.url));
2130
+ ui.kv("Deployed", new Date(deployment.deployedAt).toLocaleString());
2131
+ ui.kv("Target", deployment.deployTarget);
2132
+ ui.br();
2133
+ if ((deployment.deployTarget === "cloudflare-pages" || deployment.deployTarget === "cloudflare-workers") && deployment.cloudflareProjectName) {
2134
+ const cfCreds = getCloudflareCredentials();
2135
+ if (cfCreds) {
2136
+ const spinner = ui.spinner("Fetching live status from Cloudflare");
2137
+ const cf = new CloudflarePages(cfCreds.apiToken, cfCreds.accountId);
2138
+ try {
2139
+ const status = await cf.getDeploymentStatus(
2140
+ deployment.cloudflareProjectName,
2141
+ deployment.deploymentId
2142
+ );
2143
+ if (status) {
2144
+ spinner.succeed("Status fetched");
2145
+ ui.kv("Deployment ID", status.id);
2146
+ ui.kv(
2147
+ "Status",
2148
+ status.latest_stage?.status === "success" ? chalk5.green("\u2713 Active") : chalk5.yellow(status.latest_stage?.status ?? "unknown")
2149
+ );
2150
+ ui.br();
2151
+ if (status.stages && status.stages.length > 0) {
2152
+ ui.section("Build stages:");
2153
+ for (const stage of status.stages) {
2154
+ const icon = stage.status === "success" ? chalk5.green("\u2713") : stage.status === "failure" ? chalk5.red("\u2717") : chalk5.gray("\xB7");
2155
+ ui.info(`${icon} ${stage.name}`);
2156
+ }
2157
+ ui.br();
2158
+ }
2159
+ } else {
2160
+ spinner.warn("Could not fetch live status");
2161
+ }
2162
+ } catch {
2163
+ spinner.warn("Could not fetch live status from Cloudflare");
2164
+ }
2165
+ }
2166
+ }
2167
+ ui.url("Live URL", deployment.url);
2168
+ if (deployment.cloudflareProjectName) {
2169
+ ui.url(
2170
+ "Dashboard",
2171
+ `https://dash.cloudflare.com/${deployment.cloudflareAccountId}/pages/view/${deployment.cloudflareProjectName}`
2172
+ );
2173
+ }
2174
+ ui.br();
2175
+ }
2176
+
2177
+ // src/commands/logs.ts
2178
+ import chalk6 from "chalk";
2179
+ async function logsCommand(options) {
2180
+ const cwd = process.cwd();
2181
+ const config = readProjectConfig(cwd);
2182
+ if (!config.deployment) {
2183
+ ui.warn("No deployment found in this directory.");
2184
+ ui.dim("Run `shipem` to deploy your app.");
2185
+ process.exit(0);
2186
+ }
2187
+ const { deployment } = config;
2188
+ if (deployment.deployTarget !== "cloudflare-pages" && deployment.deployTarget !== "cloudflare-workers") {
2189
+ 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
+ process.exit(0);
2192
+ }
2193
+ if (!deployment.cloudflareProjectName) {
2194
+ ui.fatal("Missing Cloudflare project name in deployment config.");
2195
+ }
2196
+ const cfCreds = getCloudflareCredentials();
2197
+ if (!cfCreds) {
2198
+ ui.fatal(
2199
+ "Cloudflare credentials not found.",
2200
+ "Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables."
2201
+ );
2202
+ }
2203
+ ui.section("Deployment Logs");
2204
+ ui.br();
2205
+ ui.kv("App", deployment.projectName);
2206
+ ui.kv("Deployment", deployment.deploymentId);
2207
+ ui.br();
2208
+ const spinner = ui.spinner("Fetching logs");
2209
+ const cf = new CloudflarePages(cfCreds.apiToken, cfCreds.accountId);
2210
+ try {
2211
+ const logs = await cf.getDeploymentLogs(
2212
+ deployment.cloudflareProjectName,
2213
+ deployment.deploymentId
2214
+ );
2215
+ spinner.stop();
2216
+ if (logs.length === 0) {
2217
+ ui.info("No logs available for this deployment.");
2218
+ } else {
2219
+ const linesToShow = options.lines ?? logs.length;
2220
+ const displayLogs = logs.slice(-linesToShow);
2221
+ for (const line of displayLogs) {
2222
+ if (line.includes("ERROR") || line.includes("error") || line.includes("FAIL")) {
2223
+ console.log(chalk6.red(` ${line}`));
2224
+ } else if (line.includes("WARN") || line.includes("warn")) {
2225
+ console.log(chalk6.yellow(` ${line}`));
2226
+ } else {
2227
+ console.log(chalk6.gray(` ${line}`));
2228
+ }
2229
+ }
2230
+ }
2231
+ } catch (err) {
2232
+ spinner.fail("Failed to fetch logs");
2233
+ ui.error(err instanceof Error ? err.message : "Unknown error");
2234
+ }
2235
+ ui.br();
2236
+ }
2237
+
2238
+ // src/commands/down.ts
2239
+ import inquirer2 from "inquirer";
2240
+ import axios3 from "axios";
2241
+ async function downCommand(options) {
2242
+ const cwd = process.cwd();
2243
+ const config = readProjectConfig(cwd);
2244
+ if (!config.deployment) {
2245
+ ui.warn("No deployment found in this directory.");
2246
+ ui.dim("Run `shipem` to deploy your app first.");
2247
+ process.exit(0);
2248
+ }
2249
+ const { deployment } = config;
2250
+ ui.section("Taking down deployment");
2251
+ ui.br();
2252
+ ui.kv("App", deployment.projectName);
2253
+ ui.kv("URL", deployment.url);
2254
+ ui.br();
2255
+ if (!options.yes) {
2256
+ const { confirmed } = await inquirer2.prompt([
2257
+ {
2258
+ type: "confirm",
2259
+ name: "confirmed",
2260
+ message: `Are you sure you want to take down ${deployment.projectName}? This cannot be undone.`,
2261
+ default: false
2262
+ }
2263
+ ]);
2264
+ if (!confirmed) {
2265
+ ui.info("Cancelled.");
2266
+ process.exit(0);
2267
+ }
2268
+ }
2269
+ ui.br();
2270
+ if ((deployment.deployTarget === "cloudflare-pages" || deployment.deployTarget === "cloudflare-workers") && deployment.cloudflareProjectName) {
2271
+ const cfCreds = getCloudflareCredentials();
2272
+ if (!cfCreds) {
2273
+ ui.fatal(
2274
+ "Cloudflare credentials not found.",
2275
+ "Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables."
2276
+ );
2277
+ }
2278
+ const spinner = ui.spinner(`Deleting ${deployment.projectName} from Cloudflare Pages`);
2279
+ const cf = new CloudflarePages(cfCreds.apiToken, cfCreds.accountId);
2280
+ try {
2281
+ await cf.deleteProject(deployment.cloudflareProjectName);
2282
+ spinner.succeed("Deployment deleted from Cloudflare Pages");
2283
+ } catch (err) {
2284
+ spinner.fail("Failed to delete deployment");
2285
+ ui.error(err instanceof Error ? err.message : "Unknown error");
2286
+ process.exit(1);
2287
+ }
2288
+ } else if ((deployment.deployTarget === "cloudflare-pages" || deployment.deployTarget === "cloudflare-workers") && !deployment.cloudflareProjectName) {
2289
+ const token = getSessionToken();
2290
+ if (!token) {
2291
+ ui.fatal("Not logged in. Run `shipem login` first.");
2292
+ }
2293
+ const spinner = ui.spinner(`Deleting ${deployment.projectName} from Shipem`);
2294
+ try {
2295
+ await axios3.delete(`${SHIPEM_API_URL}/projects/${deployment.deploymentId}`, {
2296
+ headers: { Authorization: `Bearer ${token}` }
2297
+ });
2298
+ spinner.succeed("Deployment deleted");
2299
+ } catch (err) {
2300
+ spinner.fail("Failed to delete deployment");
2301
+ ui.error(err instanceof Error ? err.message : "Unknown error");
2302
+ process.exit(1);
2303
+ }
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
+ }
2312
+ writeProjectConfig({ project: config.project }, cwd);
2313
+ ui.br();
2314
+ ui.success("Deployment removed successfully.");
2315
+ ui.dim("Your local code is untouched. Run `shipem` to deploy again.");
2316
+ ui.br();
2317
+ }
2318
+
2319
+ // src/commands/logout.ts
2320
+ import axios4 from "axios";
2321
+ async function logoutCommand() {
2322
+ const token = getSessionToken();
2323
+ if (!token) {
2324
+ ui.info("You are not currently logged in.");
2325
+ ui.br();
2326
+ return;
2327
+ }
2328
+ try {
2329
+ await axios4.post(`${SHIPEM_API_URL}/auth/logout`, { token }, { timeout: 5e3 });
2330
+ } catch {
2331
+ }
2332
+ clearSessionToken();
2333
+ ui.success("Logged out successfully.");
2334
+ ui.br();
2335
+ }
2336
+
2337
+ // src/index.ts
2338
+ import { readFileSync as readFileSync6 } from "fs";
2339
+ import { fileURLToPath } from "url";
2340
+ import { dirname, join as join7 } from "path";
2341
+ var __filename2 = fileURLToPath(import.meta.url);
2342
+ var __dirname2 = dirname(__filename2);
2343
+ var version = "0.1.0";
2344
+ try {
2345
+ const pkg = JSON.parse(
2346
+ readFileSync6(join7(__dirname2, "../package.json"), "utf-8")
2347
+ );
2348
+ version = pkg.version;
2349
+ } catch {
2350
+ }
2351
+ var program = new Command();
2352
+ program.name("shipem").description(
2353
+ "One-command deployment for apps built by AI coding tools.\n\nYour AI built it. We'll ship it."
2354
+ ).version(version, "-v, --version", "Display version number").addHelpText("before", () => {
2355
+ ui.banner();
2356
+ return "";
2357
+ }).addHelpText("after", `
2358
+ Quick start:
2359
+ npx shipem Deploy your app
2360
+ npx shipem login Authenticate with GitHub
2361
+ npx shipem status Check deployment status
2362
+ npx shipem down Take your app offline
2363
+ `);
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) => {
2365
+ try {
2366
+ await deployCommand(options);
2367
+ } catch (err) {
2368
+ ui.br();
2369
+ ui.error(err instanceof Error ? err.message : "An unexpected error occurred.");
2370
+ if (process.env.DEBUG) {
2371
+ console.error(err);
2372
+ }
2373
+ process.exit(1);
2374
+ }
2375
+ });
2376
+ program.command("login").description("Log in to Shipem via GitHub").action(async () => {
2377
+ try {
2378
+ await loginCommand();
2379
+ } catch (err) {
2380
+ ui.error(err instanceof Error ? err.message : "Login failed.");
2381
+ process.exit(1);
2382
+ }
2383
+ });
2384
+ program.command("logout").description("Log out of Shipem").action(async () => {
2385
+ try {
2386
+ await logoutCommand();
2387
+ } catch (err) {
2388
+ ui.error(err instanceof Error ? err.message : "Logout failed.");
2389
+ process.exit(1);
2390
+ }
2391
+ });
2392
+ program.command("status").description("Check deployment status").action(async () => {
2393
+ try {
2394
+ await statusCommand();
2395
+ } catch (err) {
2396
+ ui.error(err instanceof Error ? err.message : "An unexpected error occurred.");
2397
+ process.exit(1);
2398
+ }
2399
+ });
2400
+ program.command("logs").description("View recent deployment logs").option("-n, --lines <number>", "Number of log lines to show", "100").action(async (options) => {
2401
+ try {
2402
+ await logsCommand({ lines: options.lines ? parseInt(options.lines, 10) : void 0 });
2403
+ } catch (err) {
2404
+ ui.error(err instanceof Error ? err.message : "An unexpected error occurred.");
2405
+ process.exit(1);
2406
+ }
2407
+ });
2408
+ program.command("down").description("Take down a deployment").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
2409
+ try {
2410
+ await downCommand(options);
2411
+ } catch (err) {
2412
+ ui.error(err instanceof Error ? err.message : "An unexpected error occurred.");
2413
+ process.exit(1);
2414
+ }
2415
+ });
2416
+ var domainsCmd = program.command("domains").description("Manage custom domains (coming soon)");
2417
+ domainsCmd.command("add <domain>").description("Add a custom domain to your deployment").action(() => {
2418
+ ui.warn("Custom domains are coming soon!");
2419
+ ui.dim("Follow our changelog at https://shipem.dev/changelog for updates.");
2420
+ ui.br();
2421
+ });
2422
+ program.on("command:*", (cmds) => {
2423
+ ui.error(`Unknown command: ${cmds[0]}`);
2424
+ ui.dim("Run `shipem --help` to see available commands.");
2425
+ process.exit(1);
2426
+ });
2427
+ program.parse(process.argv);