@carrierllc/mcp 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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();
598
454
  }
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
- };
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]));
641
473
  }
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
- };
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);
662
477
  }
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
- ]
686
- }
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)]
687
500
  ];
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
- });
703
- }
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
- };
501
+ }
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");
714
522
  }
715
523
 
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);
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
+ );
726
540
  }
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
- });
735
- }
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);
742
- }
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);
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);
765
554
  }
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
- });
555
+ if (value) out[key] = value;
556
+ }
557
+ return out;
777
558
  }
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
- });
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
+ }
566
+ }
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;
575
+ }
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;
591
+ }
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);
597
+ }
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);
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";
803
614
  }
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
- }
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;
619
+ }
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;
624
+ }
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 };
629
+ }
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)
919
649
  },
920
- secretArgs: (key, value) => ({ args: ["secrets", "set", `${key}=${value}`] })
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
+ };
665
+ }
666
+ chosen = wanted;
667
+ } else {
668
+ chosen = rankTargets(statuses)[0];
921
669
  }
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;
670
+ if (!chosen) {
671
+ return {
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(" | ")
676
+ };
677
+ }
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
+ }
700
+ return {
701
+ ok: true,
702
+ projectName,
703
+ statuses,
704
+ target: chosen.id,
705
+ url: outcome.url,
706
+ secrets: { pushed: staged.staged, failed: staged.failed }
707
+ };
708
+ } finally {
709
+ await staged.cleanup();
710
+ }
711
+ }
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;
931
724
  }
932
725
  return void 0;
933
726
  }
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;
727
+ function looksPublishable(key) {
728
+ return /^pk_(test|live)_/.test(key);
951
729
  }
952
- async function readFileText(path) {
953
- const { readFile: readFile2 } = await import("./fsx-BDDIQ3Y7.js");
954
- return readFile2(path, "utf8");
730
+ function looksSecret(key) {
731
+ return /^sk_(test|live)_/.test(key);
955
732
  }
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.` };
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) };
959
756
  }
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
757
  }
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;
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;
974
764
  }
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;
982
- }
983
- }
984
- const resolved = await resolveBin(def);
985
- if (!resolved) {
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
774
+ });
775
+ if (!res.ok) {
986
776
  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).`
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}`}`
994
780
  };
995
781
  }
996
- const who = await run(resolved.bin, [...resolved.prefix, ...def.whoami], {
997
- cwd: storefront,
998
- timeoutMs: 6e4
999
- });
1000
- if (!who.ok) {
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) {
1001
787
  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}\`.`
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."
1009
791
  };
1010
792
  }
1011
793
  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)
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
+ }
1019
803
  };
1020
804
  }
1021
- async function probeAll(storefront) {
1022
- return Promise.all(TARGET_IDS.map((id) => probeTarget(id, storefront)));
1023
- }
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
- });
1029
- }
1030
- function buildScriptFor(id) {
1031
- return DEFS[id].buildScript;
1032
- }
1033
- async function artifactMissing(id, storefront) {
1034
- const artifact = DEFS[id].artifact;
1035
- if (!artifact) return false;
1036
- return !await exists(join(storefront, artifact));
1037
- }
1038
- function parseDeployedUrl(output) {
1039
- return output.match(
1040
- /https:\/\/[^\s"']+\.(?:workers\.dev|vercel\.app|netlify\.app|fly\.dev)[^\s"']*/
1041
- )?.[0];
1042
- }
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
1052
- });
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 });
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
+ }
1070
831
  return {
1071
- staged: entries.map(([k]) => k),
1072
- failed: [],
1073
- secretsFile: file,
1074
- cleanup: async () => {
1075
- await rm(dir, { recursive: true, force: true });
1076
- }
832
+ ok: true,
833
+ tier: "cli-keyless",
834
+ credentials: { publishableKey, secretKey, tier: "cli-keyless" }
1077
835
  };
836
+ } finally {
837
+ await restore();
1078
838
  }
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
839
+ }
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
1084
869
  });
870
+ if (created.ok) return created;
871
+ notes.push(created.reason ?? "Clerk Platform API call failed.");
872
+ }
873
+ const discovered = discoverClerkCredentials(opts.env);
874
+ if (discovered) {
1085
875
  return {
1086
- staged: r.ok ? entries.map(([k]) => k) : [],
1087
- failed: r.ok ? [] : entries.map(([k]) => k),
1088
- cleanup: async () => {
1089
- }
876
+ ok: true,
877
+ tier: "discovered",
878
+ credentials: discovered,
879
+ reason: notes.length ? notes.join(" ") : void 0
1090
880
  };
1091
881
  }
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);
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.");
1097
888
  }
1098
- return { staged, failed, cleanup: async () => {
1099
- } };
1100
- }
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.` };
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
+ );
1106
893
  }
1107
- await def.ensureConfig?.(storefront, projectName);
1108
- if (await artifactMissing(id, storefront)) {
1109
- return {
1110
- ok: false,
1111
- target: id,
1112
- projectName,
1113
- reason: `No ${def.artifact} build found \u2014 run the build step first.`
1114
- };
894
+ return {
895
+ ok: false,
896
+ tier: "manual",
897
+ reason: notes.join(" "),
898
+ guidance: MANUAL_STEPS
899
+ };
900
+ }
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}` });
1115
912
  }
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.` };
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}` });
1126
917
  }
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]));
1182
- }
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);
1186
- }
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);
918
+ return { ok: failed.length === 0, applied, failed };
1190
919
  }
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
- ];
920
+ function dedupe(values) {
921
+ return [...new Set(values.map((v) => v.trim()).filter(Boolean))];
1210
922
  }
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");
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
+ };
1231
933
  }
1232
934
 
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
- );
1249
- }
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);
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
+ };
1263
975
  }
1264
- if (value) out[key] = value;
1265
- }
1266
- return out;
976
+ };
1267
977
  }
1268
- async function readEnvFile(path) {
1269
- if (!await exists(path)) return {};
978
+
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()}`;
1270
988
  try {
1271
- return parseEnvFile(await readFile(path, "utf8"));
1272
- } catch {
1273
- return {};
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) };
1274
997
  }
1275
998
  }
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;
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";
1284
1009
  }
1285
- return merged;
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";
1286
1014
  }
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;
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));
1300
1038
  }
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);
1039
+ diagnosis = diagnose(probes, secrets);
1040
+ if (diagnosis !== "not-deployed" || attempt === attempts) break;
1041
+ await sleep(delayMs);
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
+ };
1052
+ }
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
+ };
1070
+ }
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;
1081
+ }
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"
1307
1145
  }
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;
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"
1176
+ }
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;
1441
1313
  }
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
- }
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);
1466
1320
  }
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;
1321
+ function heading(text, color) {
1322
+ return paint(text.toUpperCase(), ANSI.bold, color);
1473
1323
  }
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
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);
1483
1335
  });
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
- };
1336
+ }
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}`;
1349
+ });
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 };
1602
+ return {
1603
+ id: "usage",
1604
+ title: "Usage",
1605
+ subtitle: input.subject,
1606
+ sections,
1607
+ actions: [{ label: "Project depletion", command: "usage_projection" }]
1608
+ };
1628
1609
  }
1629
- function dedupe(values) {
1630
- return [...new Set(values.map((v) => v.trim()).filter(Boolean))];
1610
+ function packagesScreen(rows) {
1611
+ return {
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
+ ])
1636
+ }
1637
+ ],
1638
+ actions: [{ label: "Assign to a subscriber", command: "assign_package" }]
1639
+ };
1631
1640
  }
1632
- function storefrontClerkUrls(deployedUrl, domain) {
1633
- const bases = dedupe([
1634
- deployedUrl?.replace(/\/$/, "") ?? "",
1635
- domain ? `https://${domain.replace(/^https?:\/\//, "").replace(/\/$/, "")}` : "",
1636
- "http://localhost:3000"
1637
- ]);
1641
+ function billingScreen(input) {
1642
+ const spend = input.events.reduce((sum, e) => sum + (e.amount > 0 ? e.amount : 0), 0);
1638
1643
  return {
1639
- allowedOrigins: bases,
1640
- redirectUrls: bases.flatMap((b) => [b, `${b}/checkout/success`, `${b}/dashboard`])
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)])
1670
+ }
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)
1701
+ }
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."
1709
+ }
1710
+ ] : []
1711
+ ],
1712
+ actions: [
1713
+ { label: "Top up", command: "wallet_topup_checkout" },
1714
+ { label: "Configure auto top-up", command: "wallet_auto_topup" }
1715
+ ]
1641
1716
  };
1642
1717
  }
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() {
1718
+ function greenzoneScreen(entries) {
1650
1719
  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;
1662
- }
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);
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"])
1672
1731
  }
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 });
1679
- }
1680
- } finally {
1681
- await rm2(backup, { recursive: true, force: true });
1682
- }
1683
- };
1684
- }
1732
+ ],
1733
+ actions: [
1734
+ { label: "Add an entry", command: "greenzone_whitelist_add" },
1735
+ { label: "Remove an entry", command: "greenzone_whitelist_remove" }
1736
+ ]
1685
1737
  };
1686
1738
  }
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));
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
+ ]
1747
1762
  }
1748
- diagnosis = diagnose(probes, secrets);
1749
- if (diagnosis !== "not-deployed" || attempt === attempts) break;
1750
- await sleep(delayMs);
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
+ });
1772
+ }
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
+ });
1751
1779
  }
1752
- const ok = diagnosis === "healthy";
1753
1780
  return {
1754
- ok,
1755
- url,
1756
- probes,
1757
- diagnosis,
1758
- summary: SUMMARIES[diagnosis],
1759
- repairable: !ok && REPAIRABLE.has(diagnosis)
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
+ ]
1760
1789
  };
1761
1790
  }
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
- };
1779
- }
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
- };
1788
- }
1789
- return void 0;
1790
- }
1791
1791
 
1792
1792
  // package.json
1793
1793
  var package_default = {
1794
1794
  name: "@carrierllc/mcp",
1795
- version: "0.5.0",
1795
+ version: "0.7.0",
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)",
@@ -1823,7 +1823,8 @@ var package_default = {
1823
1823
  lint: "eslint src",
1824
1824
  prepublishOnly: "pnpm run build",
1825
1825
  test: "pnpm run build && node --test test/*.test.js",
1826
- "check:pack": "node scripts/check-pack-size.mjs"
1826
+ "check:pack": "node scripts/check-pack-size.mjs",
1827
+ "generate:domains": "node scripts/generate-domains.mjs"
1827
1828
  },
1828
1829
  dependencies: {
1829
1830
  "@clack/prompts": "^0.7.0",
@@ -2274,6 +2275,118 @@ function normalizePackageTemplateChanges(changes) {
2274
2275
  return out;
2275
2276
  }
2276
2277
 
2278
+ // ../../packages/ocs-client/dist/index.js
2279
+ function buildListSubscriberParams(args) {
2280
+ const params = {};
2281
+ if (args.imsi) params.imsiPrefix = args.imsi;
2282
+ if (args.iccid) params.iccidPrefix = args.iccid;
2283
+ if (args.activationCode) params.activationCode = args.activationCode;
2284
+ if (args.accountId !== void 0) params.accountId = args.accountId;
2285
+ if (args.msisdn) params.msisdnPrefix = args.msisdn;
2286
+ return params;
2287
+ }
2288
+ function extractSubscriberStatus(row) {
2289
+ if (row === null || typeof row !== "object") return null;
2290
+ const raw = row.status;
2291
+ if (typeof raw === "string" && raw.length > 0) return raw;
2292
+ if (Array.isArray(raw)) {
2293
+ for (const entry of raw) {
2294
+ if (entry && typeof entry === "object") {
2295
+ const name = entry.status;
2296
+ if (typeof name === "string" && name.length > 0) return name;
2297
+ }
2298
+ }
2299
+ }
2300
+ return null;
2301
+ }
2302
+ function unwrapSubscriberList(raw) {
2303
+ if (Array.isArray(raw)) return { rows: raw, envelope: null };
2304
+ if (raw !== null && typeof raw === "object") {
2305
+ const obj = raw;
2306
+ if (Array.isArray(obj.subscriberList)) {
2307
+ return { rows: obj.subscriberList, envelope: obj };
2308
+ }
2309
+ }
2310
+ return { rows: null, envelope: null };
2311
+ }
2312
+ function rewrapSubscriberList(raw, envelope, rows, filtered, matched) {
2313
+ if (rows === null || filtered === null) return raw;
2314
+ const total = matched ?? filtered.length;
2315
+ const dropped = total - filtered.length;
2316
+ if (envelope === null) return filtered;
2317
+ const out = {
2318
+ ...envelope,
2319
+ subscriberList: filtered,
2320
+ nbFound: total,
2321
+ hasMore: filtered.length < total
2322
+ };
2323
+ if (dropped > 0) {
2324
+ out.truncated = true;
2325
+ out.note = `${dropped} of ${total} matching subscribers omitted by offset/limit. Narrow with \`status\`, \`accountId\` or \`iccid\`, or page with \`offset\`.`;
2326
+ }
2327
+ return out;
2328
+ }
2329
+ function applySubscriberFilters(raw, args) {
2330
+ const { rows, envelope } = unwrapSubscriberList(raw);
2331
+ let filtered = rows;
2332
+ if (filtered && args.status) {
2333
+ const wanted = String(args.status).trim().toLowerCase();
2334
+ filtered = filtered.filter((row) => {
2335
+ const status = extractSubscriberStatus(row);
2336
+ return status !== null && status.toLowerCase() === wanted;
2337
+ });
2338
+ }
2339
+ const matched = filtered?.length;
2340
+ if (filtered && typeof args.offset === "number" && args.offset > 0) {
2341
+ filtered = filtered.slice(args.offset);
2342
+ }
2343
+ if (filtered && typeof args.limit === "number" && args.limit >= 0) {
2344
+ filtered = filtered.slice(0, args.limit);
2345
+ }
2346
+ return rewrapSubscriberList(raw, envelope, rows, filtered, matched);
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.`;
2389
+
2277
2390
  // src/lib/storefront-logo.ts
2278
2391
  var OPENAI_KEY_NAMES = [
2279
2392
  "OPENAI_API_KEY",
@@ -2411,16 +2524,11 @@ export {
2411
2524
  esimStatusPerAccountParams,
2412
2525
  normalizePackageTemplate,
2413
2526
  normalizePackageTemplateChanges,
2414
- renderHtml,
2415
- renderTui,
2416
- fleetScreen,
2417
- subscribersScreen,
2418
- usageScreen,
2419
- packagesScreen,
2420
- billingScreen,
2421
- walletScreen,
2422
- greenzoneScreen,
2423
- storefrontScreen,
2527
+ buildListSubscriberParams,
2528
+ applySubscriberFilters,
2529
+ recordToolDescription,
2530
+ buildRouterCatalog,
2531
+ ROUTER_RULES,
2424
2532
  generateStorefrontLogo,
2425
2533
  run,
2426
2534
  which,
@@ -2447,6 +2555,16 @@ export {
2447
2555
  verifyStorefront,
2448
2556
  formatProbes,
2449
2557
  repairPlanFor,
2558
+ renderHtml,
2559
+ renderTui,
2560
+ fleetScreen,
2561
+ subscribersScreen,
2562
+ usageScreen,
2563
+ packagesScreen,
2564
+ billingScreen,
2565
+ walletScreen,
2566
+ greenzoneScreen,
2567
+ storefrontScreen,
2450
2568
  CARRIER_VERSION
2451
2569
  };
2452
- //# sourceMappingURL=chunk-QZBALDKD.js.map
2570
+ //# sourceMappingURL=chunk-CQ6EOLA7.js.map