@carrierllc/mcp 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,11 +1,53 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ CARRIER_BRAND,
3
4
  CARRIER_VERSION,
4
5
  OCS_MAX_USAGE_WINDOW_DAYS,
6
+ TARGET_IDS,
7
+ billingScreen,
8
+ buildSite,
5
9
  clampUsagePeriod,
10
+ clerkCliDeps,
11
+ configureClerkInstance,
12
+ deploySite,
13
+ deriveAccentDark,
14
+ deriveAccentLight,
15
+ deriveAccentPaletteSubs,
16
+ fleetScreen,
17
+ formatProbes,
6
18
  generateStorefrontLogo,
7
- lastNDaysPeriod
8
- } from "./chunk-IYCGWBQU.js";
19
+ greenzoneScreen,
20
+ installDeps,
21
+ isTargetId,
22
+ lastNDaysPeriod,
23
+ loadStorefrontBrand,
24
+ mergeEnvLocal,
25
+ packagesScreen,
26
+ probeAll,
27
+ provisionClerk,
28
+ rankTargets,
29
+ renderEnv,
30
+ renderTui,
31
+ repairPlanFor,
32
+ rollback,
33
+ run,
34
+ slug,
35
+ storefrontClerkUrls,
36
+ storefrontScreen,
37
+ subscribersScreen,
38
+ usageScreen,
39
+ verifyStorefront,
40
+ walletScreen,
41
+ which
42
+ } from "./chunk-6OI56RSR.js";
43
+ import {
44
+ copyTree,
45
+ exists,
46
+ pruneTree,
47
+ readFile,
48
+ replaceInTree,
49
+ writeFile
50
+ } from "./chunk-SHKKVIIA.js";
9
51
 
10
52
  // src/cli/index.ts
11
53
  import { Command } from "commander";
@@ -13,101 +55,8 @@ import * as p3 from "@clack/prompts";
13
55
  import pc4 from "picocolors";
14
56
  import { resolve as resolve2 } from "path";
15
57
 
16
- // src/cli/lib/brand.ts
17
- var CARRIER_BRAND = {
18
- name: "Carrier",
19
- legalName: "Lifecycle Innovations Limited",
20
- tagline: "Programmable connectivity, on demand.",
21
- domain: "carrier.llc",
22
- supportEmail: "support@carrier.llc",
23
- supportUrl: "https://carrier.llc/help",
24
- supportWhatsapp: "+17864604829",
25
- colors: {
26
- bg: "#080C16",
27
- accent: "#FF6B35",
28
- accentDark: "#D9461C",
29
- text: "#F5F1EA"
30
- },
31
- social: {
32
- x: "@carrier_llc",
33
- instagram: "@carrier.llc",
34
- tiktok: "@carrier.llc"
35
- },
36
- carrierApiUrl: "https://api.carrier.llc"
37
- };
38
- var CARRIER_ACCENT_LIGHT = "#FFB088";
39
- var CARRIER_ACCENT_GRADIENT_START = "#FF7A45";
40
- function hexToRgb(hex) {
41
- const h = hex.replace(/^#/, "");
42
- if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
43
- return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
44
- }
45
- function rgbToHex(r, g, b, lower = false) {
46
- const fmt = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
47
- const out = `#${fmt(r)}${fmt(g)}${fmt(b)}`;
48
- return lower ? out : out.toUpperCase();
49
- }
50
- function mixHexWithWhite(hex, whiteRatio) {
51
- const rgb = hexToRgb(hex);
52
- if (!rgb) return hex;
53
- const mix = (n) => n + (255 - n) * whiteRatio;
54
- return rgbToHex(mix(rgb[0]), mix(rgb[1]), mix(rgb[2]));
55
- }
56
- function darkenHex(hex, factor) {
57
- const rgb = hexToRgb(hex);
58
- if (!rgb) return hex;
59
- const scale = (n) => n * factor;
60
- return rgbToHex(scale(rgb[0]), scale(rgb[1]), scale(rgb[2]), true);
61
- }
62
- function deriveAccentDark(accent, seed = CARRIER_BRAND) {
63
- if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return seed.colors.accentDark;
64
- const rgb = hexToRgb(accent);
65
- if (!rgb) return accent;
66
- const scale = (n) => Math.max(0, Math.min(255, Math.round(n * 0.75)));
67
- return rgbToHex(scale(rgb[0]), scale(rgb[1]), scale(rgb[2]));
68
- }
69
- function deriveAccentLight(accent, seed = CARRIER_BRAND) {
70
- if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return CARRIER_ACCENT_LIGHT;
71
- return mixHexWithWhite(accent, 0.45);
72
- }
73
- function deriveAccentGradientStart(accent, seed = CARRIER_BRAND) {
74
- if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return CARRIER_ACCENT_GRADIENT_START;
75
- return mixHexWithWhite(accent, 0.08);
76
- }
77
- function deriveAccentPaletteSubs(accent, accentDark) {
78
- if (accent.toUpperCase() === CARRIER_BRAND.colors.accent.toUpperCase()) return [];
79
- const accentLight = deriveAccentLight(accent);
80
- const gradientStart = deriveAccentGradientStart(accent);
81
- return [
82
- ["#FF6B35", accent],
83
- ["#D9461C", accentDark],
84
- ["#FFB088", accentLight],
85
- ["#FF7A45", gradientStart],
86
- ["#fff4ef", mixHexWithWhite(accent, 0.94).toLowerCase()],
87
- ["#ffe0d0", mixHexWithWhite(accent, 0.85).toLowerCase()],
88
- ["#ffbfa0", mixHexWithWhite(accent, 0.7).toLowerCase()],
89
- ["#ff9970", mixHexWithWhite(accent, 0.55).toLowerCase()],
90
- ["#ff7d4d", mixHexWithWhite(accent, 0.4).toLowerCase()],
91
- ["#b33a17", darkenHex(accentDark, 0.75)],
92
- ["#8a2d12", darkenHex(accentDark, 0.58)],
93
- ["#5e1e0c", darkenHex(accentDark, 0.4)],
94
- ["#3a1107", darkenHex(accentDark, 0.25)]
95
- ];
96
- }
97
- function renderEnv(brand) {
98
- return [
99
- `# Generated by @carrierllc/mcp`,
100
- `NEXT_PUBLIC_BRAND_NAME=${JSON.stringify(brand.name)}`,
101
- `NEXT_PUBLIC_CARRIER_API_URL=${JSON.stringify(brand.carrierApiUrl)}`,
102
- `# Clerk (fill in from your Clerk dashboard to enable auth + checkout)`,
103
- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=`,
104
- `CLERK_SECRET_KEY=`,
105
- ``
106
- ].join("\n");
107
- }
108
-
109
58
  // src/cli/lib/plugin.ts
110
- import { join as join3 } from "path";
59
+ import { join as join2 } from "path";
111
60
 
112
61
  // src/cli/lib/paths.ts
113
62
  import { fileURLToPath } from "url";
@@ -142,161 +91,8 @@ function installedPluginDir() {
142
91
  return join(claudePluginsDir(), "carrier");
143
92
  }
144
93
 
145
- // src/cli/lib/fsx.ts
146
- import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "fs/promises";
147
- import { existsSync as existsSync2 } from "fs";
148
- import { join as join2 } from "path";
149
- var SKIP = /* @__PURE__ */ new Set([
150
- "node_modules",
151
- ".next",
152
- ".turbo",
153
- ".vercel",
154
- "dist",
155
- ".git",
156
- "test-results",
157
- "playwright-report"
158
- ]);
159
- var cpFilter = (s) => {
160
- const base = s.split("/").pop() ?? "";
161
- return !SKIP.has(base);
162
- };
163
- async function copyTree(src, dest) {
164
- await mkdir(dest, { recursive: true });
165
- for (const entry of await readdir(src, { withFileTypes: true })) {
166
- if (SKIP.has(entry.name)) continue;
167
- await cp(join2(src, entry.name), join2(dest, entry.name), {
168
- recursive: true,
169
- filter: cpFilter
170
- });
171
- }
172
- }
173
- async function pruneTree(src, dest) {
174
- if (!await isDir(dest)) return;
175
- for (const entry of await readdir(dest, { withFileTypes: true })) {
176
- if (SKIP.has(entry.name)) continue;
177
- const srcPath = join2(src, entry.name);
178
- const destPath = join2(dest, entry.name);
179
- if (!existsSync2(srcPath)) {
180
- await rm(destPath, { recursive: true, force: true });
181
- continue;
182
- }
183
- if (entry.isDirectory() && await isDir(srcPath)) await pruneTree(srcPath, destPath);
184
- }
185
- }
186
- async function* walk(dir) {
187
- for (const entry of await readdir(dir, { withFileTypes: true })) {
188
- if (SKIP.has(entry.name)) continue;
189
- const full = join2(dir, entry.name);
190
- if (entry.isDirectory()) yield* walk(full);
191
- else yield full;
192
- }
193
- }
194
- var TEXT_EXT = /* @__PURE__ */ new Set([
195
- ".ts",
196
- ".tsx",
197
- ".js",
198
- ".jsx",
199
- ".mjs",
200
- ".cjs",
201
- ".json",
202
- ".css",
203
- ".md",
204
- ".mdx",
205
- ".html",
206
- ".txt",
207
- ".env",
208
- ".example",
209
- ".yml",
210
- ".yaml",
211
- ".toml"
212
- ]);
213
- function isTextFile(path) {
214
- const dot = path.lastIndexOf(".");
215
- if (dot === -1) return false;
216
- return TEXT_EXT.has(path.slice(dot));
217
- }
218
- async function replaceInTree(dir, subs) {
219
- let touched = 0;
220
- for await (const file of walk(dir)) {
221
- if (!isTextFile(file)) continue;
222
- const before = await readFile(file, "utf8");
223
- let after = before;
224
- for (const [from, to] of subs) {
225
- after = typeof from === "string" ? after.split(from).join(to) : after.replace(from, to);
226
- }
227
- if (after !== before) {
228
- await writeFile(file, after);
229
- touched++;
230
- }
231
- }
232
- return touched;
233
- }
234
- async function exists(p4) {
235
- return existsSync2(p4);
236
- }
237
- async function isDir(p4) {
238
- try {
239
- return (await stat(p4)).isDirectory();
240
- } catch {
241
- return false;
242
- }
243
- }
244
-
245
- // src/cli/lib/exec.ts
246
- import { spawn } from "child_process";
247
- function run(cmd, args, opts = {}) {
248
- return new Promise((resolve3) => {
249
- const child = spawn(cmd, args, { cwd: opts.cwd, shell: false });
250
- let stdout = "";
251
- let stderr = "";
252
- let settled = false;
253
- const finish = (result) => {
254
- if (settled) return;
255
- settled = true;
256
- resolve3(result);
257
- };
258
- let timer;
259
- if (opts.timeoutMs && opts.timeoutMs > 0) {
260
- timer = setTimeout(() => {
261
- try {
262
- child.kill("SIGTERM");
263
- } catch {
264
- }
265
- finish({
266
- ok: false,
267
- code: null,
268
- stdout,
269
- stderr: stderr || `timeout after ${opts.timeoutMs}ms`
270
- });
271
- }, opts.timeoutMs);
272
- }
273
- child.stdout?.on("data", (d) => stdout += d.toString());
274
- child.stderr?.on("data", (d) => stderr += d.toString());
275
- child.on("error", () => {
276
- if (timer) clearTimeout(timer);
277
- finish({ ok: false, code: null, stdout, stderr });
278
- });
279
- child.on("close", (code) => {
280
- if (timer) clearTimeout(timer);
281
- finish({ ok: code === 0, code, stdout, stderr });
282
- });
283
- });
284
- }
285
- function runInherit(cmd, args, opts = {}) {
286
- return new Promise((resolve3) => {
287
- const child = spawn(cmd, args, { cwd: opts.cwd, shell: false, stdio: "inherit" });
288
- child.on("error", () => resolve3({ ok: false, code: null, stdout: "", stderr: "" }));
289
- child.on("close", (code) => resolve3({ ok: code === 0, code, stdout: "", stderr: "" }));
290
- });
291
- }
292
- async function which(bin) {
293
- const probe = process.platform === "win32" ? "where" : "which";
294
- const r = await run(probe, [bin]);
295
- return r.ok && r.stdout.trim().length > 0;
296
- }
297
-
298
94
  // src/cli/lib/urls.ts
299
- import { spawn as spawn2 } from "child_process";
95
+ import { spawn } from "child_process";
300
96
  var MCP_URL = "https://mcp.carrier.llc/mcp";
301
97
  var MCP_HOME = "https://mcp.carrier.llc";
302
98
  var SIGN_UP_URL = "https://accounts.carrier.llc/sign-up";
@@ -320,7 +116,7 @@ async function openUrl(url) {
320
116
  if (r.ok) return { ok: true, hint: `Opened ${url}` };
321
117
  }
322
118
  try {
323
- spawn2("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
119
+ spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
324
120
  return { ok: true, hint: `Launched browser for ${url}` };
325
121
  } catch {
326
122
  return { ok: false, hint: `Could not open browser. Visit:
@@ -361,7 +157,7 @@ async function installPlugin() {
361
157
  }
362
158
  async function pluginStatus() {
363
159
  const dir = installedPluginDir();
364
- const manifest = join3(dir, ".claude-plugin", "plugin.json");
160
+ const manifest = join2(dir, ".claude-plugin", "plugin.json");
365
161
  if (!await exists(manifest)) return { installed: false, dir };
366
162
  try {
367
163
  const json = JSON.parse(await readFile(manifest, "utf8"));
@@ -388,7 +184,7 @@ function installAuthGuidance() {
388
184
  }
389
185
 
390
186
  // src/cli/lib/whitelabel.ts
391
- import { join as join4 } from "path";
187
+ import { join as join3 } from "path";
392
188
  function preserveClerkFromEnv(existing, fresh) {
393
189
  let out = fresh;
394
190
  for (const key of ["NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", "CLERK_SECRET_KEY"]) {
@@ -407,7 +203,7 @@ async function scaffoldStorefront(target, brand) {
407
203
  `Storefront template not found at ${template}. The @carrierllc/mcp package may be corrupt \u2014 reinstall it.`
408
204
  );
409
205
  }
410
- const envPath = join4(target, ".env.local");
206
+ const envPath = join3(target, ".env.local");
411
207
  const existingEnv = await exists(envPath) ? await readFile(envPath, "utf8") : void 0;
412
208
  await copyTree(template, target);
413
209
  await pruneTree(template, target);
@@ -419,10 +215,10 @@ async function scaffoldStorefront(target, brand) {
419
215
  if (brand.colors.bg.toUpperCase() !== "#080C16") subs.push(["#080C16", brand.colors.bg]);
420
216
  if (brand.colors.text.toUpperCase() !== "#F5F1EA") subs.push(["#F5F1EA", brand.colors.text]);
421
217
  await replaceInTree(target, subs);
422
- await patchBrandConfig(join4(target, "src", "brand.config.ts"), brand);
218
+ await patchBrandConfig(join3(target, "src", "brand.config.ts"), brand);
423
219
  const envBody = existingEnv ? preserveClerkFromEnv(existingEnv, renderEnv(brand)) : renderEnv(brand);
424
220
  await writeFile(envPath, envBody);
425
- await writeFile(join4(target, ".npmrc"), "ignore-workspace-root-check=true\n");
221
+ await writeFile(join3(target, ".npmrc"), "ignore-workspace-root-check=true\n");
426
222
  }
427
223
  async function patchBrandConfig(path, brand) {
428
224
  if (!await exists(path)) return;
@@ -454,89 +250,168 @@ ${socialBody}
454
250
  }
455
251
 
456
252
  // src/cli/lib/logo.ts
457
- import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
458
- import { join as join5 } from "path";
253
+ import { mkdir, writeFile as writeFile2 } from "fs/promises";
254
+ import { join as join4 } from "path";
459
255
  async function writeStorefrontLogo(target, brand) {
460
256
  const logo = await generateStorefrontLogo({
461
257
  name: brand.name,
462
258
  accent: brand.colors.accent,
463
259
  tagline: brand.tagline
464
260
  });
465
- const dir = join5(target, "public", "brand");
466
- await mkdir2(dir, { recursive: true });
467
- const svgPath = join5(dir, "logo.svg");
261
+ const dir = join4(target, "public", "brand");
262
+ await mkdir(dir, { recursive: true });
263
+ const svgPath = join4(dir, "logo.svg");
468
264
  await writeFile2(svgPath, logo.svg, "utf8");
469
265
  if (logo.pngBase64) {
470
- await writeFile2(join5(dir, "logo.png"), Buffer.from(logo.pngBase64, "base64"));
266
+ await writeFile2(join4(dir, "logo.png"), Buffer.from(logo.pngBase64, "base64"));
471
267
  }
472
268
  return { path: svgPath, source: logo.source };
473
269
  }
474
270
 
475
- // src/cli/lib/site.ts
476
- import { join as join6 } from "path";
477
- function slug(s) {
478
- return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "storefront";
479
- }
480
- async function installDeps(target) {
481
- const pkgMgr = await which("pnpm") ? "pnpm" : "npm";
482
- const r = await runInherit(pkgMgr, ["install"], { cwd: target });
483
- return r.ok;
484
- }
485
- async function buildSite(target) {
486
- const pkgMgr = await which("pnpm") ? "pnpm" : "npm";
487
- const r = await runInherit(pkgMgr, ["run", "cf:build"], { cwd: target });
488
- return r.ok;
489
- }
490
- async function loadStorefrontBrand(target, overrides) {
491
- const configPath = join6(target, "src", "brand.config.ts");
492
- if (!await exists(configPath)) {
493
- return { ...CARRIER_BRAND, ...overrides };
494
- }
495
- const src = await readFile(configPath, "utf8");
496
- const pick = (field, fallback) => {
497
- const m = src.match(new RegExp(`\\b${field}:\\s*(?:[^"\\n]*\\?\\?\\s*)?"([^"]*)"`));
498
- return m?.[1] ?? fallback;
499
- };
500
- return {
501
- ...CARRIER_BRAND,
502
- name: overrides?.name ?? pick("name", CARRIER_BRAND.name),
503
- domain: pick("domain", CARRIER_BRAND.domain),
504
- supportEmail: pick("supportEmail", CARRIER_BRAND.supportEmail),
505
- supportUrl: pick("supportUrl", CARRIER_BRAND.supportUrl),
506
- tagline: pick("tagline", CARRIER_BRAND.tagline),
507
- legalName: pick("legalName", CARRIER_BRAND.legalName),
508
- colors: {
509
- ...CARRIER_BRAND.colors,
510
- accent: pick("accent", CARRIER_BRAND.colors.accent),
511
- accentDark: pick("accentDark", CARRIER_BRAND.colors.accentDark),
512
- bg: pick("bg", CARRIER_BRAND.colors.bg),
513
- text: pick("text", CARRIER_BRAND.colors.text)
514
- },
515
- carrierApiUrl: CARRIER_BRAND.carrierApiUrl
516
- };
517
- }
518
- async function deploySite(target, brand) {
519
- const projectName = slug(brand.name);
520
- const workerBundle = join6(target, ".open-next", "worker.js");
521
- if (!await exists(workerBundle)) {
522
- return {
523
- ok: false,
524
- projectName,
525
- reason: "No OpenNext build found \u2014 run `carrier site deploy` (build step) or `pnpm run build` in the storefront first."
526
- };
271
+ // src/cli/lib/dash.ts
272
+ var DASH_DOMAINS = [
273
+ "fleet",
274
+ "subscribers",
275
+ "usage",
276
+ "packages",
277
+ "billing",
278
+ "wallet",
279
+ "greenzone",
280
+ "storefront"
281
+ ];
282
+ function isDashDomain(value) {
283
+ return DASH_DOMAINS.includes(value);
284
+ }
285
+ var DASH_TOOL = {
286
+ fleet: "fleet_health_app",
287
+ subscribers: "list_subscribers",
288
+ usage: "subscriber_usage",
289
+ packages: "list_package_templates",
290
+ billing: "billing_events",
291
+ wallet: "wallet_balance",
292
+ greenzone: "greenzone_whitelist_list",
293
+ storefront: ""
294
+ };
295
+ function screenFromStructured(structured) {
296
+ const rec = structured;
297
+ const screen = rec?.screen;
298
+ if (screen && typeof screen === "object" && Array.isArray(screen.sections) && screen.title) {
299
+ return screen;
527
300
  }
528
- if (!await which("wrangler") && !await which("npx")) {
529
- return { ok: false, projectName, reason: "wrangler/npx not found \u2014 install wrangler to deploy." };
301
+ return void 0;
302
+ }
303
+ var num = (value, fallback = 0) => {
304
+ const n = Number(value);
305
+ return Number.isFinite(n) ? n : fallback;
306
+ };
307
+ var str = (value, fallback = "") => value === void 0 || value === null ? fallback : String(value);
308
+ function rows(payload, keys) {
309
+ if (Array.isArray(payload)) return payload;
310
+ const rec = payload ?? {};
311
+ for (const key of keys) {
312
+ const candidate = rec[key];
313
+ if (Array.isArray(candidate)) return candidate;
314
+ }
315
+ return [];
316
+ }
317
+ function buildDashScreen(domain, payload) {
318
+ switch (domain) {
319
+ case "fleet": {
320
+ const accounts = rows(payload, ["accounts", "account"]);
321
+ return fleetScreen({
322
+ active: num(payload?.active),
323
+ suspended: num(payload?.suspended),
324
+ inventory: num(payload?.inventory),
325
+ other: num(payload?.other),
326
+ accounts: accounts.map((a) => ({
327
+ name: str(a.name ?? a.accountId, "?"),
328
+ balance: num(a.balance),
329
+ active: num(a.active),
330
+ suspended: num(a.suspended),
331
+ inventory: num(a.inventory),
332
+ other: num(a.other),
333
+ packageOnly: Boolean(a.packageOnly)
334
+ }))
335
+ });
336
+ }
337
+ case "subscribers":
338
+ return subscribersScreen(
339
+ rows(payload, ["subscribers", "subscriber", "data"]).map((s) => ({
340
+ iccid: str(s.iccid ?? s.ICCID, "?"),
341
+ msisdn: s.msisdn ? str(s.msisdn) : void 0,
342
+ status: str(s.status ?? s.statusStr, "unknown"),
343
+ account: s.accountName ? str(s.accountName) : void 0,
344
+ dataUsedBytes: s.dataUsed === void 0 ? void 0 : num(s.dataUsed)
345
+ }))
346
+ );
347
+ case "usage":
348
+ return usageScreen({
349
+ subject: str(payload?.subject, "Subscriber"),
350
+ timeline: rows(payload, ["timeline", "days", "usage"]).map((p4) => ({
351
+ date: str(p4.date ?? p4.day, "?"),
352
+ bytes: num(p4.bytes ?? p4.dataUsed)
353
+ })),
354
+ countries: rows(payload, ["countries"]).map((c) => ({
355
+ country: str(c.country ?? c.countryName, "?"),
356
+ bytes: num(c.bytes ?? c.dataUsed)
357
+ }))
358
+ });
359
+ case "packages":
360
+ return packagesScreen(
361
+ rows(payload, ["template", "templates", "data"]).map((t) => ({
362
+ name: str(t.prepaidpackagetemplatename ?? t.name, "?"),
363
+ id: str(t.prepaidpackagetemplateid ?? t.id, "?"),
364
+ dataLimitBytes: t.databyte === void 0 ? void 0 : num(t.databyte),
365
+ validityDays: t.perioddays === void 0 ? void 0 : num(t.perioddays),
366
+ price: t.cost === void 0 ? void 0 : num(t.cost),
367
+ recurring: Boolean(t.recurring)
368
+ }))
369
+ );
370
+ case "billing":
371
+ return billingScreen({
372
+ balance: payload?.balance === void 0 ? void 0 : num(payload.balance),
373
+ currency: str(payload?.currency, ""),
374
+ events: rows(payload, ["events", "data"]).map((e) => ({
375
+ date: str(e.date ?? e.timestamp, "?"),
376
+ description: str(e.description ?? e.type, "\u2014"),
377
+ amount: num(e.amount)
378
+ }))
379
+ });
380
+ case "wallet": {
381
+ const p4 = payload ?? {};
382
+ return walletScreen({
383
+ balance: num(p4.balance),
384
+ currency: str(p4.currency, ""),
385
+ autoTopupEnabled: Boolean(p4.autoTopupEnabled ?? p4.auto_topup_enabled),
386
+ threshold: p4.threshold === void 0 ? void 0 : num(p4.threshold),
387
+ credits: p4.credits === void 0 ? void 0 : num(p4.credits)
388
+ });
389
+ }
390
+ case "greenzone":
391
+ return greenzoneScreen(
392
+ rows(payload, ["entries", "whitelist", "data"]).map((e) => ({
393
+ value: str(e.value ?? e.prefix ?? e.destination, "?"),
394
+ note: e.note ? str(e.note) : void 0
395
+ }))
396
+ );
397
+ case "storefront": {
398
+ const p4 = payload ?? {};
399
+ return storefrontScreen({
400
+ brand: str(p4.brand ?? p4.project, "storefront"),
401
+ url: p4.url ? str(p4.url) : void 0,
402
+ target: p4.target ? str(p4.target) : void 0,
403
+ verified: p4.verified === void 0 ? null : Boolean(p4.verified),
404
+ diagnosis: p4.diagnosis ? str(p4.diagnosis) : void 0,
405
+ secretsStaged: Array.isArray(p4.secrets_staged) ? p4.secrets_staged : void 0,
406
+ plans: p4.plans === void 0 ? void 0 : num(p4.plans)
407
+ });
408
+ }
530
409
  }
531
- const bin = await which("wrangler") ? "wrangler" : "npx";
532
- const args = bin === "wrangler" ? ["deploy", "--name", projectName] : ["wrangler", "deploy", "--name", projectName];
533
- const r = await runInherit(bin, args, { cwd: target });
534
- return r.ok ? { ok: true, projectName } : { ok: false, projectName, reason: "wrangler deploy failed \u2014 run `wrangler login` then retry `carrier site deploy`." };
535
410
  }
536
411
 
537
412
  // src/cli/lib/status.ts
538
413
  import { homedir as homedir2 } from "os";
539
- import { join as join7 } from "path";
414
+ import { join as join5 } from "path";
540
415
  function detectEnvToken() {
541
416
  const api = process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || "";
542
417
  if (api.startsWith("ak_") || api.length > 0) {
@@ -581,10 +456,10 @@ function scanMcpServers(servers) {
581
456
  }
582
457
  async function probeMcpFromConfig() {
583
458
  const candidates = [
584
- join7(homedir2(), ".claude.json"),
585
- join7(claudeHome(), "settings.json"),
586
- join7(claudeHome(), ".mcp.json"),
587
- join7(process.cwd(), ".mcp.json")
459
+ join5(homedir2(), ".claude.json"),
460
+ join5(claudeHome(), "settings.json"),
461
+ join5(claudeHome(), ".mcp.json"),
462
+ join5(process.cwd(), ".mcp.json")
588
463
  ];
589
464
  let best = null;
590
465
  let configSeen = false;
@@ -768,6 +643,280 @@ function withNextStep(errorMsg, next) {
768
643
  import * as p from "@clack/prompts";
769
644
  import pc2 from "picocolors";
770
645
 
646
+ // src/cli/lib/auth.ts
647
+ import { createHash, randomBytes, timingSafeEqual } from "crypto";
648
+ import { createServer } from "http";
649
+
650
+ // src/cli/lib/credentials.ts
651
+ import { homedir as homedir3 } from "os";
652
+ import { join as join6 } from "path";
653
+ import { chmod, mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile3 } from "fs/promises";
654
+ function carrierHome() {
655
+ return process.env.CARRIER_HOME?.trim() || join6(homedir3(), ".carrier");
656
+ }
657
+ function credentialsPath() {
658
+ return join6(carrierHome(), "credentials.json");
659
+ }
660
+ async function readCredentials() {
661
+ try {
662
+ const raw = await readFile2(credentialsPath(), "utf8");
663
+ const parsed = JSON.parse(raw);
664
+ if (!parsed?.accessToken || typeof parsed.accessToken !== "string") return null;
665
+ return parsed;
666
+ } catch {
667
+ return null;
668
+ }
669
+ }
670
+ async function writeCredentials(creds) {
671
+ const dir = carrierHome();
672
+ await mkdir2(dir, { recursive: true, mode: 448 });
673
+ await chmod(dir, 448).catch(() => void 0);
674
+ const file = credentialsPath();
675
+ await writeFile3(file, `${JSON.stringify(creds, null, 2)}
676
+ `, { mode: 384 });
677
+ await chmod(file, 384).catch(() => void 0);
678
+ }
679
+ async function clearCredentials() {
680
+ await rm(credentialsPath(), { force: true });
681
+ }
682
+ function isExpired(creds, skewMs = 6e4) {
683
+ if (!creds.expiresAt) return false;
684
+ return Date.now() >= creds.expiresAt - skewMs;
685
+ }
686
+
687
+ // src/cli/lib/auth.ts
688
+ var DISCOVERY_PATH = "/.well-known/oauth-authorization-server";
689
+ var CLIENT_NAME = "Carrier CLI";
690
+ var DEFAULT_SCOPE = "read write";
691
+ var CALLBACK_PORTS = [3118, 3119, 8976, 8977, 0];
692
+ async function discover(base = MCP_HOME) {
693
+ const res = await fetch(`${base}${DISCOVERY_PATH}`, {
694
+ headers: { Accept: "application/json" }
695
+ });
696
+ if (!res.ok) throw new Error(`OAuth discovery failed (${res.status}) at ${base}${DISCOVERY_PATH}`);
697
+ const meta = await res.json();
698
+ if (!meta.authorization_endpoint || !meta.token_endpoint) {
699
+ throw new Error("OAuth discovery returned no authorization or token endpoint");
700
+ }
701
+ return meta;
702
+ }
703
+ function base64url(buf) {
704
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
705
+ }
706
+ function pkcePair() {
707
+ const verifier = base64url(randomBytes(32));
708
+ const challenge = base64url(createHash("sha256").update(verifier).digest());
709
+ return { verifier, challenge };
710
+ }
711
+ function sameState(a, b) {
712
+ const left = Buffer.from(a);
713
+ const right = Buffer.from(b);
714
+ if (left.length !== right.length) return false;
715
+ return timingSafeEqual(left, right);
716
+ }
717
+ async function registerClient(meta, redirectUri) {
718
+ if (!meta.registration_endpoint) {
719
+ throw new Error("This server does not support dynamic client registration");
720
+ }
721
+ const res = await fetch(meta.registration_endpoint, {
722
+ method: "POST",
723
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
724
+ body: JSON.stringify({
725
+ client_name: CLIENT_NAME,
726
+ redirect_uris: [redirectUri],
727
+ grant_types: ["authorization_code", "refresh_token"],
728
+ response_types: ["code"],
729
+ // Installed apps cannot keep a secret; register as a public client.
730
+ token_endpoint_auth_method: "none",
731
+ application_type: "native"
732
+ })
733
+ });
734
+ if (!res.ok) {
735
+ const body = (await res.text().catch(() => "")).slice(0, 200);
736
+ throw new Error(`Client registration failed (${res.status}). ${body}`);
737
+ }
738
+ const json = await res.json();
739
+ if (!json.client_id) throw new Error("Client registration returned no client_id");
740
+ return json.client_id;
741
+ }
742
+ function escapeHtml(text3) {
743
+ return text3.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
744
+ }
745
+ function page(title, detail) {
746
+ const safeTitle = escapeHtml(title);
747
+ const safeDetail = escapeHtml(detail);
748
+ return `<!doctype html><meta charset="utf-8"><title>${safeTitle}</title>
749
+ <body style="font-family:system-ui,sans-serif;max-width:32rem;margin:20vh auto;text-align:center">
750
+ <h1 style="font-size:1.25rem">${safeTitle}</h1><p style="color:#555">${safeDetail}</p></body>`;
751
+ }
752
+ async function startCallbackServer() {
753
+ let resolveCode;
754
+ let rejectCode;
755
+ let expected = "";
756
+ const server = createServer((req, res) => {
757
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
758
+ if (url.pathname !== "/callback") {
759
+ res.writeHead(404).end();
760
+ return;
761
+ }
762
+ const err = url.searchParams.get("error");
763
+ const code = url.searchParams.get("code");
764
+ const state = url.searchParams.get("state") ?? "";
765
+ if (err) {
766
+ res.writeHead(400, { "Content-Type": "text/html" });
767
+ res.end(page("Sign-in failed", err));
768
+ rejectCode?.(new Error(`Authorization server returned "${err}"`));
769
+ return;
770
+ }
771
+ if (!code || !expected || !sameState(state, expected)) {
772
+ res.writeHead(400, { "Content-Type": "text/html" });
773
+ res.end(page("Sign-in failed", "The redirect did not match this login attempt."));
774
+ rejectCode?.(new Error("State mismatch \u2014 ignoring the redirect"));
775
+ return;
776
+ }
777
+ res.writeHead(200, { "Content-Type": "text/html" });
778
+ res.end(page("You're signed in", "Close this tab and return to your terminal."));
779
+ resolveCode?.(code);
780
+ });
781
+ let bound = false;
782
+ let lastErr;
783
+ for (const port2 of CALLBACK_PORTS) {
784
+ try {
785
+ await new Promise((resolve3, reject) => {
786
+ server.once("error", reject);
787
+ server.listen(port2, "127.0.0.1", () => {
788
+ server.removeListener("error", reject);
789
+ resolve3();
790
+ });
791
+ });
792
+ bound = true;
793
+ break;
794
+ } catch (e) {
795
+ lastErr = e;
796
+ }
797
+ }
798
+ if (!bound) {
799
+ throw new Error(
800
+ `Could not bind a local callback port (tried ${CALLBACK_PORTS.filter(Boolean).join(", ")}): ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
801
+ );
802
+ }
803
+ const { port } = server.address();
804
+ return {
805
+ redirectUri: `http://localhost:${port}/callback`,
806
+ // `close()` alone only stops new connections; a keep-alive socket from the
807
+ // browser would hold the event loop open and the CLI would never exit.
808
+ waitForCode: (expectedState, timeoutMs) => {
809
+ expected = expectedState;
810
+ return new Promise((resolve3, reject) => {
811
+ const timer = setTimeout(
812
+ () => reject(new Error(`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for the browser`)),
813
+ timeoutMs
814
+ );
815
+ resolveCode = (code) => {
816
+ clearTimeout(timer);
817
+ resolve3(code);
818
+ };
819
+ rejectCode = (err) => {
820
+ clearTimeout(timer);
821
+ reject(err);
822
+ };
823
+ });
824
+ },
825
+ close: () => {
826
+ server.closeAllConnections();
827
+ server.close();
828
+ }
829
+ };
830
+ }
831
+ async function postToken(meta, body) {
832
+ const res = await fetch(meta.token_endpoint, {
833
+ method: "POST",
834
+ headers: {
835
+ "Content-Type": "application/x-www-form-urlencoded",
836
+ Accept: "application/json"
837
+ },
838
+ body: new URLSearchParams(body).toString()
839
+ });
840
+ const json = await res.json().catch(() => ({}));
841
+ if (!res.ok || json.error) {
842
+ throw new Error(json.error_description || json.error || `Token endpoint returned ${res.status}`);
843
+ }
844
+ if (!json.access_token) throw new Error("Token endpoint returned no access_token");
845
+ return json;
846
+ }
847
+ function store(meta, token, clientId) {
848
+ return {
849
+ issuer: meta.issuer,
850
+ accessToken: token.access_token,
851
+ refreshToken: token.refresh_token,
852
+ expiresAt: Date.now() + (token.expires_in ?? 900) * 1e3,
853
+ scope: token.scope,
854
+ clientId
855
+ };
856
+ }
857
+ async function login(opts) {
858
+ const meta = await discover();
859
+ const server = await startCallbackServer();
860
+ try {
861
+ const existing = await readCredentials();
862
+ const clientId = existing?.clientId && existing.issuer === meta.issuer ? existing.clientId : await registerClient(meta, server.redirectUri);
863
+ const { verifier, challenge } = pkcePair();
864
+ const state = base64url(randomBytes(16));
865
+ const authUrl = new URL(meta.authorization_endpoint);
866
+ authUrl.searchParams.set("response_type", "code");
867
+ authUrl.searchParams.set("client_id", clientId);
868
+ authUrl.searchParams.set("redirect_uri", server.redirectUri);
869
+ authUrl.searchParams.set("scope", pickScope(meta));
870
+ authUrl.searchParams.set("state", state);
871
+ authUrl.searchParams.set("code_challenge", challenge);
872
+ authUrl.searchParams.set("code_challenge_method", "S256");
873
+ const pendingCode = server.waitForCode(state, opts.timeoutMs ?? 3e5);
874
+ pendingCode.catch(() => void 0);
875
+ opts.onUrl?.(authUrl.toString());
876
+ await opts.openBrowser(authUrl.toString());
877
+ const code = await pendingCode;
878
+ const token = await postToken(meta, {
879
+ grant_type: "authorization_code",
880
+ code,
881
+ redirect_uri: server.redirectUri,
882
+ client_id: clientId,
883
+ code_verifier: verifier
884
+ });
885
+ await writeCredentials(store(meta, token, clientId));
886
+ return { ok: true, message: "Signed in.", scope: token.scope };
887
+ } finally {
888
+ server.close();
889
+ }
890
+ }
891
+ function pickScope(meta) {
892
+ const supported = meta.scopes_supported ?? [];
893
+ if (!supported.length) return DEFAULT_SCOPE;
894
+ return DEFAULT_SCOPE.split(" ").filter((s) => supported.includes(s)).join(" ") || supported[0];
895
+ }
896
+ async function getAccessToken() {
897
+ const creds = await readCredentials();
898
+ if (!creds) return null;
899
+ if (!isExpired(creds)) return creds.accessToken;
900
+ if (!creds.refreshToken || !creds.clientId) return null;
901
+ try {
902
+ const meta = await discover();
903
+ const token = await postToken(meta, {
904
+ grant_type: "refresh_token",
905
+ refresh_token: creds.refreshToken,
906
+ client_id: creds.clientId
907
+ });
908
+ const next = store(meta, token, creds.clientId);
909
+ if (!next.refreshToken) next.refreshToken = creds.refreshToken;
910
+ await writeCredentials(next);
911
+ return next.accessToken;
912
+ } catch {
913
+ return null;
914
+ }
915
+ }
916
+ async function logout() {
917
+ await clearCredentials();
918
+ }
919
+
771
920
  // src/cli/lib/ask.ts
772
921
  var DEFAULT_ERROR_HINTS = [
773
922
  "Re-check the arguments: carrier <domain> <command> --help",
@@ -800,15 +949,14 @@ function parseRpcBody(raw) {
800
949
  return JSON.parse(dataLine.slice(5).trim());
801
950
  }
802
951
  async function callMcpTool(tool, args, opts = {}) {
803
- const token = resolveCliToken();
952
+ const token = resolveCliToken() ?? await getAccessToken();
804
953
  if (!token) {
805
954
  return {
806
955
  ok: false,
807
- text: withNextStep("No headless token in the environment.", [
808
- "Interactive path: open Claude and say your intent \u2014 OAuth runs on first use.",
809
- "Or export CARRIER_API_KEY=ak_\u2026 from Console \u2192 Settings \u2192 API Keys.",
810
- 'Then retry: carrier ask "show fleet health"',
811
- "Or run a discrete command: carrier subscribers list --account-id 123"
956
+ text: withNextStep("Not signed in.", [
957
+ "Sign in from this terminal: carrier login",
958
+ "No account yet? carrier open signup",
959
+ "Headless/CI instead: export CARRIER_API_KEY=ak_\u2026 from Console \u2192 Settings \u2192 API Keys"
812
960
  ])
813
961
  };
814
962
  }
@@ -903,7 +1051,11 @@ async function callMcpTool(tool, args, opts = {}) {
903
1051
  }
904
1052
  const parts = json.result?.content ?? [];
905
1053
  const text3 = parts.map((p4) => p4.text).filter(Boolean).join("\n").trim() || JSON.stringify(json.result ?? json, null, 2);
906
- return { ok: !json.result?.isError, text: text3 };
1054
+ return {
1055
+ ok: !json.result?.isError,
1056
+ text: text3,
1057
+ structured: json.result?.structuredContent
1058
+ };
907
1059
  })(),
908
1060
  deadline
909
1061
  ]);
@@ -936,17 +1088,60 @@ async function callMcpTool(tool, args, opts = {}) {
936
1088
  if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
937
1089
  }
938
1090
  }
1091
+ var ASK_ERROR_HINTS = [
1092
+ "Rephrase the intent more specifically (include ICCID / MSISDN if relevant)",
1093
+ "Or use a discrete command: carrier --help lists every domain"
1094
+ ];
1095
+ function parseRouting(text3) {
1096
+ const trimmed = text3.trim();
1097
+ if (!trimmed.startsWith("{")) return void 0;
1098
+ try {
1099
+ const parsed = JSON.parse(trimmed);
1100
+ if (!parsed || typeof parsed !== "object") return void 0;
1101
+ const routes = typeof parsed.resolved_tool === "string" && parsed.resolved_tool;
1102
+ const asksBack = typeof parsed.clarifying_question === "string" || Array.isArray(parsed.candidates);
1103
+ return routes || asksBack ? parsed : void 0;
1104
+ } catch {
1105
+ return void 0;
1106
+ }
1107
+ }
939
1108
  async function carrierAsk(intent) {
940
- return callMcpTool(
941
- "carrier_ask",
942
- { intent },
943
- {
944
- errorHints: [
945
- "Rephrase the intent more specifically (include ICCID / MSISDN if relevant)",
946
- "Or use a discrete command: carrier --help lists every domain"
947
- ]
948
- }
949
- );
1109
+ const routed = await callMcpTool("carrier_ask", { intent }, { errorHints: ASK_ERROR_HINTS });
1110
+ if (!routed.ok) return routed;
1111
+ const routing = parseRouting(routed.text);
1112
+ if (!routing) return routed;
1113
+ if (!routing.resolved_tool) {
1114
+ const candidates = (routing.candidates ?? []).map((c) => c.tool ? `Candidate: ${c.tool}${c.reason ? ` \u2014 ${c.reason}` : ""}` : void 0).filter((s) => Boolean(s));
1115
+ return {
1116
+ ok: false,
1117
+ text: withNextStep(routing.clarifying_question?.trim() || "Carrier could not tell which tool you meant.", [
1118
+ ...candidates,
1119
+ ...ASK_ERROR_HINTS
1120
+ ])
1121
+ };
1122
+ }
1123
+ const tool = routing.resolved_tool;
1124
+ const args = routing.resolved_params ?? {};
1125
+ if (routing.match && routing.match !== "confirmed") {
1126
+ return {
1127
+ ok: false,
1128
+ text: withNextStep(`Carrier could not confirm what you meant (match: ${routing.match}).`, [
1129
+ `Closest tool: ${tool}`,
1130
+ ...ASK_ERROR_HINTS
1131
+ ])
1132
+ };
1133
+ }
1134
+ if (routing.confirm_required) {
1135
+ return {
1136
+ ok: false,
1137
+ text: withNextStep(`"${intent}" resolves to ${tool}, which changes live service or billing.`, [
1138
+ `Params: ${JSON.stringify(args)}`,
1139
+ "Run the matching discrete command so every flag is explicit and reviewable",
1140
+ "carrier --help lists every domain"
1141
+ ])
1142
+ };
1143
+ }
1144
+ return callMcpTool(tool, args, { errorHints: ASK_ERROR_HINTS });
950
1145
  }
951
1146
 
952
1147
  // src/cli/lib/home.ts
@@ -1159,7 +1354,10 @@ var CLI_DOMAINS = [
1159
1354
  { flags: "--status <status>", description: "Filter by status, e.g. ACTIVE", arg: "status", type: "string" },
1160
1355
  { flags: "--limit <n>", description: "Max rows to return", arg: "limit", type: "number" },
1161
1356
  { flags: "--offset <n>", description: "Pagination offset", arg: "offset", type: "number" }
1162
- ]
1357
+ ],
1358
+ // OCS rejects an unfiltered list. Catch it here so the user gets the
1359
+ // flags to pass instead of a raw schema-validation dump from the tool.
1360
+ requireOneOf: ["accountId", "iccid", "msisdn", "imsi", "activationCode"]
1163
1361
  },
1164
1362
  {
1165
1363
  name: "get",
@@ -1693,7 +1891,8 @@ var CLI_DOMAINS = [
1693
1891
  flags: "--zone-id <id>",
1694
1892
  description: "Filter to one location zone",
1695
1893
  arg: "locationZoneId",
1696
- type: "number"
1894
+ type: "number",
1895
+ required: true
1697
1896
  }
1698
1897
  ]
1699
1898
  },
@@ -1749,7 +1948,8 @@ var CLI_DOMAINS = [
1749
1948
  flags: "--destination-list-id <id>",
1750
1949
  description: "Destination list ID from `carrier zones destinations`",
1751
1950
  arg: "destinationListId",
1752
- type: "number"
1951
+ type: "number",
1952
+ required: true
1753
1953
  }
1754
1954
  ]
1755
1955
  }
@@ -2155,6 +2355,14 @@ function buildToolArgs(cmd, opts) {
2155
2355
  if (cmd.write) args.dry_run = true;
2156
2356
  return { args, errors, notes };
2157
2357
  }
2358
+ function formatJsonOutput(text3) {
2359
+ try {
2360
+ JSON.parse(text3);
2361
+ return text3;
2362
+ } catch {
2363
+ return JSON.stringify(text3);
2364
+ }
2365
+ }
2158
2366
  async function runCapability(domain, cmd, opts) {
2159
2367
  const label = `${domain.name} ${cmd.name}`;
2160
2368
  const quiet = opts.json === true;
@@ -2192,7 +2400,7 @@ async function runCapability(domain, cmd, opts) {
2192
2400
  return;
2193
2401
  }
2194
2402
  if (quiet) {
2195
- process.stdout.write(`${result.text}
2403
+ process.stdout.write(`${formatJsonOutput(result.text)}
2196
2404
  `);
2197
2405
  return;
2198
2406
  }
@@ -2328,6 +2536,11 @@ async function doSiteCreate(target, brand) {
2328
2536
  info(pc4.yellow(String(e instanceof Error ? e.message : e)));
2329
2537
  }
2330
2538
  }
2539
+ async function resolveBuildTarget(storefront, preferred) {
2540
+ if (preferred) return preferred;
2541
+ const ranked = rankTargets(await probeAll(storefront));
2542
+ return ranked[0]?.id ?? "cloudflare";
2543
+ }
2331
2544
  async function maybeBuildDeploy(target, brand, opts) {
2332
2545
  if (opts.install) {
2333
2546
  const s = p3.spinner();
@@ -2345,10 +2558,12 @@ async function maybeBuildDeploy(target, brand, opts) {
2345
2558
  return;
2346
2559
  }
2347
2560
  }
2561
+ const buildTarget = await resolveBuildTarget(target, opts.preferred);
2562
+ if (opts.clerk) await runClerkStep(target, brand);
2348
2563
  if (opts.build) {
2349
2564
  const s = p3.spinner();
2350
- s.start("Building storefront (next build)");
2351
- const okb = await buildSite(target);
2565
+ s.start(`Building storefront for ${buildTarget}`);
2566
+ const okb = await buildSite(target, buildTarget);
2352
2567
  s.stop(okb ? "Build succeeded" : "Build failed \u2014 see output above");
2353
2568
  if (!okb) {
2354
2569
  p3.note(
@@ -2360,20 +2575,145 @@ async function maybeBuildDeploy(target, brand, opts) {
2360
2575
  }
2361
2576
  if (opts.deploy) {
2362
2577
  const s = p3.spinner();
2363
- s.start("Deploying to Cloudflare Workers");
2364
- const r = await deploySite(target, brand);
2365
- s.stop(r.ok ? `Deployed: ${r.projectName}` : "Deploy skipped");
2366
- if (!r.ok && r.reason) {
2367
- info(pc4.yellow(r.reason));
2578
+ s.start("Deploying");
2579
+ const r = await deploySite(target, brand, {
2580
+ preferred: opts.preferred,
2581
+ pushSecrets: opts.pushSecrets,
2582
+ customDomain: opts.customDomain
2583
+ });
2584
+ s.stop(r.ok ? `Deployed to ${r.target}: ${r.url ?? r.projectName}` : "Deploy skipped");
2585
+ if (!r.ok) {
2586
+ if (r.reason) info(pc4.yellow(r.reason));
2587
+ const hints = r.statuses.filter((t) => !t.ready && t.reason).map((t) => `${t.label}: ${t.reason}`);
2588
+ p3.note(
2589
+ [...hints.length ? hints : ["Log in to a supported host, then retry."], `Then: carrier site deploy ${target}`].join("\n"),
2590
+ "Next"
2591
+ );
2592
+ return;
2593
+ }
2594
+ if (r.secrets) {
2595
+ if (r.secrets.pushed.length) info(pc4.dim(`Secrets staged: ${r.secrets.pushed.join(", ")}`));
2596
+ if (r.secrets.failed.length) {
2597
+ info(pc4.yellow(`Secrets that failed to stage: ${r.secrets.failed.join(", ")}`));
2598
+ }
2599
+ }
2600
+ if (!r.secrets?.pushed.includes("CARRIER_API_KEY")) {
2601
+ info(
2602
+ pc4.yellow(
2603
+ "CARRIER_API_KEY was not staged \u2014 / and /shop will return 500. Set it in .env.local and redeploy."
2604
+ )
2605
+ );
2606
+ }
2607
+ if (r.url) info(pc4.green(r.url));
2608
+ if (opts.clerk && r.url) {
2609
+ const secretKey = process.env.CLERK_SECRET_KEY?.trim();
2610
+ if (secretKey) {
2611
+ const { allowedOrigins, redirectUrls } = storefrontClerkUrls(r.url, brand.domain);
2612
+ const configured = await configureClerkInstance(secretKey, { allowedOrigins, redirectUrls });
2613
+ if (configured.applied.length) {
2614
+ info(pc4.dim(`Clerk now allows the deployed origin (${configured.applied.length} settings)`));
2615
+ }
2616
+ for (const f of configured.failed) info(pc4.yellow(`Clerk ${f.step}: ${f.reason}`));
2617
+ }
2618
+ }
2619
+ if (opts.verify !== false && r.url && r.target) {
2620
+ await verifyAndRepair(target, brand, r.url, r.target, r.projectName, opts);
2621
+ }
2622
+ }
2623
+ }
2624
+ async function verifyAndRepair(storefront, brand, url, targetId, projectName, opts) {
2625
+ const s = p3.spinner();
2626
+ s.start("Verifying the deployed storefront");
2627
+ let result = await verifyStorefront(url, storefront, process.env);
2628
+ s.stop(result.ok ? "Storefront is serving" : `Storefront is not serving \u2014 ${result.diagnosis}`);
2629
+ info(pc4.dim(formatProbes(result.probes)));
2630
+ if (result.ok) return;
2631
+ info(pc4.yellow(result.summary));
2632
+ const plan = repairPlanFor(result.diagnosis);
2633
+ if (!plan) {
2634
+ if (result.diagnosis === "unclassified" && opts.autofixAgent) {
2368
2635
  p3.note(
2369
2636
  [
2370
- "Install wrangler and login: npx wrangler login",
2371
- `Then: carrier site deploy ${target}`
2637
+ "No deterministic repair matches this symptom.",
2638
+ `Probes: ${formatProbes(result.probes)}`,
2639
+ `URL: ${url}`,
2640
+ "Re-run with an agent that can read the host logs, or inspect manually."
2372
2641
  ].join("\n"),
2373
- "Next"
2642
+ "Needs investigation"
2374
2643
  );
2375
2644
  }
2645
+ process.exitCode = 1;
2646
+ return;
2647
+ }
2648
+ const rs = p3.spinner();
2649
+ rs.start(`Repairing: ${plan.note}`);
2650
+ if (plan.runClerk) await runClerkStep(storefront, brand, url);
2651
+ if (plan.rebuild) {
2652
+ const rebuilt = await buildSite(storefront, targetId);
2653
+ if (!rebuilt) {
2654
+ rs.stop("Repair failed at the rebuild step");
2655
+ process.exitCode = 1;
2656
+ return;
2657
+ }
2658
+ }
2659
+ const redeploy = await deploySite(storefront, brand, {
2660
+ preferred: targetId,
2661
+ pushSecrets: opts.pushSecrets !== false
2662
+ });
2663
+ rs.stop(redeploy.ok ? "Repaired and redeployed" : "Redeploy failed");
2664
+ if (!redeploy.ok) {
2665
+ info(pc4.yellow(redeploy.reason ?? "Redeploy failed."));
2666
+ process.exitCode = 1;
2667
+ return;
2668
+ }
2669
+ result = await verifyStorefront(redeploy.url ?? url, storefront, process.env);
2670
+ info(pc4.dim(formatProbes(result.probes)));
2671
+ if (result.ok) {
2672
+ info(pc4.green("Storefront is serving after repair."));
2673
+ return;
2674
+ }
2675
+ info(pc4.yellow(`Still failing after one repair: ${result.summary}`));
2676
+ const rb = await rollback(targetId, storefront, projectName);
2677
+ info(rb.ok ? pc4.yellow("Rolled back to the previous version.") : pc4.yellow(rb.reason ?? "No rollback available."));
2678
+ process.exitCode = 1;
2679
+ }
2680
+ async function runClerkStep(storefront, brand, deployedUrl) {
2681
+ const s = p3.spinner();
2682
+ s.start("Provisioning Clerk");
2683
+ const result = await provisionClerk({
2684
+ name: brand.name,
2685
+ domain: brand.domain,
2686
+ env: process.env,
2687
+ storefront,
2688
+ cli: clerkCliDeps()
2689
+ });
2690
+ if (!result.ok || !result.credentials) {
2691
+ s.stop("Clerk not configured");
2692
+ if (result.reason) info(pc4.yellow(result.reason));
2693
+ if (result.guidance) p3.note(result.guidance.join("\n"), "Set up Clerk");
2694
+ return;
2376
2695
  }
2696
+ const { credentials } = result;
2697
+ s.stop(
2698
+ {
2699
+ platform: `Clerk application created (${credentials.applicationId ?? "new"})`,
2700
+ "cli-keyless": "Clerk keyless application minted \u2014 claim it at dashboard.clerk.com to keep it",
2701
+ discovered: "Clerk keys found on this machine",
2702
+ manual: "Clerk not configured"
2703
+ }[result.tier]
2704
+ );
2705
+ const written = await mergeEnvLocal(storefront, {
2706
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: credentials.publishableKey,
2707
+ CLERK_SECRET_KEY: credentials.secretKey
2708
+ });
2709
+ if (written.length) info(pc4.dim(`Wrote to .env.local: ${written.join(", ")}`));
2710
+ const { allowedOrigins, redirectUrls } = storefrontClerkUrls(deployedUrl, brand.domain);
2711
+ const configured = await configureClerkInstance(credentials.secretKey, {
2712
+ allowedOrigins,
2713
+ redirectUrls
2714
+ });
2715
+ if (configured.applied.length) info(pc4.dim(`Clerk configured: ${configured.applied.length} settings`));
2716
+ for (const f of configured.failed) info(pc4.yellow(`Clerk ${f.step}: ${f.reason}`));
2377
2717
  }
2378
2718
  async function interactiveSiteCreate() {
2379
2719
  const brand = await promptBrand(CARRIER_BRAND);
@@ -2539,6 +2879,65 @@ program.command("status").description("Show MCP / plugin / auth status and next
2539
2879
  p3.note(oauthFirstUseNote(), "Auth");
2540
2880
  p3.outro("");
2541
2881
  });
2882
+ program.command("login").description("Sign in from this terminal. Opens a browser once, then refreshes silently.").action(async () => {
2883
+ header();
2884
+ const s = p3.spinner();
2885
+ s.start("Waiting for browser sign-in");
2886
+ try {
2887
+ const r = await login({
2888
+ openBrowser: (url) => openUrl(url),
2889
+ onUrl: (url) => s.message(`Sign in at ${url.slice(0, 60)}\u2026`)
2890
+ });
2891
+ s.stop(r.ok ? "Signed in" : "Failed");
2892
+ if (!r.ok) {
2893
+ p3.log.error(r.message);
2894
+ process.exitCode = 1;
2895
+ return;
2896
+ }
2897
+ p3.note(
2898
+ [`Scope: ${r.scope ?? "read write"}`, "", "Try it: carrier intelligence fleet-health"].join("\n"),
2899
+ "Signed in"
2900
+ );
2901
+ } catch (e) {
2902
+ s.stop("Failed");
2903
+ p3.log.error(
2904
+ withNextStep(e instanceof Error ? e.message : String(e), [
2905
+ "Check outbound HTTPS to mcp.carrier.llc",
2906
+ "Headless/CI instead: export CARRIER_API_KEY=ak_\u2026"
2907
+ ])
2908
+ );
2909
+ process.exitCode = 1;
2910
+ }
2911
+ p3.outro("");
2912
+ });
2913
+ program.command("logout").description("Forget the stored sign-in on this machine.").action(async () => {
2914
+ header();
2915
+ await logout();
2916
+ p3.log.success("Signed out.");
2917
+ p3.outro("");
2918
+ });
2919
+ program.command("whoami").description("Show the account, org and scopes the current credential resolves to.").option("--json", "Print the raw response on stdout and nothing else").action(async (opts) => {
2920
+ if (!opts.json) header();
2921
+ const s = opts.json ? null : p3.spinner();
2922
+ s?.start("Calling environment_info");
2923
+ const r = await callMcpTool("environment_info", {}, {
2924
+ errorHints: ["Sign in first: carrier login", "Or export CARRIER_API_KEY=ak_\u2026"]
2925
+ });
2926
+ s?.stop(r.ok ? "Done" : "Failed");
2927
+ if (!r.ok) {
2928
+ if (opts.json) console.error(r.text);
2929
+ else p3.log.error(r.text);
2930
+ process.exitCode = 1;
2931
+ return;
2932
+ }
2933
+ if (opts.json) {
2934
+ process.stdout.write(`${r.text}
2935
+ `);
2936
+ return;
2937
+ }
2938
+ p3.note(r.text, "environment_info");
2939
+ p3.outro("");
2940
+ });
2542
2941
  var openCmd = program.command("open").description("Open Carrier account / product URLs in your browser.");
2543
2942
  for (const [name, url, desc] of [
2544
2943
  ["signup", SIGN_UP_URL, "Create a Carrier account (Clerk sign-up)"],
@@ -2638,6 +3037,50 @@ for (const domain of CLI_DOMAINS) {
2638
3037
  });
2639
3038
  }
2640
3039
  }
3040
+ program.command("dash [domain]").description(`Render a status screen in the terminal: ${DASH_DOMAINS.join(" | ")}`).option("--json", "Emit the screen model as JSON instead of drawing it").option("--no-color", "Plain text, safe to pipe").option("--width <n>", "Terminal width override").action(async (domain, o) => {
3041
+ const target = domain ?? "fleet";
3042
+ if (!isDashDomain(target)) {
3043
+ fail(`Unknown dashboard "${target}".`, [`Available: ${DASH_DOMAINS.join(", ")}`]);
3044
+ process.exitCode = 1;
3045
+ return;
3046
+ }
3047
+ const tool = DASH_TOOL[target];
3048
+ if (!tool) {
3049
+ fail(`\`carrier dash ${target}\` reads local state, not a tool.`, [
3050
+ "Run it from a storefront directory: carrier site deploy --no-secrets"
3051
+ ]);
3052
+ process.exitCode = 1;
3053
+ return;
3054
+ }
3055
+ if (!o.json) header();
3056
+ const result = await callMcpTool(tool, {}, {
3057
+ errorHints: [`Check auth: carrier status`, `Or ask in words: carrier ask "\u2026"`]
3058
+ });
3059
+ if (!result.ok) {
3060
+ fail(result.text, ["carrier status"]);
3061
+ process.exitCode = 1;
3062
+ return;
3063
+ }
3064
+ let payload;
3065
+ try {
3066
+ payload = JSON.parse(result.text);
3067
+ } catch {
3068
+ payload = void 0;
3069
+ }
3070
+ const screen = screenFromStructured(result.structured) ?? buildDashScreen(target, payload);
3071
+ if (o.json) {
3072
+ process.stdout.write(`${JSON.stringify(screen, null, 2)}
3073
+ `);
3074
+ return;
3075
+ }
3076
+ process.stdout.write(
3077
+ `${renderTui(screen, {
3078
+ color: o.color !== false,
3079
+ width: o.width ? Number(o.width) : process.stdout.columns
3080
+ })}
3081
+ `
3082
+ );
3083
+ });
2641
3084
  var site = program.command("site").description("Scaffold and deploy a white-labeled storefront.");
2642
3085
  site.command("create [dir]").description("Scaffold a white-labeled storefront from the Mango template.").option("--name <name>", "Brand name").option("--domain <domain>", "Domain").option("--accent <hex>", "Accent color").option("--yes", "Carrier defaults, no prompts").action(async (dir, o) => {
2643
3086
  header();
@@ -2681,22 +3124,101 @@ site.command("create [dir]").description("Scaffold a white-labeled storefront fr
2681
3124
  }
2682
3125
  p3.outro(pc4.green(`Scaffolded. cd ${dir ?? "storefront"} && pnpm install && pnpm dev`));
2683
3126
  });
2684
- site.command("deploy [dir]").description("Build + deploy a storefront to Cloudflare Workers.").option("--name <name>", "Cloudflare Worker name (defaults from brand)").action(async (dir, o) => {
2685
- header();
2686
- const target = resolve2(dir ?? "./storefront");
2687
- try {
2688
- const brand = await loadStorefrontBrand(target, o.name ? { name: o.name } : void 0);
2689
- await maybeBuildDeploy(target, brand, { install: true, build: true, deploy: true });
2690
- } catch (e) {
2691
- fail(String(e instanceof Error ? e.message : e), [
2692
- "Scaffold first: carrier site create",
2693
- "Ensure wrangler is logged in for deploy"
2694
- ]);
2695
- process.exitCode = 1;
2696
- return;
3127
+ site.command("deploy [dir]").description("Build + deploy a storefront to whichever host is available on this box.").option("--name <name>", "Project/worker name (defaults from brand)").option(`--target <target>`, `Force a host: ${TARGET_IDS.join(" | ")}`).option("--no-secrets", "Skip staging runtime secrets on the host").option("--clerk", "Provision and configure Clerk before building").option("--no-verify", "Skip the post-deploy check that the site actually serves").option("--autofix-agent", "On an unrecognised failure, print an agent-ready diagnosis bundle").option("--domain", "Route the brand domain to the deployment (Cloudflare provisions DNS + cert)").action(
3128
+ async (dir, o) => {
3129
+ header();
3130
+ const target = resolve2(dir ?? "./storefront");
3131
+ if (o.target && !isTargetId(o.target)) {
3132
+ fail(`Unknown target "${o.target}".`, [`Supported: ${TARGET_IDS.join(", ")}`]);
3133
+ process.exitCode = 1;
3134
+ return;
3135
+ }
3136
+ try {
3137
+ const brand = await loadStorefrontBrand(target, o.name ? { name: o.name } : void 0);
3138
+ await maybeBuildDeploy(target, brand, {
3139
+ install: true,
3140
+ build: true,
3141
+ deploy: true,
3142
+ preferred: o.target,
3143
+ pushSecrets: o.secrets !== false,
3144
+ clerk: o.clerk === true,
3145
+ verify: o.verify !== false,
3146
+ autofixAgent: o.autofixAgent === true,
3147
+ customDomain: o.domain === true
3148
+ });
3149
+ } catch (e) {
3150
+ fail(String(e instanceof Error ? e.message : e), [
3151
+ "Scaffold first: carrier site create",
3152
+ "Check host logins: carrier site targets"
3153
+ ]);
3154
+ process.exitCode = 1;
3155
+ return;
3156
+ }
3157
+ p3.outro("");
2697
3158
  }
3159
+ );
3160
+ site.command("targets [dir]").description("Show which deploy hosts are installed, logged in, and ready.").action(async (dir) => {
3161
+ header();
3162
+ const storefront = resolve2(dir ?? "./storefront");
3163
+ const statuses = await probeAll(storefront);
3164
+ const ranked = rankTargets(statuses);
3165
+ const rows2 = statuses.map((s) => {
3166
+ const mark2 = s.ready ? pc4.green("ready") : pc4.dim("not ready");
3167
+ const detail = s.ready ? [s.account, s.configured ? "configured" : void 0].filter(Boolean).join(", ") : s.reason ?? "";
3168
+ return `${s.label.padEnd(20)} ${mark2} ${pc4.dim(detail)}`;
3169
+ });
3170
+ p3.note(rows2.join("\n"), "Deploy hosts");
3171
+ info(
3172
+ ranked.length ? pc4.green(`Default on this box: ${ranked[0].label}`) : pc4.yellow("No host is ready \u2014 log in to one, then re-run.")
3173
+ );
2698
3174
  p3.outro("");
2699
3175
  });
3176
+ site.command("clerk [dir]").description("Create or discover a Clerk instance and configure it for the storefront.").option("--name <name>", "Application name (defaults from brand)").option("--production", "Also create a production instance (Platform API only)").option("--no-create", "Never create a new application; only reuse existing keys").option("--url <url>", "Deployed URL to whitelist as an origin/redirect").action(
3177
+ async (dir, o) => {
3178
+ header();
3179
+ const storefront = resolve2(dir ?? "./storefront");
3180
+ try {
3181
+ const brand = await loadStorefrontBrand(storefront, o.name ? { name: o.name } : void 0);
3182
+ const result = await provisionClerk({
3183
+ name: o.name ?? brand.name,
3184
+ domain: brand.domain,
3185
+ production: o.production === true,
3186
+ noCreate: o.create === false,
3187
+ env: process.env,
3188
+ storefront,
3189
+ cli: clerkCliDeps()
3190
+ });
3191
+ if (!result.ok || !result.credentials) {
3192
+ if (result.reason) info(pc4.yellow(result.reason));
3193
+ p3.note((result.guidance ?? []).join("\n"), "Set up Clerk");
3194
+ process.exitCode = 1;
3195
+ return;
3196
+ }
3197
+ const written = await mergeEnvLocal(storefront, {
3198
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: result.credentials.publishableKey,
3199
+ CLERK_SECRET_KEY: result.credentials.secretKey
3200
+ });
3201
+ info(
3202
+ result.tier === "platform" ? pc4.green(`Created Clerk application ${result.credentials.applicationId ?? ""}`.trim()) : pc4.green("Reused Clerk keys already on this machine")
3203
+ );
3204
+ if (written.length) info(pc4.dim(`Wrote to .env.local: ${written.join(", ")}`));
3205
+ const { allowedOrigins, redirectUrls } = storefrontClerkUrls(o.url, brand.domain);
3206
+ const configured = await configureClerkInstance(result.credentials.secretKey, {
3207
+ allowedOrigins,
3208
+ redirectUrls
3209
+ });
3210
+ if (configured.applied.length) {
3211
+ p3.note(configured.applied.join("\n"), "Applied to the Clerk instance");
3212
+ }
3213
+ for (const f of configured.failed) info(pc4.yellow(`${f.step}: ${f.reason}`));
3214
+ } catch (e) {
3215
+ fail(String(e instanceof Error ? e.message : e), ["Scaffold first: carrier site create"]);
3216
+ process.exitCode = 1;
3217
+ return;
3218
+ }
3219
+ p3.outro("");
3220
+ }
3221
+ );
2700
3222
  site.command("logo [dir]").description("Generate a storefront logo (SVG always; PNG if an image key is on this box or Carrier).").action(async (dir) => {
2701
3223
  header();
2702
3224
  const target = resolve2(dir ?? "./storefront");