@carrierllc/mcp 0.6.0 → 0.7.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.
@@ -4,1795 +4,1795 @@ import {
4
4
  writeFile
5
5
  } from "./chunk-SHKKVIIA.js";
6
6
 
7
- // ../../packages/screens/dist/index.js
8
- function isBlankValue(value) {
9
- const text = String(value).trim();
10
- return text === "" || text === "\u2014" || text === "-" || Number(text) === 0;
7
+ // src/cli/lib/deploy-targets.ts
8
+ import { join } from "path";
9
+ import { tmpdir } from "os";
10
+ import { mkdtemp, rm, writeFile as writeFileMode } from "fs/promises";
11
+
12
+ // src/cli/lib/exec.ts
13
+ import { spawn } from "child_process";
14
+ var SAFE_COMMAND = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
15
+ function isSafeCommand(cmd) {
16
+ return SAFE_COMMAND.test(cmd);
11
17
  }
12
- function isEmptyScreen(screen) {
13
- return screen.sections.every((section) => {
14
- switch (section.kind) {
15
- case "metrics":
16
- return section.items.every((m) => isBlankValue(m.value));
17
- case "bars":
18
- return section.items.every((b) => b.value === 0);
19
- case "keyvalue":
20
- return section.items.every((kv) => isBlankValue(kv.value));
21
- case "table":
22
- return section.rows.length === 0;
23
- case "note":
24
- return false;
18
+ function run(cmd, args, opts = {}) {
19
+ if (!isSafeCommand(cmd)) {
20
+ return Promise.resolve({
21
+ ok: false,
22
+ code: null,
23
+ stdout: "",
24
+ stderr: `refusing to run unsafe command: ${cmd}`
25
+ });
26
+ }
27
+ return new Promise((resolve) => {
28
+ const child = spawn(cmd, args, { cwd: opts.cwd, shell: false });
29
+ if (opts.stdin !== void 0) {
30
+ child.stdin?.on("error", () => {
31
+ });
32
+ child.stdin?.end(opts.stdin);
33
+ }
34
+ let stdout = "";
35
+ let stderr = "";
36
+ let settled = false;
37
+ const finish = (result) => {
38
+ if (settled) return;
39
+ settled = true;
40
+ resolve(result);
41
+ };
42
+ let timer;
43
+ if (opts.timeoutMs && opts.timeoutMs > 0) {
44
+ timer = setTimeout(() => {
45
+ try {
46
+ child.kill("SIGTERM");
47
+ } catch {
48
+ }
49
+ finish({
50
+ ok: false,
51
+ code: null,
52
+ stdout,
53
+ stderr: stderr || `timeout after ${opts.timeoutMs}ms`
54
+ });
55
+ }, opts.timeoutMs);
25
56
  }
57
+ child.stdout?.on("data", (d) => stdout += d.toString());
58
+ child.stderr?.on("data", (d) => stderr += d.toString());
59
+ child.on("error", () => {
60
+ if (timer) clearTimeout(timer);
61
+ finish({ ok: false, code: null, stdout, stderr });
62
+ });
63
+ child.on("close", (code) => {
64
+ if (timer) clearTimeout(timer);
65
+ finish({ ok: code === 0, code, stdout, stderr });
66
+ });
26
67
  });
27
68
  }
28
- var colors = {
29
- /** Base page background — deepest ink */
30
- backgroundDark: "#080C16",
31
- backgroundDarkOklch: "oklch(0.09 0.02 260)",
32
- /** Surface elevation ladder — alpha blends, no drop-shadows */
33
- surface0: "#080C16",
34
- surface1: "rgba(15, 20, 34, 0.80)",
35
- surface2: "rgba(22, 28, 46, 0.90)",
36
- surface3: "rgba(31, 38, 56, 0.95)",
37
- /** Legacy alias — kept for backward compat */
38
- surfaceDark: "#0F1422",
39
- surfaceCard: "rgba(15, 20, 34, 0.75)",
40
- /** Borders */
41
- borderCard: "#1F2638",
42
- borderMuted: "rgba(255, 255, 255, 0.07)",
43
- borderSubtle: "rgba(255, 255, 255, 0.04)",
44
- /** Brand accent — Carrier Flame orange (do not replace with violet) */
45
- accentFlame: "#FF6B35",
46
- accentEmber: "#D9461C",
47
- accentSpark: "#FFB088",
48
- /** Legacy violet/fuchsia — used on esimmcp co-brand surface only */
49
- accentViolet: "#a78bfa",
50
- accentFuchsia: "#e879f9",
51
- /** Text hierarchy — tight ratio, generous contrast */
52
- textPrimary: "#F5F1EA",
53
- textSecondary: "#C9CCD6",
54
- textMuted: "#8A92A8",
55
- textFaint: "#5A6278",
56
- /** Status — do not deviate */
57
- statusSuccess: "#10b981",
58
- statusWarning: "#f59e0b",
59
- statusError: "#ef4444",
60
- /** Light mode equivalents */
61
- light: {
62
- background: "#ffffff",
63
- backgroundSecondary: "#f8fafc",
64
- surfaceCard: "rgba(248, 250, 252, 0.9)",
65
- borderCard: "#e2e8f0",
66
- textPrimary: "#0f172a",
67
- textSecondary: "#475569",
68
- textMuted: "#94a3b8"
69
- }
70
- };
71
- var typography = {
72
- fontSans: "var(--font-sans, Inter), system-ui, -apple-system, sans-serif",
73
- fontMono: "var(--font-mono, 'JetBrains Mono'), ui-monospace, monospace",
74
- fontSerif: "var(--font-serif, Georgia), 'Times New Roman', serif",
75
- /** Weights — only these three, per brand guide */
76
- weightRegular: "400",
77
- weightBold: "700",
78
- weightBlack: "900",
79
- /** Line heights */
80
- lineHeightHeadline: "1.05",
81
- lineHeightSubheading: "1.2",
82
- lineHeightBody: "1.5",
83
- /** Letter spacing — tight on big headlines */
84
- trackingTight: "-0.04em",
85
- trackingNormal: "0em",
86
- trackingWide: "0.05em",
87
- /** Type scale (rem) */
88
- scale: {
89
- xs: "0.75rem",
90
- sm: "0.875rem",
91
- base: "1rem",
92
- lg: "1.125rem",
93
- xl: "1.25rem",
94
- "2xl": "1.5rem",
95
- "3xl": "1.875rem",
96
- "4xl": "2.25rem",
97
- "5xl": "3rem",
98
- "6xl": "3.75rem",
99
- "7xl": "4.5rem"
69
+ function runInherit(cmd, args, opts = {}) {
70
+ if (!isSafeCommand(cmd)) {
71
+ return Promise.resolve({
72
+ ok: false,
73
+ code: null,
74
+ stdout: "",
75
+ stderr: `refusing to run unsafe command: ${cmd}`
76
+ });
100
77
  }
101
- };
102
- var radius = {
103
- sm: "0.375rem",
104
- md: "0.5rem",
105
- lg: "0.75rem",
106
- xl: "1rem",
107
- "2xl": "1.5rem",
108
- full: "9999px"
109
- };
110
- var TONE_COLOR = {
111
- ok: colors.statusSuccess,
112
- info: colors.accentFlame,
113
- warn: colors.statusWarning,
114
- critical: colors.statusError,
115
- muted: colors.textMuted
116
- };
117
- function esc(value) {
118
- return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
119
- }
120
- function toneColor(tone, fallback = colors.textPrimary) {
121
- return tone ? TONE_COLOR[tone] : fallback;
122
- }
123
- function metricsHtml(items) {
124
- const cards = items.map(
125
- (m) => `
126
- <div style="background:${colors.surface1};border:1px solid ${colors.borderCard};border-radius:${radius.lg};padding:16px 18px;min-width:0">
127
- <div style="font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:${colors.textMuted}">${esc(m.label)}</div>
128
- <div style="font-size:28px;font-weight:600;margin-top:6px;color:${toneColor(m.tone)};line-height:1.1">${esc(m.value)}</div>
129
- ${m.hint ? `<div style="font-size:12px;color:${colors.textFaint};margin-top:4px">${esc(m.hint)}</div>` : ""}
130
- </div>`
131
- ).join("");
132
- return `<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px">${cards}</div>`;
133
- }
134
- function barsHtml(items, empty) {
135
- if (items.length === 0) return emptyHtml(empty ?? "Nothing to show.");
136
- const ceiling = Math.max(...items.map((b) => b.max ?? b.value), 1);
137
- const rows = items.map((b) => {
138
- const pct2 = Math.max(0, Math.min(100, (b.value / (b.max ?? ceiling) || 0) * 100));
139
- return `
140
- <div style="margin-bottom:10px">
141
- <div style="display:flex;justify-content:space-between;font-size:13px;color:${colors.textSecondary};margin-bottom:4px">
142
- <span>${esc(b.label)}</span>
143
- <span style="color:${colors.textMuted}">${esc(b.hint ?? b.value)}</span>
144
- </div>
145
- <div style="height:8px;background:${colors.surface2};border-radius:${radius.full};overflow:hidden">
146
- <div style="height:100%;width:${pct2.toFixed(1)}%;background:${toneColor(b.tone, colors.accentFlame)}"></div>
147
- </div>
148
- </div>`;
149
- }).join("");
150
- return `<div>${rows}</div>`;
151
- }
152
- function tableHtml(section) {
153
- if (section.rows.length === 0) return emptyHtml(section.empty ?? "No rows.");
154
- const numeric = new Set(section.numeric ?? []);
155
- const head = section.columns.map(
156
- (c, i) => `<th style="text-align:${numeric.has(i) ? "right" : "left"};padding:8px 12px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:${colors.textMuted};border-bottom:1px solid ${colors.borderCard};white-space:nowrap">${esc(c)}</th>`
157
- ).join("");
158
- const body = section.rows.map(
159
- (row) => `<tr>${row.map(
160
- (cell, i) => `<td style="text-align:${numeric.has(i) ? "right" : "left"};padding:8px 12px;font-size:13px;color:${colors.textSecondary};border-bottom:1px solid ${colors.borderSubtle};white-space:nowrap">${esc(cell)}</td>`
161
- ).join("")}</tr>`
162
- ).join("");
163
- return `<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse">${`<thead><tr>${head}</tr></thead>`}<tbody>${body}</tbody></table></div>`;
164
- }
165
- function keyValueHtml(items) {
166
- const rows = items.map(
167
- (kv) => `
168
- <div style="display:flex;justify-content:space-between;gap:16px;padding:7px 0;border-bottom:1px solid ${colors.borderSubtle}">
169
- <span style="font-size:13px;color:${colors.textMuted}">${esc(kv.label)}</span>
170
- <span style="font-size:13px;color:${toneColor(kv.tone, colors.textPrimary)};text-align:right">${esc(kv.value)}</span>
171
- </div>`
172
- ).join("");
173
- return `<div>${rows}</div>`;
78
+ return new Promise((resolve) => {
79
+ const child = spawn(cmd, args, { cwd: opts.cwd, shell: false, stdio: "inherit" });
80
+ child.on("error", () => resolve({ ok: false, code: null, stdout: "", stderr: "" }));
81
+ child.on("close", (code) => resolve({ ok: code === 0, code, stdout: "", stderr: "" }));
82
+ });
174
83
  }
175
- function noteHtml(tone, text) {
176
- const c = toneColor(tone, colors.accentFlame);
177
- return `<div style="border-left:3px solid ${c};background:${colors.surface1};padding:10px 14px;border-radius:${radius.md};font-size:13px;color:${colors.textSecondary}">${esc(text)}</div>`;
84
+ async function which(bin) {
85
+ const probe2 = process.platform === "win32" ? "where" : "which";
86
+ const r = await run(probe2, [bin]);
87
+ return r.ok && r.stdout.trim().length > 0;
178
88
  }
179
- function emptyHtml(text) {
180
- return `<div style="padding:14px;border:1px dashed ${colors.borderCard};border-radius:${radius.md};font-size:13px;color:${colors.textMuted}">${esc(text)}</div>`;
89
+
90
+ // src/cli/lib/deploy-targets.ts
91
+ var TARGET_IDS = ["cloudflare", "vercel", "netlify", "fly"];
92
+ function isTargetId(v) {
93
+ return TARGET_IDS.includes(v);
181
94
  }
182
- function sectionHtml(section) {
183
- const title = "title" in section && section.title ? `<h2 style="font-size:13px;font-weight:600;letter-spacing:.04em;color:${colors.textPrimary};margin:0 0 10px">${esc(section.title)}</h2>` : "";
184
- let body;
185
- switch (section.kind) {
186
- case "metrics":
187
- body = metricsHtml(section.items);
188
- break;
189
- case "bars":
190
- body = barsHtml(section.items, section.empty);
191
- break;
192
- case "table":
193
- body = tableHtml(section);
194
- break;
195
- case "keyvalue":
196
- body = keyValueHtml(section.items);
197
- break;
198
- case "note":
199
- body = noteHtml(section.tone, section.text);
200
- break;
95
+ var DEFS = {
96
+ cloudflare: {
97
+ id: "cloudflare",
98
+ label: "Cloudflare Workers",
99
+ bin: "wrangler",
100
+ npxPkg: "wrangler",
101
+ markers: ["wrangler.jsonc", "wrangler.json", "wrangler.toml"],
102
+ buildScript: "cf:build",
103
+ artifact: join(".open-next", "worker.js"),
104
+ whoami: ["whoami"],
105
+ loginHint: "wrangler login",
106
+ // wrangler writes this mid-sentence, so the trailing period is not part of it.
107
+ parseAccount: (stdout, stderr) => `${stdout}${stderr}`.match(/associated with the email\s+(\S+?)[.,]?(?:\s|$)/)?.[1],
108
+ deployArgs: (name) => ["deploy", "--name", name],
109
+ secretArgs: (key, value, name) => ({ args: ["secret", "put", key, "--name", name], stdin: value })
110
+ },
111
+ vercel: {
112
+ id: "vercel",
113
+ label: "Vercel",
114
+ bin: "vercel",
115
+ npxPkg: "vercel",
116
+ markers: ["vercel.json", ".vercel"],
117
+ buildScript: "build",
118
+ whoami: ["whoami"],
119
+ loginHint: "vercel login",
120
+ // The username is the only thing on stdout; the version banner goes to stderr.
121
+ parseAccount: (stdout) => stdout.trim().split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith(">") && !/^Vercel CLI/i.test(l)).pop(),
122
+ deployArgs: () => ["deploy", "--prod", "--yes"],
123
+ secretArgs: (key, value) => ({ args: ["env", "add", key, "production", "--force"], stdin: value })
124
+ },
125
+ netlify: {
126
+ id: "netlify",
127
+ label: "Netlify",
128
+ bin: "netlify",
129
+ npxPkg: "netlify-cli",
130
+ markers: ["netlify.toml"],
131
+ buildScript: "build",
132
+ whoami: ["status"],
133
+ loginHint: "netlify login",
134
+ parseAccount: (stdout, stderr) => `${stdout}${stderr}`.match(/Email:\s*(\S+)/)?.[1],
135
+ deployArgs: () => ["deploy", "--build", "--prod"],
136
+ ensureConfig: async (storefront) => {
137
+ const path = join(storefront, "netlify.toml");
138
+ if (await exists(path)) return;
139
+ await writeFile(
140
+ path,
141
+ [
142
+ "# Written by @carrierllc/mcp",
143
+ "[build]",
144
+ ' command = "npm run build"',
145
+ ' publish = ".next"',
146
+ "",
147
+ "[[plugins]]",
148
+ ' package = "@netlify/plugin-nextjs"',
149
+ ""
150
+ ].join("\n")
151
+ );
152
+ },
153
+ secretArgs: (key, value) => ({ args: ["env:set", key, value] })
154
+ },
155
+ fly: {
156
+ id: "fly",
157
+ label: "Fly.io",
158
+ bin: "flyctl",
159
+ npxPkg: "",
160
+ markers: ["fly.toml"],
161
+ buildScript: "build",
162
+ whoami: ["auth", "whoami"],
163
+ loginHint: "flyctl auth login",
164
+ parseAccount: (out) => out.trim().split("\n").pop()?.trim(),
165
+ deployArgs: () => ["deploy", "--now"],
166
+ ensureConfig: async (storefront, projectName) => {
167
+ const toml = join(storefront, "fly.toml");
168
+ if (!await exists(toml)) {
169
+ await writeFile(
170
+ toml,
171
+ [
172
+ "# Written by @carrierllc/mcp",
173
+ `app = "${projectName}"`,
174
+ "",
175
+ "[build]",
176
+ ' dockerfile = "Dockerfile"',
177
+ "",
178
+ "[http_service]",
179
+ " internal_port = 3000",
180
+ " force_https = true",
181
+ " auto_stop_machines = true",
182
+ " auto_start_machines = true",
183
+ ""
184
+ ].join("\n")
185
+ );
186
+ }
187
+ const dockerfile = join(storefront, "Dockerfile");
188
+ if (!await exists(dockerfile)) {
189
+ await writeFile(
190
+ dockerfile,
191
+ [
192
+ "# Written by @carrierllc/mcp",
193
+ "FROM node:22-slim AS build",
194
+ "WORKDIR /app",
195
+ "COPY package*.json ./",
196
+ "RUN npm install",
197
+ "COPY . .",
198
+ "RUN npm run build",
199
+ "",
200
+ "FROM node:22-slim",
201
+ "WORKDIR /app",
202
+ "ENV NODE_ENV=production PORT=3000",
203
+ "COPY --from=build /app ./",
204
+ "EXPOSE 3000",
205
+ 'CMD ["npm", "run", "start"]',
206
+ ""
207
+ ].join("\n")
208
+ );
209
+ }
210
+ },
211
+ secretArgs: (key, value) => ({ args: ["secrets", "set", `${key}=${value}`] })
201
212
  }
202
- return `<section style="margin-bottom:22px">${title}${body}</section>`;
203
- }
204
- function renderHtml(screen) {
205
- const sections = isEmptyScreen(screen) ? emptyHtml(
206
- "No data came back for this view. That is a real result, not a loading state \u2014 check the account scope and credentials."
207
- ) : screen.sections.map(sectionHtml).join("");
208
- const actions = screen.actions?.length ? `<section style="margin-top:6px;display:flex;flex-wrap:wrap;gap:8px">${screen.actions.map(
209
- (a) => `<span title="${esc(a.description ?? a.command)}" style="font-size:12px;color:${colors.textSecondary};background:${colors.surface2};border:1px solid ${colors.borderCard};border-radius:${radius.full};padding:6px 12px">${esc(a.label)} <code style="color:${colors.textFaint}">${esc(a.command)}</code></span>`
210
- ).join("")}</section>` : "";
211
- return `<!doctype html>
212
- <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
213
- <title>${esc(screen.title)}</title></head>
214
- <body style="margin:0;background:${colors.surface0};color:${colors.textPrimary};font-family:${typography.fontSans};padding:20px">
215
- <header style="margin-bottom:20px">
216
- <h1 style="font-size:18px;font-weight:600;margin:0;letter-spacing:-.01em">${esc(screen.title)}</h1>
217
- ${screen.subtitle ? `<p style="margin:4px 0 0;font-size:13px;color:${colors.textMuted}">${esc(screen.subtitle)}</p>` : ""}
218
- </header>
219
- ${sections}
220
- ${actions}
221
- ${screen.footer ? `<footer style="margin-top:18px;font-size:11px;color:${colors.textFaint}">${esc(screen.footer)}</footer>` : ""}
222
- </body></html>`;
223
- }
224
- var ANSI = {
225
- ok: "\x1B[32m",
226
- info: "\x1B[38;5;209m",
227
- // Carrier flame, nearest 256-colour
228
- warn: "\x1B[33m",
229
- critical: "\x1B[31m",
230
- muted: "\x1B[90m",
231
- reset: "\x1B[0m",
232
- bold: "\x1B[1m",
233
- dim: "\x1B[2m"
234
213
  };
235
- function paint(text, code, color) {
236
- return color ? `${code}${text}${ANSI.reset}` : text;
237
- }
238
- function displayWidth(text) {
239
- return text.replace(/\[[0-9;]*m/g, "").length;
214
+ async function configuredProjectName(id, storefront) {
215
+ if (id !== "cloudflare") return void 0;
216
+ for (const marker of DEFS.cloudflare.markers) {
217
+ const path = join(storefront, marker);
218
+ if (!await exists(path)) continue;
219
+ const body = await readFileText(path);
220
+ const name = body.match(/^\s*"?name"?\s*:\s*"([^"]+)"/m)?.[1];
221
+ if (name) return name;
222
+ }
223
+ return void 0;
240
224
  }
241
- function pad(text, width, align) {
242
- const gap = Math.max(0, width - displayWidth(text));
243
- return align === "right" ? " ".repeat(gap) + text : text + " ".repeat(gap);
225
+ async function setCustomDomain(storefront, domain) {
226
+ const clean = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
227
+ if (!clean || !clean.includes(".")) return false;
228
+ const path = join(storefront, "wrangler.jsonc");
229
+ if (!await exists(path)) return false;
230
+ const body = await readFileText(path);
231
+ if (body.includes(`"pattern": "${clean}"`)) return true;
232
+ const routes = ` "routes": [
233
+ { "pattern": "${clean}", "custom_domain": true }
234
+ ],
235
+ `;
236
+ const anchor = body.indexOf(`"main"`);
237
+ if (anchor === -1) return false;
238
+ const lineStart = body.lastIndexOf("\n", anchor) + 1;
239
+ const patched = body.slice(0, lineStart) + routes + body.slice(lineStart);
240
+ await writeFile(path, patched);
241
+ return true;
244
242
  }
245
- function heading(text, color) {
246
- return paint(text.toUpperCase(), ANSI.bold, color);
243
+ async function readFileText(path) {
244
+ const { readFile: readFile2 } = await import("./fsx-BDDIQ3Y7.js");
245
+ return readFile2(path, "utf8");
247
246
  }
248
- function metricsTui(items, width, color) {
249
- if (items.length === 0) return [];
250
- const labelWidth = Math.min(
251
- 28,
252
- Math.max(...items.map((m) => m.label.length))
247
+ async function rollback(id, storefront, projectName) {
248
+ if (id !== "cloudflare") {
249
+ return { ok: false, reason: `${DEFS[id].label} has no one-command rollback \u2014 revert manually.` };
250
+ }
251
+ const def = DEFS[id];
252
+ const resolved = await resolveBin(def);
253
+ if (!resolved) return { ok: false, reason: "wrangler not found." };
254
+ const r = await run(
255
+ resolved.bin,
256
+ [...resolved.prefix, "rollback", "--name", projectName, "--yes"],
257
+ { cwd: storefront, timeoutMs: 3e5 }
253
258
  );
254
- return items.map((m) => {
255
- const label = paint(pad(m.label, labelWidth, "left"), ANSI.muted, color);
256
- const value = paint(String(m.value), m.tone ? ANSI[m.tone] : ANSI.bold, color);
257
- const hint = m.hint ? paint(` ${m.hint}`, ANSI.dim, color) : "";
258
- return ` ${label} ${value}${hint}`.slice(0, width + 64);
259
- });
259
+ return r.ok ? { ok: true } : { ok: false, reason: `${r.stderr || r.stdout}`.trim().split("\n").slice(-2).join(" ").slice(0, 300) };
260
260
  }
261
- function barsTui(items, width, color, empty) {
262
- if (items.length === 0) return [` ${paint(empty ?? "Nothing to show.", ANSI.muted, color)}`];
263
- const ceiling = Math.max(...items.map((b) => b.max ?? b.value), 1);
264
- const labelWidth = Math.min(24, Math.max(...items.map((b) => b.label.length)));
265
- const barWidth = Math.max(10, Math.min(40, width - labelWidth - 22));
266
- return items.map((b) => {
267
- const ratio = b.value / (b.max ?? ceiling) || 0;
268
- const filled = Math.max(0, Math.min(barWidth, Math.round(ratio * barWidth)));
269
- const bar = paint("\u2588".repeat(filled), b.tone ? ANSI[b.tone] : ANSI.info, color) + paint("\u2591".repeat(barWidth - filled), ANSI.dim, color);
270
- const label = paint(pad(b.label, labelWidth, "left"), ANSI.muted, color);
271
- const value = paint(String(b.hint ?? b.value), ANSI.dim, color);
272
- return ` ${label} ${bar} ${value}`;
273
- });
261
+ async function resolveBin(def) {
262
+ if (await which(def.bin)) return { bin: def.bin, prefix: [] };
263
+ if (def.npxPkg && await which("npx")) return { bin: "npx", prefix: [def.npxPkg] };
264
+ return void 0;
274
265
  }
275
- function tableTui(section, width, color) {
276
- if (section.rows.length === 0) {
277
- return [` ${paint(section.empty ?? "No rows.", ANSI.muted, color)}`];
266
+ async function probeTarget(id, storefront) {
267
+ const def = DEFS[id];
268
+ let configured = false;
269
+ for (const marker of def.markers) {
270
+ if (await exists(join(storefront, marker))) {
271
+ configured = true;
272
+ break;
273
+ }
278
274
  }
279
- const numeric = new Set(section.numeric ?? []);
280
- const cols = section.columns.length;
281
- const widths = Array.from(
282
- { length: cols },
283
- (_, i) => Math.max(
284
- section.columns[i]?.length ?? 0,
285
- ...section.rows.map((r) => String(r[i] ?? "").length)
286
- )
287
- );
288
- let total = widths.reduce((a, b) => a + b + 2, 2);
289
- while (total > width && Math.max(...widths) > 8) {
290
- const widest = widths.indexOf(Math.max(...widths));
291
- widths[widest] -= 1;
292
- total -= 1;
275
+ const resolved = await resolveBin(def);
276
+ if (!resolved) {
277
+ return {
278
+ id,
279
+ label: def.label,
280
+ installed: false,
281
+ authenticated: false,
282
+ configured,
283
+ ready: false,
284
+ reason: def.npxPkg ? `${def.bin} not found \u2014 install it, or make npx available.` : `${def.bin} not found \u2014 install the Fly CLI (brew install flyctl).`
285
+ };
293
286
  }
294
- const clip = (cell, i) => {
295
- const text = String(cell ?? "");
296
- return text.length > widths[i] ? `${text.slice(0, Math.max(1, widths[i] - 1))}\u2026` : text;
287
+ const who = await run(resolved.bin, [...resolved.prefix, ...def.whoami], {
288
+ cwd: storefront,
289
+ timeoutMs: 6e4
290
+ });
291
+ if (!who.ok) {
292
+ return {
293
+ id,
294
+ label: def.label,
295
+ installed: true,
296
+ authenticated: false,
297
+ configured,
298
+ ready: false,
299
+ reason: `${def.bin} is not logged in \u2014 run \`${def.loginHint}\`.`
300
+ };
301
+ }
302
+ return {
303
+ id,
304
+ label: def.label,
305
+ installed: true,
306
+ authenticated: true,
307
+ configured,
308
+ ready: true,
309
+ account: def.parseAccount?.(who.stdout, who.stderr)
297
310
  };
298
- const header = " " + section.columns.map((c, i) => paint(pad(clip(c, i), widths[i], numeric.has(i) ? "right" : "left"), ANSI.muted, color)).join(" ");
299
- const rule = " " + paint(widths.map((w) => "\u2500".repeat(w)).join(" "), ANSI.dim, color);
300
- const body = section.rows.map(
301
- (row) => " " + row.map((cell, i) => pad(clip(cell, i), widths[i], numeric.has(i) ? "right" : "left")).join(" ")
302
- );
303
- return [header, rule, ...body];
304
311
  }
305
- function keyValueTui(items, color) {
306
- if (items.length === 0) return [];
307
- const labelWidth = Math.min(30, Math.max(...items.map((kv) => kv.label.length)));
308
- return items.map(
309
- (kv) => ` ${paint(pad(kv.label, labelWidth, "left"), ANSI.muted, color)} ${paint(kv.value, kv.tone ? ANSI[kv.tone] : ANSI.reset, color)}`
310
- );
312
+ async function probeAll(storefront) {
313
+ return Promise.all(TARGET_IDS.map((id) => probeTarget(id, storefront)));
311
314
  }
312
- var NOTE_PREFIX = {
313
- ok: "ok",
314
- info: "note",
315
- warn: "warning",
316
- critical: "critical",
317
- muted: "note"
318
- };
319
- function sectionTui(section, width, color) {
320
- const lines = [];
321
- if ("title" in section && section.title) lines.push(heading(section.title, color));
322
- switch (section.kind) {
323
- case "metrics":
324
- lines.push(...metricsTui(section.items, width, color));
325
- break;
326
- case "bars":
327
- lines.push(...barsTui(section.items, width, color, section.empty));
328
- break;
329
- case "table":
330
- lines.push(...tableTui(section, width, color));
331
- break;
332
- case "keyvalue":
333
- lines.push(...keyValueTui(section.items, color));
334
- break;
335
- case "note":
336
- lines.push(
337
- ` ${paint(`${NOTE_PREFIX[section.tone]}:`, ANSI[section.tone], color)} ${section.text}`
338
- );
339
- break;
340
- }
341
- lines.push("");
342
- return lines;
315
+ function rankTargets(statuses) {
316
+ return statuses.filter((s) => s.ready).sort((a, b) => {
317
+ if (a.configured !== b.configured) return a.configured ? -1 : 1;
318
+ return TARGET_IDS.indexOf(a.id) - TARGET_IDS.indexOf(b.id);
319
+ });
343
320
  }
344
- function renderTui(screen, opts = {}) {
345
- const width = Math.max(40, Math.min(160, opts.width ?? 80));
346
- const color = opts.color ?? true;
347
- const lines = [];
348
- lines.push(paint(screen.title, ANSI.bold, color));
349
- if (screen.subtitle) lines.push(paint(screen.subtitle, ANSI.muted, color));
350
- lines.push(paint("\u2500".repeat(width), ANSI.dim, color));
351
- lines.push("");
352
- if (isEmptyScreen(screen)) {
353
- lines.push(
354
- ` ${paint("No data came back for this view.", ANSI.warn, color)} That is a real result, not a`,
355
- " loading state \u2014 check the account scope and credentials.",
356
- ""
357
- );
358
- } else {
359
- for (const section of screen.sections) lines.push(...sectionTui(section, width, color));
360
- }
361
- if (screen.actions?.length) {
362
- lines.push(heading("next", color));
363
- for (const a of screen.actions) {
364
- lines.push(` ${paint(a.command, ANSI.info, color)} ${paint(a.label, ANSI.dim, color)}`);
365
- }
366
- lines.push("");
367
- }
368
- if (screen.footer) lines.push(paint(screen.footer, ANSI.dim, color));
369
- return lines.join("\n");
321
+ function buildScriptFor(id) {
322
+ return DEFS[id].buildScript;
370
323
  }
371
- var pct = (part, total) => total > 0 ? `${(part / total * 100).toFixed(1)}%` : "0%";
372
- var money = (value, currency = "") => `${currency}${value.toFixed(2)}`.trim();
373
- function humanBytes(bytes) {
374
- if (!Number.isFinite(bytes) || bytes <= 0) return "0 MB";
375
- const gb = bytes / 1073741824;
376
- if (gb >= 1) return `${gb.toFixed(gb >= 10 ? 0 : 1)} GB`;
377
- return `${(bytes / 1048576).toFixed(0)} MB`;
324
+ async function artifactMissing(id, storefront) {
325
+ const artifact = DEFS[id].artifact;
326
+ if (!artifact) return false;
327
+ return !await exists(join(storefront, artifact));
378
328
  }
379
- function fleetScreen(input) {
380
- const total = input.active + input.suspended + input.inventory + input.other;
381
- const lowBalance = input.accounts.filter((a) => a.balance < 10);
382
- const criticals = lowBalance.filter((a) => a.packageOnly && a.balance === 0);
383
- const sections = [
384
- {
385
- kind: "metrics",
386
- items: [
387
- { label: "Total eSIMs", value: total },
388
- { label: "Active", value: input.active, hint: pct(input.active, total), tone: "ok" },
389
- { label: "Inventory", value: input.inventory, hint: pct(input.inventory, total) },
390
- {
391
- label: "Suspended",
392
- value: input.suspended,
393
- hint: pct(input.suspended, total),
394
- tone: input.suspended > input.active * 0.1 ? "warn" : void 0
395
- },
396
- { label: "Accounts", value: input.accounts.length },
397
- {
398
- label: "Low balance",
399
- value: lowBalance.length,
400
- tone: lowBalance.length > 0 ? "warn" : "ok"
401
- }
402
- ]
403
- }
404
- ];
405
- const hasPerAccountCounts = input.accounts.some(
406
- (a) => a.active + a.suspended + a.inventory + a.other > 0
407
- );
408
- if (hasPerAccountCounts) {
409
- sections.push({
410
- kind: "bars",
411
- title: "eSIMs by account",
412
- items: [...input.accounts].sort((a, b) => b.active + b.inventory - (a.active + a.inventory)).slice(0, 10).map((a) => ({
413
- label: a.name,
414
- value: a.active + a.suspended + a.inventory + a.other,
415
- hint: `${a.active} active`,
416
- tone: a.active > 0 ? "ok" : "muted"
417
- }))
418
- });
419
- }
420
- sections.push({
421
- kind: "table",
422
- title: "Accounts",
423
- columns: ["Account", "Balance", "Active", "Inventory"],
424
- numeric: [1, 2, 3],
425
- empty: "No accounts under this reseller.",
426
- rows: input.accounts.map((a) => [a.name, money(a.balance), a.active, a.inventory])
329
+ function parseDeployedUrl(output) {
330
+ return output.match(
331
+ /https:\/\/[^\s"']+\.(?:workers\.dev|vercel\.app|netlify\.app|fly\.dev)[^\s"']*/
332
+ )?.[0];
333
+ }
334
+ async function putSecret(id, storefront, projectName, key, value) {
335
+ const def = DEFS[id];
336
+ const resolved = await resolveBin(def);
337
+ if (!resolved) return false;
338
+ const { args, stdin } = def.secretArgs(key, value, projectName);
339
+ const r = await run(resolved.bin, [...resolved.prefix, ...args], {
340
+ cwd: storefront,
341
+ timeoutMs: 12e4,
342
+ stdin
427
343
  });
428
- if (criticals.length > 0) {
429
- sections.push({
430
- kind: "note",
431
- tone: "critical",
432
- text: `${criticals.length} package-only account(s) at zero balance \u2014 packages cannot be assigned until topped up: ${criticals.map((a) => a.name).join(", ")}.`
344
+ return r.ok;
345
+ }
346
+ async function stageSecrets(id, storefront, projectName, secrets) {
347
+ const entries = Object.entries(secrets).filter(([, v]) => v?.trim());
348
+ const noop = { staged: [], failed: [], cleanup: async () => {
349
+ } };
350
+ if (entries.length === 0) return noop;
351
+ const def = DEFS[id];
352
+ const resolved = await resolveBin(def);
353
+ if (!resolved) return { staged: [], failed: entries.map(([k]) => k), cleanup: async () => {
354
+ } };
355
+ if (id === "cloudflare") {
356
+ const dir = await mkdtemp(join(tmpdir(), "carrier-secrets-"));
357
+ const file = join(dir, ".env");
358
+ const body = entries.map(([k, v]) => `${k}=${v}`).join("\n");
359
+ await writeFileMode(file, `${body}
360
+ `, { mode: 384 });
361
+ return {
362
+ staged: entries.map(([k]) => k),
363
+ failed: [],
364
+ secretsFile: file,
365
+ cleanup: async () => {
366
+ await rm(dir, { recursive: true, force: true });
367
+ }
368
+ };
369
+ }
370
+ if (id === "fly") {
371
+ const args = ["secrets", "set", "--stage", ...entries.map(([k, v]) => `${k}=${v}`)];
372
+ const r = await run(resolved.bin, [...resolved.prefix, ...args], {
373
+ cwd: storefront,
374
+ timeoutMs: 18e4
433
375
  });
376
+ return {
377
+ staged: r.ok ? entries.map(([k]) => k) : [],
378
+ failed: r.ok ? [] : entries.map(([k]) => k),
379
+ cleanup: async () => {
380
+ }
381
+ };
434
382
  }
435
- for (const missing of input.unavailable ?? []) {
436
- sections.push({ kind: "note", tone: "warn", text: `Unavailable: ${missing}` });
383
+ const staged = [];
384
+ const failed = [];
385
+ for (const [key, value] of entries) {
386
+ const ok = await putSecret(id, storefront, projectName, key, value);
387
+ (ok ? staged : failed).push(key);
437
388
  }
438
- return {
439
- id: "fleet-health",
440
- title: "Fleet health",
441
- subtitle: `${total} eSIMs across ${input.accounts.length} account(s) \xB7 ${pct(input.active, total)} utilisation`,
442
- sections,
443
- actions: [
444
- { label: "Top up an account", command: "wallet_topup_checkout" },
445
- { label: "Per-account eSIM status", command: "esim_status_per_account" }
446
- ]
447
- };
448
- }
449
- function subscribersScreen(rows, opts = {}) {
450
- const byStatus = /* @__PURE__ */ new Map();
451
- for (const r of rows) byStatus.set(r.status, (byStatus.get(r.status) ?? 0) + 1);
452
- return {
453
- id: "subscribers",
454
- title: "Subscribers",
455
- subtitle: opts.account ? `Account ${opts.account} \xB7 ${rows.length} shown` : `${rows.length} shown`,
456
- sections: [
457
- {
458
- kind: "metrics",
459
- items: [
460
- { label: "Listed", value: rows.length },
461
- ...[...byStatus.entries()].map(([status, count]) => ({
462
- label: status,
463
- value: count,
464
- tone: status.toLowerCase() === "active" ? "ok" : void 0
465
- }))
466
- ]
467
- },
468
- {
469
- kind: "table",
470
- title: "Records",
471
- columns: ["ICCID", "MSISDN", "Status", "Account", "Data used"],
472
- numeric: [4],
473
- empty: "No subscribers matched. Widen the filter or check the account scope.",
474
- rows: rows.map((r) => [
475
- r.iccid,
476
- r.msisdn ?? "\u2014",
477
- r.status,
478
- r.account ?? "\u2014",
479
- r.dataUsedBytes === void 0 ? "\u2014" : humanBytes(r.dataUsedBytes)
480
- ])
481
- }
482
- ],
483
- actions: [
484
- { label: "Diagnose one", command: "diagnose_subscriber" },
485
- { label: "Usage detail", command: "subscriber_usage" }
486
- ]
487
- };
389
+ return { staged, failed, cleanup: async () => {
390
+ } };
488
391
  }
489
- function usageScreen(input) {
490
- const total = input.totalBytes ?? input.timeline.reduce((sum, p) => sum + p.bytes, 0);
491
- const peak = input.timeline.reduce(
492
- (best, p) => p.bytes > best.bytes ? p : best,
493
- { date: "\u2014", bytes: 0 }
494
- );
495
- const sections = [
496
- {
497
- kind: "metrics",
498
- items: [
499
- { label: "Total", value: humanBytes(total) },
500
- { label: "Days", value: input.timeline.length },
501
- { label: "Peak day", value: humanBytes(peak.bytes), hint: peak.date },
502
- {
503
- label: "Daily average",
504
- value: humanBytes(input.timeline.length ? total / input.timeline.length : 0)
505
- }
506
- ]
507
- },
508
- {
509
- kind: "bars",
510
- title: "Daily usage",
511
- empty: "No usage recorded in this window.",
512
- items: input.timeline.map((p) => ({
513
- label: p.date,
514
- value: p.bytes,
515
- hint: humanBytes(p.bytes)
516
- }))
517
- }
518
- ];
519
- if (input.countries?.length) {
520
- sections.push({
521
- kind: "bars",
522
- title: "By country",
523
- items: input.countries.slice(0, 10).map((c) => ({ label: c.country, value: c.bytes, hint: humanBytes(c.bytes) }))
524
- });
392
+ async function deployTo(id, storefront, projectName, opts = {}) {
393
+ const def = DEFS[id];
394
+ const resolved = await resolveBin(def);
395
+ if (!resolved) {
396
+ return { ok: false, target: id, projectName, reason: `${def.bin} not found.` };
525
397
  }
526
- return {
527
- id: "usage",
528
- title: "Usage",
529
- subtitle: input.subject,
530
- sections,
531
- actions: [{ label: "Project depletion", command: "usage_projection" }]
532
- };
398
+ await def.ensureConfig?.(storefront, projectName);
399
+ if (await artifactMissing(id, storefront)) {
400
+ return {
401
+ ok: false,
402
+ target: id,
403
+ projectName,
404
+ reason: `No ${def.artifact} build found \u2014 run the build step first.`
405
+ };
406
+ }
407
+ const args = [...resolved.prefix, ...def.deployArgs(projectName)];
408
+ if (id === "cloudflare" && opts.secretsFile) args.push("--secrets-file", opts.secretsFile);
409
+ const r = await run(resolved.bin, args, {
410
+ cwd: storefront,
411
+ timeoutMs: 9e5
412
+ });
413
+ const combined = `${r.stdout}${r.stderr}`;
414
+ if (!r.ok) {
415
+ const tail = combined.trim().split("\n").slice(-3).join(" ").slice(0, 400);
416
+ return { ok: false, target: id, projectName, reason: tail || `${def.bin} deploy failed.` };
417
+ }
418
+ return { ok: true, target: id, projectName, url: parseDeployedUrl(combined) };
533
419
  }
534
- function packagesScreen(rows) {
535
- return {
536
- id: "packages",
537
- title: "Package catalog",
538
- subtitle: `${rows.length} template(s)`,
539
- sections: [
540
- {
541
- kind: "metrics",
542
- items: [
543
- { label: "Templates", value: rows.length },
544
- { label: "Recurring", value: rows.filter((r) => r.recurring).length }
545
- ]
546
- },
547
- {
548
- kind: "table",
549
- title: "Templates",
550
- columns: ["Name", "ID", "Data", "Validity", "Price"],
551
- numeric: [2, 3, 4],
552
- empty: "No package templates visible to this account.",
553
- rows: rows.map((r) => [
554
- r.name,
555
- String(r.id),
556
- r.dataLimitBytes === void 0 ? "\u2014" : humanBytes(r.dataLimitBytes),
557
- r.validityDays === void 0 ? "\u2014" : `${r.validityDays}d`,
558
- r.price === void 0 ? "\u2014" : money(r.price)
559
- ])
560
- }
561
- ],
562
- actions: [{ label: "Assign to a subscriber", command: "assign_package" }]
563
- };
420
+
421
+ // src/cli/lib/brand.ts
422
+ var CARRIER_BRAND = {
423
+ name: "Carrier",
424
+ legalName: "Lifecycle Innovations Limited",
425
+ tagline: "Programmable connectivity, on demand.",
426
+ domain: "carrier.llc",
427
+ supportEmail: "support@carrier.llc",
428
+ supportUrl: "https://carrier.llc/help",
429
+ supportWhatsapp: "+17864604829",
430
+ colors: {
431
+ bg: "#080C16",
432
+ accent: "#FF6B35",
433
+ accentDark: "#D9461C",
434
+ text: "#F5F1EA"
435
+ },
436
+ social: {
437
+ x: "@carrier_llc",
438
+ instagram: "@carrier.llc",
439
+ tiktok: "@carrier.llc"
440
+ },
441
+ carrierApiUrl: "https://api.carrier.llc"
442
+ };
443
+ var CARRIER_ACCENT_LIGHT = "#FFB088";
444
+ var CARRIER_ACCENT_GRADIENT_START = "#FF7A45";
445
+ function hexToRgb(hex) {
446
+ const h = hex.replace(/^#/, "");
447
+ if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
448
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
564
449
  }
565
- function billingScreen(input) {
566
- const spend = input.events.reduce((sum, e) => sum + (e.amount > 0 ? e.amount : 0), 0);
567
- return {
568
- id: "billing",
569
- title: "Billing",
570
- subtitle: `${input.events.length} recent event(s)`,
571
- sections: [
572
- {
573
- kind: "metrics",
574
- items: [
575
- {
576
- label: "Balance",
577
- value: input.balance === void 0 ? "\u2014" : money(input.balance, input.currency),
578
- tone: (input.balance ?? 0) < 10 ? "warn" : "ok"
579
- },
580
- {
581
- label: "Pending",
582
- value: input.pending === void 0 ? "\u2014" : money(input.pending, input.currency)
583
- },
584
- { label: "Recent spend", value: money(spend, input.currency) }
585
- ]
586
- },
587
- {
588
- kind: "table",
589
- title: "Recent events",
590
- columns: ["Date", "Description", "Amount"],
591
- numeric: [2],
592
- empty: "No billing events in this window.",
593
- rows: input.events.map((e) => [e.date, e.description, money(e.amount, input.currency)])
594
- }
595
- ],
596
- actions: [{ label: "Check payouts", command: "stripe_connect_payouts" }]
597
- };
450
+ function rgbToHex(r, g, b, lower = false) {
451
+ const fmt = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
452
+ const out = `#${fmt(r)}${fmt(g)}${fmt(b)}`;
453
+ return lower ? out : out.toUpperCase();
454
+ }
455
+ function mixHexWithWhite(hex, whiteRatio) {
456
+ const rgb = hexToRgb(hex);
457
+ if (!rgb) return hex;
458
+ const mix2 = (n) => n + (255 - n) * whiteRatio;
459
+ return rgbToHex(mix2(rgb[0]), mix2(rgb[1]), mix2(rgb[2]));
460
+ }
461
+ function darkenHex(hex, factor) {
462
+ const rgb = hexToRgb(hex);
463
+ if (!rgb) return hex;
464
+ const scale = (n) => n * factor;
465
+ return rgbToHex(scale(rgb[0]), scale(rgb[1]), scale(rgb[2]), true);
466
+ }
467
+ function deriveAccentDark(accent, seed = CARRIER_BRAND) {
468
+ if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return seed.colors.accentDark;
469
+ const rgb = hexToRgb(accent);
470
+ if (!rgb) return accent;
471
+ const scale = (n) => Math.max(0, Math.min(255, Math.round(n * 0.75)));
472
+ return rgbToHex(scale(rgb[0]), scale(rgb[1]), scale(rgb[2]));
473
+ }
474
+ function deriveAccentLight(accent, seed = CARRIER_BRAND) {
475
+ if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return CARRIER_ACCENT_LIGHT;
476
+ return mixHexWithWhite(accent, 0.45);
477
+ }
478
+ function deriveAccentGradientStart(accent, seed = CARRIER_BRAND) {
479
+ if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return CARRIER_ACCENT_GRADIENT_START;
480
+ return mixHexWithWhite(accent, 0.08);
481
+ }
482
+ function deriveAccentPaletteSubs(accent, accentDark) {
483
+ if (accent.toUpperCase() === CARRIER_BRAND.colors.accent.toUpperCase()) return [];
484
+ const accentLight = deriveAccentLight(accent);
485
+ const gradientStart = deriveAccentGradientStart(accent);
486
+ return [
487
+ ["#FF6B35", accent],
488
+ ["#D9461C", accentDark],
489
+ ["#FFB088", accentLight],
490
+ ["#FF7A45", gradientStart],
491
+ ["#fff4ef", mixHexWithWhite(accent, 0.94).toLowerCase()],
492
+ ["#ffe0d0", mixHexWithWhite(accent, 0.85).toLowerCase()],
493
+ ["#ffbfa0", mixHexWithWhite(accent, 0.7).toLowerCase()],
494
+ ["#ff9970", mixHexWithWhite(accent, 0.55).toLowerCase()],
495
+ ["#ff7d4d", mixHexWithWhite(accent, 0.4).toLowerCase()],
496
+ ["#b33a17", darkenHex(accentDark, 0.75)],
497
+ ["#8a2d12", darkenHex(accentDark, 0.58)],
498
+ ["#5e1e0c", darkenHex(accentDark, 0.4)],
499
+ ["#3a1107", darkenHex(accentDark, 0.25)]
500
+ ];
598
501
  }
599
- function walletScreen(input) {
600
- const low = input.balance < (input.threshold ?? 10);
601
- return {
602
- id: "wallet",
603
- title: "Wallet",
604
- subtitle: input.autoTopupEnabled ? "Auto top-up is on" : "Auto top-up is off",
605
- sections: [
606
- {
607
- kind: "metrics",
608
- items: [
609
- {
610
- label: "Balance",
611
- value: money(input.balance, input.currency),
612
- tone: low ? "warn" : "ok"
613
- },
614
- ...input.credits === void 0 ? [] : [{ label: "Credits", value: input.credits }]
615
- ]
616
- },
617
- {
618
- kind: "keyvalue",
619
- title: "Settings",
620
- items: [
621
- { label: "Auto top-up", value: input.autoTopupEnabled ? "enabled" : "disabled" },
622
- {
623
- label: "Threshold",
624
- value: input.threshold === void 0 ? "\u2014" : money(input.threshold, input.currency)
625
- }
626
- ]
627
- },
628
- ...low ? [
629
- {
630
- kind: "note",
631
- tone: "warn",
632
- text: "Balance is under the top-up threshold. Package assignment fails at zero on package-only accounts."
633
- }
634
- ] : []
635
- ],
636
- actions: [
637
- { label: "Top up", command: "wallet_topup_checkout" },
638
- { label: "Configure auto top-up", command: "wallet_auto_topup" }
639
- ]
640
- };
502
+ function renderEnv(brand) {
503
+ return [
504
+ `# Generated by @carrierllc/mcp`,
505
+ `NEXT_PUBLIC_BRAND_NAME=${JSON.stringify(brand.name)}`,
506
+ `NEXT_PUBLIC_CARRIER_API_URL=${JSON.stringify(brand.carrierApiUrl)}`,
507
+ `# Public site origin. Used to build absolute URLs for guest checkout.`,
508
+ `# \`carrier site deploy\` writes the deployed URL back here.`,
509
+ `NEXT_PUBLIC_APP_URL=`,
510
+ `# Required whenever NEXT_PUBLIC_CARRIER_API_URL points at a live origin:`,
511
+ `# the catalog client throws without it, so / and /shop return 500.`,
512
+ `# \`carrier site deploy\` fills this in and pushes it to the deploy target.`,
513
+ `CARRIER_API_KEY=`,
514
+ `# Guest checkout calls Stripe server-side. Without this, checkout fails at`,
515
+ `# request time while every other page keeps working.`,
516
+ `STRIPE_SECRET_KEY=`,
517
+ `# Clerk \u2014 \`carrier site clerk\` fills these in, or paste them from dashboard.clerk.com`,
518
+ `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=`,
519
+ `CLERK_SECRET_KEY=`,
520
+ ``
521
+ ].join("\n");
641
522
  }
642
- function greenzoneScreen(entries) {
643
- return {
644
- id: "greenzone",
645
- title: "Greenzone whitelist",
646
- subtitle: `${entries.length} entr${entries.length === 1 ? "y" : "ies"}`,
647
- sections: [
648
- { kind: "metrics", items: [{ label: "Entries", value: entries.length }] },
649
- {
650
- kind: "table",
651
- title: "Whitelisted",
652
- columns: ["Value", "Note"],
653
- empty: "Whitelist is empty \u2014 every destination follows the default policy.",
654
- rows: entries.map((e) => [e.value, e.note ?? "\u2014"])
655
- }
656
- ],
657
- actions: [
658
- { label: "Add an entry", command: "greenzone_whitelist_add" },
659
- { label: "Remove an entry", command: "greenzone_whitelist_remove" }
660
- ]
661
- };
523
+
524
+ // src/cli/lib/storefront-secrets.ts
525
+ import { homedir } from "os";
526
+ import { join as join2 } from "path";
527
+ var TEMPLATE_ENV_KEYS = {
528
+ NEXT_PUBLIC_BRAND_NAME: "public",
529
+ NEXT_PUBLIC_CARRIER_API_URL: "public",
530
+ NEXT_PUBLIC_APP_URL: "public",
531
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: "public",
532
+ CARRIER_API_KEY: "secret",
533
+ CLERK_SECRET_KEY: "secret",
534
+ STRIPE_SECRET_KEY: "secret"
535
+ };
536
+ function keysOfKind(kind) {
537
+ return Object.keys(TEMPLATE_ENV_KEYS).filter(
538
+ (k) => TEMPLATE_ENV_KEYS[k] === kind
539
+ );
662
540
  }
663
- function storefrontScreen(input) {
664
- const sections = [
665
- {
666
- kind: "metrics",
667
- items: [
668
- {
669
- label: "Status",
670
- value: input.verified === true ? "serving" : input.verified === false ? "broken" : "unknown",
671
- tone: input.verified === true ? "ok" : input.verified === false ? "critical" : "muted"
672
- },
673
- { label: "Host", value: input.target ?? "\u2014" },
674
- { label: "Plans", value: input.plans ?? "\u2014" },
675
- { label: "Secrets", value: input.secretsStaged?.length ?? 0 }
676
- ]
677
- },
678
- {
679
- kind: "keyvalue",
680
- title: "Deployment",
681
- items: [
682
- { label: "Brand", value: input.brand },
683
- { label: "URL", value: input.url ?? "not deployed" },
684
- ...input.diagnosis ? [{ label: "Diagnosis", value: input.diagnosis, tone: "warn" }] : []
685
- ]
541
+ var RUNTIME_SECRET_KEYS = keysOfKind("secret");
542
+ var PUBLIC_ENV_KEYS = keysOfKind("public");
543
+ function parseEnvFile(body) {
544
+ const out = {};
545
+ for (const raw of body.split("\n")) {
546
+ const line = raw.trim();
547
+ if (!line || line.startsWith("#")) continue;
548
+ const eq = line.indexOf("=");
549
+ if (eq <= 0) continue;
550
+ const key = line.slice(0, eq).trim();
551
+ let value = line.slice(eq + 1).trim();
552
+ if (value.startsWith('"') && value.endsWith('"') && value.length > 1 || value.startsWith("'") && value.endsWith("'") && value.length > 1) {
553
+ value = value.slice(1, -1);
686
554
  }
687
- ];
688
- if (input.probes?.length) {
689
- sections.push({
690
- kind: "table",
691
- title: "Probes",
692
- columns: ["Path", "Status"],
693
- numeric: [1],
694
- rows: input.probes.map((p) => [p.path, p.status === 0 ? "unreachable" : p.status])
695
- });
696
- }
697
- if (input.secretsMissing?.length) {
698
- sections.push({
699
- kind: "note",
700
- tone: "critical",
701
- text: `Missing runtime secret(s): ${input.secretsMissing.join(", ")}. The build and deploy both succeed without them and the site fails at request time.`
702
- });
555
+ if (value) out[key] = value;
703
556
  }
704
- return {
705
- id: "storefront",
706
- title: "Storefront",
707
- subtitle: input.url ?? input.brand,
708
- sections,
709
- actions: [
710
- { label: "Deploy", command: "carrier site deploy" },
711
- { label: "Check hosts", command: "carrier site targets" }
712
- ]
713
- };
557
+ return out;
714
558
  }
715
-
716
- // src/cli/lib/deploy-targets.ts
717
- import { join } from "path";
718
- import { tmpdir } from "os";
719
- import { mkdtemp, rm, writeFile as writeFileMode } from "fs/promises";
720
-
721
- // src/cli/lib/exec.ts
722
- import { spawn } from "child_process";
723
- var SAFE_COMMAND = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
724
- function isSafeCommand(cmd) {
725
- return SAFE_COMMAND.test(cmd);
559
+ async function readEnvFile(path) {
560
+ if (!await exists(path)) return {};
561
+ try {
562
+ return parseEnvFile(await readFile(path, "utf8"));
563
+ } catch {
564
+ return {};
565
+ }
726
566
  }
727
- function run(cmd, args, opts = {}) {
728
- if (!isSafeCommand(cmd)) {
729
- return Promise.resolve({
730
- ok: false,
731
- code: null,
732
- stdout: "",
733
- stderr: `refusing to run unsafe command: ${cmd}`
734
- });
567
+ async function discoverSecrets(storefront, env = process.env, home = homedir()) {
568
+ const local = await readEnvFile(join2(storefront, ".env.local"));
569
+ const global = await readEnvFile(join2(home, ".env"));
570
+ const merged = {};
571
+ const keys = [...RUNTIME_SECRET_KEYS, ...PUBLIC_ENV_KEYS];
572
+ for (const key of keys) {
573
+ const value = local[key]?.trim() || env[key]?.trim() || global[key]?.trim();
574
+ if (value) merged[key] = value;
735
575
  }
736
- return new Promise((resolve) => {
737
- const child = spawn(cmd, args, { cwd: opts.cwd, shell: false });
738
- if (opts.stdin !== void 0) {
739
- child.stdin?.on("error", () => {
740
- });
741
- child.stdin?.end(opts.stdin);
576
+ return merged;
577
+ }
578
+ async function mergeEnvLocal(storefront, updates, opts = {}) {
579
+ const path = join2(storefront, ".env.local");
580
+ const overwrite = new Set(opts.overwrite ?? []);
581
+ const original = await exists(path) ? await readFile(path, "utf8") : "";
582
+ const lines = original ? original.split("\n") : [];
583
+ const written = [];
584
+ for (const [key, value] of Object.entries(updates)) {
585
+ if (!value) continue;
586
+ const index = lines.findIndex((l) => l.trim().startsWith(`${key}=`));
587
+ if (index === -1) {
588
+ lines.push(`${key}=${value}`);
589
+ written.push(key);
590
+ continue;
742
591
  }
743
- let stdout = "";
744
- let stderr = "";
745
- let settled = false;
746
- const finish = (result) => {
747
- if (settled) return;
748
- settled = true;
749
- resolve(result);
750
- };
751
- let timer;
752
- if (opts.timeoutMs && opts.timeoutMs > 0) {
753
- timer = setTimeout(() => {
754
- try {
755
- child.kill("SIGTERM");
756
- } catch {
757
- }
758
- finish({
759
- ok: false,
760
- code: null,
761
- stdout,
762
- stderr: stderr || `timeout after ${opts.timeoutMs}ms`
763
- });
764
- }, opts.timeoutMs);
592
+ const current = lines[index].slice(lines[index].indexOf("=") + 1).trim();
593
+ const isEmpty = current === "" || current === '""' || current === "''";
594
+ if (isEmpty || overwrite.has(key)) {
595
+ lines[index] = `${key}=${value}`;
596
+ written.push(key);
765
597
  }
766
- child.stdout?.on("data", (d) => stdout += d.toString());
767
- child.stderr?.on("data", (d) => stderr += d.toString());
768
- child.on("error", () => {
769
- if (timer) clearTimeout(timer);
770
- finish({ ok: false, code: null, stdout, stderr });
771
- });
772
- child.on("close", (code) => {
773
- if (timer) clearTimeout(timer);
774
- finish({ ok: code === 0, code, stdout, stderr });
775
- });
776
- });
777
- }
778
- function runInherit(cmd, args, opts = {}) {
779
- if (!isSafeCommand(cmd)) {
780
- return Promise.resolve({
781
- ok: false,
782
- code: null,
783
- stdout: "",
784
- stderr: `refusing to run unsafe command: ${cmd}`
785
- });
786
598
  }
787
- return new Promise((resolve) => {
788
- const child = spawn(cmd, args, { cwd: opts.cwd, shell: false, stdio: "inherit" });
789
- child.on("error", () => resolve({ ok: false, code: null, stdout: "", stderr: "" }));
790
- child.on("close", (code) => resolve({ ok: code === 0, code, stdout: "", stderr: "" }));
791
- });
599
+ if (written.length === 0) return [];
600
+ const body = lines.join("\n").replace(/\n{3,}$/, "\n");
601
+ await writeFile(path, body.endsWith("\n") ? body : `${body}
602
+ `);
603
+ return written;
792
604
  }
793
- async function which(bin) {
794
- const probe2 = process.platform === "win32" ? "where" : "which";
795
- const r = await run(probe2, [bin]);
796
- return r.ok && r.stdout.trim().length > 0;
605
+ function needsCarrierKey(secrets) {
606
+ const url = secrets.NEXT_PUBLIC_CARRIER_API_URL?.trim();
607
+ return Boolean(url) && !secrets.CARRIER_API_KEY?.trim();
797
608
  }
798
609
 
799
- // src/cli/lib/deploy-targets.ts
800
- var TARGET_IDS = ["cloudflare", "vercel", "netlify", "fly"];
801
- function isTargetId(v) {
802
- return TARGET_IDS.includes(v);
803
- }
804
- var DEFS = {
805
- cloudflare: {
806
- id: "cloudflare",
807
- label: "Cloudflare Workers",
808
- bin: "wrangler",
809
- npxPkg: "wrangler",
810
- markers: ["wrangler.jsonc", "wrangler.json", "wrangler.toml"],
811
- buildScript: "cf:build",
812
- artifact: join(".open-next", "worker.js"),
813
- whoami: ["whoami"],
814
- loginHint: "wrangler login",
815
- // wrangler writes this mid-sentence, so the trailing period is not part of it.
816
- parseAccount: (stdout, stderr) => `${stdout}${stderr}`.match(/associated with the email\s+(\S+?)[.,]?(?:\s|$)/)?.[1],
817
- deployArgs: (name) => ["deploy", "--name", name],
818
- secretArgs: (key, value, name) => ({ args: ["secret", "put", key, "--name", name], stdin: value })
819
- },
820
- vercel: {
821
- id: "vercel",
822
- label: "Vercel",
823
- bin: "vercel",
824
- npxPkg: "vercel",
825
- markers: ["vercel.json", ".vercel"],
826
- buildScript: "build",
827
- whoami: ["whoami"],
828
- loginHint: "vercel login",
829
- // The username is the only thing on stdout; the version banner goes to stderr.
830
- parseAccount: (stdout) => stdout.trim().split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith(">") && !/^Vercel CLI/i.test(l)).pop(),
831
- deployArgs: () => ["deploy", "--prod", "--yes"],
832
- secretArgs: (key, value) => ({ args: ["env", "add", key, "production", "--force"], stdin: value })
833
- },
834
- netlify: {
835
- id: "netlify",
836
- label: "Netlify",
837
- bin: "netlify",
838
- npxPkg: "netlify-cli",
839
- markers: ["netlify.toml"],
840
- buildScript: "build",
841
- whoami: ["status"],
842
- loginHint: "netlify login",
843
- parseAccount: (stdout, stderr) => `${stdout}${stderr}`.match(/Email:\s*(\S+)/)?.[1],
844
- deployArgs: () => ["deploy", "--build", "--prod"],
845
- ensureConfig: async (storefront) => {
846
- const path = join(storefront, "netlify.toml");
847
- if (await exists(path)) return;
848
- await writeFile(
849
- path,
850
- [
851
- "# Written by @carrierllc/mcp",
852
- "[build]",
853
- ' command = "npm run build"',
854
- ' publish = ".next"',
855
- "",
856
- "[[plugins]]",
857
- ' package = "@netlify/plugin-nextjs"',
858
- ""
859
- ].join("\n")
860
- );
861
- },
862
- secretArgs: (key, value) => ({ args: ["env:set", key, value] })
863
- },
864
- fly: {
865
- id: "fly",
866
- label: "Fly.io",
867
- bin: "flyctl",
868
- npxPkg: "",
869
- markers: ["fly.toml"],
870
- buildScript: "build",
871
- whoami: ["auth", "whoami"],
872
- loginHint: "flyctl auth login",
873
- parseAccount: (out) => out.trim().split("\n").pop()?.trim(),
874
- deployArgs: () => ["deploy", "--now"],
875
- ensureConfig: async (storefront, projectName) => {
876
- const toml = join(storefront, "fly.toml");
877
- if (!await exists(toml)) {
878
- await writeFile(
879
- toml,
880
- [
881
- "# Written by @carrierllc/mcp",
882
- `app = "${projectName}"`,
883
- "",
884
- "[build]",
885
- ' dockerfile = "Dockerfile"',
886
- "",
887
- "[http_service]",
888
- " internal_port = 3000",
889
- " force_https = true",
890
- " auto_stop_machines = true",
891
- " auto_start_machines = true",
892
- ""
893
- ].join("\n")
894
- );
895
- }
896
- const dockerfile = join(storefront, "Dockerfile");
897
- if (!await exists(dockerfile)) {
898
- await writeFile(
899
- dockerfile,
900
- [
901
- "# Written by @carrierllc/mcp",
902
- "FROM node:22-slim AS build",
903
- "WORKDIR /app",
904
- "COPY package*.json ./",
905
- "RUN npm install",
906
- "COPY . .",
907
- "RUN npm run build",
908
- "",
909
- "FROM node:22-slim",
910
- "WORKDIR /app",
911
- "ENV NODE_ENV=production PORT=3000",
912
- "COPY --from=build /app ./",
913
- "EXPOSE 3000",
914
- 'CMD ["npm", "run", "start"]',
915
- ""
916
- ].join("\n")
917
- );
918
- }
919
- },
920
- secretArgs: (key, value) => ({ args: ["secrets", "set", `${key}=${value}`] })
921
- }
922
- };
923
- async function configuredProjectName(id, storefront) {
924
- if (id !== "cloudflare") return void 0;
925
- for (const marker of DEFS.cloudflare.markers) {
926
- const path = join(storefront, marker);
927
- if (!await exists(path)) continue;
928
- const body = await readFileText(path);
929
- const name = body.match(/^\s*"?name"?\s*:\s*"([^"]+)"/m)?.[1];
930
- if (name) return name;
931
- }
932
- return void 0;
610
+ // src/cli/lib/site.ts
611
+ import { join as join3 } from "path";
612
+ function slug(s) {
613
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "storefront";
933
614
  }
934
- async function setCustomDomain(storefront, domain) {
935
- const clean = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
936
- if (!clean || !clean.includes(".")) return false;
937
- const path = join(storefront, "wrangler.jsonc");
938
- if (!await exists(path)) return false;
939
- const body = await readFileText(path);
940
- if (body.includes(`"pattern": "${clean}"`)) return true;
941
- const routes = ` "routes": [
942
- { "pattern": "${clean}", "custom_domain": true }
943
- ],
944
- `;
945
- const anchor = body.indexOf(`"main"`);
946
- if (anchor === -1) return false;
947
- const lineStart = body.lastIndexOf("\n", anchor) + 1;
948
- const patched = body.slice(0, lineStart) + routes + body.slice(lineStart);
949
- await writeFile(path, patched);
950
- return true;
615
+ async function installDeps(target) {
616
+ const pkgMgr = await which("pnpm") ? "pnpm" : "npm";
617
+ const r = await runInherit(pkgMgr, ["install"], { cwd: target });
618
+ return r.ok;
951
619
  }
952
- async function readFileText(path) {
953
- const { readFile: readFile2 } = await import("./fsx-BDDIQ3Y7.js");
954
- return readFile2(path, "utf8");
620
+ async function buildSite(target, forTarget = "cloudflare") {
621
+ const pkgMgr = await which("pnpm") ? "pnpm" : "npm";
622
+ const r = await runInherit(pkgMgr, ["run", buildScriptFor(forTarget)], { cwd: target });
623
+ return r.ok;
955
624
  }
956
- async function rollback(id, storefront, projectName) {
957
- if (id !== "cloudflare") {
958
- return { ok: false, reason: `${DEFS[id].label} has no one-command rollback \u2014 revert manually.` };
625
+ async function loadStorefrontBrand(target, overrides) {
626
+ const configPath = join3(target, "src", "brand.config.ts");
627
+ if (!await exists(configPath)) {
628
+ return { ...CARRIER_BRAND, ...overrides };
959
629
  }
960
- const def = DEFS[id];
961
- const resolved = await resolveBin(def);
962
- if (!resolved) return { ok: false, reason: "wrangler not found." };
963
- const r = await run(
964
- resolved.bin,
965
- [...resolved.prefix, "rollback", "--name", projectName, "--yes"],
966
- { cwd: storefront, timeoutMs: 3e5 }
967
- );
968
- return r.ok ? { ok: true } : { ok: false, reason: `${r.stderr || r.stdout}`.trim().split("\n").slice(-2).join(" ").slice(0, 300) };
969
- }
970
- async function resolveBin(def) {
971
- if (await which(def.bin)) return { bin: def.bin, prefix: [] };
972
- if (def.npxPkg && await which("npx")) return { bin: "npx", prefix: [def.npxPkg] };
973
- return void 0;
974
- }
975
- async function probeTarget(id, storefront) {
976
- const def = DEFS[id];
977
- let configured = false;
978
- for (const marker of def.markers) {
979
- if (await exists(join(storefront, marker))) {
980
- configured = true;
981
- break;
630
+ const src = await readFile(configPath, "utf8");
631
+ const pick = (field, fallback) => {
632
+ const m = src.match(new RegExp(`\\b${field}:\\s*(?:[^"\\n]*\\?\\?\\s*)?"([^"]*)"`));
633
+ return m?.[1] ?? fallback;
634
+ };
635
+ return {
636
+ ...CARRIER_BRAND,
637
+ name: overrides?.name ?? pick("name", CARRIER_BRAND.name),
638
+ domain: pick("domain", CARRIER_BRAND.domain),
639
+ supportEmail: pick("supportEmail", CARRIER_BRAND.supportEmail),
640
+ supportUrl: pick("supportUrl", CARRIER_BRAND.supportUrl),
641
+ tagline: pick("tagline", CARRIER_BRAND.tagline),
642
+ legalName: pick("legalName", CARRIER_BRAND.legalName),
643
+ colors: {
644
+ ...CARRIER_BRAND.colors,
645
+ accent: pick("accent", CARRIER_BRAND.colors.accent),
646
+ accentDark: pick("accentDark", CARRIER_BRAND.colors.accentDark),
647
+ bg: pick("bg", CARRIER_BRAND.colors.bg),
648
+ text: pick("text", CARRIER_BRAND.colors.text)
649
+ },
650
+ carrierApiUrl: CARRIER_BRAND.carrierApiUrl
651
+ };
652
+ }
653
+ async function deploySite(target, brand, opts = {}) {
654
+ const statuses = await probeAll(target);
655
+ let chosen;
656
+ if (opts.preferred) {
657
+ const wanted = statuses.find((s) => s.id === opts.preferred);
658
+ if (!wanted?.ready) {
659
+ return {
660
+ ok: false,
661
+ projectName: slug(brand.name),
662
+ statuses,
663
+ reason: wanted?.reason ?? `${opts.preferred} is not available on this machine.`
664
+ };
982
665
  }
666
+ chosen = wanted;
667
+ } else {
668
+ chosen = rankTargets(statuses)[0];
983
669
  }
984
- const resolved = await resolveBin(def);
985
- if (!resolved) {
670
+ if (!chosen) {
986
671
  return {
987
- id,
988
- label: def.label,
989
- installed: false,
990
- authenticated: false,
991
- configured,
992
- ready: false,
993
- reason: def.npxPkg ? `${def.bin} not found \u2014 install it, or make npx available.` : `${def.bin} not found \u2014 install the Fly CLI (brew install flyctl).`
672
+ ok: false,
673
+ projectName: slug(brand.name),
674
+ statuses,
675
+ reason: "No deploy target is ready. " + statuses.map((s) => `${s.label}: ${s.reason ?? "unavailable"}`).join(" | ")
994
676
  };
995
677
  }
996
- const who = await run(resolved.bin, [...resolved.prefix, ...def.whoami], {
997
- cwd: storefront,
998
- timeoutMs: 6e4
999
- });
1000
- if (!who.ok) {
678
+ const projectName = await configuredProjectName(chosen.id, target) ?? slug(brand.name);
679
+ if (opts.customDomain && chosen.id === "cloudflare" && brand.domain) {
680
+ await setCustomDomain(target, brand.domain);
681
+ }
682
+ let staged = { staged: [], failed: [], cleanup: async () => {
683
+ } };
684
+ if (opts.pushSecrets !== false) {
685
+ const discovered = await discoverSecrets(target, opts.env ?? process.env);
686
+ const runtime = {};
687
+ for (const key of RUNTIME_SECRET_KEYS) {
688
+ const value = discovered[key];
689
+ if (value) runtime[key] = value;
690
+ }
691
+ staged = await stageSecrets(chosen.id, target, projectName, runtime);
692
+ }
693
+ try {
694
+ const outcome = await deployTo(chosen.id, target, projectName, {
695
+ secretsFile: staged.secretsFile
696
+ });
697
+ if (!outcome.ok) {
698
+ return { ok: false, projectName, statuses, target: chosen.id, reason: outcome.reason };
699
+ }
1001
700
  return {
1002
- id,
1003
- label: def.label,
1004
- installed: true,
1005
- authenticated: false,
1006
- configured,
1007
- ready: false,
1008
- reason: `${def.bin} is not logged in \u2014 run \`${def.loginHint}\`.`
701
+ ok: true,
702
+ projectName,
703
+ statuses,
704
+ target: chosen.id,
705
+ url: outcome.url,
706
+ secrets: { pushed: staged.staged, failed: staged.failed }
1009
707
  };
708
+ } finally {
709
+ await staged.cleanup();
1010
710
  }
1011
- return {
1012
- id,
1013
- label: def.label,
1014
- installed: true,
1015
- authenticated: true,
1016
- configured,
1017
- ready: true,
1018
- account: def.parseAccount?.(who.stdout, who.stderr)
1019
- };
1020
711
  }
1021
- async function probeAll(storefront) {
1022
- return Promise.all(TARGET_IDS.map((id) => probeTarget(id, storefront)));
712
+
713
+ // src/cli/lib/clerk.ts
714
+ var CLERK_API_BASE = "https://api.clerk.com/v1";
715
+ var PUBLISHABLE_KEYS = [
716
+ "CLERK_PUBLISHABLE_KEY",
717
+ "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY"
718
+ ];
719
+ var SECRET_KEYS = ["CLERK_SECRET_KEY"];
720
+ function firstNonEmpty(env, names) {
721
+ for (const name of names) {
722
+ const value = env[name]?.trim();
723
+ if (value) return value;
724
+ }
725
+ return void 0;
1023
726
  }
1024
- function rankTargets(statuses) {
1025
- return statuses.filter((s) => s.ready).sort((a, b) => {
1026
- if (a.configured !== b.configured) return a.configured ? -1 : 1;
1027
- return TARGET_IDS.indexOf(a.id) - TARGET_IDS.indexOf(b.id);
1028
- });
727
+ function looksPublishable(key) {
728
+ return /^pk_(test|live)_/.test(key);
1029
729
  }
1030
- function buildScriptFor(id) {
1031
- return DEFS[id].buildScript;
730
+ function looksSecret(key) {
731
+ return /^sk_(test|live)_/.test(key);
1032
732
  }
1033
- async function artifactMissing(id, storefront) {
1034
- const artifact = DEFS[id].artifact;
1035
- if (!artifact) return false;
1036
- return !await exists(join(storefront, artifact));
733
+ async function clerkFetch(path, token, init = {}) {
734
+ try {
735
+ const res = await fetch(`${CLERK_API_BASE}${path}`, {
736
+ method: init.method ?? "GET",
737
+ headers: {
738
+ Authorization: `Bearer ${token}`,
739
+ "Content-Type": "application/json"
740
+ },
741
+ body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
742
+ });
743
+ const text = await res.text();
744
+ let json;
745
+ try {
746
+ json = text ? JSON.parse(text) : void 0;
747
+ } catch {
748
+ json = void 0;
749
+ }
750
+ if (!res.ok) {
751
+ return { ok: false, status: res.status, json, error: clerkError(json) ?? text.slice(0, 300) };
752
+ }
753
+ return { ok: true, status: res.status, json };
754
+ } catch (e) {
755
+ return { ok: false, status: 0, error: e instanceof Error ? e.message : String(e) };
756
+ }
1037
757
  }
1038
- function parseDeployedUrl(output) {
1039
- return output.match(
1040
- /https:\/\/[^\s"']+\.(?:workers\.dev|vercel\.app|netlify\.app|fly\.dev)[^\s"']*/
1041
- )?.[0];
758
+ function clerkError(json) {
759
+ if (typeof json !== "object" || json === null) return void 0;
760
+ const errors = json.errors;
761
+ if (!Array.isArray(errors) || errors.length === 0) return void 0;
762
+ const first = errors[0];
763
+ return first.long_message ?? first.message;
1042
764
  }
1043
- async function putSecret(id, storefront, projectName, key, value) {
1044
- const def = DEFS[id];
1045
- const resolved = await resolveBin(def);
1046
- if (!resolved) return false;
1047
- const { args, stdin } = def.secretArgs(key, value, projectName);
1048
- const r = await run(resolved.bin, [...resolved.prefix, ...args], {
1049
- cwd: storefront,
1050
- timeoutMs: 12e4,
1051
- stdin
765
+ async function createClerkApplication(platformToken, opts) {
766
+ const body = {
767
+ name: opts.name,
768
+ environment_types: opts.production ? ["development", "production"] : ["development"]
769
+ };
770
+ if (opts.domain) body.domain = opts.domain;
771
+ const res = await clerkFetch("/platform/applications", platformToken, {
772
+ method: "POST",
773
+ body
1052
774
  });
1053
- return r.ok;
1054
- }
1055
- async function stageSecrets(id, storefront, projectName, secrets) {
1056
- const entries = Object.entries(secrets).filter(([, v]) => v?.trim());
1057
- const noop = { staged: [], failed: [], cleanup: async () => {
1058
- } };
1059
- if (entries.length === 0) return noop;
1060
- const def = DEFS[id];
1061
- const resolved = await resolveBin(def);
1062
- if (!resolved) return { staged: [], failed: entries.map(([k]) => k), cleanup: async () => {
1063
- } };
1064
- if (id === "cloudflare") {
1065
- const dir = await mkdtemp(join(tmpdir(), "carrier-secrets-"));
1066
- const file = join(dir, ".env");
1067
- const body = entries.map(([k, v]) => `${k}=${v}`).join("\n");
1068
- await writeFileMode(file, `${body}
1069
- `, { mode: 384 });
775
+ if (!res.ok) {
1070
776
  return {
1071
- staged: entries.map(([k]) => k),
1072
- failed: [],
1073
- secretsFile: file,
1074
- cleanup: async () => {
1075
- await rm(dir, { recursive: true, force: true });
1076
- }
777
+ ok: false,
778
+ tier: "platform",
779
+ reason: res.status === 401 || res.status === 403 ? `Clerk Platform API rejected the token (HTTP ${res.status}). Check CLERK_PLATFORM_API_KEY.` : `Clerk Platform API error: ${res.error ?? `HTTP ${res.status}`}`
1077
780
  };
1078
781
  }
1079
- if (id === "fly") {
1080
- const args = ["secrets", "set", "--stage", ...entries.map(([k, v]) => `${k}=${v}`)];
1081
- const r = await run(resolved.bin, [...resolved.prefix, ...args], {
1082
- cwd: storefront,
1083
- timeoutMs: 18e4
1084
- });
782
+ const payload = res.json;
783
+ const instances = payload?.instances ?? [];
784
+ const wanted = opts.production ? "production" : "development";
785
+ const instance = instances.find((i) => i.environment_type === wanted && i.secret_key && i.publishable_key) ?? instances.find((i) => i.secret_key && i.publishable_key);
786
+ if (!instance?.secret_key || !instance.publishable_key) {
1085
787
  return {
1086
- staged: r.ok ? entries.map(([k]) => k) : [],
1087
- failed: r.ok ? [] : entries.map(([k]) => k),
1088
- cleanup: async () => {
1089
- }
788
+ ok: false,
789
+ tier: "platform",
790
+ reason: "Clerk created the application but returned no instance keys. Read them with GET /platform/applications?include_secret_keys=true."
1090
791
  };
1091
792
  }
1092
- const staged = [];
1093
- const failed = [];
1094
- for (const [key, value] of entries) {
1095
- const ok = await putSecret(id, storefront, projectName, key, value);
1096
- (ok ? staged : failed).push(key);
793
+ return {
794
+ ok: true,
795
+ tier: "platform",
796
+ credentials: {
797
+ publishableKey: instance.publishable_key,
798
+ secretKey: instance.secret_key,
799
+ tier: "platform",
800
+ applicationId: payload?.application_id,
801
+ instanceId: instance.instance_id
802
+ }
803
+ };
804
+ }
805
+ async function mintKeylessClerkApp(storefront, deps) {
806
+ if (!await deps.which("npx")) {
807
+ return { ok: false, tier: "cli-keyless", reason: "npx not found \u2014 cannot run the Clerk CLI." };
808
+ }
809
+ const restore = await deps.snapshot(storefront);
810
+ try {
811
+ const r = await deps.run(
812
+ "npx",
813
+ ["--yes", "clerk@latest", "init", "--framework", "next", "--keyless", "--no-skills", "-y"],
814
+ { cwd: storefront, timeoutMs: 6e5 }
815
+ );
816
+ const envPath = `${storefront}/.env.local`;
817
+ const body = await deps.readFileIfExists(envPath) ?? "";
818
+ const publishableKey = matchEnv(body, "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY");
819
+ const secretKey = matchEnv(body, "CLERK_SECRET_KEY");
820
+ if (!publishableKey || !secretKey) {
821
+ const tail = `${r.stdout}${r.stderr}`.trim().split("\n").slice(-2).join(" ").slice(0, 300);
822
+ return {
823
+ ok: false,
824
+ tier: "cli-keyless",
825
+ reason: `Clerk CLI did not produce keys${tail ? `: ${tail}` : "."}`
826
+ };
827
+ }
828
+ if (!looksPublishable(publishableKey) || !looksSecret(secretKey)) {
829
+ return { ok: false, tier: "cli-keyless", reason: "Clerk CLI wrote keys in an unexpected format." };
830
+ }
831
+ return {
832
+ ok: true,
833
+ tier: "cli-keyless",
834
+ credentials: { publishableKey, secretKey, tier: "cli-keyless" }
835
+ };
836
+ } finally {
837
+ await restore();
1097
838
  }
1098
- return { staged, failed, cleanup: async () => {
1099
- } };
1100
839
  }
1101
- async function deployTo(id, storefront, projectName, opts = {}) {
1102
- const def = DEFS[id];
1103
- const resolved = await resolveBin(def);
1104
- if (!resolved) {
1105
- return { ok: false, target: id, projectName, reason: `${def.bin} not found.` };
840
+ function matchEnv(body, key) {
841
+ const line = body.split("\n").find((l) => l.trim().startsWith(`${key}=`));
842
+ const value = line?.slice(line.indexOf("=") + 1).trim();
843
+ return value || void 0;
844
+ }
845
+ function discoverClerkCredentials(env) {
846
+ const publishableKey = firstNonEmpty(env, PUBLISHABLE_KEYS);
847
+ const secretKey = firstNonEmpty(env, SECRET_KEYS);
848
+ if (!publishableKey || !secretKey) return void 0;
849
+ if (!looksPublishable(publishableKey) || !looksSecret(secretKey)) return void 0;
850
+ return { publishableKey, secretKey, tier: "discovered" };
851
+ }
852
+ var MANUAL_STEPS = [
853
+ "Open https://dashboard.clerk.com and create an application.",
854
+ "Copy the Publishable key (pk_...) and Secret key (sk_...) from API keys.",
855
+ "Put them in the storefront's .env.local as NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY.",
856
+ "Re-run `carrier site deploy` \u2014 the keys are pushed to the deploy target automatically."
857
+ ];
858
+ async function provisionClerk(opts) {
859
+ const platformToken = firstNonEmpty(opts.env, [
860
+ "CLERK_PLATFORM_API_KEY",
861
+ "CLERK_PLATFORM_TOKEN"
862
+ ]);
863
+ const notes = [];
864
+ if (platformToken && !opts.noCreate) {
865
+ const created = await createClerkApplication(platformToken, {
866
+ name: opts.name,
867
+ domain: opts.domain,
868
+ production: opts.production
869
+ });
870
+ if (created.ok) return created;
871
+ notes.push(created.reason ?? "Clerk Platform API call failed.");
1106
872
  }
1107
- await def.ensureConfig?.(storefront, projectName);
1108
- if (await artifactMissing(id, storefront)) {
873
+ const discovered = discoverClerkCredentials(opts.env);
874
+ if (discovered) {
1109
875
  return {
1110
- ok: false,
1111
- target: id,
1112
- projectName,
1113
- reason: `No ${def.artifact} build found \u2014 run the build step first.`
876
+ ok: true,
877
+ tier: "discovered",
878
+ credentials: discovered,
879
+ reason: notes.length ? notes.join(" ") : void 0
1114
880
  };
1115
881
  }
1116
- const args = [...resolved.prefix, ...def.deployArgs(projectName)];
1117
- if (id === "cloudflare" && opts.secretsFile) args.push("--secrets-file", opts.secretsFile);
1118
- const r = await run(resolved.bin, args, {
1119
- cwd: storefront,
1120
- timeoutMs: 9e5
1121
- });
1122
- const combined = `${r.stdout}${r.stderr}`;
1123
- if (!r.ok) {
1124
- const tail = combined.trim().split("\n").slice(-3).join(" ").slice(0, 400);
1125
- return { ok: false, target: id, projectName, reason: tail || `${def.bin} deploy failed.` };
882
+ if (opts.cli && opts.storefront && !opts.noCreate) {
883
+ const minted = await mintKeylessClerkApp(opts.storefront, opts.cli);
884
+ if (minted.ok) {
885
+ return { ...minted, reason: notes.length ? notes.join(" ") : void 0 };
886
+ }
887
+ notes.push(minted.reason ?? "Clerk CLI keyless provisioning failed.");
1126
888
  }
1127
- return { ok: true, target: id, projectName, url: parseDeployedUrl(combined) };
1128
- }
1129
-
1130
- // src/cli/lib/brand.ts
1131
- var CARRIER_BRAND = {
1132
- name: "Carrier",
1133
- legalName: "Lifecycle Innovations Limited",
1134
- tagline: "Programmable connectivity, on demand.",
1135
- domain: "carrier.llc",
1136
- supportEmail: "support@carrier.llc",
1137
- supportUrl: "https://carrier.llc/help",
1138
- supportWhatsapp: "+17864604829",
1139
- colors: {
1140
- bg: "#080C16",
1141
- accent: "#FF6B35",
1142
- accentDark: "#D9461C",
1143
- text: "#F5F1EA"
1144
- },
1145
- social: {
1146
- x: "@carrier_llc",
1147
- instagram: "@carrier.llc",
1148
- tiktok: "@carrier.llc"
1149
- },
1150
- carrierApiUrl: "https://api.carrier.llc"
1151
- };
1152
- var CARRIER_ACCENT_LIGHT = "#FFB088";
1153
- var CARRIER_ACCENT_GRADIENT_START = "#FF7A45";
1154
- function hexToRgb(hex) {
1155
- const h = hex.replace(/^#/, "");
1156
- if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
1157
- return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
1158
- }
1159
- function rgbToHex(r, g, b, lower = false) {
1160
- const fmt = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
1161
- const out = `#${fmt(r)}${fmt(g)}${fmt(b)}`;
1162
- return lower ? out : out.toUpperCase();
1163
- }
1164
- function mixHexWithWhite(hex, whiteRatio) {
1165
- const rgb = hexToRgb(hex);
1166
- if (!rgb) return hex;
1167
- const mix2 = (n) => n + (255 - n) * whiteRatio;
1168
- return rgbToHex(mix2(rgb[0]), mix2(rgb[1]), mix2(rgb[2]));
1169
- }
1170
- function darkenHex(hex, factor) {
1171
- const rgb = hexToRgb(hex);
1172
- if (!rgb) return hex;
1173
- const scale = (n) => n * factor;
1174
- return rgbToHex(scale(rgb[0]), scale(rgb[1]), scale(rgb[2]), true);
1175
- }
1176
- function deriveAccentDark(accent, seed = CARRIER_BRAND) {
1177
- if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return seed.colors.accentDark;
1178
- const rgb = hexToRgb(accent);
1179
- if (!rgb) return accent;
1180
- const scale = (n) => Math.max(0, Math.min(255, Math.round(n * 0.75)));
1181
- return rgbToHex(scale(rgb[0]), scale(rgb[1]), scale(rgb[2]));
889
+ if (!platformToken) {
890
+ notes.push(
891
+ "No CLERK_PLATFORM_API_KEY set, so a new Clerk application cannot be created through the Platform API (it is a partner surface, not self-serve)."
892
+ );
893
+ }
894
+ return {
895
+ ok: false,
896
+ tier: "manual",
897
+ reason: notes.join(" "),
898
+ guidance: MANUAL_STEPS
899
+ };
1182
900
  }
1183
- function deriveAccentLight(accent, seed = CARRIER_BRAND) {
1184
- if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return CARRIER_ACCENT_LIGHT;
1185
- return mixHexWithWhite(accent, 0.45);
901
+ async function configureClerkInstance(secretKey, opts) {
902
+ const applied = [];
903
+ const failed = [];
904
+ const origins = dedupe(opts.allowedOrigins ?? []);
905
+ if (origins.length > 0) {
906
+ const res = await clerkFetch("/instance", secretKey, {
907
+ method: "PATCH",
908
+ body: { allowed_origins: origins }
909
+ });
910
+ if (res.ok) applied.push(`allowed_origins (${origins.length})`);
911
+ else failed.push({ step: "allowed_origins", reason: res.error ?? `HTTP ${res.status}` });
912
+ }
913
+ for (const url of dedupe(opts.redirectUrls ?? [])) {
914
+ const res = await clerkFetch("/redirect_urls", secretKey, { method: "POST", body: { url } });
915
+ if (res.ok) applied.push(`redirect_url ${url}`);
916
+ else failed.push({ step: `redirect_url ${url}`, reason: res.error ?? `HTTP ${res.status}` });
917
+ }
918
+ return { ok: failed.length === 0, applied, failed };
1186
919
  }
1187
- function deriveAccentGradientStart(accent, seed = CARRIER_BRAND) {
1188
- if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return CARRIER_ACCENT_GRADIENT_START;
1189
- return mixHexWithWhite(accent, 0.08);
920
+ function dedupe(values) {
921
+ return [...new Set(values.map((v) => v.trim()).filter(Boolean))];
1190
922
  }
1191
- function deriveAccentPaletteSubs(accent, accentDark) {
1192
- if (accent.toUpperCase() === CARRIER_BRAND.colors.accent.toUpperCase()) return [];
1193
- const accentLight = deriveAccentLight(accent);
1194
- const gradientStart = deriveAccentGradientStart(accent);
1195
- return [
1196
- ["#FF6B35", accent],
1197
- ["#D9461C", accentDark],
1198
- ["#FFB088", accentLight],
1199
- ["#FF7A45", gradientStart],
1200
- ["#fff4ef", mixHexWithWhite(accent, 0.94).toLowerCase()],
1201
- ["#ffe0d0", mixHexWithWhite(accent, 0.85).toLowerCase()],
1202
- ["#ffbfa0", mixHexWithWhite(accent, 0.7).toLowerCase()],
1203
- ["#ff9970", mixHexWithWhite(accent, 0.55).toLowerCase()],
1204
- ["#ff7d4d", mixHexWithWhite(accent, 0.4).toLowerCase()],
1205
- ["#b33a17", darkenHex(accentDark, 0.75)],
1206
- ["#8a2d12", darkenHex(accentDark, 0.58)],
1207
- ["#5e1e0c", darkenHex(accentDark, 0.4)],
1208
- ["#3a1107", darkenHex(accentDark, 0.25)]
1209
- ];
923
+ function storefrontClerkUrls(deployedUrl, domain) {
924
+ const bases = dedupe([
925
+ deployedUrl?.replace(/\/$/, "") ?? "",
926
+ domain ? `https://${domain.replace(/^https?:\/\//, "").replace(/\/$/, "")}` : "",
927
+ "http://localhost:3000"
928
+ ]);
929
+ return {
930
+ allowedOrigins: bases,
931
+ redirectUrls: bases.flatMap((b) => [b, `${b}/checkout/success`, `${b}/dashboard`])
932
+ };
1210
933
  }
1211
- function renderEnv(brand) {
1212
- return [
1213
- `# Generated by @carrierllc/mcp`,
1214
- `NEXT_PUBLIC_BRAND_NAME=${JSON.stringify(brand.name)}`,
1215
- `NEXT_PUBLIC_CARRIER_API_URL=${JSON.stringify(brand.carrierApiUrl)}`,
1216
- `# Public site origin. Used to build absolute URLs for guest checkout.`,
1217
- `# \`carrier site deploy\` writes the deployed URL back here.`,
1218
- `NEXT_PUBLIC_APP_URL=`,
1219
- `# Required whenever NEXT_PUBLIC_CARRIER_API_URL points at a live origin:`,
1220
- `# the catalog client throws without it, so / and /shop return 500.`,
1221
- `# \`carrier site deploy\` fills this in and pushes it to the deploy target.`,
1222
- `CARRIER_API_KEY=`,
1223
- `# Guest checkout calls Stripe server-side. Without this, checkout fails at`,
1224
- `# request time while every other page keeps working.`,
1225
- `STRIPE_SECRET_KEY=`,
1226
- `# Clerk \u2014 \`carrier site clerk\` fills these in, or paste them from dashboard.clerk.com`,
1227
- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=`,
1228
- `CLERK_SECRET_KEY=`,
1229
- ``
1230
- ].join("\n");
934
+
935
+ // src/cli/lib/clerk-cli.ts
936
+ import { join as join4 } from "path";
937
+ import { cp, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
938
+ import { tmpdir as tmpdir2 } from "os";
939
+ var SNAPSHOT_PATHS = ["src", "package.json", "next.config.mjs", "middleware.ts"];
940
+ function clerkCliDeps() {
941
+ return {
942
+ which,
943
+ run: async (cmd, args, opts) => {
944
+ const r = await run(cmd, args, opts);
945
+ return { ok: r.ok, stdout: r.stdout, stderr: r.stderr };
946
+ },
947
+ readFileIfExists: async (path) => {
948
+ if (!await exists(path)) return void 0;
949
+ try {
950
+ return await readFile(path, "utf8");
951
+ } catch {
952
+ return void 0;
953
+ }
954
+ },
955
+ snapshot: async (storefront) => {
956
+ const backup = await mkdtemp2(join4(tmpdir2(), "carrier-clerk-snap-"));
957
+ const saved = [];
958
+ for (const rel of SNAPSHOT_PATHS) {
959
+ const src = join4(storefront, rel);
960
+ if (!await exists(src)) continue;
961
+ await cp(src, join4(backup, rel), { recursive: true });
962
+ saved.push(rel);
963
+ }
964
+ return async () => {
965
+ try {
966
+ for (const rel of saved) {
967
+ const target = join4(storefront, rel);
968
+ await rm2(target, { recursive: true, force: true });
969
+ await cp(join4(backup, rel), target, { recursive: true });
970
+ }
971
+ } finally {
972
+ await rm2(backup, { recursive: true, force: true });
973
+ }
974
+ };
975
+ }
976
+ };
1231
977
  }
1232
978
 
1233
- // src/cli/lib/storefront-secrets.ts
1234
- import { homedir } from "os";
1235
- import { join as join2 } from "path";
1236
- var TEMPLATE_ENV_KEYS = {
1237
- NEXT_PUBLIC_BRAND_NAME: "public",
1238
- NEXT_PUBLIC_CARRIER_API_URL: "public",
1239
- NEXT_PUBLIC_APP_URL: "public",
1240
- NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: "public",
1241
- CARRIER_API_KEY: "secret",
1242
- CLERK_SECRET_KEY: "secret",
1243
- STRIPE_SECRET_KEY: "secret"
1244
- };
1245
- function keysOfKind(kind) {
1246
- return Object.keys(TEMPLATE_ENV_KEYS).filter(
1247
- (k) => TEMPLATE_ENV_KEYS[k] === kind
1248
- );
979
+ // src/cli/lib/verify-storefront.ts
980
+ var PROBES = [
981
+ { path: "/" },
982
+ { path: "/shop", expectBody: /·/ },
983
+ // plan names render as "Visit · Calm"
984
+ { path: "/help" }
985
+ ];
986
+ async function probe(base, path, expectBody) {
987
+ const url = `${base.replace(/\/$/, "")}${path}${path.includes("?") ? "&" : "?"}_v=${Date.now()}`;
988
+ try {
989
+ const res = await fetch(url, { redirect: "manual" });
990
+ const status = res.status;
991
+ const ok = status < 400;
992
+ if (!expectBody || !ok) return { path, status, ok };
993
+ const body = await res.text();
994
+ return { path, status, ok, bodyOk: expectBody.test(body) };
995
+ } catch (e) {
996
+ return { path, status: 0, ok: false, error: e instanceof Error ? e.message : String(e) };
997
+ }
1249
998
  }
1250
- var RUNTIME_SECRET_KEYS = keysOfKind("secret");
1251
- var PUBLIC_ENV_KEYS = keysOfKind("public");
1252
- function parseEnvFile(body) {
1253
- const out = {};
1254
- for (const raw of body.split("\n")) {
1255
- const line = raw.trim();
1256
- if (!line || line.startsWith("#")) continue;
1257
- const eq = line.indexOf("=");
1258
- if (eq <= 0) continue;
1259
- const key = line.slice(0, eq).trim();
1260
- let value = line.slice(eq + 1).trim();
1261
- if (value.startsWith('"') && value.endsWith('"') && value.length > 1 || value.startsWith("'") && value.endsWith("'") && value.length > 1) {
1262
- value = value.slice(1, -1);
1263
- }
1264
- if (value) out[key] = value;
999
+ function diagnose(probes, secrets) {
1000
+ const by = (p) => probes.find((x) => x.path === p);
1001
+ const root = by("/");
1002
+ const shop = by("/shop");
1003
+ const help = by("/help");
1004
+ if (probes.every((p) => p.ok && p.bodyOk !== false)) return "healthy";
1005
+ if (probes.every((p) => p.status === 0)) return "not-deployed";
1006
+ const catalogDown = root?.status === 500 && shop?.status === 500;
1007
+ if (catalogDown && help?.ok) {
1008
+ return needsCarrierKey(secrets) ? "carrier-key-missing" : "unclassified";
1265
1009
  }
1266
- return out;
1010
+ if (shop?.ok && shop.bodyOk === false) return "empty-catalog";
1011
+ if (root?.ok && shop?.ok && probes.some((p) => p.status >= 500)) return "clerk-keys-missing";
1012
+ if (probes.every((p) => p.status === 404)) return "not-deployed";
1013
+ return "unclassified";
1267
1014
  }
1268
- async function readEnvFile(path) {
1269
- if (!await exists(path)) return {};
1270
- try {
1271
- return parseEnvFile(await readFile(path, "utf8"));
1272
- } catch {
1273
- return {};
1015
+ var SUMMARIES = {
1016
+ healthy: "All probes returned a working page.",
1017
+ "carrier-key-missing": "Catalog pages return 500 while non-catalog pages work \u2014 CARRIER_API_KEY is missing or invalid on the host.",
1018
+ "clerk-keys-missing": "Public pages work but auth routes error \u2014 Clerk keys are missing on the host.",
1019
+ "empty-catalog": "/shop renders but no plans are in it \u2014 the Carrier catalog returned nothing sellable. Not a deploy problem.",
1020
+ "not-deployed": "Nothing served at the deployed URL \u2014 the deploy did not land, or the URL is wrong.",
1021
+ unclassified: "The storefront is not serving correctly and the symptom matches no known cause."
1022
+ };
1023
+ var REPAIRABLE = /* @__PURE__ */ new Set([
1024
+ "carrier-key-missing",
1025
+ "clerk-keys-missing"
1026
+ ]);
1027
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1028
+ async function verifyStorefront(url, storefront, env = process.env, opts = {}) {
1029
+ const attempts = Math.max(1, opts.attempts ?? 4);
1030
+ const delayMs = opts.delayMs ?? 5e3;
1031
+ const secrets = await discoverSecrets(storefront, env);
1032
+ let probes = [];
1033
+ let diagnosis = "unclassified";
1034
+ for (let attempt = 1; attempt <= attempts; attempt++) {
1035
+ probes = [];
1036
+ for (const { path, expectBody } of PROBES) {
1037
+ probes.push(await probe(url, path, expectBody));
1038
+ }
1039
+ diagnosis = diagnose(probes, secrets);
1040
+ if (diagnosis !== "not-deployed" || attempt === attempts) break;
1041
+ await sleep(delayMs);
1274
1042
  }
1043
+ const ok = diagnosis === "healthy";
1044
+ return {
1045
+ ok,
1046
+ url,
1047
+ probes,
1048
+ diagnosis,
1049
+ summary: SUMMARIES[diagnosis],
1050
+ repairable: !ok && REPAIRABLE.has(diagnosis)
1051
+ };
1275
1052
  }
1276
- async function discoverSecrets(storefront, env = process.env, home = homedir()) {
1277
- const local = await readEnvFile(join2(storefront, ".env.local"));
1278
- const global = await readEnvFile(join2(home, ".env"));
1279
- const merged = {};
1280
- const keys = [...RUNTIME_SECRET_KEYS, ...PUBLIC_ENV_KEYS];
1281
- for (const key of keys) {
1282
- const value = local[key]?.trim() || env[key]?.trim() || global[key]?.trim();
1283
- if (value) merged[key] = value;
1053
+ function formatProbes(probes) {
1054
+ return probes.map((p) => {
1055
+ const status = p.status === 0 ? "unreachable" : String(p.status);
1056
+ const body = p.bodyOk === false ? " (no plans)" : "";
1057
+ return `${p.path} ${status}${body}`;
1058
+ }).join(" ");
1059
+ }
1060
+ function repairPlanFor(diagnosis) {
1061
+ if (diagnosis === "carrier-key-missing") {
1062
+ return {
1063
+ diagnosis,
1064
+ runClerk: false,
1065
+ // CARRIER_API_KEY is read at request time, so re-staging and redeploying
1066
+ // is enough; no rebuild needed.
1067
+ rebuild: false,
1068
+ note: "Re-resolve CARRIER_API_KEY, stage it, and redeploy."
1069
+ };
1284
1070
  }
1285
- return merged;
1071
+ if (diagnosis === "clerk-keys-missing") {
1072
+ return {
1073
+ diagnosis,
1074
+ runClerk: true,
1075
+ // The publishable key is inlined at build time, so this one must rebuild.
1076
+ rebuild: true,
1077
+ note: "Provision Clerk, rebuild so the publishable key is inlined, and redeploy."
1078
+ };
1079
+ }
1080
+ return void 0;
1286
1081
  }
1287
- async function mergeEnvLocal(storefront, updates, opts = {}) {
1288
- const path = join2(storefront, ".env.local");
1289
- const overwrite = new Set(opts.overwrite ?? []);
1290
- const original = await exists(path) ? await readFile(path, "utf8") : "";
1291
- const lines = original ? original.split("\n") : [];
1292
- const written = [];
1293
- for (const [key, value] of Object.entries(updates)) {
1294
- if (!value) continue;
1295
- const index = lines.findIndex((l) => l.trim().startsWith(`${key}=`));
1296
- if (index === -1) {
1297
- lines.push(`${key}=${value}`);
1298
- written.push(key);
1299
- continue;
1300
- }
1301
- const current = lines[index].slice(lines[index].indexOf("=") + 1).trim();
1302
- const isEmpty = current === "" || current === '""' || current === "''";
1303
- if (isEmpty || overwrite.has(key)) {
1304
- lines[index] = `${key}=${value}`;
1305
- written.push(key);
1082
+
1083
+ // ../../packages/screens/dist/index.js
1084
+ function isBlankValue(value) {
1085
+ const text = String(value).trim();
1086
+ return text === "" || text === "\u2014" || text === "-" || Number(text) === 0;
1087
+ }
1088
+ function isEmptyScreen(screen) {
1089
+ return screen.sections.every((section) => {
1090
+ switch (section.kind) {
1091
+ case "metrics":
1092
+ return section.items.every((m) => isBlankValue(m.value));
1093
+ case "bars":
1094
+ return section.items.every((b) => b.value === 0);
1095
+ case "keyvalue":
1096
+ return section.items.every((kv) => isBlankValue(kv.value));
1097
+ case "table":
1098
+ return section.rows.length === 0;
1099
+ case "note":
1100
+ return false;
1306
1101
  }
1102
+ });
1103
+ }
1104
+ var colors = {
1105
+ /** Base page background — deepest ink */
1106
+ backgroundDark: "#080C16",
1107
+ backgroundDarkOklch: "oklch(0.09 0.02 260)",
1108
+ /** Surface elevation ladder — alpha blends, no drop-shadows */
1109
+ surface0: "#080C16",
1110
+ surface1: "rgba(15, 20, 34, 0.80)",
1111
+ surface2: "rgba(22, 28, 46, 0.90)",
1112
+ surface3: "rgba(31, 38, 56, 0.95)",
1113
+ /** Legacy alias — kept for backward compat */
1114
+ surfaceDark: "#0F1422",
1115
+ surfaceCard: "rgba(15, 20, 34, 0.75)",
1116
+ /** Borders */
1117
+ borderCard: "#1F2638",
1118
+ borderMuted: "rgba(255, 255, 255, 0.07)",
1119
+ borderSubtle: "rgba(255, 255, 255, 0.04)",
1120
+ /** Brand accent — Carrier Flame orange (do not replace with violet) */
1121
+ accentFlame: "#FF6B35",
1122
+ accentEmber: "#D9461C",
1123
+ accentSpark: "#FFB088",
1124
+ /** Legacy violet/fuchsia — used on esimmcp co-brand surface only */
1125
+ accentViolet: "#a78bfa",
1126
+ accentFuchsia: "#e879f9",
1127
+ /** Text hierarchy — tight ratio, generous contrast */
1128
+ textPrimary: "#F5F1EA",
1129
+ textSecondary: "#C9CCD6",
1130
+ textMuted: "#8A92A8",
1131
+ textFaint: "#5A6278",
1132
+ /** Status — do not deviate */
1133
+ statusSuccess: "#10b981",
1134
+ statusWarning: "#f59e0b",
1135
+ statusError: "#ef4444",
1136
+ /** Light mode equivalents */
1137
+ light: {
1138
+ background: "#ffffff",
1139
+ backgroundSecondary: "#f8fafc",
1140
+ surfaceCard: "rgba(248, 250, 252, 0.9)",
1141
+ borderCard: "#e2e8f0",
1142
+ textPrimary: "#0f172a",
1143
+ textSecondary: "#475569",
1144
+ textMuted: "#94a3b8"
1145
+ }
1146
+ };
1147
+ var typography = {
1148
+ fontSans: "var(--font-sans, Inter), system-ui, -apple-system, sans-serif",
1149
+ fontMono: "var(--font-mono, 'JetBrains Mono'), ui-monospace, monospace",
1150
+ fontSerif: "var(--font-serif, Georgia), 'Times New Roman', serif",
1151
+ /** Weights — only these three, per brand guide */
1152
+ weightRegular: "400",
1153
+ weightBold: "700",
1154
+ weightBlack: "900",
1155
+ /** Line heights */
1156
+ lineHeightHeadline: "1.05",
1157
+ lineHeightSubheading: "1.2",
1158
+ lineHeightBody: "1.5",
1159
+ /** Letter spacing — tight on big headlines */
1160
+ trackingTight: "-0.04em",
1161
+ trackingNormal: "0em",
1162
+ trackingWide: "0.05em",
1163
+ /** Type scale (rem) */
1164
+ scale: {
1165
+ xs: "0.75rem",
1166
+ sm: "0.875rem",
1167
+ base: "1rem",
1168
+ lg: "1.125rem",
1169
+ xl: "1.25rem",
1170
+ "2xl": "1.5rem",
1171
+ "3xl": "1.875rem",
1172
+ "4xl": "2.25rem",
1173
+ "5xl": "3rem",
1174
+ "6xl": "3.75rem",
1175
+ "7xl": "4.5rem"
1307
1176
  }
1308
- if (written.length === 0) return [];
1309
- const body = lines.join("\n").replace(/\n{3,}$/, "\n");
1310
- await writeFile(path, body.endsWith("\n") ? body : `${body}
1311
- `);
1312
- return written;
1177
+ };
1178
+ var radius = {
1179
+ sm: "0.375rem",
1180
+ md: "0.5rem",
1181
+ lg: "0.75rem",
1182
+ xl: "1rem",
1183
+ "2xl": "1.5rem",
1184
+ full: "9999px"
1185
+ };
1186
+ var TONE_COLOR = {
1187
+ ok: colors.statusSuccess,
1188
+ info: colors.accentFlame,
1189
+ warn: colors.statusWarning,
1190
+ critical: colors.statusError,
1191
+ muted: colors.textMuted
1192
+ };
1193
+ function esc(value) {
1194
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1313
1195
  }
1314
- function needsCarrierKey(secrets) {
1315
- const url = secrets.NEXT_PUBLIC_CARRIER_API_URL?.trim();
1316
- return Boolean(url) && !secrets.CARRIER_API_KEY?.trim();
1196
+ function toneColor(tone, fallback = colors.textPrimary) {
1197
+ return tone ? TONE_COLOR[tone] : fallback;
1317
1198
  }
1318
-
1319
- // src/cli/lib/site.ts
1320
- import { join as join3 } from "path";
1321
- function slug(s) {
1322
- return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "storefront";
1199
+ function metricsHtml(items) {
1200
+ const cards = items.map(
1201
+ (m) => `
1202
+ <div style="background:${colors.surface1};border:1px solid ${colors.borderCard};border-radius:${radius.lg};padding:16px 18px;min-width:0">
1203
+ <div style="font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:${colors.textMuted}">${esc(m.label)}</div>
1204
+ <div style="font-size:28px;font-weight:600;margin-top:6px;color:${toneColor(m.tone)};line-height:1.1">${esc(m.value)}</div>
1205
+ ${m.hint ? `<div style="font-size:12px;color:${colors.textFaint};margin-top:4px">${esc(m.hint)}</div>` : ""}
1206
+ </div>`
1207
+ ).join("");
1208
+ return `<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px">${cards}</div>`;
1323
1209
  }
1324
- async function installDeps(target) {
1325
- const pkgMgr = await which("pnpm") ? "pnpm" : "npm";
1326
- const r = await runInherit(pkgMgr, ["install"], { cwd: target });
1327
- return r.ok;
1210
+ function barsHtml(items, empty) {
1211
+ if (items.length === 0) return emptyHtml(empty ?? "Nothing to show.");
1212
+ const ceiling = Math.max(...items.map((b) => b.max ?? b.value), 1);
1213
+ const rows = items.map((b) => {
1214
+ const pct2 = Math.max(0, Math.min(100, (b.value / (b.max ?? ceiling) || 0) * 100));
1215
+ return `
1216
+ <div style="margin-bottom:10px">
1217
+ <div style="display:flex;justify-content:space-between;font-size:13px;color:${colors.textSecondary};margin-bottom:4px">
1218
+ <span>${esc(b.label)}</span>
1219
+ <span style="color:${colors.textMuted}">${esc(b.hint ?? b.value)}</span>
1220
+ </div>
1221
+ <div style="height:8px;background:${colors.surface2};border-radius:${radius.full};overflow:hidden">
1222
+ <div style="height:100%;width:${pct2.toFixed(1)}%;background:${toneColor(b.tone, colors.accentFlame)}"></div>
1223
+ </div>
1224
+ </div>`;
1225
+ }).join("");
1226
+ return `<div>${rows}</div>`;
1328
1227
  }
1329
- async function buildSite(target, forTarget = "cloudflare") {
1330
- const pkgMgr = await which("pnpm") ? "pnpm" : "npm";
1331
- const r = await runInherit(pkgMgr, ["run", buildScriptFor(forTarget)], { cwd: target });
1332
- return r.ok;
1228
+ function tableHtml(section) {
1229
+ if (section.rows.length === 0) return emptyHtml(section.empty ?? "No rows.");
1230
+ const numeric = new Set(section.numeric ?? []);
1231
+ const head = section.columns.map(
1232
+ (c, i) => `<th style="text-align:${numeric.has(i) ? "right" : "left"};padding:8px 12px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:${colors.textMuted};border-bottom:1px solid ${colors.borderCard};white-space:nowrap">${esc(c)}</th>`
1233
+ ).join("");
1234
+ const body = section.rows.map(
1235
+ (row) => `<tr>${row.map(
1236
+ (cell, i) => `<td style="text-align:${numeric.has(i) ? "right" : "left"};padding:8px 12px;font-size:13px;color:${colors.textSecondary};border-bottom:1px solid ${colors.borderSubtle};white-space:nowrap">${esc(cell)}</td>`
1237
+ ).join("")}</tr>`
1238
+ ).join("");
1239
+ return `<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse">${`<thead><tr>${head}</tr></thead>`}<tbody>${body}</tbody></table></div>`;
1333
1240
  }
1334
- async function loadStorefrontBrand(target, overrides) {
1335
- const configPath = join3(target, "src", "brand.config.ts");
1336
- if (!await exists(configPath)) {
1337
- return { ...CARRIER_BRAND, ...overrides };
1338
- }
1339
- const src = await readFile(configPath, "utf8");
1340
- const pick = (field, fallback) => {
1341
- const m = src.match(new RegExp(`\\b${field}:\\s*(?:[^"\\n]*\\?\\?\\s*)?"([^"]*)"`));
1342
- return m?.[1] ?? fallback;
1343
- };
1344
- return {
1345
- ...CARRIER_BRAND,
1346
- name: overrides?.name ?? pick("name", CARRIER_BRAND.name),
1347
- domain: pick("domain", CARRIER_BRAND.domain),
1348
- supportEmail: pick("supportEmail", CARRIER_BRAND.supportEmail),
1349
- supportUrl: pick("supportUrl", CARRIER_BRAND.supportUrl),
1350
- tagline: pick("tagline", CARRIER_BRAND.tagline),
1351
- legalName: pick("legalName", CARRIER_BRAND.legalName),
1352
- colors: {
1353
- ...CARRIER_BRAND.colors,
1354
- accent: pick("accent", CARRIER_BRAND.colors.accent),
1355
- accentDark: pick("accentDark", CARRIER_BRAND.colors.accentDark),
1356
- bg: pick("bg", CARRIER_BRAND.colors.bg),
1357
- text: pick("text", CARRIER_BRAND.colors.text)
1358
- },
1359
- carrierApiUrl: CARRIER_BRAND.carrierApiUrl
1360
- };
1241
+ function keyValueHtml(items) {
1242
+ const rows = items.map(
1243
+ (kv) => `
1244
+ <div style="display:flex;justify-content:space-between;gap:16px;padding:7px 0;border-bottom:1px solid ${colors.borderSubtle}">
1245
+ <span style="font-size:13px;color:${colors.textMuted}">${esc(kv.label)}</span>
1246
+ <span style="font-size:13px;color:${toneColor(kv.tone, colors.textPrimary)};text-align:right">${esc(kv.value)}</span>
1247
+ </div>`
1248
+ ).join("");
1249
+ return `<div>${rows}</div>`;
1361
1250
  }
1362
- async function deploySite(target, brand, opts = {}) {
1363
- const statuses = await probeAll(target);
1364
- let chosen;
1365
- if (opts.preferred) {
1366
- const wanted = statuses.find((s) => s.id === opts.preferred);
1367
- if (!wanted?.ready) {
1368
- return {
1369
- ok: false,
1370
- projectName: slug(brand.name),
1371
- statuses,
1372
- reason: wanted?.reason ?? `${opts.preferred} is not available on this machine.`
1373
- };
1374
- }
1375
- chosen = wanted;
1376
- } else {
1377
- chosen = rankTargets(statuses)[0];
1378
- }
1379
- if (!chosen) {
1380
- return {
1381
- ok: false,
1382
- projectName: slug(brand.name),
1383
- statuses,
1384
- reason: "No deploy target is ready. " + statuses.map((s) => `${s.label}: ${s.reason ?? "unavailable"}`).join(" | ")
1385
- };
1386
- }
1387
- const projectName = await configuredProjectName(chosen.id, target) ?? slug(brand.name);
1388
- if (opts.customDomain && chosen.id === "cloudflare" && brand.domain) {
1389
- await setCustomDomain(target, brand.domain);
1390
- }
1391
- let staged = { staged: [], failed: [], cleanup: async () => {
1392
- } };
1393
- if (opts.pushSecrets !== false) {
1394
- const discovered = await discoverSecrets(target, opts.env ?? process.env);
1395
- const runtime = {};
1396
- for (const key of RUNTIME_SECRET_KEYS) {
1397
- const value = discovered[key];
1398
- if (value) runtime[key] = value;
1399
- }
1400
- staged = await stageSecrets(chosen.id, target, projectName, runtime);
1401
- }
1402
- try {
1403
- const outcome = await deployTo(chosen.id, target, projectName, {
1404
- secretsFile: staged.secretsFile
1405
- });
1406
- if (!outcome.ok) {
1407
- return { ok: false, projectName, statuses, target: chosen.id, reason: outcome.reason };
1408
- }
1409
- return {
1410
- ok: true,
1411
- projectName,
1412
- statuses,
1413
- target: chosen.id,
1414
- url: outcome.url,
1415
- secrets: { pushed: staged.staged, failed: staged.failed }
1416
- };
1417
- } finally {
1418
- await staged.cleanup();
1419
- }
1251
+ function noteHtml(tone, text) {
1252
+ const c = toneColor(tone, colors.accentFlame);
1253
+ return `<div style="border-left:3px solid ${c};background:${colors.surface1};padding:10px 14px;border-radius:${radius.md};font-size:13px;color:${colors.textSecondary}">${esc(text)}</div>`;
1420
1254
  }
1421
-
1422
- // src/cli/lib/clerk.ts
1423
- var CLERK_API_BASE = "https://api.clerk.com/v1";
1424
- var PUBLISHABLE_KEYS = [
1425
- "CLERK_PUBLISHABLE_KEY",
1426
- "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY"
1427
- ];
1428
- var SECRET_KEYS = ["CLERK_SECRET_KEY"];
1429
- function firstNonEmpty(env, names) {
1430
- for (const name of names) {
1431
- const value = env[name]?.trim();
1432
- if (value) return value;
1255
+ function emptyHtml(text) {
1256
+ return `<div style="padding:14px;border:1px dashed ${colors.borderCard};border-radius:${radius.md};font-size:13px;color:${colors.textMuted}">${esc(text)}</div>`;
1257
+ }
1258
+ function sectionHtml(section) {
1259
+ const title = "title" in section && section.title ? `<h2 style="font-size:13px;font-weight:600;letter-spacing:.04em;color:${colors.textPrimary};margin:0 0 10px">${esc(section.title)}</h2>` : "";
1260
+ let body;
1261
+ switch (section.kind) {
1262
+ case "metrics":
1263
+ body = metricsHtml(section.items);
1264
+ break;
1265
+ case "bars":
1266
+ body = barsHtml(section.items, section.empty);
1267
+ break;
1268
+ case "table":
1269
+ body = tableHtml(section);
1270
+ break;
1271
+ case "keyvalue":
1272
+ body = keyValueHtml(section.items);
1273
+ break;
1274
+ case "note":
1275
+ body = noteHtml(section.tone, section.text);
1276
+ break;
1433
1277
  }
1434
- return void 0;
1278
+ return `<section style="margin-bottom:22px">${title}${body}</section>`;
1435
1279
  }
1436
- function looksPublishable(key) {
1437
- return /^pk_(test|live)_/.test(key);
1280
+ function renderHtml(screen) {
1281
+ const sections = isEmptyScreen(screen) ? emptyHtml(
1282
+ "No data came back for this view. That is a real result, not a loading state \u2014 check the account scope and credentials."
1283
+ ) : screen.sections.map(sectionHtml).join("");
1284
+ const actions = screen.actions?.length ? `<section style="margin-top:6px;display:flex;flex-wrap:wrap;gap:8px">${screen.actions.map(
1285
+ (a) => `<span title="${esc(a.description ?? a.command)}" style="font-size:12px;color:${colors.textSecondary};background:${colors.surface2};border:1px solid ${colors.borderCard};border-radius:${radius.full};padding:6px 12px">${esc(a.label)} <code style="color:${colors.textFaint}">${esc(a.command)}</code></span>`
1286
+ ).join("")}</section>` : "";
1287
+ return `<!doctype html>
1288
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
1289
+ <title>${esc(screen.title)}</title></head>
1290
+ <body style="margin:0;background:${colors.surface0};color:${colors.textPrimary};font-family:${typography.fontSans};padding:20px">
1291
+ <header style="margin-bottom:20px">
1292
+ <h1 style="font-size:18px;font-weight:600;margin:0;letter-spacing:-.01em">${esc(screen.title)}</h1>
1293
+ ${screen.subtitle ? `<p style="margin:4px 0 0;font-size:13px;color:${colors.textMuted}">${esc(screen.subtitle)}</p>` : ""}
1294
+ </header>
1295
+ ${sections}
1296
+ ${actions}
1297
+ ${screen.footer ? `<footer style="margin-top:18px;font-size:11px;color:${colors.textFaint}">${esc(screen.footer)}</footer>` : ""}
1298
+ </body></html>`;
1438
1299
  }
1439
- function looksSecret(key) {
1440
- return /^sk_(test|live)_/.test(key);
1300
+ var ANSI = {
1301
+ ok: "\x1B[32m",
1302
+ info: "\x1B[38;5;209m",
1303
+ // Carrier flame, nearest 256-colour
1304
+ warn: "\x1B[33m",
1305
+ critical: "\x1B[31m",
1306
+ muted: "\x1B[90m",
1307
+ reset: "\x1B[0m",
1308
+ bold: "\x1B[1m",
1309
+ dim: "\x1B[2m"
1310
+ };
1311
+ function paint(text, code, color) {
1312
+ return color ? `${code}${text}${ANSI.reset}` : text;
1313
+ }
1314
+ function displayWidth(text) {
1315
+ return text.replace(/\[[0-9;]*m/g, "").length;
1316
+ }
1317
+ function pad(text, width, align) {
1318
+ const gap = Math.max(0, width - displayWidth(text));
1319
+ return align === "right" ? " ".repeat(gap) + text : text + " ".repeat(gap);
1441
1320
  }
1442
- async function clerkFetch(path, token, init = {}) {
1443
- try {
1444
- const res = await fetch(`${CLERK_API_BASE}${path}`, {
1445
- method: init.method ?? "GET",
1446
- headers: {
1447
- Authorization: `Bearer ${token}`,
1448
- "Content-Type": "application/json"
1449
- },
1450
- body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
1451
- });
1452
- const text = await res.text();
1453
- let json;
1454
- try {
1455
- json = text ? JSON.parse(text) : void 0;
1456
- } catch {
1457
- json = void 0;
1458
- }
1459
- if (!res.ok) {
1460
- return { ok: false, status: res.status, json, error: clerkError(json) ?? text.slice(0, 300) };
1461
- }
1462
- return { ok: true, status: res.status, json };
1463
- } catch (e) {
1464
- return { ok: false, status: 0, error: e instanceof Error ? e.message : String(e) };
1465
- }
1321
+ function heading(text, color) {
1322
+ return paint(text.toUpperCase(), ANSI.bold, color);
1466
1323
  }
1467
- function clerkError(json) {
1468
- if (typeof json !== "object" || json === null) return void 0;
1469
- const errors = json.errors;
1470
- if (!Array.isArray(errors) || errors.length === 0) return void 0;
1471
- const first = errors[0];
1472
- return first.long_message ?? first.message;
1324
+ function metricsTui(items, width, color) {
1325
+ if (items.length === 0) return [];
1326
+ const labelWidth = Math.min(
1327
+ 28,
1328
+ Math.max(...items.map((m) => m.label.length))
1329
+ );
1330
+ return items.map((m) => {
1331
+ const label = paint(pad(m.label, labelWidth, "left"), ANSI.muted, color);
1332
+ const value = paint(String(m.value), m.tone ? ANSI[m.tone] : ANSI.bold, color);
1333
+ const hint = m.hint ? paint(` ${m.hint}`, ANSI.dim, color) : "";
1334
+ return ` ${label} ${value}${hint}`.slice(0, width + 64);
1335
+ });
1473
1336
  }
1474
- async function createClerkApplication(platformToken, opts) {
1475
- const body = {
1476
- name: opts.name,
1477
- environment_types: opts.production ? ["development", "production"] : ["development"]
1478
- };
1479
- if (opts.domain) body.domain = opts.domain;
1480
- const res = await clerkFetch("/platform/applications", platformToken, {
1481
- method: "POST",
1482
- body
1337
+ function barsTui(items, width, color, empty) {
1338
+ if (items.length === 0) return [` ${paint(empty ?? "Nothing to show.", ANSI.muted, color)}`];
1339
+ const ceiling = Math.max(...items.map((b) => b.max ?? b.value), 1);
1340
+ const labelWidth = Math.min(24, Math.max(...items.map((b) => b.label.length)));
1341
+ const barWidth = Math.max(10, Math.min(40, width - labelWidth - 22));
1342
+ return items.map((b) => {
1343
+ const ratio = b.value / (b.max ?? ceiling) || 0;
1344
+ const filled = Math.max(0, Math.min(barWidth, Math.round(ratio * barWidth)));
1345
+ const bar = paint("\u2588".repeat(filled), b.tone ? ANSI[b.tone] : ANSI.info, color) + paint("\u2591".repeat(barWidth - filled), ANSI.dim, color);
1346
+ const label = paint(pad(b.label, labelWidth, "left"), ANSI.muted, color);
1347
+ const value = paint(String(b.hint ?? b.value), ANSI.dim, color);
1348
+ return ` ${label} ${bar} ${value}`;
1483
1349
  });
1484
- if (!res.ok) {
1485
- return {
1486
- ok: false,
1487
- tier: "platform",
1488
- reason: res.status === 401 || res.status === 403 ? `Clerk Platform API rejected the token (HTTP ${res.status}). Check CLERK_PLATFORM_API_KEY.` : `Clerk Platform API error: ${res.error ?? `HTTP ${res.status}`}`
1489
- };
1350
+ }
1351
+ function tableTui(section, width, color) {
1352
+ if (section.rows.length === 0) {
1353
+ return [` ${paint(section.empty ?? "No rows.", ANSI.muted, color)}`];
1490
1354
  }
1491
- const payload = res.json;
1492
- const instances = payload?.instances ?? [];
1493
- const wanted = opts.production ? "production" : "development";
1494
- const instance = instances.find((i) => i.environment_type === wanted && i.secret_key && i.publishable_key) ?? instances.find((i) => i.secret_key && i.publishable_key);
1495
- if (!instance?.secret_key || !instance.publishable_key) {
1496
- return {
1497
- ok: false,
1498
- tier: "platform",
1499
- reason: "Clerk created the application but returned no instance keys. Read them with GET /platform/applications?include_secret_keys=true."
1500
- };
1355
+ const numeric = new Set(section.numeric ?? []);
1356
+ const cols = section.columns.length;
1357
+ const widths = Array.from(
1358
+ { length: cols },
1359
+ (_, i) => Math.max(
1360
+ section.columns[i]?.length ?? 0,
1361
+ ...section.rows.map((r) => String(r[i] ?? "").length)
1362
+ )
1363
+ );
1364
+ let total = widths.reduce((a, b) => a + b + 2, 2);
1365
+ while (total > width && Math.max(...widths) > 8) {
1366
+ const widest = widths.indexOf(Math.max(...widths));
1367
+ widths[widest] -= 1;
1368
+ total -= 1;
1501
1369
  }
1502
- return {
1503
- ok: true,
1504
- tier: "platform",
1505
- credentials: {
1506
- publishableKey: instance.publishable_key,
1507
- secretKey: instance.secret_key,
1508
- tier: "platform",
1509
- applicationId: payload?.application_id,
1510
- instanceId: instance.instance_id
1511
- }
1370
+ const clip = (cell, i) => {
1371
+ const text = String(cell ?? "");
1372
+ return text.length > widths[i] ? `${text.slice(0, Math.max(1, widths[i] - 1))}\u2026` : text;
1512
1373
  };
1374
+ const header = " " + section.columns.map((c, i) => paint(pad(clip(c, i), widths[i], numeric.has(i) ? "right" : "left"), ANSI.muted, color)).join(" ");
1375
+ const rule = " " + paint(widths.map((w) => "\u2500".repeat(w)).join(" "), ANSI.dim, color);
1376
+ const body = section.rows.map(
1377
+ (row) => " " + row.map((cell, i) => pad(clip(cell, i), widths[i], numeric.has(i) ? "right" : "left")).join(" ")
1378
+ );
1379
+ return [header, rule, ...body];
1513
1380
  }
1514
- async function mintKeylessClerkApp(storefront, deps) {
1515
- if (!await deps.which("npx")) {
1516
- return { ok: false, tier: "cli-keyless", reason: "npx not found \u2014 cannot run the Clerk CLI." };
1381
+ function keyValueTui(items, color) {
1382
+ if (items.length === 0) return [];
1383
+ const labelWidth = Math.min(30, Math.max(...items.map((kv) => kv.label.length)));
1384
+ return items.map(
1385
+ (kv) => ` ${paint(pad(kv.label, labelWidth, "left"), ANSI.muted, color)} ${paint(kv.value, kv.tone ? ANSI[kv.tone] : ANSI.reset, color)}`
1386
+ );
1387
+ }
1388
+ var NOTE_PREFIX = {
1389
+ ok: "ok",
1390
+ info: "note",
1391
+ warn: "warning",
1392
+ critical: "critical",
1393
+ muted: "note"
1394
+ };
1395
+ function sectionTui(section, width, color) {
1396
+ const lines = [];
1397
+ if ("title" in section && section.title) lines.push(heading(section.title, color));
1398
+ switch (section.kind) {
1399
+ case "metrics":
1400
+ lines.push(...metricsTui(section.items, width, color));
1401
+ break;
1402
+ case "bars":
1403
+ lines.push(...barsTui(section.items, width, color, section.empty));
1404
+ break;
1405
+ case "table":
1406
+ lines.push(...tableTui(section, width, color));
1407
+ break;
1408
+ case "keyvalue":
1409
+ lines.push(...keyValueTui(section.items, color));
1410
+ break;
1411
+ case "note":
1412
+ lines.push(
1413
+ ` ${paint(`${NOTE_PREFIX[section.tone]}:`, ANSI[section.tone], color)} ${section.text}`
1414
+ );
1415
+ break;
1517
1416
  }
1518
- const restore = await deps.snapshot(storefront);
1519
- try {
1520
- const r = await deps.run(
1521
- "npx",
1522
- ["--yes", "clerk@latest", "init", "--framework", "next", "--keyless", "--no-skills", "-y"],
1523
- { cwd: storefront, timeoutMs: 6e5 }
1417
+ lines.push("");
1418
+ return lines;
1419
+ }
1420
+ function renderTui(screen, opts = {}) {
1421
+ const width = Math.max(40, Math.min(160, opts.width ?? 80));
1422
+ const color = opts.color ?? true;
1423
+ const lines = [];
1424
+ lines.push(paint(screen.title, ANSI.bold, color));
1425
+ if (screen.subtitle) lines.push(paint(screen.subtitle, ANSI.muted, color));
1426
+ lines.push(paint("\u2500".repeat(width), ANSI.dim, color));
1427
+ lines.push("");
1428
+ if (isEmptyScreen(screen)) {
1429
+ lines.push(
1430
+ ` ${paint("No data came back for this view.", ANSI.warn, color)} That is a real result, not a`,
1431
+ " loading state \u2014 check the account scope and credentials.",
1432
+ ""
1524
1433
  );
1525
- const envPath = `${storefront}/.env.local`;
1526
- const body = await deps.readFileIfExists(envPath) ?? "";
1527
- const publishableKey = matchEnv(body, "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY");
1528
- const secretKey = matchEnv(body, "CLERK_SECRET_KEY");
1529
- if (!publishableKey || !secretKey) {
1530
- const tail = `${r.stdout}${r.stderr}`.trim().split("\n").slice(-2).join(" ").slice(0, 300);
1531
- return {
1532
- ok: false,
1533
- tier: "cli-keyless",
1534
- reason: `Clerk CLI did not produce keys${tail ? `: ${tail}` : "."}`
1535
- };
1536
- }
1537
- if (!looksPublishable(publishableKey) || !looksSecret(secretKey)) {
1538
- return { ok: false, tier: "cli-keyless", reason: "Clerk CLI wrote keys in an unexpected format." };
1434
+ } else {
1435
+ for (const section of screen.sections) lines.push(...sectionTui(section, width, color));
1436
+ }
1437
+ if (screen.actions?.length) {
1438
+ lines.push(heading("next", color));
1439
+ for (const a of screen.actions) {
1440
+ lines.push(` ${paint(a.command, ANSI.info, color)} ${paint(a.label, ANSI.dim, color)}`);
1539
1441
  }
1540
- return {
1541
- ok: true,
1542
- tier: "cli-keyless",
1543
- credentials: { publishableKey, secretKey, tier: "cli-keyless" }
1544
- };
1545
- } finally {
1546
- await restore();
1442
+ lines.push("");
1547
1443
  }
1444
+ if (screen.footer) lines.push(paint(screen.footer, ANSI.dim, color));
1445
+ return lines.join("\n");
1548
1446
  }
1549
- function matchEnv(body, key) {
1550
- const line = body.split("\n").find((l) => l.trim().startsWith(`${key}=`));
1551
- const value = line?.slice(line.indexOf("=") + 1).trim();
1552
- return value || void 0;
1553
- }
1554
- function discoverClerkCredentials(env) {
1555
- const publishableKey = firstNonEmpty(env, PUBLISHABLE_KEYS);
1556
- const secretKey = firstNonEmpty(env, SECRET_KEYS);
1557
- if (!publishableKey || !secretKey) return void 0;
1558
- if (!looksPublishable(publishableKey) || !looksSecret(secretKey)) return void 0;
1559
- return { publishableKey, secretKey, tier: "discovered" };
1447
+ var pct = (part, total) => total > 0 ? `${(part / total * 100).toFixed(1)}%` : "0%";
1448
+ var money = (value, currency = "") => `${currency}${value.toFixed(2)}`.trim();
1449
+ function humanBytes(bytes) {
1450
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 MB";
1451
+ const gb = bytes / 1073741824;
1452
+ if (gb >= 1) return `${gb.toFixed(gb >= 10 ? 0 : 1)} GB`;
1453
+ return `${(bytes / 1048576).toFixed(0)} MB`;
1560
1454
  }
1561
- var MANUAL_STEPS = [
1562
- "Open https://dashboard.clerk.com and create an application.",
1563
- "Copy the Publishable key (pk_...) and Secret key (sk_...) from API keys.",
1564
- "Put them in the storefront's .env.local as NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY.",
1565
- "Re-run `carrier site deploy` \u2014 the keys are pushed to the deploy target automatically."
1566
- ];
1567
- async function provisionClerk(opts) {
1568
- const platformToken = firstNonEmpty(opts.env, [
1569
- "CLERK_PLATFORM_API_KEY",
1570
- "CLERK_PLATFORM_TOKEN"
1571
- ]);
1572
- const notes = [];
1573
- if (platformToken && !opts.noCreate) {
1574
- const created = await createClerkApplication(platformToken, {
1575
- name: opts.name,
1576
- domain: opts.domain,
1577
- production: opts.production
1455
+ function fleetScreen(input) {
1456
+ const total = input.active + input.suspended + input.inventory + input.other;
1457
+ const lowBalance = input.accounts.filter((a) => a.balance < 10);
1458
+ const criticals = lowBalance.filter((a) => a.packageOnly && a.balance === 0);
1459
+ const sections = [
1460
+ {
1461
+ kind: "metrics",
1462
+ items: [
1463
+ { label: "Total eSIMs", value: total },
1464
+ { label: "Active", value: input.active, hint: pct(input.active, total), tone: "ok" },
1465
+ { label: "Inventory", value: input.inventory, hint: pct(input.inventory, total) },
1466
+ {
1467
+ label: "Suspended",
1468
+ value: input.suspended,
1469
+ hint: pct(input.suspended, total),
1470
+ tone: input.suspended > input.active * 0.1 ? "warn" : void 0
1471
+ },
1472
+ { label: "Accounts", value: input.accounts.length },
1473
+ {
1474
+ label: "Low balance",
1475
+ value: lowBalance.length,
1476
+ tone: lowBalance.length > 0 ? "warn" : "ok"
1477
+ }
1478
+ ]
1479
+ }
1480
+ ];
1481
+ const hasPerAccountCounts = input.accounts.some(
1482
+ (a) => a.active + a.suspended + a.inventory + a.other > 0
1483
+ );
1484
+ if (hasPerAccountCounts) {
1485
+ sections.push({
1486
+ kind: "bars",
1487
+ title: "eSIMs by account",
1488
+ items: [...input.accounts].sort((a, b) => b.active + b.inventory - (a.active + a.inventory)).slice(0, 10).map((a) => ({
1489
+ label: a.name,
1490
+ value: a.active + a.suspended + a.inventory + a.other,
1491
+ hint: `${a.active} active`,
1492
+ tone: a.active > 0 ? "ok" : "muted"
1493
+ }))
1578
1494
  });
1579
- if (created.ok) return created;
1580
- notes.push(created.reason ?? "Clerk Platform API call failed.");
1581
- }
1582
- const discovered = discoverClerkCredentials(opts.env);
1583
- if (discovered) {
1584
- return {
1585
- ok: true,
1586
- tier: "discovered",
1587
- credentials: discovered,
1588
- reason: notes.length ? notes.join(" ") : void 0
1589
- };
1590
1495
  }
1591
- if (opts.cli && opts.storefront && !opts.noCreate) {
1592
- const minted = await mintKeylessClerkApp(opts.storefront, opts.cli);
1593
- if (minted.ok) {
1594
- return { ...minted, reason: notes.length ? notes.join(" ") : void 0 };
1595
- }
1596
- notes.push(minted.reason ?? "Clerk CLI keyless provisioning failed.");
1496
+ sections.push({
1497
+ kind: "table",
1498
+ title: "Accounts",
1499
+ columns: ["Account", "Balance", "Active", "Inventory"],
1500
+ numeric: [1, 2, 3],
1501
+ empty: "No accounts under this reseller.",
1502
+ rows: input.accounts.map((a) => [a.name, money(a.balance), a.active, a.inventory])
1503
+ });
1504
+ if (criticals.length > 0) {
1505
+ sections.push({
1506
+ kind: "note",
1507
+ tone: "critical",
1508
+ text: `${criticals.length} package-only account(s) at zero balance \u2014 packages cannot be assigned until topped up: ${criticals.map((a) => a.name).join(", ")}.`
1509
+ });
1597
1510
  }
1598
- if (!platformToken) {
1599
- notes.push(
1600
- "No CLERK_PLATFORM_API_KEY set, so a new Clerk application cannot be created through the Platform API (it is a partner surface, not self-serve)."
1601
- );
1511
+ for (const missing of input.unavailable ?? []) {
1512
+ sections.push({ kind: "note", tone: "warn", text: `Unavailable: ${missing}` });
1602
1513
  }
1603
1514
  return {
1604
- ok: false,
1605
- tier: "manual",
1606
- reason: notes.join(" "),
1607
- guidance: MANUAL_STEPS
1515
+ id: "fleet-health",
1516
+ title: "Fleet health",
1517
+ subtitle: `${total} eSIMs across ${input.accounts.length} account(s) \xB7 ${pct(input.active, total)} utilisation`,
1518
+ sections,
1519
+ actions: [
1520
+ { label: "Top up an account", command: "wallet_topup_checkout" },
1521
+ { label: "Per-account eSIM status", command: "esim_status_per_account" }
1522
+ ]
1608
1523
  };
1609
1524
  }
1610
- async function configureClerkInstance(secretKey, opts) {
1611
- const applied = [];
1612
- const failed = [];
1613
- const origins = dedupe(opts.allowedOrigins ?? []);
1614
- if (origins.length > 0) {
1615
- const res = await clerkFetch("/instance", secretKey, {
1616
- method: "PATCH",
1617
- body: { allowed_origins: origins }
1525
+ function subscribersScreen(rows, opts = {}) {
1526
+ const byStatus = /* @__PURE__ */ new Map();
1527
+ for (const r of rows) byStatus.set(r.status, (byStatus.get(r.status) ?? 0) + 1);
1528
+ return {
1529
+ id: "subscribers",
1530
+ title: "Subscribers",
1531
+ subtitle: opts.account ? `Account ${opts.account} \xB7 ${rows.length} shown` : `${rows.length} shown`,
1532
+ sections: [
1533
+ {
1534
+ kind: "metrics",
1535
+ items: [
1536
+ { label: "Listed", value: rows.length },
1537
+ ...[...byStatus.entries()].map(([status, count]) => ({
1538
+ label: status,
1539
+ value: count,
1540
+ tone: status.toLowerCase() === "active" ? "ok" : void 0
1541
+ }))
1542
+ ]
1543
+ },
1544
+ {
1545
+ kind: "table",
1546
+ title: "Records",
1547
+ columns: ["ICCID", "MSISDN", "Status", "Account", "Data used"],
1548
+ numeric: [4],
1549
+ empty: "No subscribers matched. Widen the filter or check the account scope.",
1550
+ rows: rows.map((r) => [
1551
+ r.iccid,
1552
+ r.msisdn ?? "\u2014",
1553
+ r.status,
1554
+ r.account ?? "\u2014",
1555
+ r.dataUsedBytes === void 0 ? "\u2014" : humanBytes(r.dataUsedBytes)
1556
+ ])
1557
+ }
1558
+ ],
1559
+ actions: [
1560
+ { label: "Diagnose one", command: "diagnose_subscriber" },
1561
+ { label: "Usage detail", command: "subscriber_usage" }
1562
+ ]
1563
+ };
1564
+ }
1565
+ function usageScreen(input) {
1566
+ const total = input.totalBytes ?? input.timeline.reduce((sum, p) => sum + p.bytes, 0);
1567
+ const peak = input.timeline.reduce(
1568
+ (best, p) => p.bytes > best.bytes ? p : best,
1569
+ { date: "\u2014", bytes: 0 }
1570
+ );
1571
+ const sections = [
1572
+ {
1573
+ kind: "metrics",
1574
+ items: [
1575
+ { label: "Total", value: humanBytes(total) },
1576
+ { label: "Days", value: input.timeline.length },
1577
+ { label: "Peak day", value: humanBytes(peak.bytes), hint: peak.date },
1578
+ {
1579
+ label: "Daily average",
1580
+ value: humanBytes(input.timeline.length ? total / input.timeline.length : 0)
1581
+ }
1582
+ ]
1583
+ },
1584
+ {
1585
+ kind: "bars",
1586
+ title: "Daily usage",
1587
+ empty: "No usage recorded in this window.",
1588
+ items: input.timeline.map((p) => ({
1589
+ label: p.date,
1590
+ value: p.bytes,
1591
+ hint: humanBytes(p.bytes)
1592
+ }))
1593
+ }
1594
+ ];
1595
+ if (input.countries?.length) {
1596
+ sections.push({
1597
+ kind: "bars",
1598
+ title: "By country",
1599
+ items: input.countries.slice(0, 10).map((c) => ({ label: c.country, value: c.bytes, hint: humanBytes(c.bytes) }))
1618
1600
  });
1619
- if (res.ok) applied.push(`allowed_origins (${origins.length})`);
1620
- else failed.push({ step: "allowed_origins", reason: res.error ?? `HTTP ${res.status}` });
1621
- }
1622
- for (const url of dedupe(opts.redirectUrls ?? [])) {
1623
- const res = await clerkFetch("/redirect_urls", secretKey, { method: "POST", body: { url } });
1624
- if (res.ok) applied.push(`redirect_url ${url}`);
1625
- else failed.push({ step: `redirect_url ${url}`, reason: res.error ?? `HTTP ${res.status}` });
1626
1601
  }
1627
- return { ok: failed.length === 0, applied, failed };
1628
- }
1629
- function dedupe(values) {
1630
- return [...new Set(values.map((v) => v.trim()).filter(Boolean))];
1631
- }
1632
- function storefrontClerkUrls(deployedUrl, domain) {
1633
- const bases = dedupe([
1634
- deployedUrl?.replace(/\/$/, "") ?? "",
1635
- domain ? `https://${domain.replace(/^https?:\/\//, "").replace(/\/$/, "")}` : "",
1636
- "http://localhost:3000"
1637
- ]);
1638
1602
  return {
1639
- allowedOrigins: bases,
1640
- redirectUrls: bases.flatMap((b) => [b, `${b}/checkout/success`, `${b}/dashboard`])
1603
+ id: "usage",
1604
+ title: "Usage",
1605
+ subtitle: input.subject,
1606
+ sections,
1607
+ actions: [{ label: "Project depletion", command: "usage_projection" }]
1641
1608
  };
1642
1609
  }
1643
-
1644
- // src/cli/lib/clerk-cli.ts
1645
- import { join as join4 } from "path";
1646
- import { cp, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
1647
- import { tmpdir as tmpdir2 } from "os";
1648
- var SNAPSHOT_PATHS = ["src", "package.json", "next.config.mjs", "middleware.ts"];
1649
- function clerkCliDeps() {
1610
+ function packagesScreen(rows) {
1650
1611
  return {
1651
- which,
1652
- run: async (cmd, args, opts) => {
1653
- const r = await run(cmd, args, opts);
1654
- return { ok: r.ok, stdout: r.stdout, stderr: r.stderr };
1655
- },
1656
- readFileIfExists: async (path) => {
1657
- if (!await exists(path)) return void 0;
1658
- try {
1659
- return await readFile(path, "utf8");
1660
- } catch {
1661
- return void 0;
1612
+ id: "packages",
1613
+ title: "Package catalog",
1614
+ subtitle: `${rows.length} template(s)`,
1615
+ sections: [
1616
+ {
1617
+ kind: "metrics",
1618
+ items: [
1619
+ { label: "Templates", value: rows.length },
1620
+ { label: "Recurring", value: rows.filter((r) => r.recurring).length }
1621
+ ]
1622
+ },
1623
+ {
1624
+ kind: "table",
1625
+ title: "Templates",
1626
+ columns: ["Name", "ID", "Data", "Validity", "Price"],
1627
+ numeric: [2, 3, 4],
1628
+ empty: "No package templates visible to this account.",
1629
+ rows: rows.map((r) => [
1630
+ r.name,
1631
+ String(r.id),
1632
+ r.dataLimitBytes === void 0 ? "\u2014" : humanBytes(r.dataLimitBytes),
1633
+ r.validityDays === void 0 ? "\u2014" : `${r.validityDays}d`,
1634
+ r.price === void 0 ? "\u2014" : money(r.price)
1635
+ ])
1662
1636
  }
1663
- },
1664
- snapshot: async (storefront) => {
1665
- const backup = await mkdtemp2(join4(tmpdir2(), "carrier-clerk-snap-"));
1666
- const saved = [];
1667
- for (const rel of SNAPSHOT_PATHS) {
1668
- const src = join4(storefront, rel);
1669
- if (!await exists(src)) continue;
1670
- await cp(src, join4(backup, rel), { recursive: true });
1671
- saved.push(rel);
1637
+ ],
1638
+ actions: [{ label: "Assign to a subscriber", command: "assign_package" }]
1639
+ };
1640
+ }
1641
+ function billingScreen(input) {
1642
+ const spend = input.events.reduce((sum, e) => sum + (e.amount > 0 ? e.amount : 0), 0);
1643
+ return {
1644
+ id: "billing",
1645
+ title: "Billing",
1646
+ subtitle: `${input.events.length} recent event(s)`,
1647
+ sections: [
1648
+ {
1649
+ kind: "metrics",
1650
+ items: [
1651
+ {
1652
+ label: "Balance",
1653
+ value: input.balance === void 0 ? "\u2014" : money(input.balance, input.currency),
1654
+ tone: (input.balance ?? 0) < 10 ? "warn" : "ok"
1655
+ },
1656
+ {
1657
+ label: "Pending",
1658
+ value: input.pending === void 0 ? "\u2014" : money(input.pending, input.currency)
1659
+ },
1660
+ { label: "Recent spend", value: money(spend, input.currency) }
1661
+ ]
1662
+ },
1663
+ {
1664
+ kind: "table",
1665
+ title: "Recent events",
1666
+ columns: ["Date", "Description", "Amount"],
1667
+ numeric: [2],
1668
+ empty: "No billing events in this window.",
1669
+ rows: input.events.map((e) => [e.date, e.description, money(e.amount, input.currency)])
1672
1670
  }
1673
- return async () => {
1674
- try {
1675
- for (const rel of saved) {
1676
- const target = join4(storefront, rel);
1677
- await rm2(target, { recursive: true, force: true });
1678
- await cp(join4(backup, rel), target, { recursive: true });
1671
+ ],
1672
+ actions: [{ label: "Check payouts", command: "stripe_connect_payouts" }]
1673
+ };
1674
+ }
1675
+ function walletScreen(input) {
1676
+ const low = input.balance < (input.threshold ?? 10);
1677
+ return {
1678
+ id: "wallet",
1679
+ title: "Wallet",
1680
+ subtitle: input.autoTopupEnabled ? "Auto top-up is on" : "Auto top-up is off",
1681
+ sections: [
1682
+ {
1683
+ kind: "metrics",
1684
+ items: [
1685
+ {
1686
+ label: "Balance",
1687
+ value: money(input.balance, input.currency),
1688
+ tone: low ? "warn" : "ok"
1689
+ },
1690
+ ...input.credits === void 0 ? [] : [{ label: "Credits", value: input.credits }]
1691
+ ]
1692
+ },
1693
+ {
1694
+ kind: "keyvalue",
1695
+ title: "Settings",
1696
+ items: [
1697
+ { label: "Auto top-up", value: input.autoTopupEnabled ? "enabled" : "disabled" },
1698
+ {
1699
+ label: "Threshold",
1700
+ value: input.threshold === void 0 ? "\u2014" : money(input.threshold, input.currency)
1679
1701
  }
1680
- } finally {
1681
- await rm2(backup, { recursive: true, force: true });
1702
+ ]
1703
+ },
1704
+ ...low ? [
1705
+ {
1706
+ kind: "note",
1707
+ tone: "warn",
1708
+ text: "Balance is under the top-up threshold. Package assignment fails at zero on package-only accounts."
1682
1709
  }
1683
- };
1684
- }
1710
+ ] : []
1711
+ ],
1712
+ actions: [
1713
+ { label: "Top up", command: "wallet_topup_checkout" },
1714
+ { label: "Configure auto top-up", command: "wallet_auto_topup" }
1715
+ ]
1685
1716
  };
1686
1717
  }
1687
-
1688
- // src/cli/lib/verify-storefront.ts
1689
- var PROBES = [
1690
- { path: "/" },
1691
- { path: "/shop", expectBody: /·/ },
1692
- // plan names render as "Visit · Calm"
1693
- { path: "/help" }
1694
- ];
1695
- async function probe(base, path, expectBody) {
1696
- const url = `${base.replace(/\/$/, "")}${path}${path.includes("?") ? "&" : "?"}_v=${Date.now()}`;
1697
- try {
1698
- const res = await fetch(url, { redirect: "manual" });
1699
- const status = res.status;
1700
- const ok = status < 400;
1701
- if (!expectBody || !ok) return { path, status, ok };
1702
- const body = await res.text();
1703
- return { path, status, ok, bodyOk: expectBody.test(body) };
1704
- } catch (e) {
1705
- return { path, status: 0, ok: false, error: e instanceof Error ? e.message : String(e) };
1706
- }
1707
- }
1708
- function diagnose(probes, secrets) {
1709
- const by = (p) => probes.find((x) => x.path === p);
1710
- const root = by("/");
1711
- const shop = by("/shop");
1712
- const help = by("/help");
1713
- if (probes.every((p) => p.ok && p.bodyOk !== false)) return "healthy";
1714
- if (probes.every((p) => p.status === 0)) return "not-deployed";
1715
- const catalogDown = root?.status === 500 && shop?.status === 500;
1716
- if (catalogDown && help?.ok) {
1717
- return needsCarrierKey(secrets) ? "carrier-key-missing" : "unclassified";
1718
- }
1719
- if (shop?.ok && shop.bodyOk === false) return "empty-catalog";
1720
- if (root?.ok && shop?.ok && probes.some((p) => p.status >= 500)) return "clerk-keys-missing";
1721
- if (probes.every((p) => p.status === 404)) return "not-deployed";
1722
- return "unclassified";
1723
- }
1724
- var SUMMARIES = {
1725
- healthy: "All probes returned a working page.",
1726
- "carrier-key-missing": "Catalog pages return 500 while non-catalog pages work \u2014 CARRIER_API_KEY is missing or invalid on the host.",
1727
- "clerk-keys-missing": "Public pages work but auth routes error \u2014 Clerk keys are missing on the host.",
1728
- "empty-catalog": "/shop renders but no plans are in it \u2014 the Carrier catalog returned nothing sellable. Not a deploy problem.",
1729
- "not-deployed": "Nothing served at the deployed URL \u2014 the deploy did not land, or the URL is wrong.",
1730
- unclassified: "The storefront is not serving correctly and the symptom matches no known cause."
1731
- };
1732
- var REPAIRABLE = /* @__PURE__ */ new Set([
1733
- "carrier-key-missing",
1734
- "clerk-keys-missing"
1735
- ]);
1736
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1737
- async function verifyStorefront(url, storefront, env = process.env, opts = {}) {
1738
- const attempts = Math.max(1, opts.attempts ?? 4);
1739
- const delayMs = opts.delayMs ?? 5e3;
1740
- const secrets = await discoverSecrets(storefront, env);
1741
- let probes = [];
1742
- let diagnosis = "unclassified";
1743
- for (let attempt = 1; attempt <= attempts; attempt++) {
1744
- probes = [];
1745
- for (const { path, expectBody } of PROBES) {
1746
- probes.push(await probe(url, path, expectBody));
1747
- }
1748
- diagnosis = diagnose(probes, secrets);
1749
- if (diagnosis !== "not-deployed" || attempt === attempts) break;
1750
- await sleep(delayMs);
1751
- }
1752
- const ok = diagnosis === "healthy";
1718
+ function greenzoneScreen(entries) {
1753
1719
  return {
1754
- ok,
1755
- url,
1756
- probes,
1757
- diagnosis,
1758
- summary: SUMMARIES[diagnosis],
1759
- repairable: !ok && REPAIRABLE.has(diagnosis)
1720
+ id: "greenzone",
1721
+ title: "Greenzone whitelist",
1722
+ subtitle: `${entries.length} entr${entries.length === 1 ? "y" : "ies"}`,
1723
+ sections: [
1724
+ { kind: "metrics", items: [{ label: "Entries", value: entries.length }] },
1725
+ {
1726
+ kind: "table",
1727
+ title: "Whitelisted",
1728
+ columns: ["Value", "Note"],
1729
+ empty: "Whitelist is empty \u2014 every destination follows the default policy.",
1730
+ rows: entries.map((e) => [e.value, e.note ?? "\u2014"])
1731
+ }
1732
+ ],
1733
+ actions: [
1734
+ { label: "Add an entry", command: "greenzone_whitelist_add" },
1735
+ { label: "Remove an entry", command: "greenzone_whitelist_remove" }
1736
+ ]
1760
1737
  };
1761
1738
  }
1762
- function formatProbes(probes) {
1763
- return probes.map((p) => {
1764
- const status = p.status === 0 ? "unreachable" : String(p.status);
1765
- const body = p.bodyOk === false ? " (no plans)" : "";
1766
- return `${p.path} ${status}${body}`;
1767
- }).join(" ");
1768
- }
1769
- function repairPlanFor(diagnosis) {
1770
- if (diagnosis === "carrier-key-missing") {
1771
- return {
1772
- diagnosis,
1773
- runClerk: false,
1774
- // CARRIER_API_KEY is read at request time, so re-staging and redeploying
1775
- // is enough; no rebuild needed.
1776
- rebuild: false,
1777
- note: "Re-resolve CARRIER_API_KEY, stage it, and redeploy."
1778
- };
1739
+ function storefrontScreen(input) {
1740
+ const sections = [
1741
+ {
1742
+ kind: "metrics",
1743
+ items: [
1744
+ {
1745
+ label: "Status",
1746
+ value: input.verified === true ? "serving" : input.verified === false ? "broken" : "unknown",
1747
+ tone: input.verified === true ? "ok" : input.verified === false ? "critical" : "muted"
1748
+ },
1749
+ { label: "Host", value: input.target ?? "\u2014" },
1750
+ { label: "Plans", value: input.plans ?? "\u2014" },
1751
+ { label: "Secrets", value: input.secretsStaged?.length ?? 0 }
1752
+ ]
1753
+ },
1754
+ {
1755
+ kind: "keyvalue",
1756
+ title: "Deployment",
1757
+ items: [
1758
+ { label: "Brand", value: input.brand },
1759
+ { label: "URL", value: input.url ?? "not deployed" },
1760
+ ...input.diagnosis ? [{ label: "Diagnosis", value: input.diagnosis, tone: "warn" }] : []
1761
+ ]
1762
+ }
1763
+ ];
1764
+ if (input.probes?.length) {
1765
+ sections.push({
1766
+ kind: "table",
1767
+ title: "Probes",
1768
+ columns: ["Path", "Status"],
1769
+ numeric: [1],
1770
+ rows: input.probes.map((p) => [p.path, p.status === 0 ? "unreachable" : p.status])
1771
+ });
1779
1772
  }
1780
- if (diagnosis === "clerk-keys-missing") {
1781
- return {
1782
- diagnosis,
1783
- runClerk: true,
1784
- // The publishable key is inlined at build time, so this one must rebuild.
1785
- rebuild: true,
1786
- note: "Provision Clerk, rebuild so the publishable key is inlined, and redeploy."
1787
- };
1773
+ if (input.secretsMissing?.length) {
1774
+ sections.push({
1775
+ kind: "note",
1776
+ tone: "critical",
1777
+ text: `Missing runtime secret(s): ${input.secretsMissing.join(", ")}. The build and deploy both succeed without them and the site fails at request time.`
1778
+ });
1788
1779
  }
1789
- return void 0;
1780
+ return {
1781
+ id: "storefront",
1782
+ title: "Storefront",
1783
+ subtitle: input.url ?? input.brand,
1784
+ sections,
1785
+ actions: [
1786
+ { label: "Deploy", command: "carrier site deploy" },
1787
+ { label: "Check hosts", command: "carrier site targets" }
1788
+ ]
1789
+ };
1790
1790
  }
1791
1791
 
1792
1792
  // package.json
1793
1793
  var package_default = {
1794
1794
  name: "@carrierllc/mcp",
1795
- version: "0.6.0",
1795
+ version: "0.7.1",
1796
1796
  description: "Carrier MCP \u2014 natural-language control of MVNO/eSIM fleets via eSIMVault OCS. Stdio mode for direct integration with Claude Desktop, Cursor, Windsurf, and MCP-compatible clients. Ships the `carrier` CLI (plugin install + white-label eSIM storefront scaffold).",
1797
1797
  license: "MIT",
1798
1798
  author: "Carrier (Lifecycle Innovations Limited)",
@@ -2345,6 +2345,47 @@ function applySubscriberFilters(raw, args) {
2345
2345
  }
2346
2346
  return rewrapSubscriberList(raw, envelope, rows, filtered, matched);
2347
2347
  }
2348
+ var descriptions = /* @__PURE__ */ new Map();
2349
+ function recordToolDescription(name, description) {
2350
+ if (description) descriptions.set(name, description);
2351
+ }
2352
+ function getToolDescription(name) {
2353
+ return descriptions.get(name);
2354
+ }
2355
+ function routerDescription(full) {
2356
+ if (!full) return void 0;
2357
+ const sentences = full.split(/(?<=\.)\s+(?=[A-Z`'"])/).map((s) => s.trim()).filter(Boolean);
2358
+ const kept = [
2359
+ ...sentences.slice(0, 2),
2360
+ ...sentences.slice(2).filter((s) => /\bDo NOT\b/i.test(s))
2361
+ ];
2362
+ return kept.length ? kept.join(" ") : full;
2363
+ }
2364
+ function buildRouterCatalog(opts) {
2365
+ const curatedByName = new Map(opts.curated.map((t) => [t.name, t]));
2366
+ const tools = [];
2367
+ for (const name of [...opts.toolNames].sort()) {
2368
+ if (name === "carrier_ask" || name === "carrier_clarify") continue;
2369
+ const curated = curatedByName.get(name);
2370
+ const real = routerDescription(getToolDescription(name));
2371
+ tools.push({
2372
+ name,
2373
+ description: real ?? curated?.description ?? `Carrier MCP tool \`${name}\`. Required scope: ${opts.getScope(name)}. Use when the user intent clearly matches this tool name or its domain.`,
2374
+ input_schema: curated?.input_schema ?? { type: "object", properties: {} }
2375
+ });
2376
+ }
2377
+ return [opts.clarifyTool, ...tools];
2378
+ }
2379
+ var ROUTER_RULES = `Rules:
2380
+ 1. Pick the single best-matching tool. Never pick carrier_ask (the router itself).
2381
+ 2. Extract any parameters mentioned in the intent (ICCID, account IDs, amounts, etc.) as the tool's input.
2382
+ 3. Use carrier_clarify when the intent is ambiguous between several tools, AND when no tool actually answers the question. Returning nothing useful is correct and expected; a near-miss is not.
2383
+ 4. DESTRUCTIVE tools are flagged in their descriptions \u2014 still pick them if they match; the safety layer handles the confirm flow.
2384
+ 5. Only include params explicitly mentioned in the intent.
2385
+ 6. Context fields (iccid, account_id, reseller_id) from the routing context take precedence.
2386
+ 7. A tool answers the question only if its description says it does. Do not infer capability from the tool's NAME: several names share words with unrelated questions, and picking on the name alone has produced confidently wrong answers.
2387
+ 8. Read the "Do NOT use this to \u2026" steers in a description as hard exclusions. They exist because that tool is the common wrong answer for a neighbouring question, and they name the tool to use instead.
2388
+ 9. Prefer carrier_clarify over a tool that would return real data about a different question. Answering "which subscribers erode margin" when the user asked "what do my users' megabytes cost in total" is worse than admitting the gap, because the output looks like an answer.`;
2348
2389
 
2349
2390
  // src/lib/storefront-logo.ts
2350
2391
  var OPENAI_KEY_NAMES = [
@@ -2485,16 +2526,9 @@ export {
2485
2526
  normalizePackageTemplateChanges,
2486
2527
  buildListSubscriberParams,
2487
2528
  applySubscriberFilters,
2488
- renderHtml,
2489
- renderTui,
2490
- fleetScreen,
2491
- subscribersScreen,
2492
- usageScreen,
2493
- packagesScreen,
2494
- billingScreen,
2495
- walletScreen,
2496
- greenzoneScreen,
2497
- storefrontScreen,
2529
+ recordToolDescription,
2530
+ buildRouterCatalog,
2531
+ ROUTER_RULES,
2498
2532
  generateStorefrontLogo,
2499
2533
  run,
2500
2534
  which,
@@ -2521,6 +2555,16 @@ export {
2521
2555
  verifyStorefront,
2522
2556
  formatProbes,
2523
2557
  repairPlanFor,
2558
+ renderHtml,
2559
+ renderTui,
2560
+ fleetScreen,
2561
+ subscribersScreen,
2562
+ usageScreen,
2563
+ packagesScreen,
2564
+ billingScreen,
2565
+ walletScreen,
2566
+ greenzoneScreen,
2567
+ storefrontScreen,
2524
2568
  CARRIER_VERSION
2525
2569
  };
2526
- //# sourceMappingURL=chunk-Y4JEN7M4.js.map
2570
+ //# sourceMappingURL=chunk-2TLBS4AR.js.map