@carrierllc/mcp 0.13.25 → 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/{chunk-KFIIIQ4J.js → chunk-HTIPSHMH.js} +41 -5
- package/dist/chunk-HTIPSHMH.js.map +1 -0
- package/dist/cli.js +549 -508
- package/dist/cli.js.map +1 -1
- package/dist/index.js +49 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/plugin/carrier/skills/storefront/SKILL.md +1 -1
- package/dist/chunk-KFIIIQ4J.js.map +0 -1
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-
|
|
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
|
|
1284
|
-
"Verify outbound HTTPS to
|
|
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
|
-
|
|
1221
|
+
clearTimeout(timer);
|
|
1290
1222
|
}
|
|
1291
1223
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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
|
|
1301
|
-
if (
|
|
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
|
|
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
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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
|
-
|
|
1325
|
-
|
|
1326
|
-
if (
|
|
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
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
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
|
|
1362
|
-
const
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
|
1376
|
-
|
|
1377
|
-
|
|
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
|
|
1382
|
-
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
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
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
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
|
-
|
|
1412
|
-
|
|
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
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
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 (
|
|
1429
|
-
if (
|
|
1430
|
-
if (
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
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) {
|