@carrierllc/mcp 0.13.24 → 0.13.26

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
@@ -31,6 +31,7 @@ import {
31
31
  repairPlanFor,
32
32
  rollback,
33
33
  run,
34
+ setWorkerName,
34
35
  slug,
35
36
  storefrontClerkUrls,
36
37
  storefrontScreen,
@@ -39,7 +40,7 @@ import {
39
40
  verifyStorefront,
40
41
  walletScreen,
41
42
  which
42
- } from "./chunk-YQQUV7AE.js";
43
+ } from "./chunk-HTIPSHMH.js";
43
44
  import {
44
45
  copyTree,
45
46
  exists,
@@ -275,294 +276,6 @@ async function writeStorefrontLogo(target, brand) {
275
276
  return { path: svgPath, source: logo.source };
276
277
  }
277
278
 
278
- // src/cli/lib/hosted-publish.ts
279
- import { readFileSync } from "fs";
280
-
281
- // ../../packages/sdk/src/storefront-labels.ts
282
- var regionNames = new Intl.DisplayNames(["en"], { type: "region" });
283
- var PREFERRED = /* @__PURE__ */ new Set(["CW", "BF", "BJ", "MM", "CD", "RS", "DE", "FR", "GB", "VU", "ZW", "RU", "TL", "VN", "YE"]);
284
- var NAME_TO_CODE = /* @__PURE__ */ new Map();
285
- for (let first = 65; first <= 90; first += 1) {
286
- for (let second = 65; second <= 90; second += 1) {
287
- const code = String.fromCharCode(first, second);
288
- const label = regionLabel(code);
289
- if (!label) continue;
290
- const key = label.toLowerCase();
291
- const current = NAME_TO_CODE.get(key);
292
- if (!current || PREFERRED.has(code)) NAME_TO_CODE.set(key, code);
293
- }
294
- }
295
- NAME_TO_CODE.set("uk", "GB");
296
- var CODE_ALIASES = { UK: "GB" };
297
- function parseDataGb(raw) {
298
- if (raw.trim().toLowerCase() === "unlimited") return { ok: true, value: 0, unlimited: true };
299
- const parsed = parseAmount(raw, /gigabytes?|gbs?|gb/gi, "Enter data like 1, 1 GB, or Unlimited.", 1e4, false);
300
- if (!parsed.ok) return parsed;
301
- return { ok: true, value: parsed.value, unlimited: false };
302
- }
303
- function parseValidityDays(raw) {
304
- return parseAmount(raw, /days?/gi, "Enter validity like 7 or 7 days.", 3650, true);
305
- }
306
- var ZERO_DECIMAL = /* @__PURE__ */ new Set([
307
- "BIF",
308
- "CLP",
309
- "DJF",
310
- "GNF",
311
- "JPY",
312
- "KMF",
313
- "KRW",
314
- "MGA",
315
- "PYG",
316
- "RWF",
317
- "UGX",
318
- "VND",
319
- "VUV",
320
- "XAF",
321
- "XOF",
322
- "XPF"
323
- ]);
324
- function majorToCents(input, currency = "EUR") {
325
- const text3 = input.trim();
326
- if (ZERO_DECIMAL.has(currency.trim().toUpperCase())) {
327
- if (!/^\d+$/.test(text3)) return { ok: false, message: "Enter a whole number, such as 1000." };
328
- const cents2 = Number(text3);
329
- if (!Number.isSafeInteger(cents2) || cents2 <= 0) return { ok: false, message: "Enter a price above zero." };
330
- return { ok: true, cents: cents2 };
331
- }
332
- const match = /^(\d+)(?:\.(\d{1,2}))?$/.exec(text3);
333
- if (!match) return { ok: false, message: "Enter a price like 9.00." };
334
- const major = Number(match[1]);
335
- const fraction = (match[2] ?? "").padEnd(2, "0");
336
- const cents = major * 100 + Number(fraction);
337
- if (!Number.isSafeInteger(cents) || cents <= 0) return { ok: false, message: "Enter a price above zero." };
338
- return { ok: true, cents };
339
- }
340
- function parseCountryInput(raw) {
341
- const regions = [];
342
- const parts = raw.split(",").map((part) => part.trim()).filter((part) => part !== "");
343
- for (const part of parts) {
344
- const added = codesFor(part);
345
- if (!added) return { ok: false, message: "Countries are names, such as France, or codes such as FR." };
346
- for (const code of added) {
347
- if (regions.length >= MAX_REGIONS_PER_PLAN) break;
348
- if (!regions.includes(code)) regions.push(code);
349
- }
350
- }
351
- return { ok: true, regions };
352
- }
353
- var MAX_REGIONS_PER_PLAN = 250;
354
- function parseAmount(raw, unit, message, max, whole) {
355
- const text3 = raw.trim().replace(unit, "").trim();
356
- if (text3 === "" || !/^\d+(\.\d+)?$/.test(text3)) return { ok: false, message };
357
- const value = Number(text3);
358
- if (!Number.isFinite(value) || value < 0 || value > max) return { ok: false, message };
359
- if (whole && !Number.isInteger(value)) return { ok: false, message };
360
- return { ok: true, value };
361
- }
362
- function codesFor(part) {
363
- const tokens = part.split(/\s+/).filter((token) => token !== "");
364
- if (tokens.length > 1 && tokens.every((token) => /^[A-Za-z]{2}$/.test(token))) {
365
- return tokens.map((token) => canonical(token));
366
- }
367
- if (/^[A-Za-z]{2}$/.test(part)) return [canonical(part)];
368
- const code = NAME_TO_CODE.get(part.toLowerCase());
369
- return code ? [code] : null;
370
- }
371
- function canonical(code) {
372
- const upper = code.toUpperCase();
373
- return CODE_ALIASES[upper] ?? upper;
374
- }
375
- var LOGO_DATA = /^data:image\/(png|jpeg|webp|gif);base64,[A-Za-z0-9+/]+={0,2}$/;
376
- var LOGO_MAX_CHARS = 1e5;
377
- var LOGO_MAX_BYTES = 70 * 1024;
378
- function logoFileDataUrl(bytes) {
379
- const mime = logoMime(bytes);
380
- if (!mime || bytes.byteLength < 1 || bytes.byteLength > LOGO_MAX_BYTES) {
381
- return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
382
- }
383
- const url = `data:${mime};base64,${toBase64(bytes)}`;
384
- if (url.length > LOGO_MAX_CHARS || !LOGO_DATA.test(url)) {
385
- return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
386
- }
387
- return { ok: true, url };
388
- }
389
- function logoReference(value) {
390
- const trimmed = value.trim();
391
- if (trimmed.startsWith("data:")) {
392
- if (trimmed.length <= LOGO_MAX_CHARS && LOGO_DATA.test(trimmed)) return { ok: true, url: trimmed };
393
- return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
394
- }
395
- try {
396
- const url = new URL(trimmed);
397
- if (url.protocol === "https:") return { ok: true, url: url.toString() };
398
- } catch {
399
- return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
400
- }
401
- return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
402
- }
403
- function toBase64(bytes) {
404
- let binary = "";
405
- for (const byte of bytes) binary += String.fromCharCode(byte);
406
- return btoa(binary);
407
- }
408
- function logoMime(bytes) {
409
- if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
410
- return "image/png";
411
- }
412
- if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
413
- if (bytes.length >= 6 && bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) return "image/gif";
414
- if (bytes.length >= 12 && bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) {
415
- return "image/webp";
416
- }
417
- return null;
418
- }
419
- function regionLabel(code) {
420
- if (!/^[A-Z]{2}$/.test(code)) return void 0;
421
- try {
422
- const label = regionNames.of(code);
423
- if (!label || label.toUpperCase() === code) return void 0;
424
- return label;
425
- } catch {
426
- return void 0;
427
- }
428
- }
429
-
430
- // ../../packages/sdk/src/storefront-write.ts
431
- var THEME_FIELDS = [
432
- ["accent", "accent"],
433
- ["accentDark", "accentDark"],
434
- ["bg", "bg"],
435
- ["text", "text"],
436
- ["logoUrl", "logoUrl"]
437
- ];
438
- var COPY_FIELDS = [
439
- ["hero", "hero"],
440
- ["tagline", "tagline"],
441
- ["supportEmail", "supportEmail"],
442
- ["legalName", "legalName"]
443
- ];
444
- function storefrontBrandPatch(input, existing) {
445
- const theme = overlay(existing?.theme, input, THEME_FIELDS);
446
- const copy = overlay(existing?.copy, input, COPY_FIELDS);
447
- return {
448
- ...theme ? { theme } : {},
449
- ...copy ? { copy } : {}
450
- };
451
- }
452
- function completePlanInput(input, plans) {
453
- if ((input.productId?.trim() ?? "") !== "") return input;
454
- const touched = [
455
- input.slug,
456
- input.name,
457
- input.priceCents,
458
- input.currency,
459
- input.templateId,
460
- input.dataGb,
461
- input.unlimited,
462
- input.validityDays,
463
- input.regions,
464
- input.description,
465
- input.active,
466
- input.sortOrder
467
- ].some((value) => value !== void 0);
468
- if (!touched) return input;
469
- const slug2 = input.slug?.trim();
470
- const match = slug2 ? plans.find((plan) => plan.slug === slug2) : plans.length === 1 ? plans[0] : void 0;
471
- if (!match) {
472
- const currency = sharedCurrency(plans);
473
- if (!input.currency && currency) return { ...input, currency };
474
- return input;
475
- }
476
- return {
477
- ...input,
478
- slug: input.slug ?? match.slug,
479
- name: input.name ?? match.name,
480
- currency: input.currency ?? match.currency,
481
- templateId: input.templateId ?? match.ocs_package_template_id
482
- };
483
- }
484
- function sharedCurrency(plans) {
485
- const codes = new Set(plans.map((plan) => plan.currency.trim().toUpperCase()).filter((code) => code !== ""));
486
- if (codes.size !== 1) return void 0;
487
- return [...codes][0];
488
- }
489
- function storefrontPlanBody(input) {
490
- const productId = input.productId?.trim() ?? "";
491
- if (productId !== "") {
492
- const body2 = { product_id: productId };
493
- if (input.priceCents !== void 0) body2.price_cents = input.priceCents;
494
- if (input.active !== void 0) body2.active = input.active;
495
- if (input.sortOrder !== void 0) body2.sort_order = input.sortOrder;
496
- return body2;
497
- }
498
- const touched = [
499
- input.slug,
500
- input.name,
501
- input.priceCents,
502
- input.currency,
503
- input.templateId,
504
- input.dataGb,
505
- input.unlimited,
506
- input.validityDays,
507
- input.regions,
508
- input.description,
509
- input.active,
510
- input.sortOrder
511
- ].some((value) => value !== void 0);
512
- if (!touched) return null;
513
- if (!input.slug || !input.name || input.priceCents === void 0 || !input.currency || input.templateId === void 0) {
514
- return { error: "A plan needs a slug, name, price in minor units, currency, and OCS template id." };
515
- }
516
- if (input.dataGb !== void 0 && input.dataGb <= 0 && input.unlimited !== true) {
517
- return { error: "data_gb of 0 is refused. Send unlimited: true to sell an unlimited plan." };
518
- }
519
- const body = {
520
- slug: input.slug,
521
- name: input.name,
522
- price_cents: input.priceCents,
523
- currency: input.currency,
524
- ocs_package_template_id: input.templateId
525
- };
526
- if (input.dataGb !== void 0) body.data_gb = input.dataGb;
527
- if (input.unlimited !== void 0) body.unlimited = input.unlimited;
528
- if (input.validityDays !== void 0) body.validity_days = input.validityDays;
529
- if (input.regions !== void 0) body.regions = input.regions;
530
- if (input.description !== void 0) body.description = input.description;
531
- if (input.active !== void 0) body.active = input.active;
532
- if (input.sortOrder !== void 0) body.sort_order = input.sortOrder;
533
- return body;
534
- }
535
- function isStorefrontPlanError(value) {
536
- return value !== null && "error" in value;
537
- }
538
- function parseWholeNumber(value, label) {
539
- if (value === void 0) return { ok: true, value: void 0 };
540
- if (!/^\d+$/.test(value)) return { ok: false, message: `${label} must be a whole number.` };
541
- return { ok: true, value: Number(value) };
542
- }
543
- function overlay(base, input, fields) {
544
- const patch = {};
545
- let touched = false;
546
- for (const [field, jsonKey] of fields) {
547
- const value = input[field];
548
- if (typeof value !== "string" || value.trim() === "") continue;
549
- patch[jsonKey] = value.trim();
550
- touched = true;
551
- }
552
- if (!touched) return void 0;
553
- return { ...base ?? {}, ...patch };
554
- }
555
- var RESERVED_SLUGS = /* @__PURE__ */ new Set(["www", "app", "api", "stores", "checkout", "dashboard", "admin", "mail"]);
556
- function hostedStorefrontUrl(slug2) {
557
- return `https://${slug2}.stores.carrier.llc`;
558
- }
559
- function storefrontSlugFrom(raw) {
560
- const folded = raw.trim().toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
561
- const clipped = folded.slice(0, 40).replace(/-+$/g, "");
562
- if (RESERVED_SLUGS.has(clipped)) return "";
563
- return /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/.test(clipped) ? clipped : "";
564
- }
565
-
566
279
  // src/cli/lib/auth.ts
567
280
  import { createHash, randomBytes, timingSafeEqual } from "crypto";
568
281
  import { createServer } from "http";
@@ -1266,247 +979,542 @@ async function callMcpTool(tool, args, opts = {}) {
1266
979
  return {
1267
980
  ok: false,
1268
981
  text: withNextStep(
1269
- `MCP call to ${tool} timed out after ${secs}s (no response).`,
1270
- [
1271
- "The tool is likely returning more data than the MCP transport can deliver \u2014 see issue #663",
1272
- "Narrow the request (a specific --account-id / --iccid returns far less than a whole-fleet call)",
1273
- `Raise the deadline for one slow call: CARRIER_MCP_TIMEOUT_MS=${timeoutMs * 2} carrier \u2026`,
1274
- "Confirm the service is up: check network access to https://mcp.carrier.llc/mcp",
1275
- "Or open Claude and ask there with full tool routing"
1276
- ]
982
+ `MCP call to ${tool} timed out after ${secs}s (no response).`,
983
+ [
984
+ "The tool is likely returning more data than the MCP transport can deliver \u2014 see issue #663",
985
+ "Narrow the request (a specific --account-id / --iccid returns far less than a whole-fleet call)",
986
+ `Raise the deadline for one slow call: CARRIER_MCP_TIMEOUT_MS=${timeoutMs * 2} carrier \u2026`,
987
+ "Confirm the service is up: check network access to https://mcp.carrier.llc/mcp",
988
+ "Or open Claude and ask there with full tool routing"
989
+ ]
990
+ )
991
+ };
992
+ }
993
+ const msg = e instanceof Error ? e.message : String(e);
994
+ return {
995
+ ok: false,
996
+ text: withNextStep(`Could not reach MCP: ${msg}`, [
997
+ "Verify outbound HTTPS to mcp.carrier.llc",
998
+ "Use the interactive Claude path while offline from this machine"
999
+ ])
1000
+ };
1001
+ } finally {
1002
+ if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
1003
+ }
1004
+ }
1005
+ var ASK_ERROR_HINTS = [
1006
+ "Rephrase the intent more specifically (include ICCID / MSISDN if relevant)",
1007
+ "Or use a discrete command: carrier --help lists every domain"
1008
+ ];
1009
+ function parseRouting(text3) {
1010
+ const trimmed = text3.trim();
1011
+ if (!trimmed.startsWith("{")) return void 0;
1012
+ try {
1013
+ const parsed = JSON.parse(trimmed);
1014
+ if (!parsed || typeof parsed !== "object") return void 0;
1015
+ const routes = typeof parsed.resolved_tool === "string" && parsed.resolved_tool;
1016
+ const asksBack = typeof parsed.clarifying_question === "string" || Array.isArray(parsed.candidates);
1017
+ return routes || asksBack ? parsed : void 0;
1018
+ } catch {
1019
+ return void 0;
1020
+ }
1021
+ }
1022
+ async function carrierAsk(intent) {
1023
+ const routed = await callMcpTool("carrier_ask", { intent }, { errorHints: ASK_ERROR_HINTS });
1024
+ if (!routed.ok) return routed;
1025
+ const routing = parseRouting(routed.text);
1026
+ if (!routing) return routed;
1027
+ if (!routing.resolved_tool) {
1028
+ const candidates = (routing.candidates ?? []).map((c) => c.tool ? `Candidate: ${c.tool}${c.reason ? ` \u2014 ${c.reason}` : ""}` : void 0).filter((s) => Boolean(s));
1029
+ return {
1030
+ ok: false,
1031
+ text: withNextStep(routing.clarifying_question?.trim() || "Carrier could not tell which tool you meant.", [
1032
+ ...candidates,
1033
+ ...ASK_ERROR_HINTS
1034
+ ])
1035
+ };
1036
+ }
1037
+ const tool = routing.resolved_tool;
1038
+ const args = routing.resolved_params ?? {};
1039
+ if (routing.match && routing.match !== "confirmed") {
1040
+ return {
1041
+ ok: false,
1042
+ text: withNextStep(`Carrier could not confirm what you meant (match: ${routing.match}).`, [
1043
+ `Closest tool: ${tool}`,
1044
+ ...ASK_ERROR_HINTS
1045
+ ])
1046
+ };
1047
+ }
1048
+ if (routing.confirm_required) {
1049
+ return {
1050
+ ok: false,
1051
+ text: withNextStep(`"${intent}" resolves to ${tool}, which changes live service or billing.`, [
1052
+ `Params: ${JSON.stringify(args)}`,
1053
+ "Run the matching discrete command so every flag is explicit and reviewable",
1054
+ "carrier --help lists every domain"
1055
+ ])
1056
+ };
1057
+ }
1058
+ return callMcpTool(tool, args, { errorHints: ASK_ERROR_HINTS });
1059
+ }
1060
+
1061
+ // src/cli/lib/rest.ts
1062
+ var DEFAULT_ERROR_HINTS2 = [
1063
+ "Re-check the arguments: carrier <domain> <command> --help",
1064
+ "Or open Claude and ask there with full tool routing"
1065
+ ];
1066
+ function resolveApiKey() {
1067
+ return process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || null;
1068
+ }
1069
+ function ocsTokenOnly() {
1070
+ return Boolean(
1071
+ process.env.ESIMVAULT_API_TOKEN?.trim() || process.env.CARRIER_OCS_API_TOKEN?.trim()
1072
+ );
1073
+ }
1074
+ function applyPath(path, args) {
1075
+ const rest = { ...args };
1076
+ const missing = [];
1077
+ const filled = path.replace(/\{(\w+)\}/g, (_match, name) => {
1078
+ const value = rest[name];
1079
+ if (value === void 0 || value === null || value === "") {
1080
+ missing.push(name);
1081
+ return `{${name}}`;
1082
+ }
1083
+ delete rest[name];
1084
+ return encodeURIComponent(String(value));
1085
+ });
1086
+ return { path: filled, rest, missing };
1087
+ }
1088
+ function queryString(args) {
1089
+ const params = new URLSearchParams();
1090
+ for (const [key, value] of Object.entries(args)) {
1091
+ if (value === void 0 || value === null) continue;
1092
+ params.set(key, String(value));
1093
+ }
1094
+ const qs = params.toString();
1095
+ return qs ? `?${qs}` : "";
1096
+ }
1097
+ function bodyText(raw) {
1098
+ const trimmed = raw.trim();
1099
+ if (!trimmed) return "";
1100
+ try {
1101
+ const parsed = JSON.parse(trimmed);
1102
+ if (typeof parsed.error === "string") return parsed.error;
1103
+ if (typeof parsed.message === "string") return parsed.message;
1104
+ return trimmed;
1105
+ } catch {
1106
+ return trimmed;
1107
+ }
1108
+ }
1109
+ async function callRestEndpoint(method, path, args, opts = {}) {
1110
+ const token = resolveApiKey() ?? await getAccessToken();
1111
+ if (!token) {
1112
+ return {
1113
+ ok: false,
1114
+ text: withNextStep(
1115
+ ocsTokenOnly() ? "Not signed in. An eSIMVault/OCS token is set, but the Carrier API only accepts Carrier credentials." : "Not signed in.",
1116
+ [
1117
+ "Sign in from this terminal: carrier login",
1118
+ "No account yet? carrier open signup",
1119
+ "Headless/CI instead: export CARRIER_API_KEY=ak_\u2026 from Console \u2192 Settings \u2192 API Keys"
1120
+ ]
1121
+ )
1122
+ };
1123
+ }
1124
+ const { path: resolvedPath, rest, missing } = applyPath(path, args);
1125
+ if (missing.length) {
1126
+ return {
1127
+ ok: false,
1128
+ text: withNextStep(`Cannot build ${method} ${path}.`, [
1129
+ `Missing path value(s): ${missing.join(", ")}`,
1130
+ "See every flag: carrier <domain> <command> --help"
1131
+ ])
1132
+ };
1133
+ }
1134
+ const sendsBody = method === "POST" || method === "PUT" || method === "PATCH";
1135
+ const url = `${API_URL}${resolvedPath}${sendsBody ? "" : queryString(rest)}`;
1136
+ const headers = {
1137
+ Authorization: `Bearer ${token}`,
1138
+ Accept: "application/json",
1139
+ "User-Agent": `carrier-cli/${CARRIER_VERSION}`
1140
+ };
1141
+ if (sendsBody) headers["Content-Type"] = "application/json";
1142
+ if (opts.confirmToken) headers["X-Confirm-Token"] = opts.confirmToken;
1143
+ if (opts.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
1144
+ const timeoutMs = mcpTimeoutMs();
1145
+ const controller = new AbortController();
1146
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1147
+ try {
1148
+ const res = await fetch(url, {
1149
+ method,
1150
+ headers,
1151
+ signal: controller.signal,
1152
+ body: sendsBody ? JSON.stringify(rest) : void 0
1153
+ });
1154
+ const raw = await res.text();
1155
+ if (res.status === 202) {
1156
+ let confirmToken;
1157
+ let expires;
1158
+ let message = "";
1159
+ try {
1160
+ const parsed = JSON.parse(raw);
1161
+ confirmToken = parsed.confirm_token;
1162
+ expires = parsed.expires_in_seconds;
1163
+ message = parsed.message ?? "";
1164
+ } catch {
1165
+ }
1166
+ return {
1167
+ ok: false,
1168
+ text: withNextStep(
1169
+ message || `${method} ${path} needs an explicit confirmation.`,
1170
+ confirmToken ? [
1171
+ `Re-run the same command with: --confirm-token ${confirmToken}`,
1172
+ `The token is single-use and expires in ${expires ?? 120}s.`
1173
+ ] : ["The server asked for confirmation but returned no token \u2014 retry the command."]
1174
+ )
1175
+ };
1176
+ }
1177
+ if (res.status === 401 || res.status === 403) {
1178
+ return {
1179
+ ok: false,
1180
+ text: withNextStep(
1181
+ `Carrier API returned ${res.status} (auth rejected).`,
1182
+ [
1183
+ "Confirm CARRIER_API_KEY is a valid org key (ak_\u2026) from app.carrier.llc",
1184
+ "Or sign in from this terminal: carrier login",
1185
+ ...res.status === 403 ? ["A 403 here usually means the key lacks write scope."] : []
1186
+ ]
1187
+ )
1188
+ };
1189
+ }
1190
+ if (!res.ok) {
1191
+ return {
1192
+ ok: false,
1193
+ text: withNextStep(
1194
+ redact(`${method} ${path} failed (${res.status}). ${bodyText(raw).slice(0, 500)}`),
1195
+ opts.errorHints ?? DEFAULT_ERROR_HINTS2
1277
1196
  )
1278
1197
  };
1279
1198
  }
1199
+ return { ok: true, text: raw.trim() || JSON.stringify({ status: res.status }) };
1200
+ } catch (e) {
1201
+ const aborted = e instanceof Error && (e.name === "AbortError" || e.name === "TimeoutError");
1202
+ if (aborted) {
1203
+ const secs = Math.round(timeoutMs / 1e3);
1204
+ return {
1205
+ ok: false,
1206
+ text: withNextStep(`${method} ${path} timed out after ${secs}s (no response).`, [
1207
+ `Raise the deadline for one slow call: CARRIER_MCP_TIMEOUT_MS=${timeoutMs * 2} carrier \u2026`,
1208
+ "Confirm the service is up: check network access to https://api.carrier.llc"
1209
+ ])
1210
+ };
1211
+ }
1280
1212
  const msg = e instanceof Error ? e.message : String(e);
1281
1213
  return {
1282
1214
  ok: false,
1283
- text: withNextStep(`Could not reach MCP: ${msg}`, [
1284
- "Verify outbound HTTPS to mcp.carrier.llc",
1215
+ text: withNextStep(redact(`Could not reach the Carrier API: ${msg}`), [
1216
+ "Verify outbound HTTPS to api.carrier.llc",
1285
1217
  "Use the interactive Claude path while offline from this machine"
1286
1218
  ])
1287
1219
  };
1288
1220
  } finally {
1289
- if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
1221
+ clearTimeout(timer);
1290
1222
  }
1291
1223
  }
1292
- var ASK_ERROR_HINTS = [
1293
- "Rephrase the intent more specifically (include ICCID / MSISDN if relevant)",
1294
- "Or use a discrete command: carrier --help lists every domain"
1295
- ];
1296
- function parseRouting(text3) {
1297
- const trimmed = text3.trim();
1298
- if (!trimmed.startsWith("{")) return void 0;
1224
+
1225
+ // src/cli/lib/hosted-publish.ts
1226
+ import { readFileSync } from "fs";
1227
+
1228
+ // ../../packages/sdk/src/storefront-labels.ts
1229
+ var regionNames = new Intl.DisplayNames(["en"], { type: "region" });
1230
+ var PREFERRED = /* @__PURE__ */ new Set(["CW", "BF", "BJ", "MM", "CD", "RS", "DE", "FR", "GB", "VU", "ZW", "RU", "TL", "VN", "YE"]);
1231
+ var NAME_TO_CODE = /* @__PURE__ */ new Map();
1232
+ for (let first = 65; first <= 90; first += 1) {
1233
+ for (let second = 65; second <= 90; second += 1) {
1234
+ const code = String.fromCharCode(first, second);
1235
+ const label = regionLabel(code);
1236
+ if (!label) continue;
1237
+ const key = label.toLowerCase();
1238
+ const current = NAME_TO_CODE.get(key);
1239
+ if (!current || PREFERRED.has(code)) NAME_TO_CODE.set(key, code);
1240
+ }
1241
+ }
1242
+ NAME_TO_CODE.set("uk", "GB");
1243
+ var CODE_ALIASES = { UK: "GB" };
1244
+ function parseDataGb(raw) {
1245
+ if (raw.trim().toLowerCase() === "unlimited") return { ok: true, value: 0, unlimited: true };
1246
+ const parsed = parseAmount(raw, /gigabytes?|gbs?|gb/gi, "Enter data like 1, 1 GB, or Unlimited.", 1e4, false);
1247
+ if (!parsed.ok) return parsed;
1248
+ return { ok: true, value: parsed.value, unlimited: false };
1249
+ }
1250
+ function parseValidityDays(raw) {
1251
+ return parseAmount(raw, /days?/gi, "Enter validity like 7 or 7 days.", 3650, true);
1252
+ }
1253
+ var ZERO_DECIMAL = /* @__PURE__ */ new Set([
1254
+ "BIF",
1255
+ "CLP",
1256
+ "DJF",
1257
+ "GNF",
1258
+ "JPY",
1259
+ "KMF",
1260
+ "KRW",
1261
+ "MGA",
1262
+ "PYG",
1263
+ "RWF",
1264
+ "UGX",
1265
+ "VND",
1266
+ "VUV",
1267
+ "XAF",
1268
+ "XOF",
1269
+ "XPF"
1270
+ ]);
1271
+ function majorToCents(input, currency = "EUR") {
1272
+ const text3 = input.trim();
1273
+ if (ZERO_DECIMAL.has(currency.trim().toUpperCase())) {
1274
+ if (!/^\d+$/.test(text3)) return { ok: false, message: "Enter a whole number, such as 1000." };
1275
+ const cents2 = Number(text3);
1276
+ if (!Number.isSafeInteger(cents2) || cents2 <= 0) return { ok: false, message: "Enter a price above zero." };
1277
+ return { ok: true, cents: cents2 };
1278
+ }
1279
+ const match = /^(\d+)(?:\.(\d{1,2}))?$/.exec(text3);
1280
+ if (!match) return { ok: false, message: "Enter a price like 9.00." };
1281
+ const major = Number(match[1]);
1282
+ const fraction = (match[2] ?? "").padEnd(2, "0");
1283
+ const cents = major * 100 + Number(fraction);
1284
+ if (!Number.isSafeInteger(cents) || cents <= 0) return { ok: false, message: "Enter a price above zero." };
1285
+ return { ok: true, cents };
1286
+ }
1287
+ function parseCountryInput(raw) {
1288
+ const regions = [];
1289
+ const parts = raw.split(",").map((part) => part.trim()).filter((part) => part !== "");
1290
+ for (const part of parts) {
1291
+ const added = codesFor(part);
1292
+ if (!added) return { ok: false, message: "Countries are names, such as France, or codes such as FR." };
1293
+ for (const code of added) {
1294
+ if (regions.length >= MAX_REGIONS_PER_PLAN) break;
1295
+ if (!regions.includes(code)) regions.push(code);
1296
+ }
1297
+ }
1298
+ return { ok: true, regions };
1299
+ }
1300
+ var MAX_REGIONS_PER_PLAN = 250;
1301
+ function parseAmount(raw, unit, message, max, whole) {
1302
+ const text3 = raw.trim().replace(unit, "").trim();
1303
+ if (text3 === "" || !/^\d+(\.\d+)?$/.test(text3)) return { ok: false, message };
1304
+ const value = Number(text3);
1305
+ if (!Number.isFinite(value) || value < 0 || value > max) return { ok: false, message };
1306
+ if (whole && !Number.isInteger(value)) return { ok: false, message };
1307
+ return { ok: true, value };
1308
+ }
1309
+ function codesFor(part) {
1310
+ const tokens = part.split(/\s+/).filter((token) => token !== "");
1311
+ if (tokens.length > 1 && tokens.every((token) => /^[A-Za-z]{2}$/.test(token))) {
1312
+ return tokens.map((token) => canonical(token));
1313
+ }
1314
+ if (/^[A-Za-z]{2}$/.test(part)) return [canonical(part)];
1315
+ const code = NAME_TO_CODE.get(part.toLowerCase());
1316
+ return code ? [code] : null;
1317
+ }
1318
+ function canonical(code) {
1319
+ const upper = code.toUpperCase();
1320
+ return CODE_ALIASES[upper] ?? upper;
1321
+ }
1322
+ var LOGO_DATA = /^data:image\/(png|jpeg|webp|gif);base64,[A-Za-z0-9+/]+={0,2}$/;
1323
+ var LOGO_MAX_CHARS = 1e5;
1324
+ var LOGO_MAX_BYTES = 70 * 1024;
1325
+ function logoFileDataUrl(bytes) {
1326
+ const mime = logoMime(bytes);
1327
+ if (!mime || bytes.byteLength < 1 || bytes.byteLength > LOGO_MAX_BYTES) {
1328
+ return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
1329
+ }
1330
+ const url = `data:${mime};base64,${toBase64(bytes)}`;
1331
+ if (url.length > LOGO_MAX_CHARS || !LOGO_DATA.test(url)) {
1332
+ return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
1333
+ }
1334
+ return { ok: true, url };
1335
+ }
1336
+ function logoReference(value) {
1337
+ const trimmed = value.trim();
1338
+ if (trimmed.startsWith("data:")) {
1339
+ if (trimmed.length <= LOGO_MAX_CHARS && LOGO_DATA.test(trimmed)) return { ok: true, url: trimmed };
1340
+ return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
1341
+ }
1299
1342
  try {
1300
- const parsed = JSON.parse(trimmed);
1301
- if (!parsed || typeof parsed !== "object") return void 0;
1302
- const routes = typeof parsed.resolved_tool === "string" && parsed.resolved_tool;
1303
- const asksBack = typeof parsed.clarifying_question === "string" || Array.isArray(parsed.candidates);
1304
- return routes || asksBack ? parsed : void 0;
1343
+ const url = new URL(trimmed);
1344
+ if (url.protocol === "https:") return { ok: true, url: url.toString() };
1305
1345
  } catch {
1306
- return void 0;
1346
+ return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
1307
1347
  }
1348
+ return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
1308
1349
  }
1309
- async function carrierAsk(intent) {
1310
- const routed = await callMcpTool("carrier_ask", { intent }, { errorHints: ASK_ERROR_HINTS });
1311
- if (!routed.ok) return routed;
1312
- const routing = parseRouting(routed.text);
1313
- if (!routing) return routed;
1314
- if (!routing.resolved_tool) {
1315
- const candidates = (routing.candidates ?? []).map((c) => c.tool ? `Candidate: ${c.tool}${c.reason ? ` \u2014 ${c.reason}` : ""}` : void 0).filter((s) => Boolean(s));
1316
- return {
1317
- ok: false,
1318
- text: withNextStep(routing.clarifying_question?.trim() || "Carrier could not tell which tool you meant.", [
1319
- ...candidates,
1320
- ...ASK_ERROR_HINTS
1321
- ])
1322
- };
1350
+ function toBase64(bytes) {
1351
+ let binary = "";
1352
+ for (const byte of bytes) binary += String.fromCharCode(byte);
1353
+ return btoa(binary);
1354
+ }
1355
+ function logoMime(bytes) {
1356
+ if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
1357
+ return "image/png";
1323
1358
  }
1324
- const tool = routing.resolved_tool;
1325
- const args = routing.resolved_params ?? {};
1326
- if (routing.match && routing.match !== "confirmed") {
1327
- return {
1328
- ok: false,
1329
- text: withNextStep(`Carrier could not confirm what you meant (match: ${routing.match}).`, [
1330
- `Closest tool: ${tool}`,
1331
- ...ASK_ERROR_HINTS
1332
- ])
1333
- };
1359
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
1360
+ if (bytes.length >= 6 && bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) return "image/gif";
1361
+ if (bytes.length >= 12 && bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) {
1362
+ return "image/webp";
1334
1363
  }
1335
- if (routing.confirm_required) {
1336
- return {
1337
- ok: false,
1338
- text: withNextStep(`"${intent}" resolves to ${tool}, which changes live service or billing.`, [
1339
- `Params: ${JSON.stringify(args)}`,
1340
- "Run the matching discrete command so every flag is explicit and reviewable",
1341
- "carrier --help lists every domain"
1342
- ])
1343
- };
1364
+ return null;
1365
+ }
1366
+ function regionLabel(code) {
1367
+ if (!/^[A-Z]{2}$/.test(code)) return void 0;
1368
+ try {
1369
+ const label = regionNames.of(code);
1370
+ if (!label || label.toUpperCase() === code) return void 0;
1371
+ return label;
1372
+ } catch {
1373
+ return void 0;
1374
+ }
1375
+ }
1376
+
1377
+ // ../../packages/sdk/src/storefront-write.ts
1378
+ var THEME_FIELDS = [
1379
+ ["accent", "accent"],
1380
+ ["accentDark", "accentDark"],
1381
+ ["bg", "bg"],
1382
+ ["text", "text"],
1383
+ ["logoUrl", "logoUrl"]
1384
+ ];
1385
+ var COPY_FIELDS = [
1386
+ ["hero", "hero"],
1387
+ ["tagline", "tagline"],
1388
+ ["supportEmail", "supportEmail"],
1389
+ ["legalName", "legalName"]
1390
+ ];
1391
+ function storefrontBrandPatch(input, existing) {
1392
+ const theme = overlay(existing?.theme, input, THEME_FIELDS);
1393
+ const copy = overlay(existing?.copy, input, COPY_FIELDS);
1394
+ return {
1395
+ ...theme ? { theme } : {},
1396
+ ...copy ? { copy } : {}
1397
+ };
1398
+ }
1399
+ function completePlanInput(input, plans) {
1400
+ if ((input.productId?.trim() ?? "") !== "") return input;
1401
+ const touched = [
1402
+ input.slug,
1403
+ input.name,
1404
+ input.priceCents,
1405
+ input.currency,
1406
+ input.templateId,
1407
+ input.accountId,
1408
+ input.dataGb,
1409
+ input.unlimited,
1410
+ input.validityDays,
1411
+ input.regions,
1412
+ input.description,
1413
+ input.active,
1414
+ input.sortOrder
1415
+ ].some((value) => value !== void 0);
1416
+ if (!touched) return input;
1417
+ const slug2 = input.slug?.trim();
1418
+ const match = slug2 ? plans.find((plan) => plan.slug === slug2) : plans.length === 1 ? plans[0] : void 0;
1419
+ if (!match) {
1420
+ const currency = sharedCurrency(plans);
1421
+ if (!input.currency && currency) return { ...input, currency };
1422
+ return input;
1344
1423
  }
1345
- return callMcpTool(tool, args, { errorHints: ASK_ERROR_HINTS });
1346
- }
1347
-
1348
- // src/cli/lib/rest.ts
1349
- var DEFAULT_ERROR_HINTS2 = [
1350
- "Re-check the arguments: carrier <domain> <command> --help",
1351
- "Or open Claude and ask there with full tool routing"
1352
- ];
1353
- function resolveApiKey() {
1354
- return process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || null;
1355
- }
1356
- function ocsTokenOnly() {
1357
- return Boolean(
1358
- process.env.ESIMVAULT_API_TOKEN?.trim() || process.env.CARRIER_OCS_API_TOKEN?.trim()
1359
- );
1424
+ return {
1425
+ ...input,
1426
+ slug: input.slug ?? match.slug,
1427
+ name: input.name ?? match.name,
1428
+ currency: input.currency ?? match.currency,
1429
+ templateId: input.templateId ?? match.ocs_package_template_id
1430
+ };
1360
1431
  }
1361
- function applyPath(path, args) {
1362
- const rest = { ...args };
1363
- const missing = [];
1364
- const filled = path.replace(/\{(\w+)\}/g, (_match, name) => {
1365
- const value = rest[name];
1366
- if (value === void 0 || value === null || value === "") {
1367
- missing.push(name);
1368
- return `{${name}}`;
1369
- }
1370
- delete rest[name];
1371
- return encodeURIComponent(String(value));
1372
- });
1373
- return { path: filled, rest, missing };
1432
+ function sharedCurrency(plans) {
1433
+ const codes = new Set(plans.map((plan) => plan.currency.trim().toUpperCase()).filter((code) => code !== ""));
1434
+ if (codes.size !== 1) return void 0;
1435
+ return [...codes][0];
1374
1436
  }
1375
- function queryString(args) {
1376
- const params = new URLSearchParams();
1377
- for (const [key, value] of Object.entries(args)) {
1378
- if (value === void 0 || value === null) continue;
1379
- params.set(key, String(value));
1437
+ function storefrontPlanBody(input) {
1438
+ if (input.accountId !== void 0 && (!Number.isSafeInteger(input.accountId) || input.accountId <= 0)) {
1439
+ return { error: "OCS account id must be a positive integer." };
1380
1440
  }
1381
- const qs = params.toString();
1382
- return qs ? `?${qs}` : "";
1383
- }
1384
- function bodyText(raw) {
1385
- const trimmed = raw.trim();
1386
- if (!trimmed) return "";
1387
- try {
1388
- const parsed = JSON.parse(trimmed);
1389
- if (typeof parsed.error === "string") return parsed.error;
1390
- if (typeof parsed.message === "string") return parsed.message;
1391
- return trimmed;
1392
- } catch {
1393
- return trimmed;
1441
+ const productId = input.productId?.trim() ?? "";
1442
+ if (productId !== "") {
1443
+ const body2 = { product_id: productId };
1444
+ if (input.accountId !== void 0) body2.ocs_account_id = input.accountId;
1445
+ if (input.priceCents !== void 0) body2.price_cents = input.priceCents;
1446
+ if (input.active !== void 0) body2.active = input.active;
1447
+ if (input.sortOrder !== void 0) body2.sort_order = input.sortOrder;
1448
+ return body2;
1394
1449
  }
1395
- }
1396
- async function callRestEndpoint(method, path, args, opts = {}) {
1397
- const token = resolveApiKey() ?? await getAccessToken();
1398
- if (!token) {
1399
- return {
1400
- ok: false,
1401
- text: withNextStep(
1402
- ocsTokenOnly() ? "Not signed in. An eSIMVault/OCS token is set, but the Carrier API only accepts Carrier credentials." : "Not signed in.",
1403
- [
1404
- "Sign in from this terminal: carrier login",
1405
- "No account yet? carrier open signup",
1406
- "Headless/CI instead: export CARRIER_API_KEY=ak_\u2026 from Console \u2192 Settings \u2192 API Keys"
1407
- ]
1408
- )
1409
- };
1450
+ const touched = [
1451
+ input.slug,
1452
+ input.name,
1453
+ input.priceCents,
1454
+ input.currency,
1455
+ input.templateId,
1456
+ input.accountId,
1457
+ input.dataGb,
1458
+ input.unlimited,
1459
+ input.validityDays,
1460
+ input.regions,
1461
+ input.description,
1462
+ input.active,
1463
+ input.sortOrder
1464
+ ].some((value) => value !== void 0);
1465
+ if (!touched) return null;
1466
+ if (!input.slug || !input.name || input.priceCents === void 0 || !input.currency || input.templateId === void 0) {
1467
+ return { error: "A plan needs a slug, name, price in minor units, currency, and OCS template id." };
1410
1468
  }
1411
- const { path: resolvedPath, rest, missing } = applyPath(path, args);
1412
- if (missing.length) {
1413
- return {
1414
- ok: false,
1415
- text: withNextStep(`Cannot build ${method} ${path}.`, [
1416
- `Missing path value(s): ${missing.join(", ")}`,
1417
- "See every flag: carrier <domain> <command> --help"
1418
- ])
1419
- };
1469
+ if (input.dataGb !== void 0 && input.dataGb <= 0 && input.unlimited !== true) {
1470
+ return { error: "data_gb of 0 is refused. Send unlimited: true to sell an unlimited plan." };
1420
1471
  }
1421
- const sendsBody = method === "POST" || method === "PUT" || method === "PATCH";
1422
- const url = `${API_URL}${resolvedPath}${sendsBody ? "" : queryString(rest)}`;
1423
- const headers = {
1424
- Authorization: `Bearer ${token}`,
1425
- Accept: "application/json",
1426
- "User-Agent": `carrier-cli/${CARRIER_VERSION}`
1472
+ const body = {
1473
+ slug: input.slug,
1474
+ name: input.name,
1475
+ price_cents: input.priceCents,
1476
+ currency: input.currency,
1477
+ ocs_package_template_id: input.templateId
1427
1478
  };
1428
- if (sendsBody) headers["Content-Type"] = "application/json";
1429
- if (opts.confirmToken) headers["X-Confirm-Token"] = opts.confirmToken;
1430
- if (opts.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
1431
- const timeoutMs = mcpTimeoutMs();
1432
- const controller = new AbortController();
1433
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1434
- try {
1435
- const res = await fetch(url, {
1436
- method,
1437
- headers,
1438
- signal: controller.signal,
1439
- body: sendsBody ? JSON.stringify(rest) : void 0
1440
- });
1441
- const raw = await res.text();
1442
- if (res.status === 202) {
1443
- let confirmToken;
1444
- let expires;
1445
- let message = "";
1446
- try {
1447
- const parsed = JSON.parse(raw);
1448
- confirmToken = parsed.confirm_token;
1449
- expires = parsed.expires_in_seconds;
1450
- message = parsed.message ?? "";
1451
- } catch {
1452
- }
1453
- return {
1454
- ok: false,
1455
- text: withNextStep(
1456
- message || `${method} ${path} needs an explicit confirmation.`,
1457
- confirmToken ? [
1458
- `Re-run the same command with: --confirm-token ${confirmToken}`,
1459
- `The token is single-use and expires in ${expires ?? 120}s.`
1460
- ] : ["The server asked for confirmation but returned no token \u2014 retry the command."]
1461
- )
1462
- };
1463
- }
1464
- if (res.status === 401 || res.status === 403) {
1465
- return {
1466
- ok: false,
1467
- text: withNextStep(
1468
- `Carrier API returned ${res.status} (auth rejected).`,
1469
- [
1470
- "Confirm CARRIER_API_KEY is a valid org key (ak_\u2026) from app.carrier.llc",
1471
- "Or sign in from this terminal: carrier login",
1472
- ...res.status === 403 ? ["A 403 here usually means the key lacks write scope."] : []
1473
- ]
1474
- )
1475
- };
1476
- }
1477
- if (!res.ok) {
1478
- return {
1479
- ok: false,
1480
- text: withNextStep(
1481
- redact(`${method} ${path} failed (${res.status}). ${bodyText(raw).slice(0, 500)}`),
1482
- opts.errorHints ?? DEFAULT_ERROR_HINTS2
1483
- )
1484
- };
1485
- }
1486
- return { ok: true, text: raw.trim() || JSON.stringify({ status: res.status }) };
1487
- } catch (e) {
1488
- const aborted = e instanceof Error && (e.name === "AbortError" || e.name === "TimeoutError");
1489
- if (aborted) {
1490
- const secs = Math.round(timeoutMs / 1e3);
1491
- return {
1492
- ok: false,
1493
- text: withNextStep(`${method} ${path} timed out after ${secs}s (no response).`, [
1494
- `Raise the deadline for one slow call: CARRIER_MCP_TIMEOUT_MS=${timeoutMs * 2} carrier \u2026`,
1495
- "Confirm the service is up: check network access to https://api.carrier.llc"
1496
- ])
1497
- };
1498
- }
1499
- const msg = e instanceof Error ? e.message : String(e);
1500
- return {
1501
- ok: false,
1502
- text: withNextStep(redact(`Could not reach the Carrier API: ${msg}`), [
1503
- "Verify outbound HTTPS to api.carrier.llc",
1504
- "Use the interactive Claude path while offline from this machine"
1505
- ])
1506
- };
1507
- } finally {
1508
- clearTimeout(timer);
1479
+ if (input.accountId !== void 0) body.ocs_account_id = input.accountId;
1480
+ if (input.dataGb !== void 0) body.data_gb = input.dataGb;
1481
+ if (input.unlimited !== void 0) body.unlimited = input.unlimited;
1482
+ if (input.validityDays !== void 0) body.validity_days = input.validityDays;
1483
+ if (input.regions !== void 0) body.regions = input.regions;
1484
+ if (input.description !== void 0) body.description = input.description;
1485
+ if (input.active !== void 0) body.active = input.active;
1486
+ if (input.sortOrder !== void 0) body.sort_order = input.sortOrder;
1487
+ return body;
1488
+ }
1489
+ function isStorefrontPlanError(value) {
1490
+ return value !== null && "error" in value;
1491
+ }
1492
+ function parseWholeNumber(value, label) {
1493
+ if (value === void 0) return { ok: true, value: void 0 };
1494
+ if (!/^\d+$/.test(value)) return { ok: false, message: `${label} must be a whole number.` };
1495
+ return { ok: true, value: Number(value) };
1496
+ }
1497
+ function overlay(base, input, fields) {
1498
+ const patch = {};
1499
+ let touched = false;
1500
+ for (const [field, jsonKey] of fields) {
1501
+ const value = input[field];
1502
+ if (typeof value !== "string" || value.trim() === "") continue;
1503
+ patch[jsonKey] = value.trim();
1504
+ touched = true;
1509
1505
  }
1506
+ if (!touched) return void 0;
1507
+ return { ...base ?? {}, ...patch };
1508
+ }
1509
+ var RESERVED_SLUGS = /* @__PURE__ */ new Set(["www", "app", "api", "stores", "checkout", "dashboard", "admin", "mail"]);
1510
+ function hostedStorefrontUrl(slug2) {
1511
+ return `https://${slug2}.stores.carrier.llc`;
1512
+ }
1513
+ function storefrontSlugFrom(raw) {
1514
+ const folded = raw.trim().toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1515
+ const clipped = folded.slice(0, 40).replace(/-+$/g, "");
1516
+ if (RESERVED_SLUGS.has(clipped)) return "";
1517
+ return /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/.test(clipped) ? clipped : "";
1510
1518
  }
1511
1519
 
1512
1520
  // src/cli/lib/hosted-publish.ts
@@ -1557,6 +1565,10 @@ function hostedPublishInputFromFlags(flags) {
1557
1565
  if (!status) return { ok: false, text: "Status must be draft or published." };
1558
1566
  const price = readPrice(flags);
1559
1567
  if (!price.ok) return { ok: false, text: price.text };
1568
+ const accountId = flags.ocsAccountId === void 0 ? void 0 : Number(flags.ocsAccountId);
1569
+ if (accountId !== void 0 && (!/^\d+$/.test(flags.ocsAccountId) || !Number.isSafeInteger(accountId) || accountId <= 0)) {
1570
+ return { ok: false, text: "OCS account id must be a positive integer." };
1571
+ }
1560
1572
  const template = parseWholeNumber(flags.templateId, "Template id");
1561
1573
  if (!template.ok) return { ok: false, text: template.message };
1562
1574
  const data = readDataGb(flags.dataGb, flags.unlimited);
@@ -1590,6 +1602,7 @@ function hostedPublishInputFromFlags(flags) {
1590
1602
  priceCents: price.value,
1591
1603
  currency: flags.currency,
1592
1604
  templateId: template.value,
1605
+ accountId,
1593
1606
  dataGb: data.value,
1594
1607
  unlimited: data.unlimited,
1595
1608
  validityDays: validity.value,
@@ -1650,6 +1663,7 @@ function planInput(input) {
1650
1663
  priceCents: input.priceCents,
1651
1664
  currency: input.currency,
1652
1665
  templateId: input.templateId,
1666
+ accountId: input.accountId,
1653
1667
  dataGb: input.dataGb,
1654
1668
  unlimited: input.unlimited,
1655
1669
  validityDays: input.validityDays,
@@ -5393,7 +5407,9 @@ async function maybeBuildDeploy(target, brand, opts) {
5393
5407
  const r = await deploySite(target, brand, {
5394
5408
  preferred: opts.preferred,
5395
5409
  pushSecrets: opts.pushSecrets,
5396
- customDomain: opts.customDomain
5410
+ customDomain: opts.customDomain,
5411
+ explicitName: opts.explicitName,
5412
+ force: opts.force
5397
5413
  });
5398
5414
  s.stop(r.ok ? `Deployed to ${r.target}: ${r.url ?? r.projectName}` : "Deploy skipped");
5399
5415
  if (!r.ok) {
@@ -5472,7 +5488,12 @@ async function verifyAndRepair(storefront, brand, url, targetId, projectName, op
5472
5488
  }
5473
5489
  const redeploy = await deploySite(storefront, brand, {
5474
5490
  preferred: targetId,
5475
- pushSecrets: opts.pushSecrets !== false
5491
+ pushSecrets: opts.pushSecrets !== false,
5492
+ // Keep the original deployment identity and acknowledgement: without
5493
+ // these, a valid --force deploy to the scaffold default would be refused
5494
+ // during repair, and an explicit --name could resolve differently.
5495
+ explicitName: opts.explicitName,
5496
+ force: opts.force
5476
5497
  });
5477
5498
  rs.stop(redeploy.ok ? "Repaired and redeployed" : "Redeploy failed");
5478
5499
  if (!redeploy.ok) {
@@ -6036,6 +6057,18 @@ program.command("dash [domain]").description(`Render a status screen in the term
6036
6057
  );
6037
6058
  });
6038
6059
  var site = program.command("site").description("Scaffold and deploy a white-labeled storefront.");
6060
+ site.command("list").description("List hosted storefronts with stored OCS account ownership.").action(async () => {
6061
+ const result = await callRestEndpoint("GET", "/v1/storefronts", {});
6062
+ process.stdout.write(`${result.text}
6063
+ `);
6064
+ if (!result.ok) process.exitCode = 1;
6065
+ });
6066
+ site.command("show").description("Show a hosted storefront and its plans' OCS accounts.").requiredOption("--id <id>", "Storefront id").action(async (o) => {
6067
+ const result = await callRestEndpoint("GET", "/v1/storefronts/{id}", { id: o.id });
6068
+ process.stdout.write(`${result.text}
6069
+ `);
6070
+ if (!result.ok) process.exitCode = 1;
6071
+ });
6039
6072
  site.command("create [dir]").description("Scaffold a white-labeled storefront from the bundled 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) => {
6040
6073
  header();
6041
6074
  let brand = { ...CARRIER_BRAND };
@@ -6079,9 +6112,15 @@ site.command("create [dir]").description("Scaffold a white-labeled storefront fr
6079
6112
  process.exitCode = 1;
6080
6113
  return;
6081
6114
  }
6115
+ if (o.name) {
6116
+ const workerName = slug(o.name);
6117
+ if (await setWorkerName(target, workerName)) {
6118
+ info(pc4.dim(`Worker name set to "${workerName}" in wrangler.jsonc`));
6119
+ }
6120
+ }
6082
6121
  p3.outro(pc4.green(`Scaffolded. cd ${dir ?? "storefront"} && pnpm install && pnpm dev`));
6083
6122
  });
6084
- 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(
6123
+ 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("--force", "Deploy under the scaffold default name anyway (acknowledges the overwrite risk)").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(
6085
6124
  async (dir, o) => {
6086
6125
  header();
6087
6126
  const target = resolve2(dir ?? "./storefront");
@@ -6101,7 +6140,9 @@ site.command("deploy [dir]").description("Build + deploy a storefront to whichev
6101
6140
  clerk: o.clerk === true,
6102
6141
  verify: o.verify !== false,
6103
6142
  autofixAgent: o.autofixAgent === true,
6104
- customDomain: o.domain === true
6143
+ customDomain: o.domain === true,
6144
+ explicitName: o.name,
6145
+ force: o.force === true
6105
6146
  });
6106
6147
  } catch (e) {
6107
6148
  fail(String(e instanceof Error ? e.message : e), [
@@ -6114,7 +6155,7 @@ site.command("deploy [dir]").description("Build + deploy a storefront to whichev
6114
6155
  p3.outro("");
6115
6156
  }
6116
6157
  );
6117
- site.command("publish").description("Publish a Carrier-hosted storefront. Does not deploy a self-hosted copy.").requiredOption("--slug <slug>", "Shop address, such as acme or Acme Mobile").requiredOption("--name <name>", "Brand name").option("--status <status>", "draft or published", "published").option("--accent <hex>", "Accent color, such as #112233").option("--accent-dark <hex>", "Darker accent").option("--bg <hex>", "Page background").option("--text <hex>", "Body text color").option("--logo <url-or-file>", "https logo address, or a PNG, JPEG, WebP, or GIF under 70 KB").option("--hero <text>", "Shop headline").option("--tagline <text>", "Line under the headline").option("--support-email <email>", "Support address").option("--legal-name <name>", "Legal name in the footer").option("--product <id>", "Connectivity plan id. Copies its template, price, and allowance.").option("--plan <slug>", "Plan slug, such as eu-1gb").option("--plan-name <name>", "Plan name on the shop").option("--price <amount>", "Retail price, such as 9.00").option("--price-cents <cents>", "Retail price in minor units").option("--currency <code>", "Three-letter currency code").option("--template-id <id>", "OCS package template id").option("--data-gb <gb>", "Data allowance, such as 1 or 1 GB, or the text Unlimited").option("--unlimited", "The only way to sell an unlimited plan. A 0 or missing --data-gb without this is refused.").option("--validity-days <days>", "Validity, such as 7 or 7 days").option("--countries <names>", "Country names or codes, such as France, DE").option("--plan-text <text>", "Buyer-facing sentence. Omit to leave the saved text unchanged.").option("--inactive", "Keep the plan off the shop").option("--plan-sort-order <n>", "Plan display order on the shop (ascending, default 0)").action(async (o) => {
6158
+ site.command("publish").description("Publish a Carrier-hosted storefront. Does not deploy a self-hosted copy.").requiredOption("--slug <slug>", "Shop address, such as acme or Acme Mobile").requiredOption("--name <name>", "Brand name").option("--status <status>", "draft or published", "published").option("--accent <hex>", "Accent color, such as #112233").option("--accent-dark <hex>", "Darker accent").option("--bg <hex>", "Page background").option("--text <hex>", "Body text color").option("--logo <url-or-file>", "https logo address, or a PNG, JPEG, WebP, or GIF under 70 KB").option("--hero <text>", "Shop headline").option("--tagline <text>", "Line under the headline").option("--support-email <email>", "Support address").option("--legal-name <name>", "Legal name in the footer").option("--product <id>", "Connectivity plan id. Copies its template, price, and allowance.").option("--plan <slug>", "Plan slug, such as eu-1gb").option("--plan-name <name>", "Plan name on the shop").option("--price <amount>", "Retail price, such as 9.00").option("--price-cents <cents>", "Retail price in minor units").option("--currency <code>", "Three-letter currency code").option("--template-id <id>", "OCS package template id").option("--ocs-account-id <id>", "Owned OCS account override; omit for automatic resolution").option("--data-gb <gb>", "Data allowance, such as 1 or 1 GB, or the text Unlimited").option("--unlimited", "The only way to sell an unlimited plan. A 0 or missing --data-gb without this is refused.").option("--validity-days <days>", "Validity, such as 7 or 7 days").option("--countries <names>", "Country names or codes, such as France, DE").option("--plan-text <text>", "Buyer-facing sentence. Omit to leave the saved text unchanged.").option("--inactive", "Keep the plan off the shop").option("--plan-sort-order <n>", "Plan display order on the shop (ascending, default 0)").action(async (o) => {
6118
6159
  header();
6119
6160
  const built = hostedPublishInputFromFlags(o);
6120
6161
  if (!built.ok) {