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