ship-em 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib.js ADDED
@@ -0,0 +1,1923 @@
1
+ // src/detect/scanner.ts
2
+ import { readFileSync, existsSync, readdirSync, statSync } from "fs";
3
+ import { join } from "path";
4
+ function readJson(filePath) {
5
+ try {
6
+ return JSON.parse(readFileSync(filePath, "utf-8"));
7
+ } catch {
8
+ return null;
9
+ }
10
+ }
11
+ function readFile(filePath) {
12
+ try {
13
+ return readFileSync(filePath, "utf-8");
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+ function listTopLevelFiles(cwd) {
19
+ try {
20
+ return readdirSync(cwd).filter((f) => {
21
+ try {
22
+ return statSync(join(cwd, f)).isFile();
23
+ } catch {
24
+ return false;
25
+ }
26
+ });
27
+ } catch {
28
+ return [];
29
+ }
30
+ }
31
+ function listTopLevelDirs(cwd) {
32
+ try {
33
+ return readdirSync(cwd).filter((f) => {
34
+ try {
35
+ return statSync(join(cwd, f)).isDirectory();
36
+ } catch {
37
+ return false;
38
+ }
39
+ });
40
+ } catch {
41
+ return [];
42
+ }
43
+ }
44
+ function detectEnvVarsFromFiles(cwd) {
45
+ const envVars = [];
46
+ const seen = /* @__PURE__ */ new Set();
47
+ const envExampleFiles = [".env.example", ".env.local.example", ".env.sample", ".env.template"];
48
+ for (const file of envExampleFiles) {
49
+ const content = readFile(join(cwd, file));
50
+ if (content) {
51
+ for (const line of content.split("\n")) {
52
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
53
+ if (match && !seen.has(match[1])) {
54
+ seen.add(match[1]);
55
+ envVars.push({
56
+ name: match[1],
57
+ description: line.includes("#") ? line.split("#")[1].trim() : "",
58
+ required: true
59
+ });
60
+ }
61
+ }
62
+ }
63
+ }
64
+ const envContent = readFile(join(cwd, ".env"));
65
+ if (envContent) {
66
+ for (const line of envContent.split("\n")) {
67
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
68
+ if (match && !seen.has(match[1])) {
69
+ seen.add(match[1]);
70
+ envVars.push({
71
+ name: match[1],
72
+ description: "",
73
+ required: true
74
+ });
75
+ }
76
+ }
77
+ }
78
+ return envVars;
79
+ }
80
+ function getNodeVersion(pkg, cwd) {
81
+ const nvmrc = readFile(join(cwd, ".nvmrc"));
82
+ if (nvmrc) return nvmrc.trim().replace("v", "");
83
+ const nodeVersion = readFile(join(cwd, ".node-version"));
84
+ if (nodeVersion) return nodeVersion.trim().replace("v", "");
85
+ if (pkg.engines?.node) {
86
+ const match = pkg.engines.node.match(/(\d+)/);
87
+ if (match) return match[1];
88
+ }
89
+ return "20";
90
+ }
91
+ function scanProject(cwd = process.cwd()) {
92
+ const result = scanProjectInternal(cwd);
93
+ const monorepo = detectMonorepo(cwd);
94
+ if (monorepo) result.monorepo = monorepo;
95
+ return result;
96
+ }
97
+ function scanProjectInternal(cwd) {
98
+ const files = listTopLevelFiles(cwd);
99
+ const dirs = listTopLevelDirs(cwd);
100
+ const allEntries = [...files, ...dirs];
101
+ const hasFile = (name) => files.includes(name);
102
+ const hasDir = (name) => dirs.includes(name);
103
+ if (hasFile("package.json")) {
104
+ const pkg = readJson(join(cwd, "package.json"));
105
+ if (!pkg) {
106
+ return buildUnknown(cwd, files);
107
+ }
108
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
109
+ const projectName = pkg.name ?? cwd.split("/").pop() ?? "my-app";
110
+ const nodeVersion = getNodeVersion(pkg, cwd);
111
+ const envVars = detectEnvVarsFromFiles(cwd);
112
+ if (deps["next"]) {
113
+ const isAppRouter = hasDir("app") || existsSync(join(cwd, "src/app"));
114
+ const hasServerComponents = isAppRouter;
115
+ 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"));
116
+ const serverType = hasServerComponents || hasApiRoutes ? "serverless" : "static";
117
+ const commonNextEnvVars = [];
118
+ if (deps["@supabase/supabase-js"] || deps["@supabase/auth-helpers-nextjs"]) {
119
+ commonNextEnvVars.push(
120
+ { name: "NEXT_PUBLIC_SUPABASE_URL", description: "Supabase project URL", required: true, autoProvision: true },
121
+ { name: "NEXT_PUBLIC_SUPABASE_ANON_KEY", description: "Supabase anon key", required: true, autoProvision: true }
122
+ );
123
+ }
124
+ if (deps["openai"]) {
125
+ commonNextEnvVars.push({ name: "OPENAI_API_KEY", description: "OpenAI API key", required: true });
126
+ }
127
+ if (deps["@anthropic-ai/sdk"] || deps["@anthropic-ai/claude"]) {
128
+ commonNextEnvVars.push({ name: "ANTHROPIC_API_KEY", description: "Anthropic API key", required: true });
129
+ }
130
+ if (deps["stripe"]) {
131
+ commonNextEnvVars.push(
132
+ { name: "STRIPE_SECRET_KEY", description: "Stripe secret key", required: true },
133
+ { name: "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY", description: "Stripe publishable key", required: true }
134
+ );
135
+ }
136
+ const mergedEnvVars = mergeEnvVars(envVars, commonNextEnvVars);
137
+ return {
138
+ framework: "nextjs",
139
+ buildCommand: pkg.scripts?.build ?? "npm run build",
140
+ outputDirectory: ".next",
141
+ installCommand: detectPackageManager(cwd),
142
+ serverType,
143
+ deployTarget: "cloudflare-pages",
144
+ nodeVersion,
145
+ envVars: mergedEnvVars,
146
+ projectName,
147
+ confidence: 0.98,
148
+ files: allEntries
149
+ };
150
+ }
151
+ if (deps["astro"]) {
152
+ return {
153
+ framework: "astro",
154
+ buildCommand: pkg.scripts?.build ?? "npm run build",
155
+ outputDirectory: "dist",
156
+ installCommand: detectPackageManager(cwd),
157
+ serverType: "static",
158
+ deployTarget: "cloudflare-pages",
159
+ nodeVersion,
160
+ envVars,
161
+ projectName,
162
+ confidence: 0.97,
163
+ files: allEntries
164
+ };
165
+ }
166
+ if (deps["@sveltejs/kit"]) {
167
+ return {
168
+ framework: "sveltekit",
169
+ buildCommand: pkg.scripts?.build ?? "npm run build",
170
+ outputDirectory: ".svelte-kit",
171
+ installCommand: detectPackageManager(cwd),
172
+ serverType: "serverless",
173
+ deployTarget: "cloudflare-pages",
174
+ nodeVersion,
175
+ envVars,
176
+ projectName,
177
+ confidence: 0.97,
178
+ files: allEntries
179
+ };
180
+ }
181
+ if (deps["nuxt"] || deps["nuxt3"]) {
182
+ return {
183
+ framework: "nuxt",
184
+ buildCommand: pkg.scripts?.build ?? "npm run build",
185
+ outputDirectory: ".output",
186
+ installCommand: detectPackageManager(cwd),
187
+ serverType: "serverless",
188
+ deployTarget: "cloudflare-pages",
189
+ nodeVersion,
190
+ envVars,
191
+ projectName,
192
+ confidence: 0.97,
193
+ files: allEntries
194
+ };
195
+ }
196
+ if (deps["@remix-run/node"] || deps["@remix-run/react"]) {
197
+ return {
198
+ framework: "remix",
199
+ buildCommand: pkg.scripts?.build ?? "npm run build",
200
+ outputDirectory: "build",
201
+ installCommand: detectPackageManager(cwd),
202
+ serverType: "serverless",
203
+ deployTarget: "cloudflare-pages",
204
+ nodeVersion,
205
+ envVars,
206
+ projectName,
207
+ confidence: 0.97,
208
+ files: allEntries
209
+ };
210
+ }
211
+ if (deps["gatsby"]) {
212
+ return {
213
+ framework: "gatsby",
214
+ buildCommand: pkg.scripts?.build ?? "npm run build",
215
+ outputDirectory: "public",
216
+ installCommand: detectPackageManager(cwd),
217
+ serverType: "static",
218
+ deployTarget: "cloudflare-pages",
219
+ nodeVersion,
220
+ envVars,
221
+ projectName,
222
+ confidence: 0.97,
223
+ files: allEntries
224
+ };
225
+ }
226
+ if (deps["vite"]) {
227
+ let framework = "vite-react";
228
+ if (deps["vue"] || deps["@vue/core"]) framework = "vite-vue";
229
+ else if (deps["svelte"]) framework = "vite-svelte";
230
+ return {
231
+ framework,
232
+ buildCommand: pkg.scripts?.build ?? "npm run build",
233
+ outputDirectory: "dist",
234
+ installCommand: detectPackageManager(cwd),
235
+ serverType: "static",
236
+ deployTarget: "cloudflare-pages",
237
+ nodeVersion,
238
+ envVars,
239
+ projectName,
240
+ confidence: 0.95,
241
+ files: allEntries
242
+ };
243
+ }
244
+ if (deps["react-scripts"]) {
245
+ return {
246
+ framework: "create-react-app",
247
+ buildCommand: pkg.scripts?.build ?? "npm run build",
248
+ outputDirectory: "build",
249
+ installCommand: detectPackageManager(cwd),
250
+ serverType: "static",
251
+ deployTarget: "cloudflare-pages",
252
+ nodeVersion,
253
+ envVars,
254
+ projectName,
255
+ confidence: 0.95,
256
+ files: allEntries
257
+ };
258
+ }
259
+ if (deps["hono"]) {
260
+ return {
261
+ framework: "hono",
262
+ buildCommand: pkg.scripts?.build ?? "npm run build",
263
+ outputDirectory: "dist",
264
+ installCommand: detectPackageManager(cwd),
265
+ serverType: "serverless",
266
+ deployTarget: "cloudflare-workers",
267
+ nodeVersion,
268
+ envVars,
269
+ projectName,
270
+ confidence: 0.9,
271
+ files: allEntries
272
+ };
273
+ }
274
+ if (pkg.scripts?.build) {
275
+ return {
276
+ framework: "unknown",
277
+ buildCommand: pkg.scripts.build,
278
+ outputDirectory: "dist",
279
+ installCommand: detectPackageManager(cwd),
280
+ serverType: "server",
281
+ deployTarget: "cloudflare-pages",
282
+ nodeVersion,
283
+ envVars,
284
+ projectName,
285
+ confidence: 0.6,
286
+ files: allEntries
287
+ };
288
+ }
289
+ }
290
+ if (hasFile("index.html")) {
291
+ const projectName = cwd.split("/").pop() ?? "my-app";
292
+ const result = {
293
+ framework: "static-html",
294
+ buildCommand: "",
295
+ outputDirectory: ".",
296
+ installCommand: "",
297
+ serverType: "static",
298
+ deployTarget: "cloudflare-pages",
299
+ envVars: [],
300
+ projectName,
301
+ confidence: 0.9,
302
+ files: allEntries
303
+ };
304
+ return result;
305
+ }
306
+ return buildUnknown(cwd, files);
307
+ }
308
+ function buildUnknown(cwd, files) {
309
+ return {
310
+ framework: "unknown",
311
+ buildCommand: "",
312
+ outputDirectory: ".",
313
+ installCommand: "",
314
+ serverType: "static",
315
+ deployTarget: "cloudflare-pages",
316
+ envVars: [],
317
+ projectName: cwd.split("/").pop() ?? "my-app",
318
+ confidence: 0,
319
+ files
320
+ };
321
+ }
322
+ function detectPackageManager(cwd) {
323
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm install";
324
+ if (existsSync(join(cwd, "yarn.lock"))) return "yarn install";
325
+ if (existsSync(join(cwd, "bun.lockb"))) return "bun install";
326
+ return "npm install";
327
+ }
328
+ function mergeEnvVars(base, extra) {
329
+ const seen = new Set(base.map((v) => v.name));
330
+ const merged = [...base];
331
+ for (const v of extra) {
332
+ if (!seen.has(v.name)) {
333
+ seen.add(v.name);
334
+ merged.push(v);
335
+ }
336
+ }
337
+ return merged;
338
+ }
339
+ function detectMonorepo(cwd) {
340
+ const hasFile = (name) => existsSync(join(cwd, name));
341
+ if (hasFile("turbo.json")) {
342
+ const packages = findWorkspacePackages(cwd);
343
+ return { type: "turbo", packages };
344
+ }
345
+ if (hasFile("nx.json")) {
346
+ const packages = findWorkspacePackages(cwd);
347
+ return { type: "nx", packages };
348
+ }
349
+ if (hasFile("pnpm-workspace.yaml")) {
350
+ const packages = findWorkspacePackages(cwd);
351
+ return { type: "pnpm-workspaces", packages };
352
+ }
353
+ try {
354
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
355
+ if (pkg.workspaces) {
356
+ const packages = findWorkspacePackages(cwd);
357
+ if (hasFile("yarn.lock")) {
358
+ return { type: "yarn-workspaces", packages };
359
+ }
360
+ return { type: "npm-workspaces", packages };
361
+ }
362
+ } catch {
363
+ }
364
+ return void 0;
365
+ }
366
+ function findWorkspacePackages(cwd) {
367
+ const packages = [];
368
+ const commonDirs = ["packages", "apps", "projects", "services"];
369
+ for (const dir of commonDirs) {
370
+ const dirPath = join(cwd, dir);
371
+ if (existsSync(dirPath)) {
372
+ try {
373
+ const entries = readdirSync(dirPath, { withFileTypes: true });
374
+ for (const entry of entries) {
375
+ if (entry.isDirectory() && existsSync(join(dirPath, entry.name, "package.json"))) {
376
+ packages.push(entry.name);
377
+ }
378
+ }
379
+ } catch {
380
+ }
381
+ }
382
+ }
383
+ return packages;
384
+ }
385
+
386
+ // src/config.ts
387
+ import Conf from "conf";
388
+ import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2 } from "fs";
389
+ import { join as join2 } from "path";
390
+ var SHIPEM_API_URL = process.env.SHIPEM_API_URL ?? "https://api.shipem.dev";
391
+ var globalConf = new Conf({
392
+ projectName: "shipem",
393
+ schema: {
394
+ cloudflare: {
395
+ type: "object",
396
+ properties: {
397
+ apiToken: { type: "string" },
398
+ accountId: { type: "string" }
399
+ }
400
+ },
401
+ sessionToken: { type: "string" }
402
+ }
403
+ });
404
+ function getCloudflareCredentials() {
405
+ const creds = globalConf.get("cloudflare");
406
+ const token = process.env.CLOUDFLARE_API_TOKEN ?? creds?.apiToken;
407
+ const accountId = process.env.CLOUDFLARE_ACCOUNT_ID ?? creds?.accountId;
408
+ if (token && accountId) {
409
+ return { apiToken: token, accountId };
410
+ }
411
+ return null;
412
+ }
413
+ function getSessionToken() {
414
+ return globalConf.get("sessionToken") ?? null;
415
+ }
416
+ var SHIPIT_CONFIG_FILE = "shipem.json";
417
+ function readProjectConfig(cwd = process.cwd()) {
418
+ const configPath = join2(cwd, SHIPIT_CONFIG_FILE);
419
+ if (!existsSync2(configPath)) {
420
+ return {};
421
+ }
422
+ try {
423
+ const raw = readFileSync2(configPath, "utf-8");
424
+ const config = JSON.parse(raw);
425
+ warnIfConfigContainsSecrets(config, configPath);
426
+ return config;
427
+ } catch {
428
+ return {};
429
+ }
430
+ }
431
+ function writeProjectConfig(config, cwd = process.cwd()) {
432
+ const configPath = join2(cwd, SHIPIT_CONFIG_FILE);
433
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
434
+ }
435
+ var SECRET_KEY_PATTERN = /token|secret|key|password|credential|api_?key/i;
436
+ function warnIfConfigContainsSecrets(config, configPath) {
437
+ const envVars = config.project?.envVars ?? [];
438
+ const populated = envVars.filter((v) => v.value && SECRET_KEY_PATTERN.test(v.name));
439
+ if (populated.length > 0) {
440
+ const names = populated.map((v) => v.name).join(", ");
441
+ process.stderr.write(
442
+ `
443
+ [Shipem] Warning: ${configPath} contains values for sensitive env vars (${names}).
444
+ Ensure shipem.json is listed in .gitignore to avoid committing credentials.
445
+
446
+ `
447
+ );
448
+ }
449
+ }
450
+
451
+ // src/build/builder.ts
452
+ import { execa } from "execa";
453
+ import { existsSync as existsSync3 } from "fs";
454
+ import { join as join3 } from "path";
455
+ import chalk2 from "chalk";
456
+
457
+ // src/ui/terminal.ts
458
+ import chalk from "chalk";
459
+ import ora from "ora";
460
+ import { execSync } from "child_process";
461
+ var brand = {
462
+ blue: chalk.hex("#3B82F6"),
463
+ brightBlue: chalk.hex("#60A5FA"),
464
+ green: chalk.hex("#22C55E"),
465
+ yellow: chalk.hex("#EAB308"),
466
+ red: chalk.hex("#EF4444"),
467
+ gray: chalk.hex("#6B7280"),
468
+ white: chalk.white,
469
+ bold: chalk.bold,
470
+ dim: chalk.dim
471
+ };
472
+ function visibleLen(str) {
473
+ return str.replace(/\x1B\[[0-9;]*[mGKHFJsu]/g, "").length;
474
+ }
475
+ function timeAgo(isoStr) {
476
+ const diff = Date.now() - new Date(isoStr).getTime();
477
+ const mins = Math.floor(diff / 6e4);
478
+ if (mins < 1) return "just now";
479
+ if (mins === 1) return "1 minute ago";
480
+ if (mins < 60) return `${mins} minutes ago`;
481
+ const hrs = Math.floor(mins / 60);
482
+ if (hrs === 1) return "1 hour ago";
483
+ if (hrs < 24) return `${hrs} hours ago`;
484
+ const days = Math.floor(hrs / 24);
485
+ return days === 1 ? "yesterday" : `${days} days ago`;
486
+ }
487
+ var ui = {
488
+ // Banner shown at startup
489
+ banner() {
490
+ console.log("");
491
+ console.log(
492
+ 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")
493
+ );
494
+ console.log(
495
+ 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")
496
+ );
497
+ console.log(
498
+ 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")
499
+ );
500
+ console.log(
501
+ 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")
502
+ );
503
+ console.log(
504
+ 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")
505
+ );
506
+ console.log(
507
+ 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")
508
+ );
509
+ console.log("");
510
+ console.log(
511
+ ` ${brand.gray("Your AI built it.")} ${brand.blue.bold("We'll ship it.")}`
512
+ );
513
+ console.log("");
514
+ },
515
+ // Section header
516
+ section(title) {
517
+ console.log(`${brand.blue("\u203A")} ${brand.bold(title)}`);
518
+ },
519
+ // Success item
520
+ success(message) {
521
+ console.log(` ${brand.green("\u2713")} ${message}`);
522
+ },
523
+ // Info item
524
+ info(message) {
525
+ console.log(` ${brand.blue("\xB7")} ${message}`);
526
+ },
527
+ // Warning
528
+ warn(message) {
529
+ console.log(` ${brand.yellow("\u26A0")} ${brand.yellow(message)}`);
530
+ },
531
+ // Error
532
+ error(message) {
533
+ console.log(` ${brand.red("\u2717")} ${brand.red(message)}`);
534
+ },
535
+ // Dim/secondary text
536
+ dim(message) {
537
+ console.log(` ${brand.dim(message)}`);
538
+ },
539
+ // Blank line
540
+ br() {
541
+ console.log("");
542
+ },
543
+ // Key-value display
544
+ kv(key, value) {
545
+ console.log(` ${brand.gray(key + ":")} ${value}`);
546
+ },
547
+ // Highlighted URL
548
+ url(label, url) {
549
+ console.log(` ${brand.green("\u2192")} ${brand.bold(label)}: ${brand.brightBlue.underline(url)}`);
550
+ },
551
+ // Box for important info
552
+ box(lines) {
553
+ const maxLen = Math.max(...lines.map((l) => l.length));
554
+ const border = brand.blue("\u2500".repeat(maxLen + 4));
555
+ console.log(` \u256D${border}\u256E`);
556
+ for (const line of lines) {
557
+ const padding = " ".repeat(maxLen - line.length);
558
+ console.log(` \u2502 ${line}${padding} \u2502`);
559
+ }
560
+ console.log(` \u2570${border}\u256F`);
561
+ },
562
+ // Spinner
563
+ spinner(text) {
564
+ return ora({
565
+ text,
566
+ spinner: "dots",
567
+ color: "blue",
568
+ prefixText: " "
569
+ }).start();
570
+ },
571
+ // Fatal error with exit
572
+ fatal(message, hint) {
573
+ console.log("");
574
+ console.log(` ${brand.red.bold("Error:")} ${message}`);
575
+ if (hint) {
576
+ console.log(` ${brand.gray("Hint:")} ${hint}`);
577
+ }
578
+ console.log("");
579
+ process.exit(1);
580
+ },
581
+ // Structured friendly error: what happened, why, and exactly what to do
582
+ friendlyError(title, why, fix, extra) {
583
+ console.log("");
584
+ console.log(` ${brand.red("\u2717")} ${brand.red.bold(title)}`);
585
+ console.log("");
586
+ console.log(` ${brand.dim("Why:")} ${brand.dim(why)}`);
587
+ console.log(` ${chalk.bold("Fix:")} ${fix}`);
588
+ if (extra) {
589
+ console.log("");
590
+ console.log(` ${brand.dim(extra)}`);
591
+ }
592
+ console.log("");
593
+ },
594
+ // Project analysis card — shown after detection
595
+ projectAnalysis(info) {
596
+ const buildLabel = info.buildCommand ? `${info.buildCommand} \u2192 ${info.outputDir}` : `No build step \u2192 ${info.outputDir}`;
597
+ const envLabel = info.envVarCount > 0 ? `${info.envVarCount} environment variable${info.envVarCount !== 1 ? "s" : ""} found` : "No environment variables";
598
+ const rows = [
599
+ [`\u{1F4E6} ${info.name}`, `\u{1F4E6} ${brand.bold(info.name)}`],
600
+ [`\u26A1 ${info.framework}`, `\u26A1 ${ui.framework(info.framework)}`],
601
+ [`\u{1F4C1} ${info.fileCount} source files`, `\u{1F4C1} ${chalk.cyan(String(info.fileCount))} source files`],
602
+ [
603
+ `\u{1F527} ${buildLabel}`,
604
+ 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)}`
605
+ ],
606
+ [`\u{1F30D} Deploy target: ${info.deployTarget}`, `\u{1F30D} Deploy target: ${chalk.cyan(info.deployTarget)}`],
607
+ [
608
+ `\u{1F511} ${envLabel}`,
609
+ info.envVarCount > 0 ? `\u{1F511} ${chalk.cyan(envLabel)}` : `\u{1F511} ${brand.dim(envLabel)}`
610
+ ]
611
+ ];
612
+ const title = " Project Analysis ";
613
+ const maxPlain = Math.max(...rows.map(([p]) => visibleLen(p)));
614
+ const innerWidth = Math.max(maxPlain + 4, title.length + 2);
615
+ const topBorder = title + "\u2500".repeat(innerWidth - title.length);
616
+ const bottomBorder = "\u2500".repeat(innerWidth);
617
+ console.log("");
618
+ console.log(` ${brand.blue("\u250C" + topBorder + "\u2510")}`);
619
+ console.log(` ${brand.blue("\u2502")}${" ".repeat(innerWidth)}${brand.blue("\u2502")}`);
620
+ for (const [plain, colored] of rows) {
621
+ const rightPad = innerWidth - 2 - visibleLen(plain);
622
+ console.log(
623
+ ` ${brand.blue("\u2502")} ${colored}${" ".repeat(Math.max(0, rightPad))}${brand.blue("\u2502")}`
624
+ );
625
+ }
626
+ console.log(` ${brand.blue("\u2502")}${" ".repeat(innerWidth)}${brand.blue("\u2502")}`);
627
+ console.log(` ${brand.blue("\u2514" + bottomBorder + "\u2518")}`);
628
+ console.log("");
629
+ },
630
+ // Compact header for re-deploys — skips the banner
631
+ redeployHeader(projectName, lastUrl, lastDeployedAt) {
632
+ const ago = timeAgo(lastDeployedAt);
633
+ console.log("");
634
+ console.log(` \u{1F504} ${brand.bold("Re-deploying")} ${brand.blue.bold(projectName)}`);
635
+ console.log("");
636
+ console.log(
637
+ ` ${brand.dim("Last deploy:")} ${brand.dim(ago)} ${brand.dim("\u2192")} ${brand.brightBlue.underline(lastUrl)}`
638
+ );
639
+ console.log("");
640
+ },
641
+ // Copy text to clipboard (best-effort)
642
+ copyToClipboard(text) {
643
+ try {
644
+ if (process.platform === "darwin") {
645
+ execSync("pbcopy", { input: text, stdio: ["pipe", "ignore", "ignore"] });
646
+ return true;
647
+ } else if (process.platform === "linux") {
648
+ try {
649
+ execSync("xclip -selection clipboard", { input: text, stdio: ["pipe", "ignore", "ignore"] });
650
+ return true;
651
+ } catch {
652
+ try {
653
+ execSync("xsel --clipboard --input", { input: text, stdio: ["pipe", "ignore", "ignore"] });
654
+ return true;
655
+ } catch {
656
+ return false;
657
+ }
658
+ }
659
+ }
660
+ } catch {
661
+ }
662
+ return false;
663
+ },
664
+ // Deploy celebration box — shown on success
665
+ deployBox(appName, url, elapsedSec, fileCount, totalBytes, isAnonymous = false) {
666
+ const statsLine = `Deployed in ${elapsedSec.toFixed(1)}s \xB7 ${fileCount} files \xB7 ${ui.formatBytes(totalBytes)}`;
667
+ const content = [
668
+ ["\u{1F680} Shipped!", brand.green.bold("\u{1F680} Shipped!")],
669
+ null,
670
+ [`${appName} is live`, `${brand.white.bold(appName)} is live`],
671
+ null,
672
+ [`\u2192 ${url}`, `${brand.blue("\u2192")} ${brand.brightBlue.underline(url)}`],
673
+ null,
674
+ [statsLine, brand.dim(statsLine)]
675
+ ];
676
+ const maxPlain = Math.max(
677
+ ...content.filter((item) => item !== null).map(([p]) => visibleLen(p))
678
+ );
679
+ const innerWidth = maxPlain + 6;
680
+ console.log("");
681
+ console.log(` ${brand.blue("\u256D" + "\u2500".repeat(innerWidth) + "\u256E")}`);
682
+ console.log(` ${brand.blue("\u2502")}${" ".repeat(innerWidth)}${brand.blue("\u2502")}`);
683
+ for (const item of content) {
684
+ if (!item) {
685
+ console.log(` ${brand.blue("\u2502")}${" ".repeat(innerWidth)}${brand.blue("\u2502")}`);
686
+ } else {
687
+ const [plain, colored] = item;
688
+ const rightPad = innerWidth - 3 - visibleLen(plain);
689
+ console.log(
690
+ ` ${brand.blue("\u2502")} ${colored}${" ".repeat(Math.max(0, rightPad))}${brand.blue("\u2502")}`
691
+ );
692
+ }
693
+ }
694
+ console.log(` ${brand.blue("\u2502")}${" ".repeat(innerWidth)}${brand.blue("\u2502")}`);
695
+ console.log(` ${brand.blue("\u2570" + "\u2500".repeat(innerWidth) + "\u256F")}`);
696
+ const copied = ui.copyToClipboard(url);
697
+ console.log("");
698
+ console.log(` ${brand.bold("Share it:")}`);
699
+ const tweetText = encodeURIComponent(`I just shipped ${appName} live in ${elapsedSec.toFixed(0)}s with @shipem_dev
700
+
701
+ ${url}`);
702
+ console.log(` ${brand.gray("Twitter")} \u2192 ${brand.brightBlue.underline(`https://twitter.com/intent/tweet?text=${tweetText}`)}`);
703
+ console.log(` ${brand.gray("Copy URL")} \u2192 ${copied ? brand.green("Copied to clipboard!") : brand.dim(url)}`);
704
+ console.log("");
705
+ if (isAnonymous) {
706
+ console.log(` ${brand.bold("Keep it:")} Run ${chalk.cyan("shipem login")} to claim this deploy`);
707
+ console.log(` and get a custom domain.`);
708
+ console.log("");
709
+ }
710
+ console.log(` ${brand.bold("Next steps:")}`);
711
+ console.log(` ${brand.gray("Update")} \u2192 ${chalk.cyan("npx ship-em")}`);
712
+ console.log(` ${brand.gray("Status")} \u2192 ${chalk.cyan("npx ship-em status")}`);
713
+ console.log(` ${brand.gray("Remove")} \u2192 ${chalk.cyan("npx ship-em down")}`);
714
+ console.log("");
715
+ },
716
+ // Format bytes
717
+ formatBytes(bytes) {
718
+ if (bytes < 1024) return `${bytes} B`;
719
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
720
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
721
+ },
722
+ // Progress bar
723
+ progressBar(current, total, width = 30) {
724
+ const ratio = Math.min(current / total, 1);
725
+ const filled = Math.round(width * ratio);
726
+ const empty = width - filled;
727
+ const bar = brand.blue("\u2588".repeat(filled)) + brand.gray("\u2591".repeat(empty));
728
+ const pct = Math.round(ratio * 100);
729
+ return `${bar} ${pct}%`;
730
+ },
731
+ // Phase timing display
732
+ phaseTimeline(phases) {
733
+ const parts = [];
734
+ for (const phase of phases) {
735
+ const sec = (phase.durationMs / 1e3).toFixed(1);
736
+ parts.push(`${phase.name} (${sec}s)`);
737
+ }
738
+ console.log(` ${brand.dim(parts.join(" \u2192 "))}`);
739
+ },
740
+ // Deploy celebration box — enhanced with timing
741
+ deployBoxEnhanced(appName, url, phases, fileCount, totalBytes, totalElapsedSec, isAnonymous = false) {
742
+ const statsLine = `Deployed in ${totalElapsedSec.toFixed(1)}s \xB7 ${fileCount} files \xB7 ${ui.formatBytes(totalBytes)}`;
743
+ const content = [
744
+ ["\u{1F680} Shipped!", brand.green.bold("\u{1F680} Shipped!")],
745
+ null,
746
+ [`${appName} is live`, `${brand.white.bold(appName)} is live`],
747
+ null,
748
+ [`\u2192 ${url}`, `${brand.blue("\u2192")} ${brand.brightBlue.underline(url)}`],
749
+ null,
750
+ [statsLine, brand.dim(statsLine)]
751
+ ];
752
+ const maxPlain = Math.max(
753
+ ...content.filter((item) => item !== null).map(([p]) => visibleLen(p))
754
+ );
755
+ const innerWidth = maxPlain + 6;
756
+ console.log("");
757
+ console.log(` ${brand.blue("\u256D" + "\u2500".repeat(innerWidth) + "\u256E")}`);
758
+ console.log(` ${brand.blue("\u2502")}${" ".repeat(innerWidth)}${brand.blue("\u2502")}`);
759
+ for (const item of content) {
760
+ if (!item) {
761
+ console.log(` ${brand.blue("\u2502")}${" ".repeat(innerWidth)}${brand.blue("\u2502")}`);
762
+ } else {
763
+ const [plain, colored] = item;
764
+ const rightPad = innerWidth - 3 - visibleLen(plain);
765
+ console.log(
766
+ ` ${brand.blue("\u2502")} ${colored}${" ".repeat(Math.max(0, rightPad))}${brand.blue("\u2502")}`
767
+ );
768
+ }
769
+ }
770
+ console.log(` ${brand.blue("\u2502")}${" ".repeat(innerWidth)}${brand.blue("\u2502")}`);
771
+ console.log(` ${brand.blue("\u2570" + "\u2500".repeat(innerWidth) + "\u256F")}`);
772
+ if (phases.length > 0) {
773
+ console.log("");
774
+ ui.phaseTimeline(phases);
775
+ }
776
+ const copied = ui.copyToClipboard(url);
777
+ console.log("");
778
+ console.log(` ${brand.bold("Share it:")}`);
779
+ const tweetText = encodeURIComponent(`I just shipped ${appName} live in ${totalElapsedSec.toFixed(0)}s with @shipem_dev
780
+
781
+ ${url}`);
782
+ console.log(` ${brand.gray("Twitter")} \u2192 ${brand.brightBlue.underline(`https://twitter.com/intent/tweet?text=${tweetText}`)}`);
783
+ console.log(` ${brand.gray("Copy URL")} \u2192 ${copied ? brand.green("Copied to clipboard!") : brand.dim(url)}`);
784
+ console.log("");
785
+ if (isAnonymous) {
786
+ console.log(` ${brand.bold("Keep it:")} Run ${chalk.cyan("shipem login")} to claim this deploy`);
787
+ console.log(` and get a custom domain.`);
788
+ console.log("");
789
+ }
790
+ console.log(` ${brand.bold("Next steps:")}`);
791
+ console.log(` ${brand.gray("Update")} \u2192 ${chalk.cyan("npx ship-em")}`);
792
+ console.log(` ${brand.gray("Status")} \u2192 ${chalk.cyan("npx ship-em status")}`);
793
+ console.log(` ${brand.gray("Remove")} \u2192 ${chalk.cyan("npx ship-em down")}`);
794
+ console.log("");
795
+ },
796
+ // Rich status display for deploy history
797
+ deployHistory(deploys) {
798
+ if (deploys.length === 0) return;
799
+ console.log(` ${brand.bold("Recent deploys:")}`);
800
+ const show = deploys.slice(0, 5);
801
+ for (let i = 0; i < show.length; i++) {
802
+ const d = show[i];
803
+ const date = new Date(d.timestamp);
804
+ const dateStr = date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
805
+ const timeStr = date.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
806
+ const connector = i === show.length - 1 ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
807
+ console.log(
808
+ ` ${brand.gray(connector)} ${dateStr}, ${timeStr} ${brand.green("\u2713")} ${d.duration.toFixed(1)}s ${d.files} files ${d.size}`
809
+ );
810
+ }
811
+ console.log("");
812
+ },
813
+ // Colorize framework name
814
+ framework(name) {
815
+ const colors = {
816
+ nextjs: "#ffffff",
817
+ "vite-react": "#61DAFB",
818
+ "vite-vue": "#42B883",
819
+ "vite-svelte": "#FF3E00",
820
+ astro: "#FF5D01",
821
+ sveltekit: "#FF3E00",
822
+ remix: "#818cf8",
823
+ nuxt: "#00DC82",
824
+ gatsby: "#663399",
825
+ hono: "#E36002"
826
+ };
827
+ const color = colors[name] ?? "#3B82F6";
828
+ return chalk.hex(color).bold(name);
829
+ },
830
+ // Framework icon
831
+ frameworkIcon(name) {
832
+ const icons = {
833
+ nextjs: "\u25B2",
834
+ "vite-react": "\u269B",
835
+ "vite-vue": "\u{1F7E2}",
836
+ "vite-svelte": "\u{1F525}",
837
+ astro: "\u{1F680}",
838
+ sveltekit: "\u{1F525}",
839
+ remix: "\u{1F4BF}",
840
+ nuxt: "\u{1F49A}",
841
+ gatsby: "\u{1F7E3}",
842
+ hono: "\u{1F525}",
843
+ "create-react-app": "\u269B",
844
+ "static-html": "\u{1F4C4}"
845
+ };
846
+ return icons[name] ?? "\u{1F4E6}";
847
+ }
848
+ };
849
+
850
+ // src/build/builder.ts
851
+ async function buildProject(config, cwd = process.cwd()) {
852
+ const start = Date.now();
853
+ if (config.installCommand) {
854
+ const installSpinner = ui.spinner(`Installing dependencies (${config.installCommand})`);
855
+ const outputLines = [];
856
+ let lineCount = 0;
857
+ try {
858
+ const [installBin, ...installArgs] = config.installCommand.split(" ");
859
+ const installProc = execa(installBin, installArgs, {
860
+ cwd,
861
+ env: { ...process.env, CI: "true" },
862
+ timeout: 5 * 60 * 1e3,
863
+ stdout: "pipe",
864
+ stderr: "pipe"
865
+ });
866
+ installProc.stdout?.on("data", (chunk) => {
867
+ for (const line of chunk.toString().split("\n")) {
868
+ if (line.trim()) {
869
+ outputLines.push(line);
870
+ lineCount++;
871
+ installSpinner.text = `Installing dependencies... (${lineCount} lines)`;
872
+ }
873
+ }
874
+ });
875
+ installProc.stderr?.on("data", (chunk) => {
876
+ for (const line of chunk.toString().split("\n")) {
877
+ if (line.trim()) {
878
+ outputLines.push(line);
879
+ lineCount++;
880
+ installSpinner.text = `Installing dependencies... (${lineCount} lines)`;
881
+ }
882
+ }
883
+ });
884
+ await installProc;
885
+ installSpinner.succeed(`Dependencies installed (${lineCount} lines)`);
886
+ } catch (err) {
887
+ installSpinner.fail("Dependency installation failed");
888
+ const tail = outputLines.slice(-20);
889
+ if (tail.length > 0) {
890
+ console.log("");
891
+ for (const line of tail) {
892
+ process.stdout.write(chalk2.dim(" " + line) + "\n");
893
+ }
894
+ }
895
+ return {
896
+ success: false,
897
+ outputDirectory: config.outputDirectory,
898
+ durationMs: Date.now() - start,
899
+ error: err instanceof Error ? err.message : String(err)
900
+ };
901
+ }
902
+ console.log("");
903
+ }
904
+ if (config.buildCommand) {
905
+ const buildSpinner = ui.spinner("Building...");
906
+ const outputLines = [];
907
+ let lineCount = 0;
908
+ const buildEnv = {
909
+ ...process.env,
910
+ NODE_ENV: "production",
911
+ CI: "true"
912
+ };
913
+ for (const envVar of config.envVars) {
914
+ if (envVar.value) {
915
+ buildEnv[envVar.name] = envVar.value;
916
+ }
917
+ }
918
+ try {
919
+ const [finalBin, ...finalArgs] = config.buildCommand.split(" ");
920
+ const buildProc = execa(finalBin, finalArgs, {
921
+ cwd,
922
+ env: buildEnv,
923
+ timeout: 10 * 60 * 1e3,
924
+ stdout: "pipe",
925
+ stderr: "pipe"
926
+ });
927
+ buildProc.stdout?.on("data", (chunk) => {
928
+ for (const line of chunk.toString().split("\n")) {
929
+ if (line.trim()) {
930
+ outputLines.push(line);
931
+ lineCount++;
932
+ buildSpinner.text = `Building... (${lineCount} lines)`;
933
+ }
934
+ }
935
+ });
936
+ buildProc.stderr?.on("data", (chunk) => {
937
+ for (const line of chunk.toString().split("\n")) {
938
+ if (line.trim()) {
939
+ outputLines.push(line);
940
+ lineCount++;
941
+ buildSpinner.text = `Building... (${lineCount} lines)`;
942
+ }
943
+ }
944
+ });
945
+ await buildProc;
946
+ const durationSec = ((Date.now() - start) / 1e3).toFixed(1);
947
+ buildSpinner.succeed(`Build successful (${lineCount} lines, ${durationSec}s)`);
948
+ } catch (err) {
949
+ buildSpinner.fail("Build failed");
950
+ const tail = outputLines.slice(-30);
951
+ if (tail.length > 0) {
952
+ console.log("");
953
+ for (const line of tail) {
954
+ process.stdout.write(chalk2.dim(" " + line) + "\n");
955
+ }
956
+ }
957
+ const errMsg = err instanceof Error ? err.message : String(err);
958
+ return {
959
+ success: false,
960
+ outputDirectory: config.outputDirectory,
961
+ durationMs: Date.now() - start,
962
+ error: parseErrorMessage(errMsg, outputLines)
963
+ };
964
+ }
965
+ console.log("");
966
+ }
967
+ const outputPath = join3(cwd, config.outputDirectory);
968
+ if (config.buildCommand && !existsSync3(outputPath)) {
969
+ return {
970
+ success: false,
971
+ outputDirectory: config.outputDirectory,
972
+ durationMs: Date.now() - start,
973
+ error: `Build completed but output directory '${config.outputDirectory}' not found. Check your build configuration.`
974
+ };
975
+ }
976
+ return {
977
+ success: true,
978
+ outputDirectory: config.outputDirectory,
979
+ durationMs: Date.now() - start
980
+ };
981
+ }
982
+ function parseErrorMessage(raw, outputLines) {
983
+ const errorLines = outputLines.filter(
984
+ (l) => l.includes("Error:") || l.includes("error:") || l.includes("ERROR") || l.includes("Failed") || l.includes("Cannot find") || l.includes("Module not found")
985
+ );
986
+ if (errorLines.length > 0) {
987
+ return errorLines.slice(0, 3).join("\n");
988
+ }
989
+ if (outputLines.length > 0) {
990
+ return outputLines.slice(-5).join("\n");
991
+ }
992
+ const lines = raw.split("\n").filter((l) => l.trim());
993
+ return lines.slice(-5).join("\n");
994
+ }
995
+
996
+ // src/deploy/cloudflare.ts
997
+ import axios from "axios";
998
+ import { createHash } from "crypto";
999
+ import { readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync4 } from "fs";
1000
+ import { join as join5, relative } from "path";
1001
+
1002
+ // src/deploy/exclude.ts
1003
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1004
+ import { join as join4 } from "path";
1005
+ import chalk3 from "chalk";
1006
+ var DEFAULT_PATTERNS = [
1007
+ ".git",
1008
+ "node_modules",
1009
+ ".env",
1010
+ ".env.local",
1011
+ ".env.production",
1012
+ ".DS_Store",
1013
+ "shipem.json",
1014
+ "__pycache__",
1015
+ ".pytest_cache",
1016
+ ".venv",
1017
+ "venv",
1018
+ ".idea",
1019
+ ".vscode"
1020
+ ];
1021
+ var DEFAULT_GLOB_PATTERNS = [
1022
+ ".env*.local",
1023
+ ".shipem*",
1024
+ "*.pem",
1025
+ "*.key",
1026
+ "*.p12"
1027
+ ];
1028
+ function matchesGlob(name, pattern) {
1029
+ const starIdx = pattern.indexOf("*");
1030
+ if (starIdx === -1) return name === pattern;
1031
+ const prefix = pattern.slice(0, starIdx);
1032
+ const suffix = pattern.slice(starIdx + 1);
1033
+ return name.startsWith(prefix) && name.endsWith(suffix) && name.length >= prefix.length + suffix.length;
1034
+ }
1035
+ function matchesAnyDefaultPattern(segment) {
1036
+ if (DEFAULT_PATTERNS.includes(segment)) return true;
1037
+ for (const pat of DEFAULT_GLOB_PATTERNS) {
1038
+ if (matchesGlob(segment, pat)) return true;
1039
+ }
1040
+ return false;
1041
+ }
1042
+ function matchesIgnoreLine(relPath, line) {
1043
+ const trimmed = line.trim();
1044
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("!")) return false;
1045
+ const pattern = trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
1046
+ const segments = relPath.replace(/\\/g, "/").split("/").filter(Boolean);
1047
+ const last = segments[segments.length - 1];
1048
+ if (last && matchesGlob(last, pattern)) return true;
1049
+ if (matchesGlob(relPath.replace(/\\/g, "/"), pattern)) return true;
1050
+ for (const seg of segments) {
1051
+ if (matchesGlob(seg, pattern)) return true;
1052
+ }
1053
+ return false;
1054
+ }
1055
+ function loadIgnoreLines(filePath) {
1056
+ if (!existsSync4(filePath)) return [];
1057
+ try {
1058
+ return readFileSync3(filePath, "utf-8").split("\n");
1059
+ } catch {
1060
+ return [];
1061
+ }
1062
+ }
1063
+ function shouldExclude(filePath, projectRoot) {
1064
+ const normalized = filePath.replace(/\\/g, "/");
1065
+ const segments = normalized.split("/").filter((s) => s && s !== ".");
1066
+ for (const seg of segments) {
1067
+ if (matchesAnyDefaultPattern(seg)) return true;
1068
+ }
1069
+ if (projectRoot) {
1070
+ const relPath = normalized;
1071
+ const shipemIgnoreLines = loadIgnoreLines(join4(projectRoot, ".shipemignore"));
1072
+ for (const line of shipemIgnoreLines) {
1073
+ if (matchesIgnoreLine(relPath, line)) return true;
1074
+ }
1075
+ const gitIgnoreLines = loadIgnoreLines(join4(projectRoot, ".gitignore"));
1076
+ for (const line of gitIgnoreLines) {
1077
+ if (matchesIgnoreLine(relPath, line)) return true;
1078
+ }
1079
+ }
1080
+ return false;
1081
+ }
1082
+
1083
+ // src/errors.ts
1084
+ var ShipemError = class extends Error {
1085
+ /** Machine-readable error code for programmatic consumers (--json mode). */
1086
+ code;
1087
+ /** Suggested exit code: 1 = user error, 2 = system/infra error. */
1088
+ exitCode;
1089
+ constructor(message, opts = { code: "ERR_UNKNOWN" }) {
1090
+ super(message, { cause: opts.cause });
1091
+ this.name = "ShipemError";
1092
+ this.code = opts.code;
1093
+ this.exitCode = opts.exitCode ?? 1;
1094
+ }
1095
+ };
1096
+ var NetworkError = class extends ShipemError {
1097
+ constructor(message, opts) {
1098
+ super(message, { code: "ERR_NETWORK", exitCode: 2, cause: opts?.cause });
1099
+ this.name = "NetworkError";
1100
+ }
1101
+ };
1102
+ var AuthError = class extends ShipemError {
1103
+ constructor(message, opts) {
1104
+ super(message, { code: "ERR_AUTH", exitCode: 1, cause: opts?.cause });
1105
+ this.name = "AuthError";
1106
+ }
1107
+ };
1108
+ var DeployError = class extends ShipemError {
1109
+ constructor(message, opts) {
1110
+ super(message, { code: "ERR_DEPLOY", exitCode: 2, cause: opts?.cause });
1111
+ this.name = "DeployError";
1112
+ }
1113
+ };
1114
+ var BuildError = class extends ShipemError {
1115
+ constructor(message, opts) {
1116
+ super(message, { code: "ERR_BUILD", exitCode: 1, cause: opts?.cause });
1117
+ this.name = "BuildError";
1118
+ }
1119
+ };
1120
+ var ConfigError = class extends ShipemError {
1121
+ constructor(message, opts) {
1122
+ super(message, { code: "ERR_CONFIG", exitCode: 1, cause: opts?.cause });
1123
+ this.name = "ConfigError";
1124
+ }
1125
+ };
1126
+
1127
+ // src/deploy/cloudflare.ts
1128
+ var CF_API_BASE = "https://api.cloudflare.com/client/v4";
1129
+ var CF_UPLOAD_ENDPOINT = "https://upload.storageapi.cloudflare.com/pages-uploads";
1130
+ var CF_PROJECT_NAME_MAX_LENGTH = 28;
1131
+ var CF_REQUEST_TIMEOUT_MS = 6e4;
1132
+ var DEPLOY_POLL_INTERVAL_MS = 3e3;
1133
+ var DEPLOY_TIMEOUT_MS = 3 * 60 * 1e3;
1134
+ var CloudflarePages = class {
1135
+ client;
1136
+ accountId;
1137
+ constructor(apiToken, accountId) {
1138
+ this.accountId = accountId;
1139
+ this.client = axios.create({
1140
+ baseURL: CF_API_BASE,
1141
+ headers: {
1142
+ Authorization: `Bearer ${apiToken}`,
1143
+ "Content-Type": "application/json"
1144
+ },
1145
+ timeout: CF_REQUEST_TIMEOUT_MS
1146
+ });
1147
+ }
1148
+ async getOrCreateProject(projectName, config) {
1149
+ try {
1150
+ const res = await this.client.get(
1151
+ `/accounts/${this.accountId}/pages/projects/${projectName}`
1152
+ );
1153
+ if (res.data.success) {
1154
+ return res.data.result;
1155
+ }
1156
+ } catch (err) {
1157
+ const status = err.response?.status;
1158
+ if (status !== 404) {
1159
+ throw new DeployError(`Failed to check project: ${extractCFError(err)}`);
1160
+ }
1161
+ }
1162
+ try {
1163
+ const res = await this.client.post(
1164
+ `/accounts/${this.accountId}/pages/projects`,
1165
+ {
1166
+ name: projectName,
1167
+ production_branch: "main",
1168
+ build_config: {
1169
+ build_command: config.buildCommand || null,
1170
+ destination_dir: config.outputDirectory,
1171
+ root_dir: ""
1172
+ },
1173
+ deployment_configs: {
1174
+ production: {
1175
+ compatibility_date: "2024-01-01",
1176
+ environment_variables: {}
1177
+ },
1178
+ preview: {
1179
+ compatibility_date: "2024-01-01",
1180
+ environment_variables: {}
1181
+ }
1182
+ }
1183
+ }
1184
+ );
1185
+ if (!res.data.success) {
1186
+ throw new DeployError(res.data.errors[0]?.message ?? "Unknown error creating project");
1187
+ }
1188
+ return res.data.result;
1189
+ } catch (err) {
1190
+ throw new DeployError(`Failed to create Pages project: ${extractCFError(err)}`, { cause: err });
1191
+ }
1192
+ }
1193
+ async setEnvironmentVariables(projectName, envVars) {
1194
+ if (Object.keys(envVars).length === 0) return;
1195
+ const formattedVars = {};
1196
+ for (const [key, value] of Object.entries(envVars)) {
1197
+ const isSecret = key.toLowerCase().includes("secret") || key.toLowerCase().includes("key") || key.toLowerCase().includes("token") || key.toLowerCase().includes("password");
1198
+ formattedVars[key] = {
1199
+ value,
1200
+ type: isSecret ? "secret_text" : "plain_text"
1201
+ };
1202
+ }
1203
+ try {
1204
+ await this.client.patch(
1205
+ `/accounts/${this.accountId}/pages/projects/${projectName}`,
1206
+ {
1207
+ deployment_configs: {
1208
+ production: {
1209
+ environment_variables: formattedVars
1210
+ }
1211
+ }
1212
+ }
1213
+ );
1214
+ } catch (err) {
1215
+ throw new DeployError(`Failed to set environment variables: ${extractCFError(err)}`, { cause: err });
1216
+ }
1217
+ }
1218
+ async deployDirectory(projectName, outputDir, cwd = process.cwd()) {
1219
+ const fullOutputPath = join5(cwd, outputDir);
1220
+ const filePaths = collectFiles(fullOutputPath);
1221
+ let totalBytes = 0;
1222
+ const fileMap = /* @__PURE__ */ new Map();
1223
+ for (const filePath of filePaths) {
1224
+ const content = readFileSync4(filePath);
1225
+ const hash = createHash("sha256").update(content).digest("hex");
1226
+ const urlPath = "/" + relative(fullOutputPath, filePath).replace(/\\/g, "/");
1227
+ fileMap.set(urlPath, { hash, content });
1228
+ totalBytes += content.length;
1229
+ }
1230
+ const hashToContent = /* @__PURE__ */ new Map();
1231
+ for (const { hash, content } of fileMap.values()) {
1232
+ if (!hashToContent.has(hash)) hashToContent.set(hash, content);
1233
+ }
1234
+ const fileCount = filePaths.length;
1235
+ const manifest = {};
1236
+ for (const [urlPath, { hash }] of fileMap) {
1237
+ manifest[urlPath] = hash;
1238
+ }
1239
+ const deploySpinner = ui.spinner("Creating deployment...");
1240
+ let jwt;
1241
+ let requiredFiles;
1242
+ let deployment;
1243
+ try {
1244
+ const res = await this.client.post(
1245
+ `/accounts/${this.accountId}/pages/projects/${projectName}/deployments`,
1246
+ { files: manifest, branch: "main" },
1247
+ { timeout: CF_REQUEST_TIMEOUT_MS }
1248
+ );
1249
+ if (!res.data.success) {
1250
+ deploySpinner.fail("Deployment creation failed");
1251
+ throw new DeployError(res.data.errors[0]?.message ?? "Deployment failed");
1252
+ }
1253
+ jwt = res.data.result.jwt;
1254
+ requiredFiles = res.data.result.required_files ?? [];
1255
+ deployment = res.data.result;
1256
+ deploySpinner.succeed("Deployment created");
1257
+ } catch (err) {
1258
+ deploySpinner.fail("Deployment creation failed");
1259
+ throw new DeployError(`Deployment failed: ${extractCFError(err)}`, { cause: err });
1260
+ }
1261
+ if (requiredFiles.length > 0) {
1262
+ const uploadSpinner = ui.spinner(
1263
+ `Uploading ${requiredFiles.length} files (${ui.formatBytes(totalBytes)})`
1264
+ );
1265
+ let uploaded = 0;
1266
+ for (const hash of requiredFiles) {
1267
+ const content = hashToContent.get(hash);
1268
+ if (!content) continue;
1269
+ try {
1270
+ const formData = new FormData();
1271
+ formData.append(hash, new Blob([content], { type: "application/octet-stream" }), hash);
1272
+ await fetch(CF_UPLOAD_ENDPOINT, {
1273
+ method: "POST",
1274
+ headers: { Authorization: `Bearer ${jwt}` },
1275
+ body: formData
1276
+ }).then(async (res) => {
1277
+ if (!res.ok) {
1278
+ const text = await res.text().catch(() => res.statusText);
1279
+ throw new DeployError(`Upload failed (${res.status}): ${text}`);
1280
+ }
1281
+ });
1282
+ } catch (err) {
1283
+ uploadSpinner.fail("Upload failed");
1284
+ throw new DeployError(`Failed to upload file (hash ${hash}): ${err instanceof Error ? err.message : String(err)}`, { cause: err });
1285
+ }
1286
+ uploaded++;
1287
+ uploadSpinner.text = `Uploading files... ${uploaded}/${requiredFiles.length}`;
1288
+ }
1289
+ uploadSpinner.succeed(`Upload complete \u2014 ${fileCount} files (${ui.formatBytes(totalBytes)})`);
1290
+ }
1291
+ ui.success("Live!");
1292
+ return { deployment, fileCount, totalBytes };
1293
+ }
1294
+ async deployBranch(projectName, outputDir, branch, cwd = process.cwd()) {
1295
+ const fullOutputPath = join5(cwd, outputDir);
1296
+ const filePaths = collectFiles(fullOutputPath);
1297
+ let totalBytes = 0;
1298
+ const fileMap = /* @__PURE__ */ new Map();
1299
+ for (const filePath of filePaths) {
1300
+ const content = readFileSync4(filePath);
1301
+ const hash = createHash("sha256").update(content).digest("hex");
1302
+ const urlPath = "/" + relative(fullOutputPath, filePath).replace(/\\/g, "/");
1303
+ fileMap.set(urlPath, { hash, content });
1304
+ totalBytes += content.length;
1305
+ }
1306
+ const hashToContent = /* @__PURE__ */ new Map();
1307
+ for (const { hash, content } of fileMap.values()) {
1308
+ if (!hashToContent.has(hash)) hashToContent.set(hash, content);
1309
+ }
1310
+ const fileCount = filePaths.length;
1311
+ const manifest = {};
1312
+ for (const [urlPath, { hash }] of fileMap) {
1313
+ manifest[urlPath] = hash;
1314
+ }
1315
+ const deploySpinner = ui.spinner(`Creating preview deployment (branch: ${branch})...`);
1316
+ let jwt;
1317
+ let requiredFiles;
1318
+ let deployment;
1319
+ try {
1320
+ const res = await this.client.post(
1321
+ `/accounts/${this.accountId}/pages/projects/${projectName}/deployments`,
1322
+ { files: manifest, branch },
1323
+ { timeout: CF_REQUEST_TIMEOUT_MS }
1324
+ );
1325
+ if (!res.data.success) {
1326
+ deploySpinner.fail("Preview deployment creation failed");
1327
+ throw new DeployError(res.data.errors[0]?.message ?? "Preview deployment failed");
1328
+ }
1329
+ jwt = res.data.result.jwt;
1330
+ requiredFiles = res.data.result.required_files ?? [];
1331
+ deployment = res.data.result;
1332
+ deploySpinner.succeed("Preview deployment created");
1333
+ } catch (err) {
1334
+ deploySpinner.fail("Preview deployment creation failed");
1335
+ throw new DeployError(`Preview deployment failed: ${extractCFError(err)}`, { cause: err });
1336
+ }
1337
+ if (requiredFiles.length > 0) {
1338
+ const uploadSpinner = ui.spinner(
1339
+ `Uploading ${requiredFiles.length} files (${ui.formatBytes(totalBytes)})`
1340
+ );
1341
+ let uploaded = 0;
1342
+ for (const hash of requiredFiles) {
1343
+ const content = hashToContent.get(hash);
1344
+ if (!content) continue;
1345
+ try {
1346
+ const formData = new FormData();
1347
+ formData.append(hash, new Blob([content], { type: "application/octet-stream" }), hash);
1348
+ await fetch(CF_UPLOAD_ENDPOINT, {
1349
+ method: "POST",
1350
+ headers: { Authorization: `Bearer ${jwt}` },
1351
+ body: formData
1352
+ }).then(async (res) => {
1353
+ if (!res.ok) {
1354
+ const text = await res.text().catch(() => res.statusText);
1355
+ throw new DeployError(`Upload failed (${res.status}): ${text}`);
1356
+ }
1357
+ });
1358
+ } catch (err) {
1359
+ uploadSpinner.fail("Upload failed");
1360
+ throw new DeployError(`Failed to upload file (hash ${hash}): ${err instanceof Error ? err.message : String(err)}`, { cause: err });
1361
+ }
1362
+ uploaded++;
1363
+ uploadSpinner.text = `Uploading files... ${uploaded}/${requiredFiles.length}`;
1364
+ }
1365
+ uploadSpinner.succeed(`Upload complete \u2014 ${fileCount} files (${ui.formatBytes(totalBytes)})`);
1366
+ }
1367
+ ui.success("Preview deployed!");
1368
+ return { deployment, fileCount, totalBytes };
1369
+ }
1370
+ async waitForDeployment(projectName, deploymentId, timeoutMs = DEPLOY_TIMEOUT_MS) {
1371
+ const deploySpinner = ui.spinner("Deploying to Cloudflare Pages");
1372
+ const start = Date.now();
1373
+ while (Date.now() - start < timeoutMs) {
1374
+ await sleep(DEPLOY_POLL_INTERVAL_MS);
1375
+ try {
1376
+ const res = await this.client.get(
1377
+ `/accounts/${this.accountId}/pages/projects/${projectName}/deployments/${deploymentId}`
1378
+ );
1379
+ if (!res.data.success) continue;
1380
+ const deployment = res.data.result;
1381
+ const stage = deployment.latest_stage;
1382
+ deploySpinner.text = `Deploying... (${stage?.name ?? "initializing"})`;
1383
+ if (stage?.status === "success") {
1384
+ deploySpinner.succeed("Deployed successfully");
1385
+ return deployment;
1386
+ }
1387
+ if (stage?.status === "failure") {
1388
+ deploySpinner.fail("Deployment failed");
1389
+ throw new DeployError(
1390
+ `Deployment failed at stage '${stage.name}'. Check the Cloudflare Pages dashboard for details.`
1391
+ );
1392
+ }
1393
+ } catch (err) {
1394
+ if (err instanceof DeployError) {
1395
+ throw err;
1396
+ }
1397
+ }
1398
+ }
1399
+ deploySpinner.fail("Deployment timed out");
1400
+ throw new DeployError("Deployment timed out. Check the Cloudflare Pages dashboard for status.");
1401
+ }
1402
+ async getDeploymentStatus(projectName, deploymentId) {
1403
+ try {
1404
+ const res = await this.client.get(
1405
+ `/accounts/${this.accountId}/pages/projects/${projectName}/deployments/${deploymentId}`
1406
+ );
1407
+ return res.data.success ? res.data.result : null;
1408
+ } catch {
1409
+ return null;
1410
+ }
1411
+ }
1412
+ async getDeploymentLogs(projectName, deploymentId) {
1413
+ try {
1414
+ const res = await this.client.get(
1415
+ `/accounts/${this.accountId}/pages/projects/${projectName}/deployments/${deploymentId}/history/logs`
1416
+ );
1417
+ if (!res.data.success) return [];
1418
+ return res.data.result.data.map((entry) => `[${entry.ts}] ${entry.message}`);
1419
+ } catch {
1420
+ return [];
1421
+ }
1422
+ }
1423
+ async deleteProject(projectName) {
1424
+ try {
1425
+ await this.client.delete(
1426
+ `/accounts/${this.accountId}/pages/projects/${projectName}`
1427
+ );
1428
+ } catch (err) {
1429
+ throw new DeployError(`Failed to delete project: ${extractCFError(err)}`, { cause: err });
1430
+ }
1431
+ }
1432
+ getProjectUrl(projectName) {
1433
+ return `https://${projectName}.pages.dev`;
1434
+ }
1435
+ getDashboardUrl(projectName) {
1436
+ return `https://dash.cloudflare.com/${this.accountId}/pages/view/${projectName}`;
1437
+ }
1438
+ // Validate credentials and get account info
1439
+ async validateCredentials() {
1440
+ try {
1441
+ const res = await this.client.get(
1442
+ `/accounts/${this.accountId}`
1443
+ );
1444
+ return { valid: res.data.success, accountName: res.data.result?.name };
1445
+ } catch {
1446
+ return { valid: false };
1447
+ }
1448
+ }
1449
+ };
1450
+ function collectFiles(dir) {
1451
+ const results = [];
1452
+ function walk(currentDir) {
1453
+ const entries = readdirSync2(currentDir);
1454
+ for (const entry of entries) {
1455
+ const fullPath = join5(currentDir, entry);
1456
+ if (shouldExclude(fullPath)) continue;
1457
+ if (entry.startsWith(".") && entry !== ".well-known") continue;
1458
+ const stat = statSync2(fullPath);
1459
+ if (stat.isDirectory()) {
1460
+ walk(fullPath);
1461
+ } else {
1462
+ results.push(fullPath);
1463
+ }
1464
+ }
1465
+ }
1466
+ walk(dir);
1467
+ return results;
1468
+ }
1469
+ function extractCFError(err) {
1470
+ if (err instanceof Error) {
1471
+ const cfErr = err;
1472
+ const msg = cfErr.response?.data?.errors?.[0]?.message;
1473
+ if (msg) return msg;
1474
+ return err.message;
1475
+ }
1476
+ return String(err);
1477
+ }
1478
+ function sleep(ms) {
1479
+ return new Promise((resolve) => setTimeout(resolve, ms));
1480
+ }
1481
+ function sanitizeProjectName(name) {
1482
+ return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, CF_PROJECT_NAME_MAX_LENGTH);
1483
+ }
1484
+
1485
+ // src/commands/fix.ts
1486
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync2, readdirSync as readdirSync3 } from "fs";
1487
+ import { join as join6 } from "path";
1488
+ import { execa as execa2 } from "execa";
1489
+ function findMissingModules(errorOutput) {
1490
+ const modules = /* @__PURE__ */ new Set();
1491
+ const cannotFind = errorOutput.matchAll(/Cannot find module ['"]([^'"]+)['"]/g);
1492
+ for (const m of cannotFind) {
1493
+ modules.add(extractPkgName(m[1]));
1494
+ }
1495
+ const moduleNotFound = errorOutput.matchAll(/Module not found.*?['"]([^'"]+)['"]/g);
1496
+ for (const m of moduleNotFound) {
1497
+ modules.add(extractPkgName(m[1]));
1498
+ }
1499
+ const cannotFindPkg = errorOutput.matchAll(/Cannot find package ['"]([^'"]+)['"]/g);
1500
+ for (const m of cannotFindPkg) {
1501
+ modules.add(extractPkgName(m[1]));
1502
+ }
1503
+ const couldNotResolve = errorOutput.matchAll(/Could not resolve ["']([^"']+)["']/g);
1504
+ for (const m of couldNotResolve) {
1505
+ if (!m[1].startsWith(".") && !m[1].startsWith("/")) {
1506
+ modules.add(extractPkgName(m[1]));
1507
+ }
1508
+ }
1509
+ return [...modules].filter((m) => m && !m.startsWith(".") && !m.startsWith("/"));
1510
+ }
1511
+ function extractPkgName(importPath) {
1512
+ if (importPath.startsWith("@")) {
1513
+ const parts = importPath.split("/");
1514
+ return parts.slice(0, 2).join("/");
1515
+ }
1516
+ return importPath.split("/")[0];
1517
+ }
1518
+ function findMissingPeerDeps(errorOutput) {
1519
+ const peers = /* @__PURE__ */ new Set();
1520
+ const peerMatches = errorOutput.matchAll(
1521
+ /(?:requires|missing) (?:a )?peer (?:dependency|dep)[^"']*['"]([^'"]+)['"]/gi
1522
+ );
1523
+ for (const m of peerMatches) {
1524
+ peers.add(extractPkgName(m[1]));
1525
+ }
1526
+ const warnMatches = errorOutput.matchAll(
1527
+ /peerDepend[^"']*['"]([^'"]+)['"].*should be/gi
1528
+ );
1529
+ for (const m of warnMatches) {
1530
+ peers.add(extractPkgName(m[1]));
1531
+ }
1532
+ return [...peers];
1533
+ }
1534
+ function detectTsConfigFixes(errorOutput, cwd) {
1535
+ const tsErrors = errorOutput.match(/TS\d+/g) ?? [];
1536
+ if (tsErrors.length === 0) return null;
1537
+ const fixes = {};
1538
+ const descriptions = [];
1539
+ if (tsErrors.includes("TS7006") || tsErrors.includes("TS7005")) {
1540
+ fixes.noImplicitAny = false;
1541
+ descriptions.push("Disable noImplicitAny");
1542
+ }
1543
+ if (tsErrors.includes("TS2307")) {
1544
+ fixes.moduleResolution = "bundler";
1545
+ descriptions.push("Set moduleResolution to bundler");
1546
+ }
1547
+ if (tsErrors.includes("TS2769") || tsErrors.includes("TS2345") || tsErrors.includes("TS2322")) {
1548
+ fixes.strict = false;
1549
+ descriptions.push("Disable strict mode");
1550
+ }
1551
+ if (tsErrors.includes("TS18028") || tsErrors.includes("TS1259") || tsErrors.includes("TS1479")) {
1552
+ fixes.esModuleInterop = true;
1553
+ fixes.allowSyntheticDefaultImports = true;
1554
+ descriptions.push("Enable ESM interop");
1555
+ }
1556
+ if (descriptions.length === 0) return null;
1557
+ return { fixes, description: descriptions.join(", ") };
1558
+ }
1559
+ function applyTsConfigFixes(cwd, fixes) {
1560
+ const tsconfigPath = join6(cwd, "tsconfig.json");
1561
+ if (!existsSync5(tsconfigPath)) return false;
1562
+ try {
1563
+ const content = readFileSync5(tsconfigPath, "utf-8");
1564
+ const stripped = content.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
1565
+ const tsconfig = JSON.parse(stripped);
1566
+ if (!tsconfig.compilerOptions) tsconfig.compilerOptions = {};
1567
+ Object.assign(tsconfig.compilerOptions, fixes);
1568
+ writeFileSync2(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n", "utf-8");
1569
+ return true;
1570
+ } catch {
1571
+ return false;
1572
+ }
1573
+ }
1574
+ function findMissingEnvVars(cwd) {
1575
+ const referenced = /* @__PURE__ */ new Set();
1576
+ const patterns = [
1577
+ /process\.env\.([A-Z_][A-Z0-9_]*)/g,
1578
+ /import\.meta\.env\.([A-Z_][A-Z0-9_]*)/g
1579
+ ];
1580
+ function scanDir(dir, depth) {
1581
+ if (depth > 4) return;
1582
+ try {
1583
+ const entries = readdirSync3(dir, { withFileTypes: true });
1584
+ for (const entry of entries) {
1585
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist" || entry.name === ".next") continue;
1586
+ const fullPath = join6(dir, entry.name);
1587
+ if (entry.isDirectory()) {
1588
+ scanDir(fullPath, depth + 1);
1589
+ } else if (/\.(ts|tsx|js|jsx|mjs|mts|vue|svelte|astro)$/.test(entry.name)) {
1590
+ try {
1591
+ const content = readFileSync5(fullPath, "utf-8");
1592
+ for (const pat of patterns) {
1593
+ pat.lastIndex = 0;
1594
+ let match;
1595
+ while ((match = pat.exec(content)) !== null) {
1596
+ referenced.add(match[1]);
1597
+ }
1598
+ }
1599
+ } catch {
1600
+ }
1601
+ }
1602
+ }
1603
+ } catch {
1604
+ }
1605
+ }
1606
+ scanDir(cwd, 0);
1607
+ const ignoredVars = /* @__PURE__ */ new Set(["NODE_ENV", "PORT", "HOST", "CI", "HOME", "PWD", "PATH", "TERM"]);
1608
+ const missing = [];
1609
+ for (const v of referenced) {
1610
+ if (ignoredVars.has(v)) continue;
1611
+ if (!process.env[v]) missing.push(v);
1612
+ }
1613
+ return missing;
1614
+ }
1615
+ function generateEnvExample(cwd, vars) {
1616
+ if (vars.length === 0) return false;
1617
+ const envExamplePath = join6(cwd, ".env.example");
1618
+ if (existsSync5(envExamplePath)) return false;
1619
+ const content = vars.map((v) => `${v}=`).join("\n") + "\n";
1620
+ writeFileSync2(envExamplePath, content, "utf-8");
1621
+ return true;
1622
+ }
1623
+ function detectFrameworkConfigFixes(errorOutput, cwd, framework) {
1624
+ const fixes = [];
1625
+ if (framework === "nextjs") {
1626
+ const nextConfigPath = existsSync5(join6(cwd, "next.config.mjs")) ? join6(cwd, "next.config.mjs") : existsSync5(join6(cwd, "next.config.js")) ? join6(cwd, "next.config.js") : null;
1627
+ if (nextConfigPath && errorOutput.includes("output")) {
1628
+ try {
1629
+ const content = readFileSync5(nextConfigPath, "utf-8");
1630
+ if (!content.includes("output")) {
1631
+ fixes.push(`Add output: 'export' to ${nextConfigPath.split("/").pop()}`);
1632
+ }
1633
+ } catch {
1634
+ }
1635
+ }
1636
+ }
1637
+ if ((framework === "vite-react" || framework === "vite-vue" || framework === "vite-svelte") && errorOutput.includes("plugin")) {
1638
+ const pkgPath = join6(cwd, "package.json");
1639
+ try {
1640
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
1641
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1642
+ if (framework === "vite-react" && !deps["@vitejs/plugin-react"]) {
1643
+ fixes.push("Install @vitejs/plugin-react");
1644
+ }
1645
+ if (framework === "vite-vue" && !deps["@vitejs/plugin-vue"]) {
1646
+ fixes.push("Install @vitejs/plugin-vue");
1647
+ }
1648
+ } catch {
1649
+ }
1650
+ }
1651
+ return fixes;
1652
+ }
1653
+ async function runFixHeuristics(errorOutput, cwd, config) {
1654
+ const actions = [];
1655
+ let anyFixApplied = false;
1656
+ const missingModules = findMissingModules(errorOutput);
1657
+ if (missingModules.length > 0) {
1658
+ const installCmd = config.installCommand.split(" ")[0] ?? "npm";
1659
+ for (const mod of missingModules) {
1660
+ const spinner = ui.spinner(`Installing missing dependency: ${mod}`);
1661
+ try {
1662
+ await execa2(installCmd, ["add", mod], { cwd, timeout: 6e4 });
1663
+ spinner.succeed(`Installed ${mod}`);
1664
+ actions.push(`Installed missing dependency: ${mod}`);
1665
+ anyFixApplied = true;
1666
+ } catch {
1667
+ spinner.fail(`Failed to install ${mod}`);
1668
+ actions.push(`Failed to install ${mod}`);
1669
+ }
1670
+ }
1671
+ }
1672
+ const missingPeers = findMissingPeerDeps(errorOutput);
1673
+ if (missingPeers.length > 0) {
1674
+ const installCmd = config.installCommand.split(" ")[0] ?? "npm";
1675
+ for (const peer of missingPeers) {
1676
+ if (missingModules.includes(peer)) continue;
1677
+ const spinner = ui.spinner(`Installing peer dependency: ${peer}`);
1678
+ try {
1679
+ await execa2(installCmd, ["add", peer], { cwd, timeout: 6e4 });
1680
+ spinner.succeed(`Installed peer dep: ${peer}`);
1681
+ actions.push(`Installed peer dependency: ${peer}`);
1682
+ anyFixApplied = true;
1683
+ } catch {
1684
+ spinner.fail(`Failed to install peer dep: ${peer}`);
1685
+ actions.push(`Failed to install ${peer}`);
1686
+ }
1687
+ }
1688
+ }
1689
+ const tsConfigFixes = detectTsConfigFixes(errorOutput, cwd);
1690
+ if (tsConfigFixes) {
1691
+ const applied = applyTsConfigFixes(cwd, tsConfigFixes.fixes);
1692
+ if (applied) {
1693
+ actions.push(`Updated tsconfig.json: ${tsConfigFixes.description}`);
1694
+ anyFixApplied = true;
1695
+ }
1696
+ }
1697
+ const missingEnvVars = findMissingEnvVars(cwd);
1698
+ if (missingEnvVars.length > 0) {
1699
+ const generated = generateEnvExample(cwd, missingEnvVars);
1700
+ if (generated) {
1701
+ actions.push(`Generated .env.example with ${missingEnvVars.length} vars: ${missingEnvVars.join(", ")}`);
1702
+ anyFixApplied = true;
1703
+ }
1704
+ }
1705
+ const frameworkFixes = detectFrameworkConfigFixes(errorOutput, cwd, config.framework);
1706
+ for (const fix of frameworkFixes) {
1707
+ actions.push(`Suggestion: ${fix}`);
1708
+ }
1709
+ return { actions, fixed: anyFixApplied };
1710
+ }
1711
+
1712
+ // src/commands/env.ts
1713
+ import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync4, writeFileSync as writeFileSync3 } from "fs";
1714
+ import { join as join7 } from "path";
1715
+ function scanSourceForEnvVars(cwd) {
1716
+ const found = /* @__PURE__ */ new Set();
1717
+ const patterns = [
1718
+ /process\.env\.([A-Z_][A-Z0-9_]*)/g,
1719
+ /import\.meta\.env\.([A-Z_][A-Z0-9_]*)/g,
1720
+ /(?:NEXT_PUBLIC_|VITE_|NUXT_|GATSBY_)([A-Z0-9_]*)/g
1721
+ ];
1722
+ function scanDir(dir, depth) {
1723
+ if (depth > 5) return;
1724
+ try {
1725
+ const entries = readdirSync4(dir, { withFileTypes: true });
1726
+ for (const entry of entries) {
1727
+ if (["node_modules", ".git", "dist", ".next", "build", ".svelte-kit", ".output", ".turbo", "coverage"].includes(entry.name)) continue;
1728
+ const fullPath = join7(dir, entry.name);
1729
+ if (entry.isDirectory()) {
1730
+ scanDir(fullPath, depth + 1);
1731
+ } else if (/\.(ts|tsx|js|jsx|mjs|mts|vue|svelte|astro)$/.test(entry.name)) {
1732
+ try {
1733
+ const content = readFileSync6(fullPath, "utf-8");
1734
+ for (const pat of patterns) {
1735
+ pat.lastIndex = 0;
1736
+ let match;
1737
+ while ((match = pat.exec(content)) !== null) {
1738
+ if (pat.source.includes("NEXT_PUBLIC_") && !match[0].includes("process.env") && !match[0].includes("import.meta.env")) {
1739
+ found.add(match[0]);
1740
+ } else {
1741
+ found.add(match[1]);
1742
+ }
1743
+ }
1744
+ }
1745
+ } catch {
1746
+ }
1747
+ }
1748
+ }
1749
+ } catch {
1750
+ }
1751
+ }
1752
+ scanDir(cwd, 0);
1753
+ const ignore = /* @__PURE__ */ new Set(["NODE_ENV", "PORT", "HOST", "CI", "HOME", "PWD", "PATH", "TERM", "USER", "SHELL", "LANG", "TZ"]);
1754
+ return [...found].filter((v) => !ignore.has(v)).sort();
1755
+ }
1756
+ function readEnvFile(cwd) {
1757
+ const vars = /* @__PURE__ */ new Set();
1758
+ const envFiles = [".env", ".env.local", ".env.development", ".env.production"];
1759
+ for (const file of envFiles) {
1760
+ const content = readFile2(join7(cwd, file));
1761
+ if (content) {
1762
+ for (const line of content.split("\n")) {
1763
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
1764
+ if (match) vars.add(match[1]);
1765
+ }
1766
+ }
1767
+ }
1768
+ return vars;
1769
+ }
1770
+ function readFile2(path) {
1771
+ try {
1772
+ return readFileSync6(path, "utf-8");
1773
+ } catch {
1774
+ return null;
1775
+ }
1776
+ }
1777
+
1778
+ // src/commands/init.ts
1779
+ import inquirer from "inquirer";
1780
+ import { execa as execa3 } from "execa";
1781
+ import chalk5 from "chalk";
1782
+
1783
+ // src/commands/templates.ts
1784
+ import chalk4 from "chalk";
1785
+ var TEMPLATES = [
1786
+ {
1787
+ name: "astro-blog",
1788
+ description: "Astro blog with Markdown/MDX support and RSS feed",
1789
+ framework: "astro",
1790
+ createCmd: ["npm", "create", "astro@latest", "--", "--template", "blog"],
1791
+ postScaffold: ["npx", "astro", "add", "mdx", "--yes"]
1792
+ },
1793
+ {
1794
+ name: "nextjs-saas",
1795
+ description: "Next.js SaaS starter with App Router, Tailwind CSS, and TypeScript",
1796
+ framework: "nextjs",
1797
+ createCmd: ["npx", "create-next-app@latest", "--", "--ts", "--app", "--tailwind", "--eslint", "--src-dir", "--no-import-alias"]
1798
+ },
1799
+ {
1800
+ name: "vite-react",
1801
+ description: "Vite + React + TypeScript \u2014 fast SPA starter",
1802
+ framework: "vite-react",
1803
+ createCmd: ["npm", "create", "vite@latest", "--", "--template", "react-ts"]
1804
+ },
1805
+ {
1806
+ name: "sveltekit-app",
1807
+ description: "SvelteKit full-stack app with TypeScript",
1808
+ framework: "sveltekit",
1809
+ createCmd: ["npm", "create", "svelte@latest"]
1810
+ },
1811
+ {
1812
+ name: "static-portfolio",
1813
+ description: "Minimal static HTML/CSS portfolio \u2014 no build step required",
1814
+ framework: "static-html",
1815
+ createCmd: []
1816
+ // No create tool — we scaffold manually
1817
+ }
1818
+ ];
1819
+ function getTemplate(name) {
1820
+ return TEMPLATES.find((t) => t.name.toLowerCase() === name.toLowerCase());
1821
+ }
1822
+ function getTemplateNames() {
1823
+ return TEMPLATES.map((t) => t.name);
1824
+ }
1825
+
1826
+ // src/commands/init.ts
1827
+ var FRAMEWORKS = [
1828
+ {
1829
+ name: "Astro (static sites, blogs, portfolios)",
1830
+ value: "astro",
1831
+ createCmd: ["npm", "create", "astro@latest", "--", "--template", "basics"],
1832
+ keywords: ["portfolio", "blog", "personal", "marketing", "docs", "documentation", "static"]
1833
+ },
1834
+ {
1835
+ name: "Next.js (full-stack React, dashboards, SaaS)",
1836
+ value: "nextjs",
1837
+ createCmd: ["npx", "create-next-app@latest", "--", "--ts", "--app", "--tailwind", "--eslint", "--src-dir", "--no-import-alias"],
1838
+ keywords: ["dashboard", "saas", "app", "application", "admin", "full-stack", "fullstack", "e-commerce", "ecommerce"]
1839
+ },
1840
+ {
1841
+ name: "Vite + React (SPAs, landing pages)",
1842
+ value: "vite-react",
1843
+ createCmd: ["npm", "create", "vite@latest", "--", "--template", "react-ts"],
1844
+ keywords: ["landing", "page", "spa", "single", "react", "frontend", "form", "calculator"]
1845
+ },
1846
+ {
1847
+ name: "Vite + Vue (SPAs with Vue)",
1848
+ value: "vite-vue",
1849
+ createCmd: ["npm", "create", "vite@latest", "--", "--template", "vue-ts"],
1850
+ keywords: ["vue"]
1851
+ },
1852
+ {
1853
+ name: "SvelteKit (Svelte full-stack)",
1854
+ value: "sveltekit",
1855
+ createCmd: ["npm", "create", "svelte@latest"],
1856
+ keywords: ["svelte"]
1857
+ }
1858
+ ];
1859
+ function matchFramework(description) {
1860
+ const lower = description.toLowerCase();
1861
+ let bestScore = 0;
1862
+ let best = FRAMEWORKS[0];
1863
+ for (const fw of FRAMEWORKS) {
1864
+ let score = 0;
1865
+ for (const keyword of fw.keywords) {
1866
+ if (lower.includes(keyword)) score += 1;
1867
+ }
1868
+ if (score > bestScore) {
1869
+ bestScore = score;
1870
+ best = fw;
1871
+ }
1872
+ }
1873
+ return best;
1874
+ }
1875
+ function generateProjectName(description) {
1876
+ return description.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 3).join("-").slice(0, 30) || "my-app";
1877
+ }
1878
+
1879
+ // src/commands/preview.ts
1880
+ import { execa as execa4 } from "execa";
1881
+ import chalk6 from "chalk";
1882
+ async function getCurrentBranch() {
1883
+ try {
1884
+ const { stdout } = await execa4("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
1885
+ return stdout.trim();
1886
+ } catch {
1887
+ throw new ConfigError(
1888
+ "Could not detect git branch. Make sure you are inside a git repository."
1889
+ );
1890
+ }
1891
+ }
1892
+ function sanitizeBranchName(branch) {
1893
+ return branch.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 28);
1894
+ }
1895
+ export {
1896
+ AuthError,
1897
+ BuildError,
1898
+ CloudflarePages,
1899
+ ConfigError,
1900
+ DeployError,
1901
+ FRAMEWORKS,
1902
+ NetworkError,
1903
+ SHIPEM_API_URL,
1904
+ ShipemError,
1905
+ TEMPLATES,
1906
+ buildProject,
1907
+ detectMonorepo,
1908
+ generateProjectName,
1909
+ getCloudflareCredentials,
1910
+ getCurrentBranch,
1911
+ getSessionToken,
1912
+ getTemplate,
1913
+ getTemplateNames,
1914
+ matchFramework,
1915
+ readEnvFile,
1916
+ readProjectConfig,
1917
+ runFixHeuristics,
1918
+ sanitizeBranchName,
1919
+ sanitizeProjectName,
1920
+ scanProject,
1921
+ scanSourceForEnvVars,
1922
+ writeProjectConfig
1923
+ };