@carrierllc/mcp 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-QZBALDKD.js → chunk-CQ6EOLA7.js} +1778 -1660
- package/dist/chunk-CQ6EOLA7.js.map +1 -0
- package/dist/cli.js +1647 -245
- package/dist/cli.js.map +1 -1
- package/dist/index.js +6122 -6122
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-QZBALDKD.js.map +0 -1
|
@@ -4,1795 +4,1795 @@ import {
|
|
|
4
4
|
writeFile
|
|
5
5
|
} from "./chunk-SHKKVIIA.js";
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
// src/cli/lib/deploy-targets.ts
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { tmpdir } from "os";
|
|
10
|
+
import { mkdtemp, rm, writeFile as writeFileMode } from "fs/promises";
|
|
11
|
+
|
|
12
|
+
// src/cli/lib/exec.ts
|
|
13
|
+
import { spawn } from "child_process";
|
|
14
|
+
var SAFE_COMMAND = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
15
|
+
function isSafeCommand(cmd) {
|
|
16
|
+
return SAFE_COMMAND.test(cmd);
|
|
11
17
|
}
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
function run(cmd, args, opts = {}) {
|
|
19
|
+
if (!isSafeCommand(cmd)) {
|
|
20
|
+
return Promise.resolve({
|
|
21
|
+
ok: false,
|
|
22
|
+
code: null,
|
|
23
|
+
stdout: "",
|
|
24
|
+
stderr: `refusing to run unsafe command: ${cmd}`
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
const child = spawn(cmd, args, { cwd: opts.cwd, shell: false });
|
|
29
|
+
if (opts.stdin !== void 0) {
|
|
30
|
+
child.stdin?.on("error", () => {
|
|
31
|
+
});
|
|
32
|
+
child.stdin?.end(opts.stdin);
|
|
33
|
+
}
|
|
34
|
+
let stdout = "";
|
|
35
|
+
let stderr = "";
|
|
36
|
+
let settled = false;
|
|
37
|
+
const finish = (result) => {
|
|
38
|
+
if (settled) return;
|
|
39
|
+
settled = true;
|
|
40
|
+
resolve(result);
|
|
41
|
+
};
|
|
42
|
+
let timer;
|
|
43
|
+
if (opts.timeoutMs && opts.timeoutMs > 0) {
|
|
44
|
+
timer = setTimeout(() => {
|
|
45
|
+
try {
|
|
46
|
+
child.kill("SIGTERM");
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
finish({
|
|
50
|
+
ok: false,
|
|
51
|
+
code: null,
|
|
52
|
+
stdout,
|
|
53
|
+
stderr: stderr || `timeout after ${opts.timeoutMs}ms`
|
|
54
|
+
});
|
|
55
|
+
}, opts.timeoutMs);
|
|
25
56
|
}
|
|
57
|
+
child.stdout?.on("data", (d) => stdout += d.toString());
|
|
58
|
+
child.stderr?.on("data", (d) => stderr += d.toString());
|
|
59
|
+
child.on("error", () => {
|
|
60
|
+
if (timer) clearTimeout(timer);
|
|
61
|
+
finish({ ok: false, code: null, stdout, stderr });
|
|
62
|
+
});
|
|
63
|
+
child.on("close", (code) => {
|
|
64
|
+
if (timer) clearTimeout(timer);
|
|
65
|
+
finish({ ok: code === 0, code, stdout, stderr });
|
|
66
|
+
});
|
|
26
67
|
});
|
|
27
68
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
surface3: "rgba(31, 38, 56, 0.95)",
|
|
37
|
-
/** Legacy alias — kept for backward compat */
|
|
38
|
-
surfaceDark: "#0F1422",
|
|
39
|
-
surfaceCard: "rgba(15, 20, 34, 0.75)",
|
|
40
|
-
/** Borders */
|
|
41
|
-
borderCard: "#1F2638",
|
|
42
|
-
borderMuted: "rgba(255, 255, 255, 0.07)",
|
|
43
|
-
borderSubtle: "rgba(255, 255, 255, 0.04)",
|
|
44
|
-
/** Brand accent — Carrier Flame orange (do not replace with violet) */
|
|
45
|
-
accentFlame: "#FF6B35",
|
|
46
|
-
accentEmber: "#D9461C",
|
|
47
|
-
accentSpark: "#FFB088",
|
|
48
|
-
/** Legacy violet/fuchsia — used on esimmcp co-brand surface only */
|
|
49
|
-
accentViolet: "#a78bfa",
|
|
50
|
-
accentFuchsia: "#e879f9",
|
|
51
|
-
/** Text hierarchy — tight ratio, generous contrast */
|
|
52
|
-
textPrimary: "#F5F1EA",
|
|
53
|
-
textSecondary: "#C9CCD6",
|
|
54
|
-
textMuted: "#8A92A8",
|
|
55
|
-
textFaint: "#5A6278",
|
|
56
|
-
/** Status — do not deviate */
|
|
57
|
-
statusSuccess: "#10b981",
|
|
58
|
-
statusWarning: "#f59e0b",
|
|
59
|
-
statusError: "#ef4444",
|
|
60
|
-
/** Light mode equivalents */
|
|
61
|
-
light: {
|
|
62
|
-
background: "#ffffff",
|
|
63
|
-
backgroundSecondary: "#f8fafc",
|
|
64
|
-
surfaceCard: "rgba(248, 250, 252, 0.9)",
|
|
65
|
-
borderCard: "#e2e8f0",
|
|
66
|
-
textPrimary: "#0f172a",
|
|
67
|
-
textSecondary: "#475569",
|
|
68
|
-
textMuted: "#94a3b8"
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
var typography = {
|
|
72
|
-
fontSans: "var(--font-sans, Inter), system-ui, -apple-system, sans-serif",
|
|
73
|
-
fontMono: "var(--font-mono, 'JetBrains Mono'), ui-monospace, monospace",
|
|
74
|
-
fontSerif: "var(--font-serif, Georgia), 'Times New Roman', serif",
|
|
75
|
-
/** Weights — only these three, per brand guide */
|
|
76
|
-
weightRegular: "400",
|
|
77
|
-
weightBold: "700",
|
|
78
|
-
weightBlack: "900",
|
|
79
|
-
/** Line heights */
|
|
80
|
-
lineHeightHeadline: "1.05",
|
|
81
|
-
lineHeightSubheading: "1.2",
|
|
82
|
-
lineHeightBody: "1.5",
|
|
83
|
-
/** Letter spacing — tight on big headlines */
|
|
84
|
-
trackingTight: "-0.04em",
|
|
85
|
-
trackingNormal: "0em",
|
|
86
|
-
trackingWide: "0.05em",
|
|
87
|
-
/** Type scale (rem) */
|
|
88
|
-
scale: {
|
|
89
|
-
xs: "0.75rem",
|
|
90
|
-
sm: "0.875rem",
|
|
91
|
-
base: "1rem",
|
|
92
|
-
lg: "1.125rem",
|
|
93
|
-
xl: "1.25rem",
|
|
94
|
-
"2xl": "1.5rem",
|
|
95
|
-
"3xl": "1.875rem",
|
|
96
|
-
"4xl": "2.25rem",
|
|
97
|
-
"5xl": "3rem",
|
|
98
|
-
"6xl": "3.75rem",
|
|
99
|
-
"7xl": "4.5rem"
|
|
69
|
+
function runInherit(cmd, args, opts = {}) {
|
|
70
|
+
if (!isSafeCommand(cmd)) {
|
|
71
|
+
return Promise.resolve({
|
|
72
|
+
ok: false,
|
|
73
|
+
code: null,
|
|
74
|
+
stdout: "",
|
|
75
|
+
stderr: `refusing to run unsafe command: ${cmd}`
|
|
76
|
+
});
|
|
100
77
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
xl: "1rem",
|
|
107
|
-
"2xl": "1.5rem",
|
|
108
|
-
full: "9999px"
|
|
109
|
-
};
|
|
110
|
-
var TONE_COLOR = {
|
|
111
|
-
ok: colors.statusSuccess,
|
|
112
|
-
info: colors.accentFlame,
|
|
113
|
-
warn: colors.statusWarning,
|
|
114
|
-
critical: colors.statusError,
|
|
115
|
-
muted: colors.textMuted
|
|
116
|
-
};
|
|
117
|
-
function esc(value) {
|
|
118
|
-
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
119
|
-
}
|
|
120
|
-
function toneColor(tone, fallback = colors.textPrimary) {
|
|
121
|
-
return tone ? TONE_COLOR[tone] : fallback;
|
|
122
|
-
}
|
|
123
|
-
function metricsHtml(items) {
|
|
124
|
-
const cards = items.map(
|
|
125
|
-
(m) => `
|
|
126
|
-
<div style="background:${colors.surface1};border:1px solid ${colors.borderCard};border-radius:${radius.lg};padding:16px 18px;min-width:0">
|
|
127
|
-
<div style="font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:${colors.textMuted}">${esc(m.label)}</div>
|
|
128
|
-
<div style="font-size:28px;font-weight:600;margin-top:6px;color:${toneColor(m.tone)};line-height:1.1">${esc(m.value)}</div>
|
|
129
|
-
${m.hint ? `<div style="font-size:12px;color:${colors.textFaint};margin-top:4px">${esc(m.hint)}</div>` : ""}
|
|
130
|
-
</div>`
|
|
131
|
-
).join("");
|
|
132
|
-
return `<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px">${cards}</div>`;
|
|
133
|
-
}
|
|
134
|
-
function barsHtml(items, empty) {
|
|
135
|
-
if (items.length === 0) return emptyHtml(empty ?? "Nothing to show.");
|
|
136
|
-
const ceiling = Math.max(...items.map((b) => b.max ?? b.value), 1);
|
|
137
|
-
const rows = items.map((b) => {
|
|
138
|
-
const pct2 = Math.max(0, Math.min(100, (b.value / (b.max ?? ceiling) || 0) * 100));
|
|
139
|
-
return `
|
|
140
|
-
<div style="margin-bottom:10px">
|
|
141
|
-
<div style="display:flex;justify-content:space-between;font-size:13px;color:${colors.textSecondary};margin-bottom:4px">
|
|
142
|
-
<span>${esc(b.label)}</span>
|
|
143
|
-
<span style="color:${colors.textMuted}">${esc(b.hint ?? b.value)}</span>
|
|
144
|
-
</div>
|
|
145
|
-
<div style="height:8px;background:${colors.surface2};border-radius:${radius.full};overflow:hidden">
|
|
146
|
-
<div style="height:100%;width:${pct2.toFixed(1)}%;background:${toneColor(b.tone, colors.accentFlame)}"></div>
|
|
147
|
-
</div>
|
|
148
|
-
</div>`;
|
|
149
|
-
}).join("");
|
|
150
|
-
return `<div>${rows}</div>`;
|
|
151
|
-
}
|
|
152
|
-
function tableHtml(section) {
|
|
153
|
-
if (section.rows.length === 0) return emptyHtml(section.empty ?? "No rows.");
|
|
154
|
-
const numeric = new Set(section.numeric ?? []);
|
|
155
|
-
const head = section.columns.map(
|
|
156
|
-
(c, i) => `<th style="text-align:${numeric.has(i) ? "right" : "left"};padding:8px 12px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:${colors.textMuted};border-bottom:1px solid ${colors.borderCard};white-space:nowrap">${esc(c)}</th>`
|
|
157
|
-
).join("");
|
|
158
|
-
const body = section.rows.map(
|
|
159
|
-
(row) => `<tr>${row.map(
|
|
160
|
-
(cell, i) => `<td style="text-align:${numeric.has(i) ? "right" : "left"};padding:8px 12px;font-size:13px;color:${colors.textSecondary};border-bottom:1px solid ${colors.borderSubtle};white-space:nowrap">${esc(cell)}</td>`
|
|
161
|
-
).join("")}</tr>`
|
|
162
|
-
).join("");
|
|
163
|
-
return `<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse">${`<thead><tr>${head}</tr></thead>`}<tbody>${body}</tbody></table></div>`;
|
|
164
|
-
}
|
|
165
|
-
function keyValueHtml(items) {
|
|
166
|
-
const rows = items.map(
|
|
167
|
-
(kv) => `
|
|
168
|
-
<div style="display:flex;justify-content:space-between;gap:16px;padding:7px 0;border-bottom:1px solid ${colors.borderSubtle}">
|
|
169
|
-
<span style="font-size:13px;color:${colors.textMuted}">${esc(kv.label)}</span>
|
|
170
|
-
<span style="font-size:13px;color:${toneColor(kv.tone, colors.textPrimary)};text-align:right">${esc(kv.value)}</span>
|
|
171
|
-
</div>`
|
|
172
|
-
).join("");
|
|
173
|
-
return `<div>${rows}</div>`;
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
const child = spawn(cmd, args, { cwd: opts.cwd, shell: false, stdio: "inherit" });
|
|
80
|
+
child.on("error", () => resolve({ ok: false, code: null, stdout: "", stderr: "" }));
|
|
81
|
+
child.on("close", (code) => resolve({ ok: code === 0, code, stdout: "", stderr: "" }));
|
|
82
|
+
});
|
|
174
83
|
}
|
|
175
|
-
function
|
|
176
|
-
const
|
|
177
|
-
|
|
84
|
+
async function which(bin) {
|
|
85
|
+
const probe2 = process.platform === "win32" ? "where" : "which";
|
|
86
|
+
const r = await run(probe2, [bin]);
|
|
87
|
+
return r.ok && r.stdout.trim().length > 0;
|
|
178
88
|
}
|
|
179
|
-
|
|
180
|
-
|
|
89
|
+
|
|
90
|
+
// src/cli/lib/deploy-targets.ts
|
|
91
|
+
var TARGET_IDS = ["cloudflare", "vercel", "netlify", "fly"];
|
|
92
|
+
function isTargetId(v) {
|
|
93
|
+
return TARGET_IDS.includes(v);
|
|
181
94
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
95
|
+
var DEFS = {
|
|
96
|
+
cloudflare: {
|
|
97
|
+
id: "cloudflare",
|
|
98
|
+
label: "Cloudflare Workers",
|
|
99
|
+
bin: "wrangler",
|
|
100
|
+
npxPkg: "wrangler",
|
|
101
|
+
markers: ["wrangler.jsonc", "wrangler.json", "wrangler.toml"],
|
|
102
|
+
buildScript: "cf:build",
|
|
103
|
+
artifact: join(".open-next", "worker.js"),
|
|
104
|
+
whoami: ["whoami"],
|
|
105
|
+
loginHint: "wrangler login",
|
|
106
|
+
// wrangler writes this mid-sentence, so the trailing period is not part of it.
|
|
107
|
+
parseAccount: (stdout, stderr) => `${stdout}${stderr}`.match(/associated with the email\s+(\S+?)[.,]?(?:\s|$)/)?.[1],
|
|
108
|
+
deployArgs: (name) => ["deploy", "--name", name],
|
|
109
|
+
secretArgs: (key, value, name) => ({ args: ["secret", "put", key, "--name", name], stdin: value })
|
|
110
|
+
},
|
|
111
|
+
vercel: {
|
|
112
|
+
id: "vercel",
|
|
113
|
+
label: "Vercel",
|
|
114
|
+
bin: "vercel",
|
|
115
|
+
npxPkg: "vercel",
|
|
116
|
+
markers: ["vercel.json", ".vercel"],
|
|
117
|
+
buildScript: "build",
|
|
118
|
+
whoami: ["whoami"],
|
|
119
|
+
loginHint: "vercel login",
|
|
120
|
+
// The username is the only thing on stdout; the version banner goes to stderr.
|
|
121
|
+
parseAccount: (stdout) => stdout.trim().split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith(">") && !/^Vercel CLI/i.test(l)).pop(),
|
|
122
|
+
deployArgs: () => ["deploy", "--prod", "--yes"],
|
|
123
|
+
secretArgs: (key, value) => ({ args: ["env", "add", key, "production", "--force"], stdin: value })
|
|
124
|
+
},
|
|
125
|
+
netlify: {
|
|
126
|
+
id: "netlify",
|
|
127
|
+
label: "Netlify",
|
|
128
|
+
bin: "netlify",
|
|
129
|
+
npxPkg: "netlify-cli",
|
|
130
|
+
markers: ["netlify.toml"],
|
|
131
|
+
buildScript: "build",
|
|
132
|
+
whoami: ["status"],
|
|
133
|
+
loginHint: "netlify login",
|
|
134
|
+
parseAccount: (stdout, stderr) => `${stdout}${stderr}`.match(/Email:\s*(\S+)/)?.[1],
|
|
135
|
+
deployArgs: () => ["deploy", "--build", "--prod"],
|
|
136
|
+
ensureConfig: async (storefront) => {
|
|
137
|
+
const path = join(storefront, "netlify.toml");
|
|
138
|
+
if (await exists(path)) return;
|
|
139
|
+
await writeFile(
|
|
140
|
+
path,
|
|
141
|
+
[
|
|
142
|
+
"# Written by @carrierllc/mcp",
|
|
143
|
+
"[build]",
|
|
144
|
+
' command = "npm run build"',
|
|
145
|
+
' publish = ".next"',
|
|
146
|
+
"",
|
|
147
|
+
"[[plugins]]",
|
|
148
|
+
' package = "@netlify/plugin-nextjs"',
|
|
149
|
+
""
|
|
150
|
+
].join("\n")
|
|
151
|
+
);
|
|
152
|
+
},
|
|
153
|
+
secretArgs: (key, value) => ({ args: ["env:set", key, value] })
|
|
154
|
+
},
|
|
155
|
+
fly: {
|
|
156
|
+
id: "fly",
|
|
157
|
+
label: "Fly.io",
|
|
158
|
+
bin: "flyctl",
|
|
159
|
+
npxPkg: "",
|
|
160
|
+
markers: ["fly.toml"],
|
|
161
|
+
buildScript: "build",
|
|
162
|
+
whoami: ["auth", "whoami"],
|
|
163
|
+
loginHint: "flyctl auth login",
|
|
164
|
+
parseAccount: (out) => out.trim().split("\n").pop()?.trim(),
|
|
165
|
+
deployArgs: () => ["deploy", "--now"],
|
|
166
|
+
ensureConfig: async (storefront, projectName) => {
|
|
167
|
+
const toml = join(storefront, "fly.toml");
|
|
168
|
+
if (!await exists(toml)) {
|
|
169
|
+
await writeFile(
|
|
170
|
+
toml,
|
|
171
|
+
[
|
|
172
|
+
"# Written by @carrierllc/mcp",
|
|
173
|
+
`app = "${projectName}"`,
|
|
174
|
+
"",
|
|
175
|
+
"[build]",
|
|
176
|
+
' dockerfile = "Dockerfile"',
|
|
177
|
+
"",
|
|
178
|
+
"[http_service]",
|
|
179
|
+
" internal_port = 3000",
|
|
180
|
+
" force_https = true",
|
|
181
|
+
" auto_stop_machines = true",
|
|
182
|
+
" auto_start_machines = true",
|
|
183
|
+
""
|
|
184
|
+
].join("\n")
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const dockerfile = join(storefront, "Dockerfile");
|
|
188
|
+
if (!await exists(dockerfile)) {
|
|
189
|
+
await writeFile(
|
|
190
|
+
dockerfile,
|
|
191
|
+
[
|
|
192
|
+
"# Written by @carrierllc/mcp",
|
|
193
|
+
"FROM node:22-slim AS build",
|
|
194
|
+
"WORKDIR /app",
|
|
195
|
+
"COPY package*.json ./",
|
|
196
|
+
"RUN npm install",
|
|
197
|
+
"COPY . .",
|
|
198
|
+
"RUN npm run build",
|
|
199
|
+
"",
|
|
200
|
+
"FROM node:22-slim",
|
|
201
|
+
"WORKDIR /app",
|
|
202
|
+
"ENV NODE_ENV=production PORT=3000",
|
|
203
|
+
"COPY --from=build /app ./",
|
|
204
|
+
"EXPOSE 3000",
|
|
205
|
+
'CMD ["npm", "run", "start"]',
|
|
206
|
+
""
|
|
207
|
+
].join("\n")
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
secretArgs: (key, value) => ({ args: ["secrets", "set", `${key}=${value}`] })
|
|
201
212
|
}
|
|
202
|
-
return `<section style="margin-bottom:22px">${title}${body}</section>`;
|
|
203
|
-
}
|
|
204
|
-
function renderHtml(screen) {
|
|
205
|
-
const sections = isEmptyScreen(screen) ? emptyHtml(
|
|
206
|
-
"No data came back for this view. That is a real result, not a loading state \u2014 check the account scope and credentials."
|
|
207
|
-
) : screen.sections.map(sectionHtml).join("");
|
|
208
|
-
const actions = screen.actions?.length ? `<section style="margin-top:6px;display:flex;flex-wrap:wrap;gap:8px">${screen.actions.map(
|
|
209
|
-
(a) => `<span title="${esc(a.description ?? a.command)}" style="font-size:12px;color:${colors.textSecondary};background:${colors.surface2};border:1px solid ${colors.borderCard};border-radius:${radius.full};padding:6px 12px">${esc(a.label)} <code style="color:${colors.textFaint}">${esc(a.command)}</code></span>`
|
|
210
|
-
).join("")}</section>` : "";
|
|
211
|
-
return `<!doctype html>
|
|
212
|
-
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
213
|
-
<title>${esc(screen.title)}</title></head>
|
|
214
|
-
<body style="margin:0;background:${colors.surface0};color:${colors.textPrimary};font-family:${typography.fontSans};padding:20px">
|
|
215
|
-
<header style="margin-bottom:20px">
|
|
216
|
-
<h1 style="font-size:18px;font-weight:600;margin:0;letter-spacing:-.01em">${esc(screen.title)}</h1>
|
|
217
|
-
${screen.subtitle ? `<p style="margin:4px 0 0;font-size:13px;color:${colors.textMuted}">${esc(screen.subtitle)}</p>` : ""}
|
|
218
|
-
</header>
|
|
219
|
-
${sections}
|
|
220
|
-
${actions}
|
|
221
|
-
${screen.footer ? `<footer style="margin-top:18px;font-size:11px;color:${colors.textFaint}">${esc(screen.footer)}</footer>` : ""}
|
|
222
|
-
</body></html>`;
|
|
223
|
-
}
|
|
224
|
-
var ANSI = {
|
|
225
|
-
ok: "\x1B[32m",
|
|
226
|
-
info: "\x1B[38;5;209m",
|
|
227
|
-
// Carrier flame, nearest 256-colour
|
|
228
|
-
warn: "\x1B[33m",
|
|
229
|
-
critical: "\x1B[31m",
|
|
230
|
-
muted: "\x1B[90m",
|
|
231
|
-
reset: "\x1B[0m",
|
|
232
|
-
bold: "\x1B[1m",
|
|
233
|
-
dim: "\x1B[2m"
|
|
234
213
|
};
|
|
235
|
-
function
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
214
|
+
async function configuredProjectName(id, storefront) {
|
|
215
|
+
if (id !== "cloudflare") return void 0;
|
|
216
|
+
for (const marker of DEFS.cloudflare.markers) {
|
|
217
|
+
const path = join(storefront, marker);
|
|
218
|
+
if (!await exists(path)) continue;
|
|
219
|
+
const body = await readFileText(path);
|
|
220
|
+
const name = body.match(/^\s*"?name"?\s*:\s*"([^"]+)"/m)?.[1];
|
|
221
|
+
if (name) return name;
|
|
222
|
+
}
|
|
223
|
+
return void 0;
|
|
240
224
|
}
|
|
241
|
-
function
|
|
242
|
-
const
|
|
243
|
-
|
|
225
|
+
async function setCustomDomain(storefront, domain) {
|
|
226
|
+
const clean = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
227
|
+
if (!clean || !clean.includes(".")) return false;
|
|
228
|
+
const path = join(storefront, "wrangler.jsonc");
|
|
229
|
+
if (!await exists(path)) return false;
|
|
230
|
+
const body = await readFileText(path);
|
|
231
|
+
if (body.includes(`"pattern": "${clean}"`)) return true;
|
|
232
|
+
const routes = ` "routes": [
|
|
233
|
+
{ "pattern": "${clean}", "custom_domain": true }
|
|
234
|
+
],
|
|
235
|
+
`;
|
|
236
|
+
const anchor = body.indexOf(`"main"`);
|
|
237
|
+
if (anchor === -1) return false;
|
|
238
|
+
const lineStart = body.lastIndexOf("\n", anchor) + 1;
|
|
239
|
+
const patched = body.slice(0, lineStart) + routes + body.slice(lineStart);
|
|
240
|
+
await writeFile(path, patched);
|
|
241
|
+
return true;
|
|
244
242
|
}
|
|
245
|
-
function
|
|
246
|
-
|
|
243
|
+
async function readFileText(path) {
|
|
244
|
+
const { readFile: readFile2 } = await import("./fsx-BDDIQ3Y7.js");
|
|
245
|
+
return readFile2(path, "utf8");
|
|
247
246
|
}
|
|
248
|
-
function
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
247
|
+
async function rollback(id, storefront, projectName) {
|
|
248
|
+
if (id !== "cloudflare") {
|
|
249
|
+
return { ok: false, reason: `${DEFS[id].label} has no one-command rollback \u2014 revert manually.` };
|
|
250
|
+
}
|
|
251
|
+
const def = DEFS[id];
|
|
252
|
+
const resolved = await resolveBin(def);
|
|
253
|
+
if (!resolved) return { ok: false, reason: "wrangler not found." };
|
|
254
|
+
const r = await run(
|
|
255
|
+
resolved.bin,
|
|
256
|
+
[...resolved.prefix, "rollback", "--name", projectName, "--yes"],
|
|
257
|
+
{ cwd: storefront, timeoutMs: 3e5 }
|
|
253
258
|
);
|
|
254
|
-
return
|
|
255
|
-
const label = paint(pad(m.label, labelWidth, "left"), ANSI.muted, color);
|
|
256
|
-
const value = paint(String(m.value), m.tone ? ANSI[m.tone] : ANSI.bold, color);
|
|
257
|
-
const hint = m.hint ? paint(` ${m.hint}`, ANSI.dim, color) : "";
|
|
258
|
-
return ` ${label} ${value}${hint}`.slice(0, width + 64);
|
|
259
|
-
});
|
|
259
|
+
return r.ok ? { ok: true } : { ok: false, reason: `${r.stderr || r.stdout}`.trim().split("\n").slice(-2).join(" ").slice(0, 300) };
|
|
260
260
|
}
|
|
261
|
-
function
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const barWidth = Math.max(10, Math.min(40, width - labelWidth - 22));
|
|
266
|
-
return items.map((b) => {
|
|
267
|
-
const ratio = b.value / (b.max ?? ceiling) || 0;
|
|
268
|
-
const filled = Math.max(0, Math.min(barWidth, Math.round(ratio * barWidth)));
|
|
269
|
-
const bar = paint("\u2588".repeat(filled), b.tone ? ANSI[b.tone] : ANSI.info, color) + paint("\u2591".repeat(barWidth - filled), ANSI.dim, color);
|
|
270
|
-
const label = paint(pad(b.label, labelWidth, "left"), ANSI.muted, color);
|
|
271
|
-
const value = paint(String(b.hint ?? b.value), ANSI.dim, color);
|
|
272
|
-
return ` ${label} ${bar} ${value}`;
|
|
273
|
-
});
|
|
261
|
+
async function resolveBin(def) {
|
|
262
|
+
if (await which(def.bin)) return { bin: def.bin, prefix: [] };
|
|
263
|
+
if (def.npxPkg && await which("npx")) return { bin: "npx", prefix: [def.npxPkg] };
|
|
264
|
+
return void 0;
|
|
274
265
|
}
|
|
275
|
-
function
|
|
276
|
-
|
|
277
|
-
|
|
266
|
+
async function probeTarget(id, storefront) {
|
|
267
|
+
const def = DEFS[id];
|
|
268
|
+
let configured = false;
|
|
269
|
+
for (const marker of def.markers) {
|
|
270
|
+
if (await exists(join(storefront, marker))) {
|
|
271
|
+
configured = true;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
278
274
|
}
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const widest = widths.indexOf(Math.max(...widths));
|
|
291
|
-
widths[widest] -= 1;
|
|
292
|
-
total -= 1;
|
|
275
|
+
const resolved = await resolveBin(def);
|
|
276
|
+
if (!resolved) {
|
|
277
|
+
return {
|
|
278
|
+
id,
|
|
279
|
+
label: def.label,
|
|
280
|
+
installed: false,
|
|
281
|
+
authenticated: false,
|
|
282
|
+
configured,
|
|
283
|
+
ready: false,
|
|
284
|
+
reason: def.npxPkg ? `${def.bin} not found \u2014 install it, or make npx available.` : `${def.bin} not found \u2014 install the Fly CLI (brew install flyctl).`
|
|
285
|
+
};
|
|
293
286
|
}
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
287
|
+
const who = await run(resolved.bin, [...resolved.prefix, ...def.whoami], {
|
|
288
|
+
cwd: storefront,
|
|
289
|
+
timeoutMs: 6e4
|
|
290
|
+
});
|
|
291
|
+
if (!who.ok) {
|
|
292
|
+
return {
|
|
293
|
+
id,
|
|
294
|
+
label: def.label,
|
|
295
|
+
installed: true,
|
|
296
|
+
authenticated: false,
|
|
297
|
+
configured,
|
|
298
|
+
ready: false,
|
|
299
|
+
reason: `${def.bin} is not logged in \u2014 run \`${def.loginHint}\`.`
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
id,
|
|
304
|
+
label: def.label,
|
|
305
|
+
installed: true,
|
|
306
|
+
authenticated: true,
|
|
307
|
+
configured,
|
|
308
|
+
ready: true,
|
|
309
|
+
account: def.parseAccount?.(who.stdout, who.stderr)
|
|
297
310
|
};
|
|
298
|
-
const header = " " + section.columns.map((c, i) => paint(pad(clip(c, i), widths[i], numeric.has(i) ? "right" : "left"), ANSI.muted, color)).join(" ");
|
|
299
|
-
const rule = " " + paint(widths.map((w) => "\u2500".repeat(w)).join(" "), ANSI.dim, color);
|
|
300
|
-
const body = section.rows.map(
|
|
301
|
-
(row) => " " + row.map((cell, i) => pad(clip(cell, i), widths[i], numeric.has(i) ? "right" : "left")).join(" ")
|
|
302
|
-
);
|
|
303
|
-
return [header, rule, ...body];
|
|
304
311
|
}
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
const labelWidth = Math.min(30, Math.max(...items.map((kv) => kv.label.length)));
|
|
308
|
-
return items.map(
|
|
309
|
-
(kv) => ` ${paint(pad(kv.label, labelWidth, "left"), ANSI.muted, color)} ${paint(kv.value, kv.tone ? ANSI[kv.tone] : ANSI.reset, color)}`
|
|
310
|
-
);
|
|
312
|
+
async function probeAll(storefront) {
|
|
313
|
+
return Promise.all(TARGET_IDS.map((id) => probeTarget(id, storefront)));
|
|
311
314
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
muted: "note"
|
|
318
|
-
};
|
|
319
|
-
function sectionTui(section, width, color) {
|
|
320
|
-
const lines = [];
|
|
321
|
-
if ("title" in section && section.title) lines.push(heading(section.title, color));
|
|
322
|
-
switch (section.kind) {
|
|
323
|
-
case "metrics":
|
|
324
|
-
lines.push(...metricsTui(section.items, width, color));
|
|
325
|
-
break;
|
|
326
|
-
case "bars":
|
|
327
|
-
lines.push(...barsTui(section.items, width, color, section.empty));
|
|
328
|
-
break;
|
|
329
|
-
case "table":
|
|
330
|
-
lines.push(...tableTui(section, width, color));
|
|
331
|
-
break;
|
|
332
|
-
case "keyvalue":
|
|
333
|
-
lines.push(...keyValueTui(section.items, color));
|
|
334
|
-
break;
|
|
335
|
-
case "note":
|
|
336
|
-
lines.push(
|
|
337
|
-
` ${paint(`${NOTE_PREFIX[section.tone]}:`, ANSI[section.tone], color)} ${section.text}`
|
|
338
|
-
);
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
lines.push("");
|
|
342
|
-
return lines;
|
|
315
|
+
function rankTargets(statuses) {
|
|
316
|
+
return statuses.filter((s) => s.ready).sort((a, b) => {
|
|
317
|
+
if (a.configured !== b.configured) return a.configured ? -1 : 1;
|
|
318
|
+
return TARGET_IDS.indexOf(a.id) - TARGET_IDS.indexOf(b.id);
|
|
319
|
+
});
|
|
343
320
|
}
|
|
344
|
-
function
|
|
345
|
-
|
|
346
|
-
const color = opts.color ?? true;
|
|
347
|
-
const lines = [];
|
|
348
|
-
lines.push(paint(screen.title, ANSI.bold, color));
|
|
349
|
-
if (screen.subtitle) lines.push(paint(screen.subtitle, ANSI.muted, color));
|
|
350
|
-
lines.push(paint("\u2500".repeat(width), ANSI.dim, color));
|
|
351
|
-
lines.push("");
|
|
352
|
-
if (isEmptyScreen(screen)) {
|
|
353
|
-
lines.push(
|
|
354
|
-
` ${paint("No data came back for this view.", ANSI.warn, color)} That is a real result, not a`,
|
|
355
|
-
" loading state \u2014 check the account scope and credentials.",
|
|
356
|
-
""
|
|
357
|
-
);
|
|
358
|
-
} else {
|
|
359
|
-
for (const section of screen.sections) lines.push(...sectionTui(section, width, color));
|
|
360
|
-
}
|
|
361
|
-
if (screen.actions?.length) {
|
|
362
|
-
lines.push(heading("next", color));
|
|
363
|
-
for (const a of screen.actions) {
|
|
364
|
-
lines.push(` ${paint(a.command, ANSI.info, color)} ${paint(a.label, ANSI.dim, color)}`);
|
|
365
|
-
}
|
|
366
|
-
lines.push("");
|
|
367
|
-
}
|
|
368
|
-
if (screen.footer) lines.push(paint(screen.footer, ANSI.dim, color));
|
|
369
|
-
return lines.join("\n");
|
|
321
|
+
function buildScriptFor(id) {
|
|
322
|
+
return DEFS[id].buildScript;
|
|
370
323
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const gb = bytes / 1073741824;
|
|
376
|
-
if (gb >= 1) return `${gb.toFixed(gb >= 10 ? 0 : 1)} GB`;
|
|
377
|
-
return `${(bytes / 1048576).toFixed(0)} MB`;
|
|
324
|
+
async function artifactMissing(id, storefront) {
|
|
325
|
+
const artifact = DEFS[id].artifact;
|
|
326
|
+
if (!artifact) return false;
|
|
327
|
+
return !await exists(join(storefront, artifact));
|
|
378
328
|
}
|
|
379
|
-
function
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
hint: pct(input.suspended, total),
|
|
394
|
-
tone: input.suspended > input.active * 0.1 ? "warn" : void 0
|
|
395
|
-
},
|
|
396
|
-
{ label: "Accounts", value: input.accounts.length },
|
|
397
|
-
{
|
|
398
|
-
label: "Low balance",
|
|
399
|
-
value: lowBalance.length,
|
|
400
|
-
tone: lowBalance.length > 0 ? "warn" : "ok"
|
|
401
|
-
}
|
|
402
|
-
]
|
|
403
|
-
}
|
|
404
|
-
];
|
|
405
|
-
const hasPerAccountCounts = input.accounts.some(
|
|
406
|
-
(a) => a.active + a.suspended + a.inventory + a.other > 0
|
|
407
|
-
);
|
|
408
|
-
if (hasPerAccountCounts) {
|
|
409
|
-
sections.push({
|
|
410
|
-
kind: "bars",
|
|
411
|
-
title: "eSIMs by account",
|
|
412
|
-
items: [...input.accounts].sort((a, b) => b.active + b.inventory - (a.active + a.inventory)).slice(0, 10).map((a) => ({
|
|
413
|
-
label: a.name,
|
|
414
|
-
value: a.active + a.suspended + a.inventory + a.other,
|
|
415
|
-
hint: `${a.active} active`,
|
|
416
|
-
tone: a.active > 0 ? "ok" : "muted"
|
|
417
|
-
}))
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
sections.push({
|
|
421
|
-
kind: "table",
|
|
422
|
-
title: "Accounts",
|
|
423
|
-
columns: ["Account", "Balance", "Active", "Inventory"],
|
|
424
|
-
numeric: [1, 2, 3],
|
|
425
|
-
empty: "No accounts under this reseller.",
|
|
426
|
-
rows: input.accounts.map((a) => [a.name, money(a.balance), a.active, a.inventory])
|
|
329
|
+
function parseDeployedUrl(output) {
|
|
330
|
+
return output.match(
|
|
331
|
+
/https:\/\/[^\s"']+\.(?:workers\.dev|vercel\.app|netlify\.app|fly\.dev)[^\s"']*/
|
|
332
|
+
)?.[0];
|
|
333
|
+
}
|
|
334
|
+
async function putSecret(id, storefront, projectName, key, value) {
|
|
335
|
+
const def = DEFS[id];
|
|
336
|
+
const resolved = await resolveBin(def);
|
|
337
|
+
if (!resolved) return false;
|
|
338
|
+
const { args, stdin } = def.secretArgs(key, value, projectName);
|
|
339
|
+
const r = await run(resolved.bin, [...resolved.prefix, ...args], {
|
|
340
|
+
cwd: storefront,
|
|
341
|
+
timeoutMs: 12e4,
|
|
342
|
+
stdin
|
|
427
343
|
});
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
344
|
+
return r.ok;
|
|
345
|
+
}
|
|
346
|
+
async function stageSecrets(id, storefront, projectName, secrets) {
|
|
347
|
+
const entries = Object.entries(secrets).filter(([, v]) => v?.trim());
|
|
348
|
+
const noop = { staged: [], failed: [], cleanup: async () => {
|
|
349
|
+
} };
|
|
350
|
+
if (entries.length === 0) return noop;
|
|
351
|
+
const def = DEFS[id];
|
|
352
|
+
const resolved = await resolveBin(def);
|
|
353
|
+
if (!resolved) return { staged: [], failed: entries.map(([k]) => k), cleanup: async () => {
|
|
354
|
+
} };
|
|
355
|
+
if (id === "cloudflare") {
|
|
356
|
+
const dir = await mkdtemp(join(tmpdir(), "carrier-secrets-"));
|
|
357
|
+
const file = join(dir, ".env");
|
|
358
|
+
const body = entries.map(([k, v]) => `${k}=${v}`).join("\n");
|
|
359
|
+
await writeFileMode(file, `${body}
|
|
360
|
+
`, { mode: 384 });
|
|
361
|
+
return {
|
|
362
|
+
staged: entries.map(([k]) => k),
|
|
363
|
+
failed: [],
|
|
364
|
+
secretsFile: file,
|
|
365
|
+
cleanup: async () => {
|
|
366
|
+
await rm(dir, { recursive: true, force: true });
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
if (id === "fly") {
|
|
371
|
+
const args = ["secrets", "set", "--stage", ...entries.map(([k, v]) => `${k}=${v}`)];
|
|
372
|
+
const r = await run(resolved.bin, [...resolved.prefix, ...args], {
|
|
373
|
+
cwd: storefront,
|
|
374
|
+
timeoutMs: 18e4
|
|
433
375
|
});
|
|
376
|
+
return {
|
|
377
|
+
staged: r.ok ? entries.map(([k]) => k) : [],
|
|
378
|
+
failed: r.ok ? [] : entries.map(([k]) => k),
|
|
379
|
+
cleanup: async () => {
|
|
380
|
+
}
|
|
381
|
+
};
|
|
434
382
|
}
|
|
435
|
-
|
|
436
|
-
|
|
383
|
+
const staged = [];
|
|
384
|
+
const failed = [];
|
|
385
|
+
for (const [key, value] of entries) {
|
|
386
|
+
const ok = await putSecret(id, storefront, projectName, key, value);
|
|
387
|
+
(ok ? staged : failed).push(key);
|
|
437
388
|
}
|
|
438
|
-
return {
|
|
439
|
-
|
|
440
|
-
title: "Fleet health",
|
|
441
|
-
subtitle: `${total} eSIMs across ${input.accounts.length} account(s) \xB7 ${pct(input.active, total)} utilisation`,
|
|
442
|
-
sections,
|
|
443
|
-
actions: [
|
|
444
|
-
{ label: "Top up an account", command: "wallet_topup_checkout" },
|
|
445
|
-
{ label: "Per-account eSIM status", command: "esim_status_per_account" }
|
|
446
|
-
]
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
function subscribersScreen(rows, opts = {}) {
|
|
450
|
-
const byStatus = /* @__PURE__ */ new Map();
|
|
451
|
-
for (const r of rows) byStatus.set(r.status, (byStatus.get(r.status) ?? 0) + 1);
|
|
452
|
-
return {
|
|
453
|
-
id: "subscribers",
|
|
454
|
-
title: "Subscribers",
|
|
455
|
-
subtitle: opts.account ? `Account ${opts.account} \xB7 ${rows.length} shown` : `${rows.length} shown`,
|
|
456
|
-
sections: [
|
|
457
|
-
{
|
|
458
|
-
kind: "metrics",
|
|
459
|
-
items: [
|
|
460
|
-
{ label: "Listed", value: rows.length },
|
|
461
|
-
...[...byStatus.entries()].map(([status, count]) => ({
|
|
462
|
-
label: status,
|
|
463
|
-
value: count,
|
|
464
|
-
tone: status.toLowerCase() === "active" ? "ok" : void 0
|
|
465
|
-
}))
|
|
466
|
-
]
|
|
467
|
-
},
|
|
468
|
-
{
|
|
469
|
-
kind: "table",
|
|
470
|
-
title: "Records",
|
|
471
|
-
columns: ["ICCID", "MSISDN", "Status", "Account", "Data used"],
|
|
472
|
-
numeric: [4],
|
|
473
|
-
empty: "No subscribers matched. Widen the filter or check the account scope.",
|
|
474
|
-
rows: rows.map((r) => [
|
|
475
|
-
r.iccid,
|
|
476
|
-
r.msisdn ?? "\u2014",
|
|
477
|
-
r.status,
|
|
478
|
-
r.account ?? "\u2014",
|
|
479
|
-
r.dataUsedBytes === void 0 ? "\u2014" : humanBytes(r.dataUsedBytes)
|
|
480
|
-
])
|
|
481
|
-
}
|
|
482
|
-
],
|
|
483
|
-
actions: [
|
|
484
|
-
{ label: "Diagnose one", command: "diagnose_subscriber" },
|
|
485
|
-
{ label: "Usage detail", command: "subscriber_usage" }
|
|
486
|
-
]
|
|
487
|
-
};
|
|
389
|
+
return { staged, failed, cleanup: async () => {
|
|
390
|
+
} };
|
|
488
391
|
}
|
|
489
|
-
function
|
|
490
|
-
const
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
{
|
|
494
|
-
);
|
|
495
|
-
const sections = [
|
|
496
|
-
{
|
|
497
|
-
kind: "metrics",
|
|
498
|
-
items: [
|
|
499
|
-
{ label: "Total", value: humanBytes(total) },
|
|
500
|
-
{ label: "Days", value: input.timeline.length },
|
|
501
|
-
{ label: "Peak day", value: humanBytes(peak.bytes), hint: peak.date },
|
|
502
|
-
{
|
|
503
|
-
label: "Daily average",
|
|
504
|
-
value: humanBytes(input.timeline.length ? total / input.timeline.length : 0)
|
|
505
|
-
}
|
|
506
|
-
]
|
|
507
|
-
},
|
|
508
|
-
{
|
|
509
|
-
kind: "bars",
|
|
510
|
-
title: "Daily usage",
|
|
511
|
-
empty: "No usage recorded in this window.",
|
|
512
|
-
items: input.timeline.map((p) => ({
|
|
513
|
-
label: p.date,
|
|
514
|
-
value: p.bytes,
|
|
515
|
-
hint: humanBytes(p.bytes)
|
|
516
|
-
}))
|
|
517
|
-
}
|
|
518
|
-
];
|
|
519
|
-
if (input.countries?.length) {
|
|
520
|
-
sections.push({
|
|
521
|
-
kind: "bars",
|
|
522
|
-
title: "By country",
|
|
523
|
-
items: input.countries.slice(0, 10).map((c) => ({ label: c.country, value: c.bytes, hint: humanBytes(c.bytes) }))
|
|
524
|
-
});
|
|
392
|
+
async function deployTo(id, storefront, projectName, opts = {}) {
|
|
393
|
+
const def = DEFS[id];
|
|
394
|
+
const resolved = await resolveBin(def);
|
|
395
|
+
if (!resolved) {
|
|
396
|
+
return { ok: false, target: id, projectName, reason: `${def.bin} not found.` };
|
|
525
397
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
398
|
+
await def.ensureConfig?.(storefront, projectName);
|
|
399
|
+
if (await artifactMissing(id, storefront)) {
|
|
400
|
+
return {
|
|
401
|
+
ok: false,
|
|
402
|
+
target: id,
|
|
403
|
+
projectName,
|
|
404
|
+
reason: `No ${def.artifact} build found \u2014 run the build step first.`
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const args = [...resolved.prefix, ...def.deployArgs(projectName)];
|
|
408
|
+
if (id === "cloudflare" && opts.secretsFile) args.push("--secrets-file", opts.secretsFile);
|
|
409
|
+
const r = await run(resolved.bin, args, {
|
|
410
|
+
cwd: storefront,
|
|
411
|
+
timeoutMs: 9e5
|
|
412
|
+
});
|
|
413
|
+
const combined = `${r.stdout}${r.stderr}`;
|
|
414
|
+
if (!r.ok) {
|
|
415
|
+
const tail = combined.trim().split("\n").slice(-3).join(" ").slice(0, 400);
|
|
416
|
+
return { ok: false, target: id, projectName, reason: tail || `${def.bin} deploy failed.` };
|
|
417
|
+
}
|
|
418
|
+
return { ok: true, target: id, projectName, url: parseDeployedUrl(combined) };
|
|
533
419
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
};
|
|
420
|
+
|
|
421
|
+
// src/cli/lib/brand.ts
|
|
422
|
+
var CARRIER_BRAND = {
|
|
423
|
+
name: "Carrier",
|
|
424
|
+
legalName: "Lifecycle Innovations Limited",
|
|
425
|
+
tagline: "Programmable connectivity, on demand.",
|
|
426
|
+
domain: "carrier.llc",
|
|
427
|
+
supportEmail: "support@carrier.llc",
|
|
428
|
+
supportUrl: "https://carrier.llc/help",
|
|
429
|
+
supportWhatsapp: "+17864604829",
|
|
430
|
+
colors: {
|
|
431
|
+
bg: "#080C16",
|
|
432
|
+
accent: "#FF6B35",
|
|
433
|
+
accentDark: "#D9461C",
|
|
434
|
+
text: "#F5F1EA"
|
|
435
|
+
},
|
|
436
|
+
social: {
|
|
437
|
+
x: "@carrier_llc",
|
|
438
|
+
instagram: "@carrier.llc",
|
|
439
|
+
tiktok: "@carrier.llc"
|
|
440
|
+
},
|
|
441
|
+
carrierApiUrl: "https://api.carrier.llc"
|
|
442
|
+
};
|
|
443
|
+
var CARRIER_ACCENT_LIGHT = "#FFB088";
|
|
444
|
+
var CARRIER_ACCENT_GRADIENT_START = "#FF7A45";
|
|
445
|
+
function hexToRgb(hex) {
|
|
446
|
+
const h = hex.replace(/^#/, "");
|
|
447
|
+
if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
|
|
448
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
564
449
|
}
|
|
565
|
-
function
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
title: "Billing",
|
|
570
|
-
subtitle: `${input.events.length} recent event(s)`,
|
|
571
|
-
sections: [
|
|
572
|
-
{
|
|
573
|
-
kind: "metrics",
|
|
574
|
-
items: [
|
|
575
|
-
{
|
|
576
|
-
label: "Balance",
|
|
577
|
-
value: input.balance === void 0 ? "\u2014" : money(input.balance, input.currency),
|
|
578
|
-
tone: (input.balance ?? 0) < 10 ? "warn" : "ok"
|
|
579
|
-
},
|
|
580
|
-
{
|
|
581
|
-
label: "Pending",
|
|
582
|
-
value: input.pending === void 0 ? "\u2014" : money(input.pending, input.currency)
|
|
583
|
-
},
|
|
584
|
-
{ label: "Recent spend", value: money(spend, input.currency) }
|
|
585
|
-
]
|
|
586
|
-
},
|
|
587
|
-
{
|
|
588
|
-
kind: "table",
|
|
589
|
-
title: "Recent events",
|
|
590
|
-
columns: ["Date", "Description", "Amount"],
|
|
591
|
-
numeric: [2],
|
|
592
|
-
empty: "No billing events in this window.",
|
|
593
|
-
rows: input.events.map((e) => [e.date, e.description, money(e.amount, input.currency)])
|
|
594
|
-
}
|
|
595
|
-
],
|
|
596
|
-
actions: [{ label: "Check payouts", command: "stripe_connect_payouts" }]
|
|
597
|
-
};
|
|
450
|
+
function rgbToHex(r, g, b, lower = false) {
|
|
451
|
+
const fmt = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
|
|
452
|
+
const out = `#${fmt(r)}${fmt(g)}${fmt(b)}`;
|
|
453
|
+
return lower ? out : out.toUpperCase();
|
|
598
454
|
}
|
|
599
|
-
function
|
|
600
|
-
const
|
|
601
|
-
return
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
{
|
|
618
|
-
kind: "keyvalue",
|
|
619
|
-
title: "Settings",
|
|
620
|
-
items: [
|
|
621
|
-
{ label: "Auto top-up", value: input.autoTopupEnabled ? "enabled" : "disabled" },
|
|
622
|
-
{
|
|
623
|
-
label: "Threshold",
|
|
624
|
-
value: input.threshold === void 0 ? "\u2014" : money(input.threshold, input.currency)
|
|
625
|
-
}
|
|
626
|
-
]
|
|
627
|
-
},
|
|
628
|
-
...low ? [
|
|
629
|
-
{
|
|
630
|
-
kind: "note",
|
|
631
|
-
tone: "warn",
|
|
632
|
-
text: "Balance is under the top-up threshold. Package assignment fails at zero on package-only accounts."
|
|
633
|
-
}
|
|
634
|
-
] : []
|
|
635
|
-
],
|
|
636
|
-
actions: [
|
|
637
|
-
{ label: "Top up", command: "wallet_topup_checkout" },
|
|
638
|
-
{ label: "Configure auto top-up", command: "wallet_auto_topup" }
|
|
639
|
-
]
|
|
640
|
-
};
|
|
455
|
+
function mixHexWithWhite(hex, whiteRatio) {
|
|
456
|
+
const rgb = hexToRgb(hex);
|
|
457
|
+
if (!rgb) return hex;
|
|
458
|
+
const mix2 = (n) => n + (255 - n) * whiteRatio;
|
|
459
|
+
return rgbToHex(mix2(rgb[0]), mix2(rgb[1]), mix2(rgb[2]));
|
|
460
|
+
}
|
|
461
|
+
function darkenHex(hex, factor) {
|
|
462
|
+
const rgb = hexToRgb(hex);
|
|
463
|
+
if (!rgb) return hex;
|
|
464
|
+
const scale = (n) => n * factor;
|
|
465
|
+
return rgbToHex(scale(rgb[0]), scale(rgb[1]), scale(rgb[2]), true);
|
|
466
|
+
}
|
|
467
|
+
function deriveAccentDark(accent, seed = CARRIER_BRAND) {
|
|
468
|
+
if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return seed.colors.accentDark;
|
|
469
|
+
const rgb = hexToRgb(accent);
|
|
470
|
+
if (!rgb) return accent;
|
|
471
|
+
const scale = (n) => Math.max(0, Math.min(255, Math.round(n * 0.75)));
|
|
472
|
+
return rgbToHex(scale(rgb[0]), scale(rgb[1]), scale(rgb[2]));
|
|
641
473
|
}
|
|
642
|
-
function
|
|
643
|
-
return
|
|
644
|
-
|
|
645
|
-
title: "Greenzone whitelist",
|
|
646
|
-
subtitle: `${entries.length} entr${entries.length === 1 ? "y" : "ies"}`,
|
|
647
|
-
sections: [
|
|
648
|
-
{ kind: "metrics", items: [{ label: "Entries", value: entries.length }] },
|
|
649
|
-
{
|
|
650
|
-
kind: "table",
|
|
651
|
-
title: "Whitelisted",
|
|
652
|
-
columns: ["Value", "Note"],
|
|
653
|
-
empty: "Whitelist is empty \u2014 every destination follows the default policy.",
|
|
654
|
-
rows: entries.map((e) => [e.value, e.note ?? "\u2014"])
|
|
655
|
-
}
|
|
656
|
-
],
|
|
657
|
-
actions: [
|
|
658
|
-
{ label: "Add an entry", command: "greenzone_whitelist_add" },
|
|
659
|
-
{ label: "Remove an entry", command: "greenzone_whitelist_remove" }
|
|
660
|
-
]
|
|
661
|
-
};
|
|
474
|
+
function deriveAccentLight(accent, seed = CARRIER_BRAND) {
|
|
475
|
+
if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return CARRIER_ACCENT_LIGHT;
|
|
476
|
+
return mixHexWithWhite(accent, 0.45);
|
|
662
477
|
}
|
|
663
|
-
function
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
]
|
|
686
|
-
}
|
|
478
|
+
function deriveAccentGradientStart(accent, seed = CARRIER_BRAND) {
|
|
479
|
+
if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return CARRIER_ACCENT_GRADIENT_START;
|
|
480
|
+
return mixHexWithWhite(accent, 0.08);
|
|
481
|
+
}
|
|
482
|
+
function deriveAccentPaletteSubs(accent, accentDark) {
|
|
483
|
+
if (accent.toUpperCase() === CARRIER_BRAND.colors.accent.toUpperCase()) return [];
|
|
484
|
+
const accentLight = deriveAccentLight(accent);
|
|
485
|
+
const gradientStart = deriveAccentGradientStart(accent);
|
|
486
|
+
return [
|
|
487
|
+
["#FF6B35", accent],
|
|
488
|
+
["#D9461C", accentDark],
|
|
489
|
+
["#FFB088", accentLight],
|
|
490
|
+
["#FF7A45", gradientStart],
|
|
491
|
+
["#fff4ef", mixHexWithWhite(accent, 0.94).toLowerCase()],
|
|
492
|
+
["#ffe0d0", mixHexWithWhite(accent, 0.85).toLowerCase()],
|
|
493
|
+
["#ffbfa0", mixHexWithWhite(accent, 0.7).toLowerCase()],
|
|
494
|
+
["#ff9970", mixHexWithWhite(accent, 0.55).toLowerCase()],
|
|
495
|
+
["#ff7d4d", mixHexWithWhite(accent, 0.4).toLowerCase()],
|
|
496
|
+
["#b33a17", darkenHex(accentDark, 0.75)],
|
|
497
|
+
["#8a2d12", darkenHex(accentDark, 0.58)],
|
|
498
|
+
["#5e1e0c", darkenHex(accentDark, 0.4)],
|
|
499
|
+
["#3a1107", darkenHex(accentDark, 0.25)]
|
|
687
500
|
];
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
actions: [
|
|
710
|
-
{ label: "Deploy", command: "carrier site deploy" },
|
|
711
|
-
{ label: "Check hosts", command: "carrier site targets" }
|
|
712
|
-
]
|
|
713
|
-
};
|
|
501
|
+
}
|
|
502
|
+
function renderEnv(brand) {
|
|
503
|
+
return [
|
|
504
|
+
`# Generated by @carrierllc/mcp`,
|
|
505
|
+
`NEXT_PUBLIC_BRAND_NAME=${JSON.stringify(brand.name)}`,
|
|
506
|
+
`NEXT_PUBLIC_CARRIER_API_URL=${JSON.stringify(brand.carrierApiUrl)}`,
|
|
507
|
+
`# Public site origin. Used to build absolute URLs for guest checkout.`,
|
|
508
|
+
`# \`carrier site deploy\` writes the deployed URL back here.`,
|
|
509
|
+
`NEXT_PUBLIC_APP_URL=`,
|
|
510
|
+
`# Required whenever NEXT_PUBLIC_CARRIER_API_URL points at a live origin:`,
|
|
511
|
+
`# the catalog client throws without it, so / and /shop return 500.`,
|
|
512
|
+
`# \`carrier site deploy\` fills this in and pushes it to the deploy target.`,
|
|
513
|
+
`CARRIER_API_KEY=`,
|
|
514
|
+
`# Guest checkout calls Stripe server-side. Without this, checkout fails at`,
|
|
515
|
+
`# request time while every other page keeps working.`,
|
|
516
|
+
`STRIPE_SECRET_KEY=`,
|
|
517
|
+
`# Clerk \u2014 \`carrier site clerk\` fills these in, or paste them from dashboard.clerk.com`,
|
|
518
|
+
`NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=`,
|
|
519
|
+
`CLERK_SECRET_KEY=`,
|
|
520
|
+
``
|
|
521
|
+
].join("\n");
|
|
714
522
|
}
|
|
715
523
|
|
|
716
|
-
// src/cli/lib/
|
|
717
|
-
import {
|
|
718
|
-
import {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
524
|
+
// src/cli/lib/storefront-secrets.ts
|
|
525
|
+
import { homedir } from "os";
|
|
526
|
+
import { join as join2 } from "path";
|
|
527
|
+
var TEMPLATE_ENV_KEYS = {
|
|
528
|
+
NEXT_PUBLIC_BRAND_NAME: "public",
|
|
529
|
+
NEXT_PUBLIC_CARRIER_API_URL: "public",
|
|
530
|
+
NEXT_PUBLIC_APP_URL: "public",
|
|
531
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: "public",
|
|
532
|
+
CARRIER_API_KEY: "secret",
|
|
533
|
+
CLERK_SECRET_KEY: "secret",
|
|
534
|
+
STRIPE_SECRET_KEY: "secret"
|
|
535
|
+
};
|
|
536
|
+
function keysOfKind(kind) {
|
|
537
|
+
return Object.keys(TEMPLATE_ENV_KEYS).filter(
|
|
538
|
+
(k) => TEMPLATE_ENV_KEYS[k] === kind
|
|
539
|
+
);
|
|
726
540
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
if (
|
|
739
|
-
|
|
740
|
-
});
|
|
741
|
-
child.stdin?.end(opts.stdin);
|
|
742
|
-
}
|
|
743
|
-
let stdout = "";
|
|
744
|
-
let stderr = "";
|
|
745
|
-
let settled = false;
|
|
746
|
-
const finish = (result) => {
|
|
747
|
-
if (settled) return;
|
|
748
|
-
settled = true;
|
|
749
|
-
resolve(result);
|
|
750
|
-
};
|
|
751
|
-
let timer;
|
|
752
|
-
if (opts.timeoutMs && opts.timeoutMs > 0) {
|
|
753
|
-
timer = setTimeout(() => {
|
|
754
|
-
try {
|
|
755
|
-
child.kill("SIGTERM");
|
|
756
|
-
} catch {
|
|
757
|
-
}
|
|
758
|
-
finish({
|
|
759
|
-
ok: false,
|
|
760
|
-
code: null,
|
|
761
|
-
stdout,
|
|
762
|
-
stderr: stderr || `timeout after ${opts.timeoutMs}ms`
|
|
763
|
-
});
|
|
764
|
-
}, opts.timeoutMs);
|
|
541
|
+
var RUNTIME_SECRET_KEYS = keysOfKind("secret");
|
|
542
|
+
var PUBLIC_ENV_KEYS = keysOfKind("public");
|
|
543
|
+
function parseEnvFile(body) {
|
|
544
|
+
const out = {};
|
|
545
|
+
for (const raw of body.split("\n")) {
|
|
546
|
+
const line = raw.trim();
|
|
547
|
+
if (!line || line.startsWith("#")) continue;
|
|
548
|
+
const eq = line.indexOf("=");
|
|
549
|
+
if (eq <= 0) continue;
|
|
550
|
+
const key = line.slice(0, eq).trim();
|
|
551
|
+
let value = line.slice(eq + 1).trim();
|
|
552
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length > 1 || value.startsWith("'") && value.endsWith("'") && value.length > 1) {
|
|
553
|
+
value = value.slice(1, -1);
|
|
765
554
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
if (timer) clearTimeout(timer);
|
|
770
|
-
finish({ ok: false, code: null, stdout, stderr });
|
|
771
|
-
});
|
|
772
|
-
child.on("close", (code) => {
|
|
773
|
-
if (timer) clearTimeout(timer);
|
|
774
|
-
finish({ ok: code === 0, code, stdout, stderr });
|
|
775
|
-
});
|
|
776
|
-
});
|
|
555
|
+
if (value) out[key] = value;
|
|
556
|
+
}
|
|
557
|
+
return out;
|
|
777
558
|
}
|
|
778
|
-
function
|
|
779
|
-
if (!
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
559
|
+
async function readEnvFile(path) {
|
|
560
|
+
if (!await exists(path)) return {};
|
|
561
|
+
try {
|
|
562
|
+
return parseEnvFile(await readFile(path, "utf8"));
|
|
563
|
+
} catch {
|
|
564
|
+
return {};
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async function discoverSecrets(storefront, env = process.env, home = homedir()) {
|
|
568
|
+
const local = await readEnvFile(join2(storefront, ".env.local"));
|
|
569
|
+
const global = await readEnvFile(join2(home, ".env"));
|
|
570
|
+
const merged = {};
|
|
571
|
+
const keys = [...RUNTIME_SECRET_KEYS, ...PUBLIC_ENV_KEYS];
|
|
572
|
+
for (const key of keys) {
|
|
573
|
+
const value = local[key]?.trim() || env[key]?.trim() || global[key]?.trim();
|
|
574
|
+
if (value) merged[key] = value;
|
|
575
|
+
}
|
|
576
|
+
return merged;
|
|
577
|
+
}
|
|
578
|
+
async function mergeEnvLocal(storefront, updates, opts = {}) {
|
|
579
|
+
const path = join2(storefront, ".env.local");
|
|
580
|
+
const overwrite = new Set(opts.overwrite ?? []);
|
|
581
|
+
const original = await exists(path) ? await readFile(path, "utf8") : "";
|
|
582
|
+
const lines = original ? original.split("\n") : [];
|
|
583
|
+
const written = [];
|
|
584
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
585
|
+
if (!value) continue;
|
|
586
|
+
const index = lines.findIndex((l) => l.trim().startsWith(`${key}=`));
|
|
587
|
+
if (index === -1) {
|
|
588
|
+
lines.push(`${key}=${value}`);
|
|
589
|
+
written.push(key);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const current = lines[index].slice(lines[index].indexOf("=") + 1).trim();
|
|
593
|
+
const isEmpty = current === "" || current === '""' || current === "''";
|
|
594
|
+
if (isEmpty || overwrite.has(key)) {
|
|
595
|
+
lines[index] = `${key}=${value}`;
|
|
596
|
+
written.push(key);
|
|
597
|
+
}
|
|
786
598
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
599
|
+
if (written.length === 0) return [];
|
|
600
|
+
const body = lines.join("\n").replace(/\n{3,}$/, "\n");
|
|
601
|
+
await writeFile(path, body.endsWith("\n") ? body : `${body}
|
|
602
|
+
`);
|
|
603
|
+
return written;
|
|
792
604
|
}
|
|
793
|
-
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
return r.ok && r.stdout.trim().length > 0;
|
|
605
|
+
function needsCarrierKey(secrets) {
|
|
606
|
+
const url = secrets.NEXT_PUBLIC_CARRIER_API_URL?.trim();
|
|
607
|
+
return Boolean(url) && !secrets.CARRIER_API_KEY?.trim();
|
|
797
608
|
}
|
|
798
609
|
|
|
799
|
-
// src/cli/lib/
|
|
800
|
-
|
|
801
|
-
function
|
|
802
|
-
return
|
|
610
|
+
// src/cli/lib/site.ts
|
|
611
|
+
import { join as join3 } from "path";
|
|
612
|
+
function slug(s) {
|
|
613
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "storefront";
|
|
803
614
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
npxPkg: "netlify-cli",
|
|
839
|
-
markers: ["netlify.toml"],
|
|
840
|
-
buildScript: "build",
|
|
841
|
-
whoami: ["status"],
|
|
842
|
-
loginHint: "netlify login",
|
|
843
|
-
parseAccount: (stdout, stderr) => `${stdout}${stderr}`.match(/Email:\s*(\S+)/)?.[1],
|
|
844
|
-
deployArgs: () => ["deploy", "--build", "--prod"],
|
|
845
|
-
ensureConfig: async (storefront) => {
|
|
846
|
-
const path = join(storefront, "netlify.toml");
|
|
847
|
-
if (await exists(path)) return;
|
|
848
|
-
await writeFile(
|
|
849
|
-
path,
|
|
850
|
-
[
|
|
851
|
-
"# Written by @carrierllc/mcp",
|
|
852
|
-
"[build]",
|
|
853
|
-
' command = "npm run build"',
|
|
854
|
-
' publish = ".next"',
|
|
855
|
-
"",
|
|
856
|
-
"[[plugins]]",
|
|
857
|
-
' package = "@netlify/plugin-nextjs"',
|
|
858
|
-
""
|
|
859
|
-
].join("\n")
|
|
860
|
-
);
|
|
861
|
-
},
|
|
862
|
-
secretArgs: (key, value) => ({ args: ["env:set", key, value] })
|
|
863
|
-
},
|
|
864
|
-
fly: {
|
|
865
|
-
id: "fly",
|
|
866
|
-
label: "Fly.io",
|
|
867
|
-
bin: "flyctl",
|
|
868
|
-
npxPkg: "",
|
|
869
|
-
markers: ["fly.toml"],
|
|
870
|
-
buildScript: "build",
|
|
871
|
-
whoami: ["auth", "whoami"],
|
|
872
|
-
loginHint: "flyctl auth login",
|
|
873
|
-
parseAccount: (out) => out.trim().split("\n").pop()?.trim(),
|
|
874
|
-
deployArgs: () => ["deploy", "--now"],
|
|
875
|
-
ensureConfig: async (storefront, projectName) => {
|
|
876
|
-
const toml = join(storefront, "fly.toml");
|
|
877
|
-
if (!await exists(toml)) {
|
|
878
|
-
await writeFile(
|
|
879
|
-
toml,
|
|
880
|
-
[
|
|
881
|
-
"# Written by @carrierllc/mcp",
|
|
882
|
-
`app = "${projectName}"`,
|
|
883
|
-
"",
|
|
884
|
-
"[build]",
|
|
885
|
-
' dockerfile = "Dockerfile"',
|
|
886
|
-
"",
|
|
887
|
-
"[http_service]",
|
|
888
|
-
" internal_port = 3000",
|
|
889
|
-
" force_https = true",
|
|
890
|
-
" auto_stop_machines = true",
|
|
891
|
-
" auto_start_machines = true",
|
|
892
|
-
""
|
|
893
|
-
].join("\n")
|
|
894
|
-
);
|
|
895
|
-
}
|
|
896
|
-
const dockerfile = join(storefront, "Dockerfile");
|
|
897
|
-
if (!await exists(dockerfile)) {
|
|
898
|
-
await writeFile(
|
|
899
|
-
dockerfile,
|
|
900
|
-
[
|
|
901
|
-
"# Written by @carrierllc/mcp",
|
|
902
|
-
"FROM node:22-slim AS build",
|
|
903
|
-
"WORKDIR /app",
|
|
904
|
-
"COPY package*.json ./",
|
|
905
|
-
"RUN npm install",
|
|
906
|
-
"COPY . .",
|
|
907
|
-
"RUN npm run build",
|
|
908
|
-
"",
|
|
909
|
-
"FROM node:22-slim",
|
|
910
|
-
"WORKDIR /app",
|
|
911
|
-
"ENV NODE_ENV=production PORT=3000",
|
|
912
|
-
"COPY --from=build /app ./",
|
|
913
|
-
"EXPOSE 3000",
|
|
914
|
-
'CMD ["npm", "run", "start"]',
|
|
915
|
-
""
|
|
916
|
-
].join("\n")
|
|
917
|
-
);
|
|
918
|
-
}
|
|
615
|
+
async function installDeps(target) {
|
|
616
|
+
const pkgMgr = await which("pnpm") ? "pnpm" : "npm";
|
|
617
|
+
const r = await runInherit(pkgMgr, ["install"], { cwd: target });
|
|
618
|
+
return r.ok;
|
|
619
|
+
}
|
|
620
|
+
async function buildSite(target, forTarget = "cloudflare") {
|
|
621
|
+
const pkgMgr = await which("pnpm") ? "pnpm" : "npm";
|
|
622
|
+
const r = await runInherit(pkgMgr, ["run", buildScriptFor(forTarget)], { cwd: target });
|
|
623
|
+
return r.ok;
|
|
624
|
+
}
|
|
625
|
+
async function loadStorefrontBrand(target, overrides) {
|
|
626
|
+
const configPath = join3(target, "src", "brand.config.ts");
|
|
627
|
+
if (!await exists(configPath)) {
|
|
628
|
+
return { ...CARRIER_BRAND, ...overrides };
|
|
629
|
+
}
|
|
630
|
+
const src = await readFile(configPath, "utf8");
|
|
631
|
+
const pick = (field, fallback) => {
|
|
632
|
+
const m = src.match(new RegExp(`\\b${field}:\\s*(?:[^"\\n]*\\?\\?\\s*)?"([^"]*)"`));
|
|
633
|
+
return m?.[1] ?? fallback;
|
|
634
|
+
};
|
|
635
|
+
return {
|
|
636
|
+
...CARRIER_BRAND,
|
|
637
|
+
name: overrides?.name ?? pick("name", CARRIER_BRAND.name),
|
|
638
|
+
domain: pick("domain", CARRIER_BRAND.domain),
|
|
639
|
+
supportEmail: pick("supportEmail", CARRIER_BRAND.supportEmail),
|
|
640
|
+
supportUrl: pick("supportUrl", CARRIER_BRAND.supportUrl),
|
|
641
|
+
tagline: pick("tagline", CARRIER_BRAND.tagline),
|
|
642
|
+
legalName: pick("legalName", CARRIER_BRAND.legalName),
|
|
643
|
+
colors: {
|
|
644
|
+
...CARRIER_BRAND.colors,
|
|
645
|
+
accent: pick("accent", CARRIER_BRAND.colors.accent),
|
|
646
|
+
accentDark: pick("accentDark", CARRIER_BRAND.colors.accentDark),
|
|
647
|
+
bg: pick("bg", CARRIER_BRAND.colors.bg),
|
|
648
|
+
text: pick("text", CARRIER_BRAND.colors.text)
|
|
919
649
|
},
|
|
920
|
-
|
|
650
|
+
carrierApiUrl: CARRIER_BRAND.carrierApiUrl
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
async function deploySite(target, brand, opts = {}) {
|
|
654
|
+
const statuses = await probeAll(target);
|
|
655
|
+
let chosen;
|
|
656
|
+
if (opts.preferred) {
|
|
657
|
+
const wanted = statuses.find((s) => s.id === opts.preferred);
|
|
658
|
+
if (!wanted?.ready) {
|
|
659
|
+
return {
|
|
660
|
+
ok: false,
|
|
661
|
+
projectName: slug(brand.name),
|
|
662
|
+
statuses,
|
|
663
|
+
reason: wanted?.reason ?? `${opts.preferred} is not available on this machine.`
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
chosen = wanted;
|
|
667
|
+
} else {
|
|
668
|
+
chosen = rankTargets(statuses)[0];
|
|
921
669
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
670
|
+
if (!chosen) {
|
|
671
|
+
return {
|
|
672
|
+
ok: false,
|
|
673
|
+
projectName: slug(brand.name),
|
|
674
|
+
statuses,
|
|
675
|
+
reason: "No deploy target is ready. " + statuses.map((s) => `${s.label}: ${s.reason ?? "unavailable"}`).join(" | ")
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
const projectName = await configuredProjectName(chosen.id, target) ?? slug(brand.name);
|
|
679
|
+
if (opts.customDomain && chosen.id === "cloudflare" && brand.domain) {
|
|
680
|
+
await setCustomDomain(target, brand.domain);
|
|
681
|
+
}
|
|
682
|
+
let staged = { staged: [], failed: [], cleanup: async () => {
|
|
683
|
+
} };
|
|
684
|
+
if (opts.pushSecrets !== false) {
|
|
685
|
+
const discovered = await discoverSecrets(target, opts.env ?? process.env);
|
|
686
|
+
const runtime = {};
|
|
687
|
+
for (const key of RUNTIME_SECRET_KEYS) {
|
|
688
|
+
const value = discovered[key];
|
|
689
|
+
if (value) runtime[key] = value;
|
|
690
|
+
}
|
|
691
|
+
staged = await stageSecrets(chosen.id, target, projectName, runtime);
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
const outcome = await deployTo(chosen.id, target, projectName, {
|
|
695
|
+
secretsFile: staged.secretsFile
|
|
696
|
+
});
|
|
697
|
+
if (!outcome.ok) {
|
|
698
|
+
return { ok: false, projectName, statuses, target: chosen.id, reason: outcome.reason };
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
ok: true,
|
|
702
|
+
projectName,
|
|
703
|
+
statuses,
|
|
704
|
+
target: chosen.id,
|
|
705
|
+
url: outcome.url,
|
|
706
|
+
secrets: { pushed: staged.staged, failed: staged.failed }
|
|
707
|
+
};
|
|
708
|
+
} finally {
|
|
709
|
+
await staged.cleanup();
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// src/cli/lib/clerk.ts
|
|
714
|
+
var CLERK_API_BASE = "https://api.clerk.com/v1";
|
|
715
|
+
var PUBLISHABLE_KEYS = [
|
|
716
|
+
"CLERK_PUBLISHABLE_KEY",
|
|
717
|
+
"NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY"
|
|
718
|
+
];
|
|
719
|
+
var SECRET_KEYS = ["CLERK_SECRET_KEY"];
|
|
720
|
+
function firstNonEmpty(env, names) {
|
|
721
|
+
for (const name of names) {
|
|
722
|
+
const value = env[name]?.trim();
|
|
723
|
+
if (value) return value;
|
|
931
724
|
}
|
|
932
725
|
return void 0;
|
|
933
726
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
if (!clean || !clean.includes(".")) return false;
|
|
937
|
-
const path = join(storefront, "wrangler.jsonc");
|
|
938
|
-
if (!await exists(path)) return false;
|
|
939
|
-
const body = await readFileText(path);
|
|
940
|
-
if (body.includes(`"pattern": "${clean}"`)) return true;
|
|
941
|
-
const routes = ` "routes": [
|
|
942
|
-
{ "pattern": "${clean}", "custom_domain": true }
|
|
943
|
-
],
|
|
944
|
-
`;
|
|
945
|
-
const anchor = body.indexOf(`"main"`);
|
|
946
|
-
if (anchor === -1) return false;
|
|
947
|
-
const lineStart = body.lastIndexOf("\n", anchor) + 1;
|
|
948
|
-
const patched = body.slice(0, lineStart) + routes + body.slice(lineStart);
|
|
949
|
-
await writeFile(path, patched);
|
|
950
|
-
return true;
|
|
727
|
+
function looksPublishable(key) {
|
|
728
|
+
return /^pk_(test|live)_/.test(key);
|
|
951
729
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
return readFile2(path, "utf8");
|
|
730
|
+
function looksSecret(key) {
|
|
731
|
+
return /^sk_(test|live)_/.test(key);
|
|
955
732
|
}
|
|
956
|
-
async function
|
|
957
|
-
|
|
958
|
-
|
|
733
|
+
async function clerkFetch(path, token, init = {}) {
|
|
734
|
+
try {
|
|
735
|
+
const res = await fetch(`${CLERK_API_BASE}${path}`, {
|
|
736
|
+
method: init.method ?? "GET",
|
|
737
|
+
headers: {
|
|
738
|
+
Authorization: `Bearer ${token}`,
|
|
739
|
+
"Content-Type": "application/json"
|
|
740
|
+
},
|
|
741
|
+
body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
|
|
742
|
+
});
|
|
743
|
+
const text = await res.text();
|
|
744
|
+
let json;
|
|
745
|
+
try {
|
|
746
|
+
json = text ? JSON.parse(text) : void 0;
|
|
747
|
+
} catch {
|
|
748
|
+
json = void 0;
|
|
749
|
+
}
|
|
750
|
+
if (!res.ok) {
|
|
751
|
+
return { ok: false, status: res.status, json, error: clerkError(json) ?? text.slice(0, 300) };
|
|
752
|
+
}
|
|
753
|
+
return { ok: true, status: res.status, json };
|
|
754
|
+
} catch (e) {
|
|
755
|
+
return { ok: false, status: 0, error: e instanceof Error ? e.message : String(e) };
|
|
959
756
|
}
|
|
960
|
-
const def = DEFS[id];
|
|
961
|
-
const resolved = await resolveBin(def);
|
|
962
|
-
if (!resolved) return { ok: false, reason: "wrangler not found." };
|
|
963
|
-
const r = await run(
|
|
964
|
-
resolved.bin,
|
|
965
|
-
[...resolved.prefix, "rollback", "--name", projectName, "--yes"],
|
|
966
|
-
{ cwd: storefront, timeoutMs: 3e5 }
|
|
967
|
-
);
|
|
968
|
-
return r.ok ? { ok: true } : { ok: false, reason: `${r.stderr || r.stdout}`.trim().split("\n").slice(-2).join(" ").slice(0, 300) };
|
|
969
757
|
}
|
|
970
|
-
|
|
971
|
-
if (
|
|
972
|
-
|
|
973
|
-
return void 0;
|
|
758
|
+
function clerkError(json) {
|
|
759
|
+
if (typeof json !== "object" || json === null) return void 0;
|
|
760
|
+
const errors = json.errors;
|
|
761
|
+
if (!Array.isArray(errors) || errors.length === 0) return void 0;
|
|
762
|
+
const first = errors[0];
|
|
763
|
+
return first.long_message ?? first.message;
|
|
974
764
|
}
|
|
975
|
-
async function
|
|
976
|
-
const
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
if (!
|
|
765
|
+
async function createClerkApplication(platformToken, opts) {
|
|
766
|
+
const body = {
|
|
767
|
+
name: opts.name,
|
|
768
|
+
environment_types: opts.production ? ["development", "production"] : ["development"]
|
|
769
|
+
};
|
|
770
|
+
if (opts.domain) body.domain = opts.domain;
|
|
771
|
+
const res = await clerkFetch("/platform/applications", platformToken, {
|
|
772
|
+
method: "POST",
|
|
773
|
+
body
|
|
774
|
+
});
|
|
775
|
+
if (!res.ok) {
|
|
986
776
|
return {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
authenticated: false,
|
|
991
|
-
configured,
|
|
992
|
-
ready: false,
|
|
993
|
-
reason: def.npxPkg ? `${def.bin} not found \u2014 install it, or make npx available.` : `${def.bin} not found \u2014 install the Fly CLI (brew install flyctl).`
|
|
777
|
+
ok: false,
|
|
778
|
+
tier: "platform",
|
|
779
|
+
reason: res.status === 401 || res.status === 403 ? `Clerk Platform API rejected the token (HTTP ${res.status}). Check CLERK_PLATFORM_API_KEY.` : `Clerk Platform API error: ${res.error ?? `HTTP ${res.status}`}`
|
|
994
780
|
};
|
|
995
781
|
}
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
if (!
|
|
782
|
+
const payload = res.json;
|
|
783
|
+
const instances = payload?.instances ?? [];
|
|
784
|
+
const wanted = opts.production ? "production" : "development";
|
|
785
|
+
const instance = instances.find((i) => i.environment_type === wanted && i.secret_key && i.publishable_key) ?? instances.find((i) => i.secret_key && i.publishable_key);
|
|
786
|
+
if (!instance?.secret_key || !instance.publishable_key) {
|
|
1001
787
|
return {
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
authenticated: false,
|
|
1006
|
-
configured,
|
|
1007
|
-
ready: false,
|
|
1008
|
-
reason: `${def.bin} is not logged in \u2014 run \`${def.loginHint}\`.`
|
|
788
|
+
ok: false,
|
|
789
|
+
tier: "platform",
|
|
790
|
+
reason: "Clerk created the application but returned no instance keys. Read them with GET /platform/applications?include_secret_keys=true."
|
|
1009
791
|
};
|
|
1010
792
|
}
|
|
1011
793
|
return {
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
794
|
+
ok: true,
|
|
795
|
+
tier: "platform",
|
|
796
|
+
credentials: {
|
|
797
|
+
publishableKey: instance.publishable_key,
|
|
798
|
+
secretKey: instance.secret_key,
|
|
799
|
+
tier: "platform",
|
|
800
|
+
applicationId: payload?.application_id,
|
|
801
|
+
instanceId: instance.instance_id
|
|
802
|
+
}
|
|
1019
803
|
};
|
|
1020
804
|
}
|
|
1021
|
-
async function
|
|
1022
|
-
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
const { args, stdin } = def.secretArgs(key, value, projectName);
|
|
1048
|
-
const r = await run(resolved.bin, [...resolved.prefix, ...args], {
|
|
1049
|
-
cwd: storefront,
|
|
1050
|
-
timeoutMs: 12e4,
|
|
1051
|
-
stdin
|
|
1052
|
-
});
|
|
1053
|
-
return r.ok;
|
|
1054
|
-
}
|
|
1055
|
-
async function stageSecrets(id, storefront, projectName, secrets) {
|
|
1056
|
-
const entries = Object.entries(secrets).filter(([, v]) => v?.trim());
|
|
1057
|
-
const noop = { staged: [], failed: [], cleanup: async () => {
|
|
1058
|
-
} };
|
|
1059
|
-
if (entries.length === 0) return noop;
|
|
1060
|
-
const def = DEFS[id];
|
|
1061
|
-
const resolved = await resolveBin(def);
|
|
1062
|
-
if (!resolved) return { staged: [], failed: entries.map(([k]) => k), cleanup: async () => {
|
|
1063
|
-
} };
|
|
1064
|
-
if (id === "cloudflare") {
|
|
1065
|
-
const dir = await mkdtemp(join(tmpdir(), "carrier-secrets-"));
|
|
1066
|
-
const file = join(dir, ".env");
|
|
1067
|
-
const body = entries.map(([k, v]) => `${k}=${v}`).join("\n");
|
|
1068
|
-
await writeFileMode(file, `${body}
|
|
1069
|
-
`, { mode: 384 });
|
|
805
|
+
async function mintKeylessClerkApp(storefront, deps) {
|
|
806
|
+
if (!await deps.which("npx")) {
|
|
807
|
+
return { ok: false, tier: "cli-keyless", reason: "npx not found \u2014 cannot run the Clerk CLI." };
|
|
808
|
+
}
|
|
809
|
+
const restore = await deps.snapshot(storefront);
|
|
810
|
+
try {
|
|
811
|
+
const r = await deps.run(
|
|
812
|
+
"npx",
|
|
813
|
+
["--yes", "clerk@latest", "init", "--framework", "next", "--keyless", "--no-skills", "-y"],
|
|
814
|
+
{ cwd: storefront, timeoutMs: 6e5 }
|
|
815
|
+
);
|
|
816
|
+
const envPath = `${storefront}/.env.local`;
|
|
817
|
+
const body = await deps.readFileIfExists(envPath) ?? "";
|
|
818
|
+
const publishableKey = matchEnv(body, "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY");
|
|
819
|
+
const secretKey = matchEnv(body, "CLERK_SECRET_KEY");
|
|
820
|
+
if (!publishableKey || !secretKey) {
|
|
821
|
+
const tail = `${r.stdout}${r.stderr}`.trim().split("\n").slice(-2).join(" ").slice(0, 300);
|
|
822
|
+
return {
|
|
823
|
+
ok: false,
|
|
824
|
+
tier: "cli-keyless",
|
|
825
|
+
reason: `Clerk CLI did not produce keys${tail ? `: ${tail}` : "."}`
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
if (!looksPublishable(publishableKey) || !looksSecret(secretKey)) {
|
|
829
|
+
return { ok: false, tier: "cli-keyless", reason: "Clerk CLI wrote keys in an unexpected format." };
|
|
830
|
+
}
|
|
1070
831
|
return {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
cleanup: async () => {
|
|
1075
|
-
await rm(dir, { recursive: true, force: true });
|
|
1076
|
-
}
|
|
832
|
+
ok: true,
|
|
833
|
+
tier: "cli-keyless",
|
|
834
|
+
credentials: { publishableKey, secretKey, tier: "cli-keyless" }
|
|
1077
835
|
};
|
|
836
|
+
} finally {
|
|
837
|
+
await restore();
|
|
1078
838
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
839
|
+
}
|
|
840
|
+
function matchEnv(body, key) {
|
|
841
|
+
const line = body.split("\n").find((l) => l.trim().startsWith(`${key}=`));
|
|
842
|
+
const value = line?.slice(line.indexOf("=") + 1).trim();
|
|
843
|
+
return value || void 0;
|
|
844
|
+
}
|
|
845
|
+
function discoverClerkCredentials(env) {
|
|
846
|
+
const publishableKey = firstNonEmpty(env, PUBLISHABLE_KEYS);
|
|
847
|
+
const secretKey = firstNonEmpty(env, SECRET_KEYS);
|
|
848
|
+
if (!publishableKey || !secretKey) return void 0;
|
|
849
|
+
if (!looksPublishable(publishableKey) || !looksSecret(secretKey)) return void 0;
|
|
850
|
+
return { publishableKey, secretKey, tier: "discovered" };
|
|
851
|
+
}
|
|
852
|
+
var MANUAL_STEPS = [
|
|
853
|
+
"Open https://dashboard.clerk.com and create an application.",
|
|
854
|
+
"Copy the Publishable key (pk_...) and Secret key (sk_...) from API keys.",
|
|
855
|
+
"Put them in the storefront's .env.local as NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY.",
|
|
856
|
+
"Re-run `carrier site deploy` \u2014 the keys are pushed to the deploy target automatically."
|
|
857
|
+
];
|
|
858
|
+
async function provisionClerk(opts) {
|
|
859
|
+
const platformToken = firstNonEmpty(opts.env, [
|
|
860
|
+
"CLERK_PLATFORM_API_KEY",
|
|
861
|
+
"CLERK_PLATFORM_TOKEN"
|
|
862
|
+
]);
|
|
863
|
+
const notes = [];
|
|
864
|
+
if (platformToken && !opts.noCreate) {
|
|
865
|
+
const created = await createClerkApplication(platformToken, {
|
|
866
|
+
name: opts.name,
|
|
867
|
+
domain: opts.domain,
|
|
868
|
+
production: opts.production
|
|
1084
869
|
});
|
|
870
|
+
if (created.ok) return created;
|
|
871
|
+
notes.push(created.reason ?? "Clerk Platform API call failed.");
|
|
872
|
+
}
|
|
873
|
+
const discovered = discoverClerkCredentials(opts.env);
|
|
874
|
+
if (discovered) {
|
|
1085
875
|
return {
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
876
|
+
ok: true,
|
|
877
|
+
tier: "discovered",
|
|
878
|
+
credentials: discovered,
|
|
879
|
+
reason: notes.length ? notes.join(" ") : void 0
|
|
1090
880
|
};
|
|
1091
881
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
882
|
+
if (opts.cli && opts.storefront && !opts.noCreate) {
|
|
883
|
+
const minted = await mintKeylessClerkApp(opts.storefront, opts.cli);
|
|
884
|
+
if (minted.ok) {
|
|
885
|
+
return { ...minted, reason: notes.length ? notes.join(" ") : void 0 };
|
|
886
|
+
}
|
|
887
|
+
notes.push(minted.reason ?? "Clerk CLI keyless provisioning failed.");
|
|
1097
888
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
const def = DEFS[id];
|
|
1103
|
-
const resolved = await resolveBin(def);
|
|
1104
|
-
if (!resolved) {
|
|
1105
|
-
return { ok: false, target: id, projectName, reason: `${def.bin} not found.` };
|
|
889
|
+
if (!platformToken) {
|
|
890
|
+
notes.push(
|
|
891
|
+
"No CLERK_PLATFORM_API_KEY set, so a new Clerk application cannot be created through the Platform API (it is a partner surface, not self-serve)."
|
|
892
|
+
);
|
|
1106
893
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
894
|
+
return {
|
|
895
|
+
ok: false,
|
|
896
|
+
tier: "manual",
|
|
897
|
+
reason: notes.join(" "),
|
|
898
|
+
guidance: MANUAL_STEPS
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
async function configureClerkInstance(secretKey, opts) {
|
|
902
|
+
const applied = [];
|
|
903
|
+
const failed = [];
|
|
904
|
+
const origins = dedupe(opts.allowedOrigins ?? []);
|
|
905
|
+
if (origins.length > 0) {
|
|
906
|
+
const res = await clerkFetch("/instance", secretKey, {
|
|
907
|
+
method: "PATCH",
|
|
908
|
+
body: { allowed_origins: origins }
|
|
909
|
+
});
|
|
910
|
+
if (res.ok) applied.push(`allowed_origins (${origins.length})`);
|
|
911
|
+
else failed.push({ step: "allowed_origins", reason: res.error ?? `HTTP ${res.status}` });
|
|
1115
912
|
}
|
|
1116
|
-
const
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
timeoutMs: 9e5
|
|
1121
|
-
});
|
|
1122
|
-
const combined = `${r.stdout}${r.stderr}`;
|
|
1123
|
-
if (!r.ok) {
|
|
1124
|
-
const tail = combined.trim().split("\n").slice(-3).join(" ").slice(0, 400);
|
|
1125
|
-
return { ok: false, target: id, projectName, reason: tail || `${def.bin} deploy failed.` };
|
|
913
|
+
for (const url of dedupe(opts.redirectUrls ?? [])) {
|
|
914
|
+
const res = await clerkFetch("/redirect_urls", secretKey, { method: "POST", body: { url } });
|
|
915
|
+
if (res.ok) applied.push(`redirect_url ${url}`);
|
|
916
|
+
else failed.push({ step: `redirect_url ${url}`, reason: res.error ?? `HTTP ${res.status}` });
|
|
1126
917
|
}
|
|
1127
|
-
return { ok:
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// src/cli/lib/brand.ts
|
|
1131
|
-
var CARRIER_BRAND = {
|
|
1132
|
-
name: "Carrier",
|
|
1133
|
-
legalName: "Lifecycle Innovations Limited",
|
|
1134
|
-
tagline: "Programmable connectivity, on demand.",
|
|
1135
|
-
domain: "carrier.llc",
|
|
1136
|
-
supportEmail: "support@carrier.llc",
|
|
1137
|
-
supportUrl: "https://carrier.llc/help",
|
|
1138
|
-
supportWhatsapp: "+17864604829",
|
|
1139
|
-
colors: {
|
|
1140
|
-
bg: "#080C16",
|
|
1141
|
-
accent: "#FF6B35",
|
|
1142
|
-
accentDark: "#D9461C",
|
|
1143
|
-
text: "#F5F1EA"
|
|
1144
|
-
},
|
|
1145
|
-
social: {
|
|
1146
|
-
x: "@carrier_llc",
|
|
1147
|
-
instagram: "@carrier.llc",
|
|
1148
|
-
tiktok: "@carrier.llc"
|
|
1149
|
-
},
|
|
1150
|
-
carrierApiUrl: "https://api.carrier.llc"
|
|
1151
|
-
};
|
|
1152
|
-
var CARRIER_ACCENT_LIGHT = "#FFB088";
|
|
1153
|
-
var CARRIER_ACCENT_GRADIENT_START = "#FF7A45";
|
|
1154
|
-
function hexToRgb(hex) {
|
|
1155
|
-
const h = hex.replace(/^#/, "");
|
|
1156
|
-
if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
|
|
1157
|
-
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
1158
|
-
}
|
|
1159
|
-
function rgbToHex(r, g, b, lower = false) {
|
|
1160
|
-
const fmt = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
|
|
1161
|
-
const out = `#${fmt(r)}${fmt(g)}${fmt(b)}`;
|
|
1162
|
-
return lower ? out : out.toUpperCase();
|
|
1163
|
-
}
|
|
1164
|
-
function mixHexWithWhite(hex, whiteRatio) {
|
|
1165
|
-
const rgb = hexToRgb(hex);
|
|
1166
|
-
if (!rgb) return hex;
|
|
1167
|
-
const mix2 = (n) => n + (255 - n) * whiteRatio;
|
|
1168
|
-
return rgbToHex(mix2(rgb[0]), mix2(rgb[1]), mix2(rgb[2]));
|
|
1169
|
-
}
|
|
1170
|
-
function darkenHex(hex, factor) {
|
|
1171
|
-
const rgb = hexToRgb(hex);
|
|
1172
|
-
if (!rgb) return hex;
|
|
1173
|
-
const scale = (n) => n * factor;
|
|
1174
|
-
return rgbToHex(scale(rgb[0]), scale(rgb[1]), scale(rgb[2]), true);
|
|
1175
|
-
}
|
|
1176
|
-
function deriveAccentDark(accent, seed = CARRIER_BRAND) {
|
|
1177
|
-
if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return seed.colors.accentDark;
|
|
1178
|
-
const rgb = hexToRgb(accent);
|
|
1179
|
-
if (!rgb) return accent;
|
|
1180
|
-
const scale = (n) => Math.max(0, Math.min(255, Math.round(n * 0.75)));
|
|
1181
|
-
return rgbToHex(scale(rgb[0]), scale(rgb[1]), scale(rgb[2]));
|
|
1182
|
-
}
|
|
1183
|
-
function deriveAccentLight(accent, seed = CARRIER_BRAND) {
|
|
1184
|
-
if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return CARRIER_ACCENT_LIGHT;
|
|
1185
|
-
return mixHexWithWhite(accent, 0.45);
|
|
1186
|
-
}
|
|
1187
|
-
function deriveAccentGradientStart(accent, seed = CARRIER_BRAND) {
|
|
1188
|
-
if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return CARRIER_ACCENT_GRADIENT_START;
|
|
1189
|
-
return mixHexWithWhite(accent, 0.08);
|
|
918
|
+
return { ok: failed.length === 0, applied, failed };
|
|
1190
919
|
}
|
|
1191
|
-
function
|
|
1192
|
-
|
|
1193
|
-
const accentLight = deriveAccentLight(accent);
|
|
1194
|
-
const gradientStart = deriveAccentGradientStart(accent);
|
|
1195
|
-
return [
|
|
1196
|
-
["#FF6B35", accent],
|
|
1197
|
-
["#D9461C", accentDark],
|
|
1198
|
-
["#FFB088", accentLight],
|
|
1199
|
-
["#FF7A45", gradientStart],
|
|
1200
|
-
["#fff4ef", mixHexWithWhite(accent, 0.94).toLowerCase()],
|
|
1201
|
-
["#ffe0d0", mixHexWithWhite(accent, 0.85).toLowerCase()],
|
|
1202
|
-
["#ffbfa0", mixHexWithWhite(accent, 0.7).toLowerCase()],
|
|
1203
|
-
["#ff9970", mixHexWithWhite(accent, 0.55).toLowerCase()],
|
|
1204
|
-
["#ff7d4d", mixHexWithWhite(accent, 0.4).toLowerCase()],
|
|
1205
|
-
["#b33a17", darkenHex(accentDark, 0.75)],
|
|
1206
|
-
["#8a2d12", darkenHex(accentDark, 0.58)],
|
|
1207
|
-
["#5e1e0c", darkenHex(accentDark, 0.4)],
|
|
1208
|
-
["#3a1107", darkenHex(accentDark, 0.25)]
|
|
1209
|
-
];
|
|
920
|
+
function dedupe(values) {
|
|
921
|
+
return [...new Set(values.map((v) => v.trim()).filter(Boolean))];
|
|
1210
922
|
}
|
|
1211
|
-
function
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
`
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
`# \`carrier site deploy\` fills this in and pushes it to the deploy target.`,
|
|
1222
|
-
`CARRIER_API_KEY=`,
|
|
1223
|
-
`# Guest checkout calls Stripe server-side. Without this, checkout fails at`,
|
|
1224
|
-
`# request time while every other page keeps working.`,
|
|
1225
|
-
`STRIPE_SECRET_KEY=`,
|
|
1226
|
-
`# Clerk \u2014 \`carrier site clerk\` fills these in, or paste them from dashboard.clerk.com`,
|
|
1227
|
-
`NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=`,
|
|
1228
|
-
`CLERK_SECRET_KEY=`,
|
|
1229
|
-
``
|
|
1230
|
-
].join("\n");
|
|
923
|
+
function storefrontClerkUrls(deployedUrl, domain) {
|
|
924
|
+
const bases = dedupe([
|
|
925
|
+
deployedUrl?.replace(/\/$/, "") ?? "",
|
|
926
|
+
domain ? `https://${domain.replace(/^https?:\/\//, "").replace(/\/$/, "")}` : "",
|
|
927
|
+
"http://localhost:3000"
|
|
928
|
+
]);
|
|
929
|
+
return {
|
|
930
|
+
allowedOrigins: bases,
|
|
931
|
+
redirectUrls: bases.flatMap((b) => [b, `${b}/checkout/success`, `${b}/dashboard`])
|
|
932
|
+
};
|
|
1231
933
|
}
|
|
1232
934
|
|
|
1233
|
-
// src/cli/lib/
|
|
1234
|
-
import {
|
|
1235
|
-
import {
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
935
|
+
// src/cli/lib/clerk-cli.ts
|
|
936
|
+
import { join as join4 } from "path";
|
|
937
|
+
import { cp, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
938
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
939
|
+
var SNAPSHOT_PATHS = ["src", "package.json", "next.config.mjs", "middleware.ts"];
|
|
940
|
+
function clerkCliDeps() {
|
|
941
|
+
return {
|
|
942
|
+
which,
|
|
943
|
+
run: async (cmd, args, opts) => {
|
|
944
|
+
const r = await run(cmd, args, opts);
|
|
945
|
+
return { ok: r.ok, stdout: r.stdout, stderr: r.stderr };
|
|
946
|
+
},
|
|
947
|
+
readFileIfExists: async (path) => {
|
|
948
|
+
if (!await exists(path)) return void 0;
|
|
949
|
+
try {
|
|
950
|
+
return await readFile(path, "utf8");
|
|
951
|
+
} catch {
|
|
952
|
+
return void 0;
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
snapshot: async (storefront) => {
|
|
956
|
+
const backup = await mkdtemp2(join4(tmpdir2(), "carrier-clerk-snap-"));
|
|
957
|
+
const saved = [];
|
|
958
|
+
for (const rel of SNAPSHOT_PATHS) {
|
|
959
|
+
const src = join4(storefront, rel);
|
|
960
|
+
if (!await exists(src)) continue;
|
|
961
|
+
await cp(src, join4(backup, rel), { recursive: true });
|
|
962
|
+
saved.push(rel);
|
|
963
|
+
}
|
|
964
|
+
return async () => {
|
|
965
|
+
try {
|
|
966
|
+
for (const rel of saved) {
|
|
967
|
+
const target = join4(storefront, rel);
|
|
968
|
+
await rm2(target, { recursive: true, force: true });
|
|
969
|
+
await cp(join4(backup, rel), target, { recursive: true });
|
|
970
|
+
}
|
|
971
|
+
} finally {
|
|
972
|
+
await rm2(backup, { recursive: true, force: true });
|
|
973
|
+
}
|
|
974
|
+
};
|
|
1263
975
|
}
|
|
1264
|
-
|
|
1265
|
-
}
|
|
1266
|
-
return out;
|
|
976
|
+
};
|
|
1267
977
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
978
|
+
|
|
979
|
+
// src/cli/lib/verify-storefront.ts
|
|
980
|
+
var PROBES = [
|
|
981
|
+
{ path: "/" },
|
|
982
|
+
{ path: "/shop", expectBody: /·/ },
|
|
983
|
+
// plan names render as "Visit · Calm"
|
|
984
|
+
{ path: "/help" }
|
|
985
|
+
];
|
|
986
|
+
async function probe(base, path, expectBody) {
|
|
987
|
+
const url = `${base.replace(/\/$/, "")}${path}${path.includes("?") ? "&" : "?"}_v=${Date.now()}`;
|
|
1270
988
|
try {
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
989
|
+
const res = await fetch(url, { redirect: "manual" });
|
|
990
|
+
const status = res.status;
|
|
991
|
+
const ok = status < 400;
|
|
992
|
+
if (!expectBody || !ok) return { path, status, ok };
|
|
993
|
+
const body = await res.text();
|
|
994
|
+
return { path, status, ok, bodyOk: expectBody.test(body) };
|
|
995
|
+
} catch (e) {
|
|
996
|
+
return { path, status: 0, ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
1274
997
|
}
|
|
1275
998
|
}
|
|
1276
|
-
|
|
1277
|
-
const
|
|
1278
|
-
const
|
|
1279
|
-
const
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
999
|
+
function diagnose(probes, secrets) {
|
|
1000
|
+
const by = (p) => probes.find((x) => x.path === p);
|
|
1001
|
+
const root = by("/");
|
|
1002
|
+
const shop = by("/shop");
|
|
1003
|
+
const help = by("/help");
|
|
1004
|
+
if (probes.every((p) => p.ok && p.bodyOk !== false)) return "healthy";
|
|
1005
|
+
if (probes.every((p) => p.status === 0)) return "not-deployed";
|
|
1006
|
+
const catalogDown = root?.status === 500 && shop?.status === 500;
|
|
1007
|
+
if (catalogDown && help?.ok) {
|
|
1008
|
+
return needsCarrierKey(secrets) ? "carrier-key-missing" : "unclassified";
|
|
1284
1009
|
}
|
|
1285
|
-
return
|
|
1010
|
+
if (shop?.ok && shop.bodyOk === false) return "empty-catalog";
|
|
1011
|
+
if (root?.ok && shop?.ok && probes.some((p) => p.status >= 500)) return "clerk-keys-missing";
|
|
1012
|
+
if (probes.every((p) => p.status === 404)) return "not-deployed";
|
|
1013
|
+
return "unclassified";
|
|
1286
1014
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1015
|
+
var SUMMARIES = {
|
|
1016
|
+
healthy: "All probes returned a working page.",
|
|
1017
|
+
"carrier-key-missing": "Catalog pages return 500 while non-catalog pages work \u2014 CARRIER_API_KEY is missing or invalid on the host.",
|
|
1018
|
+
"clerk-keys-missing": "Public pages work but auth routes error \u2014 Clerk keys are missing on the host.",
|
|
1019
|
+
"empty-catalog": "/shop renders but no plans are in it \u2014 the Carrier catalog returned nothing sellable. Not a deploy problem.",
|
|
1020
|
+
"not-deployed": "Nothing served at the deployed URL \u2014 the deploy did not land, or the URL is wrong.",
|
|
1021
|
+
unclassified: "The storefront is not serving correctly and the symptom matches no known cause."
|
|
1022
|
+
};
|
|
1023
|
+
var REPAIRABLE = /* @__PURE__ */ new Set([
|
|
1024
|
+
"carrier-key-missing",
|
|
1025
|
+
"clerk-keys-missing"
|
|
1026
|
+
]);
|
|
1027
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1028
|
+
async function verifyStorefront(url, storefront, env = process.env, opts = {}) {
|
|
1029
|
+
const attempts = Math.max(1, opts.attempts ?? 4);
|
|
1030
|
+
const delayMs = opts.delayMs ?? 5e3;
|
|
1031
|
+
const secrets = await discoverSecrets(storefront, env);
|
|
1032
|
+
let probes = [];
|
|
1033
|
+
let diagnosis = "unclassified";
|
|
1034
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
1035
|
+
probes = [];
|
|
1036
|
+
for (const { path, expectBody } of PROBES) {
|
|
1037
|
+
probes.push(await probe(url, path, expectBody));
|
|
1300
1038
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1039
|
+
diagnosis = diagnose(probes, secrets);
|
|
1040
|
+
if (diagnosis !== "not-deployed" || attempt === attempts) break;
|
|
1041
|
+
await sleep(delayMs);
|
|
1042
|
+
}
|
|
1043
|
+
const ok = diagnosis === "healthy";
|
|
1044
|
+
return {
|
|
1045
|
+
ok,
|
|
1046
|
+
url,
|
|
1047
|
+
probes,
|
|
1048
|
+
diagnosis,
|
|
1049
|
+
summary: SUMMARIES[diagnosis],
|
|
1050
|
+
repairable: !ok && REPAIRABLE.has(diagnosis)
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
function formatProbes(probes) {
|
|
1054
|
+
return probes.map((p) => {
|
|
1055
|
+
const status = p.status === 0 ? "unreachable" : String(p.status);
|
|
1056
|
+
const body = p.bodyOk === false ? " (no plans)" : "";
|
|
1057
|
+
return `${p.path} ${status}${body}`;
|
|
1058
|
+
}).join(" ");
|
|
1059
|
+
}
|
|
1060
|
+
function repairPlanFor(diagnosis) {
|
|
1061
|
+
if (diagnosis === "carrier-key-missing") {
|
|
1062
|
+
return {
|
|
1063
|
+
diagnosis,
|
|
1064
|
+
runClerk: false,
|
|
1065
|
+
// CARRIER_API_KEY is read at request time, so re-staging and redeploying
|
|
1066
|
+
// is enough; no rebuild needed.
|
|
1067
|
+
rebuild: false,
|
|
1068
|
+
note: "Re-resolve CARRIER_API_KEY, stage it, and redeploy."
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
if (diagnosis === "clerk-keys-missing") {
|
|
1072
|
+
return {
|
|
1073
|
+
diagnosis,
|
|
1074
|
+
runClerk: true,
|
|
1075
|
+
// The publishable key is inlined at build time, so this one must rebuild.
|
|
1076
|
+
rebuild: true,
|
|
1077
|
+
note: "Provision Clerk, rebuild so the publishable key is inlined, and redeploy."
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
return void 0;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// ../../packages/screens/dist/index.js
|
|
1084
|
+
function isBlankValue(value) {
|
|
1085
|
+
const text = String(value).trim();
|
|
1086
|
+
return text === "" || text === "\u2014" || text === "-" || Number(text) === 0;
|
|
1087
|
+
}
|
|
1088
|
+
function isEmptyScreen(screen) {
|
|
1089
|
+
return screen.sections.every((section) => {
|
|
1090
|
+
switch (section.kind) {
|
|
1091
|
+
case "metrics":
|
|
1092
|
+
return section.items.every((m) => isBlankValue(m.value));
|
|
1093
|
+
case "bars":
|
|
1094
|
+
return section.items.every((b) => b.value === 0);
|
|
1095
|
+
case "keyvalue":
|
|
1096
|
+
return section.items.every((kv) => isBlankValue(kv.value));
|
|
1097
|
+
case "table":
|
|
1098
|
+
return section.rows.length === 0;
|
|
1099
|
+
case "note":
|
|
1100
|
+
return false;
|
|
1306
1101
|
}
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
var colors = {
|
|
1105
|
+
/** Base page background — deepest ink */
|
|
1106
|
+
backgroundDark: "#080C16",
|
|
1107
|
+
backgroundDarkOklch: "oklch(0.09 0.02 260)",
|
|
1108
|
+
/** Surface elevation ladder — alpha blends, no drop-shadows */
|
|
1109
|
+
surface0: "#080C16",
|
|
1110
|
+
surface1: "rgba(15, 20, 34, 0.80)",
|
|
1111
|
+
surface2: "rgba(22, 28, 46, 0.90)",
|
|
1112
|
+
surface3: "rgba(31, 38, 56, 0.95)",
|
|
1113
|
+
/** Legacy alias — kept for backward compat */
|
|
1114
|
+
surfaceDark: "#0F1422",
|
|
1115
|
+
surfaceCard: "rgba(15, 20, 34, 0.75)",
|
|
1116
|
+
/** Borders */
|
|
1117
|
+
borderCard: "#1F2638",
|
|
1118
|
+
borderMuted: "rgba(255, 255, 255, 0.07)",
|
|
1119
|
+
borderSubtle: "rgba(255, 255, 255, 0.04)",
|
|
1120
|
+
/** Brand accent — Carrier Flame orange (do not replace with violet) */
|
|
1121
|
+
accentFlame: "#FF6B35",
|
|
1122
|
+
accentEmber: "#D9461C",
|
|
1123
|
+
accentSpark: "#FFB088",
|
|
1124
|
+
/** Legacy violet/fuchsia — used on esimmcp co-brand surface only */
|
|
1125
|
+
accentViolet: "#a78bfa",
|
|
1126
|
+
accentFuchsia: "#e879f9",
|
|
1127
|
+
/** Text hierarchy — tight ratio, generous contrast */
|
|
1128
|
+
textPrimary: "#F5F1EA",
|
|
1129
|
+
textSecondary: "#C9CCD6",
|
|
1130
|
+
textMuted: "#8A92A8",
|
|
1131
|
+
textFaint: "#5A6278",
|
|
1132
|
+
/** Status — do not deviate */
|
|
1133
|
+
statusSuccess: "#10b981",
|
|
1134
|
+
statusWarning: "#f59e0b",
|
|
1135
|
+
statusError: "#ef4444",
|
|
1136
|
+
/** Light mode equivalents */
|
|
1137
|
+
light: {
|
|
1138
|
+
background: "#ffffff",
|
|
1139
|
+
backgroundSecondary: "#f8fafc",
|
|
1140
|
+
surfaceCard: "rgba(248, 250, 252, 0.9)",
|
|
1141
|
+
borderCard: "#e2e8f0",
|
|
1142
|
+
textPrimary: "#0f172a",
|
|
1143
|
+
textSecondary: "#475569",
|
|
1144
|
+
textMuted: "#94a3b8"
|
|
1307
1145
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1146
|
+
};
|
|
1147
|
+
var typography = {
|
|
1148
|
+
fontSans: "var(--font-sans, Inter), system-ui, -apple-system, sans-serif",
|
|
1149
|
+
fontMono: "var(--font-mono, 'JetBrains Mono'), ui-monospace, monospace",
|
|
1150
|
+
fontSerif: "var(--font-serif, Georgia), 'Times New Roman', serif",
|
|
1151
|
+
/** Weights — only these three, per brand guide */
|
|
1152
|
+
weightRegular: "400",
|
|
1153
|
+
weightBold: "700",
|
|
1154
|
+
weightBlack: "900",
|
|
1155
|
+
/** Line heights */
|
|
1156
|
+
lineHeightHeadline: "1.05",
|
|
1157
|
+
lineHeightSubheading: "1.2",
|
|
1158
|
+
lineHeightBody: "1.5",
|
|
1159
|
+
/** Letter spacing — tight on big headlines */
|
|
1160
|
+
trackingTight: "-0.04em",
|
|
1161
|
+
trackingNormal: "0em",
|
|
1162
|
+
trackingWide: "0.05em",
|
|
1163
|
+
/** Type scale (rem) */
|
|
1164
|
+
scale: {
|
|
1165
|
+
xs: "0.75rem",
|
|
1166
|
+
sm: "0.875rem",
|
|
1167
|
+
base: "1rem",
|
|
1168
|
+
lg: "1.125rem",
|
|
1169
|
+
xl: "1.25rem",
|
|
1170
|
+
"2xl": "1.5rem",
|
|
1171
|
+
"3xl": "1.875rem",
|
|
1172
|
+
"4xl": "2.25rem",
|
|
1173
|
+
"5xl": "3rem",
|
|
1174
|
+
"6xl": "3.75rem",
|
|
1175
|
+
"7xl": "4.5rem"
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
var radius = {
|
|
1179
|
+
sm: "0.375rem",
|
|
1180
|
+
md: "0.5rem",
|
|
1181
|
+
lg: "0.75rem",
|
|
1182
|
+
xl: "1rem",
|
|
1183
|
+
"2xl": "1.5rem",
|
|
1184
|
+
full: "9999px"
|
|
1185
|
+
};
|
|
1186
|
+
var TONE_COLOR = {
|
|
1187
|
+
ok: colors.statusSuccess,
|
|
1188
|
+
info: colors.accentFlame,
|
|
1189
|
+
warn: colors.statusWarning,
|
|
1190
|
+
critical: colors.statusError,
|
|
1191
|
+
muted: colors.textMuted
|
|
1192
|
+
};
|
|
1193
|
+
function esc(value) {
|
|
1194
|
+
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1313
1195
|
}
|
|
1314
|
-
function
|
|
1315
|
-
|
|
1316
|
-
return Boolean(url) && !secrets.CARRIER_API_KEY?.trim();
|
|
1196
|
+
function toneColor(tone, fallback = colors.textPrimary) {
|
|
1197
|
+
return tone ? TONE_COLOR[tone] : fallback;
|
|
1317
1198
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1199
|
+
function metricsHtml(items) {
|
|
1200
|
+
const cards = items.map(
|
|
1201
|
+
(m) => `
|
|
1202
|
+
<div style="background:${colors.surface1};border:1px solid ${colors.borderCard};border-radius:${radius.lg};padding:16px 18px;min-width:0">
|
|
1203
|
+
<div style="font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:${colors.textMuted}">${esc(m.label)}</div>
|
|
1204
|
+
<div style="font-size:28px;font-weight:600;margin-top:6px;color:${toneColor(m.tone)};line-height:1.1">${esc(m.value)}</div>
|
|
1205
|
+
${m.hint ? `<div style="font-size:12px;color:${colors.textFaint};margin-top:4px">${esc(m.hint)}</div>` : ""}
|
|
1206
|
+
</div>`
|
|
1207
|
+
).join("");
|
|
1208
|
+
return `<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px">${cards}</div>`;
|
|
1323
1209
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
const
|
|
1327
|
-
|
|
1210
|
+
function barsHtml(items, empty) {
|
|
1211
|
+
if (items.length === 0) return emptyHtml(empty ?? "Nothing to show.");
|
|
1212
|
+
const ceiling = Math.max(...items.map((b) => b.max ?? b.value), 1);
|
|
1213
|
+
const rows = items.map((b) => {
|
|
1214
|
+
const pct2 = Math.max(0, Math.min(100, (b.value / (b.max ?? ceiling) || 0) * 100));
|
|
1215
|
+
return `
|
|
1216
|
+
<div style="margin-bottom:10px">
|
|
1217
|
+
<div style="display:flex;justify-content:space-between;font-size:13px;color:${colors.textSecondary};margin-bottom:4px">
|
|
1218
|
+
<span>${esc(b.label)}</span>
|
|
1219
|
+
<span style="color:${colors.textMuted}">${esc(b.hint ?? b.value)}</span>
|
|
1220
|
+
</div>
|
|
1221
|
+
<div style="height:8px;background:${colors.surface2};border-radius:${radius.full};overflow:hidden">
|
|
1222
|
+
<div style="height:100%;width:${pct2.toFixed(1)}%;background:${toneColor(b.tone, colors.accentFlame)}"></div>
|
|
1223
|
+
</div>
|
|
1224
|
+
</div>`;
|
|
1225
|
+
}).join("");
|
|
1226
|
+
return `<div>${rows}</div>`;
|
|
1328
1227
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1228
|
+
function tableHtml(section) {
|
|
1229
|
+
if (section.rows.length === 0) return emptyHtml(section.empty ?? "No rows.");
|
|
1230
|
+
const numeric = new Set(section.numeric ?? []);
|
|
1231
|
+
const head = section.columns.map(
|
|
1232
|
+
(c, i) => `<th style="text-align:${numeric.has(i) ? "right" : "left"};padding:8px 12px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:${colors.textMuted};border-bottom:1px solid ${colors.borderCard};white-space:nowrap">${esc(c)}</th>`
|
|
1233
|
+
).join("");
|
|
1234
|
+
const body = section.rows.map(
|
|
1235
|
+
(row) => `<tr>${row.map(
|
|
1236
|
+
(cell, i) => `<td style="text-align:${numeric.has(i) ? "right" : "left"};padding:8px 12px;font-size:13px;color:${colors.textSecondary};border-bottom:1px solid ${colors.borderSubtle};white-space:nowrap">${esc(cell)}</td>`
|
|
1237
|
+
).join("")}</tr>`
|
|
1238
|
+
).join("");
|
|
1239
|
+
return `<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse">${`<thead><tr>${head}</tr></thead>`}<tbody>${body}</tbody></table></div>`;
|
|
1333
1240
|
}
|
|
1334
|
-
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
};
|
|
1344
|
-
return {
|
|
1345
|
-
...CARRIER_BRAND,
|
|
1346
|
-
name: overrides?.name ?? pick("name", CARRIER_BRAND.name),
|
|
1347
|
-
domain: pick("domain", CARRIER_BRAND.domain),
|
|
1348
|
-
supportEmail: pick("supportEmail", CARRIER_BRAND.supportEmail),
|
|
1349
|
-
supportUrl: pick("supportUrl", CARRIER_BRAND.supportUrl),
|
|
1350
|
-
tagline: pick("tagline", CARRIER_BRAND.tagline),
|
|
1351
|
-
legalName: pick("legalName", CARRIER_BRAND.legalName),
|
|
1352
|
-
colors: {
|
|
1353
|
-
...CARRIER_BRAND.colors,
|
|
1354
|
-
accent: pick("accent", CARRIER_BRAND.colors.accent),
|
|
1355
|
-
accentDark: pick("accentDark", CARRIER_BRAND.colors.accentDark),
|
|
1356
|
-
bg: pick("bg", CARRIER_BRAND.colors.bg),
|
|
1357
|
-
text: pick("text", CARRIER_BRAND.colors.text)
|
|
1358
|
-
},
|
|
1359
|
-
carrierApiUrl: CARRIER_BRAND.carrierApiUrl
|
|
1360
|
-
};
|
|
1241
|
+
function keyValueHtml(items) {
|
|
1242
|
+
const rows = items.map(
|
|
1243
|
+
(kv) => `
|
|
1244
|
+
<div style="display:flex;justify-content:space-between;gap:16px;padding:7px 0;border-bottom:1px solid ${colors.borderSubtle}">
|
|
1245
|
+
<span style="font-size:13px;color:${colors.textMuted}">${esc(kv.label)}</span>
|
|
1246
|
+
<span style="font-size:13px;color:${toneColor(kv.tone, colors.textPrimary)};text-align:right">${esc(kv.value)}</span>
|
|
1247
|
+
</div>`
|
|
1248
|
+
).join("");
|
|
1249
|
+
return `<div>${rows}</div>`;
|
|
1361
1250
|
}
|
|
1362
|
-
|
|
1363
|
-
const
|
|
1364
|
-
|
|
1365
|
-
if (opts.preferred) {
|
|
1366
|
-
const wanted = statuses.find((s) => s.id === opts.preferred);
|
|
1367
|
-
if (!wanted?.ready) {
|
|
1368
|
-
return {
|
|
1369
|
-
ok: false,
|
|
1370
|
-
projectName: slug(brand.name),
|
|
1371
|
-
statuses,
|
|
1372
|
-
reason: wanted?.reason ?? `${opts.preferred} is not available on this machine.`
|
|
1373
|
-
};
|
|
1374
|
-
}
|
|
1375
|
-
chosen = wanted;
|
|
1376
|
-
} else {
|
|
1377
|
-
chosen = rankTargets(statuses)[0];
|
|
1378
|
-
}
|
|
1379
|
-
if (!chosen) {
|
|
1380
|
-
return {
|
|
1381
|
-
ok: false,
|
|
1382
|
-
projectName: slug(brand.name),
|
|
1383
|
-
statuses,
|
|
1384
|
-
reason: "No deploy target is ready. " + statuses.map((s) => `${s.label}: ${s.reason ?? "unavailable"}`).join(" | ")
|
|
1385
|
-
};
|
|
1386
|
-
}
|
|
1387
|
-
const projectName = await configuredProjectName(chosen.id, target) ?? slug(brand.name);
|
|
1388
|
-
if (opts.customDomain && chosen.id === "cloudflare" && brand.domain) {
|
|
1389
|
-
await setCustomDomain(target, brand.domain);
|
|
1390
|
-
}
|
|
1391
|
-
let staged = { staged: [], failed: [], cleanup: async () => {
|
|
1392
|
-
} };
|
|
1393
|
-
if (opts.pushSecrets !== false) {
|
|
1394
|
-
const discovered = await discoverSecrets(target, opts.env ?? process.env);
|
|
1395
|
-
const runtime = {};
|
|
1396
|
-
for (const key of RUNTIME_SECRET_KEYS) {
|
|
1397
|
-
const value = discovered[key];
|
|
1398
|
-
if (value) runtime[key] = value;
|
|
1399
|
-
}
|
|
1400
|
-
staged = await stageSecrets(chosen.id, target, projectName, runtime);
|
|
1401
|
-
}
|
|
1402
|
-
try {
|
|
1403
|
-
const outcome = await deployTo(chosen.id, target, projectName, {
|
|
1404
|
-
secretsFile: staged.secretsFile
|
|
1405
|
-
});
|
|
1406
|
-
if (!outcome.ok) {
|
|
1407
|
-
return { ok: false, projectName, statuses, target: chosen.id, reason: outcome.reason };
|
|
1408
|
-
}
|
|
1409
|
-
return {
|
|
1410
|
-
ok: true,
|
|
1411
|
-
projectName,
|
|
1412
|
-
statuses,
|
|
1413
|
-
target: chosen.id,
|
|
1414
|
-
url: outcome.url,
|
|
1415
|
-
secrets: { pushed: staged.staged, failed: staged.failed }
|
|
1416
|
-
};
|
|
1417
|
-
} finally {
|
|
1418
|
-
await staged.cleanup();
|
|
1419
|
-
}
|
|
1251
|
+
function noteHtml(tone, text) {
|
|
1252
|
+
const c = toneColor(tone, colors.accentFlame);
|
|
1253
|
+
return `<div style="border-left:3px solid ${c};background:${colors.surface1};padding:10px 14px;border-radius:${radius.md};font-size:13px;color:${colors.textSecondary}">${esc(text)}</div>`;
|
|
1420
1254
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
"
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1255
|
+
function emptyHtml(text) {
|
|
1256
|
+
return `<div style="padding:14px;border:1px dashed ${colors.borderCard};border-radius:${radius.md};font-size:13px;color:${colors.textMuted}">${esc(text)}</div>`;
|
|
1257
|
+
}
|
|
1258
|
+
function sectionHtml(section) {
|
|
1259
|
+
const title = "title" in section && section.title ? `<h2 style="font-size:13px;font-weight:600;letter-spacing:.04em;color:${colors.textPrimary};margin:0 0 10px">${esc(section.title)}</h2>` : "";
|
|
1260
|
+
let body;
|
|
1261
|
+
switch (section.kind) {
|
|
1262
|
+
case "metrics":
|
|
1263
|
+
body = metricsHtml(section.items);
|
|
1264
|
+
break;
|
|
1265
|
+
case "bars":
|
|
1266
|
+
body = barsHtml(section.items, section.empty);
|
|
1267
|
+
break;
|
|
1268
|
+
case "table":
|
|
1269
|
+
body = tableHtml(section);
|
|
1270
|
+
break;
|
|
1271
|
+
case "keyvalue":
|
|
1272
|
+
body = keyValueHtml(section.items);
|
|
1273
|
+
break;
|
|
1274
|
+
case "note":
|
|
1275
|
+
body = noteHtml(section.tone, section.text);
|
|
1276
|
+
break;
|
|
1433
1277
|
}
|
|
1434
|
-
return
|
|
1278
|
+
return `<section style="margin-bottom:22px">${title}${body}</section>`;
|
|
1435
1279
|
}
|
|
1436
|
-
function
|
|
1437
|
-
|
|
1280
|
+
function renderHtml(screen) {
|
|
1281
|
+
const sections = isEmptyScreen(screen) ? emptyHtml(
|
|
1282
|
+
"No data came back for this view. That is a real result, not a loading state \u2014 check the account scope and credentials."
|
|
1283
|
+
) : screen.sections.map(sectionHtml).join("");
|
|
1284
|
+
const actions = screen.actions?.length ? `<section style="margin-top:6px;display:flex;flex-wrap:wrap;gap:8px">${screen.actions.map(
|
|
1285
|
+
(a) => `<span title="${esc(a.description ?? a.command)}" style="font-size:12px;color:${colors.textSecondary};background:${colors.surface2};border:1px solid ${colors.borderCard};border-radius:${radius.full};padding:6px 12px">${esc(a.label)} <code style="color:${colors.textFaint}">${esc(a.command)}</code></span>`
|
|
1286
|
+
).join("")}</section>` : "";
|
|
1287
|
+
return `<!doctype html>
|
|
1288
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
1289
|
+
<title>${esc(screen.title)}</title></head>
|
|
1290
|
+
<body style="margin:0;background:${colors.surface0};color:${colors.textPrimary};font-family:${typography.fontSans};padding:20px">
|
|
1291
|
+
<header style="margin-bottom:20px">
|
|
1292
|
+
<h1 style="font-size:18px;font-weight:600;margin:0;letter-spacing:-.01em">${esc(screen.title)}</h1>
|
|
1293
|
+
${screen.subtitle ? `<p style="margin:4px 0 0;font-size:13px;color:${colors.textMuted}">${esc(screen.subtitle)}</p>` : ""}
|
|
1294
|
+
</header>
|
|
1295
|
+
${sections}
|
|
1296
|
+
${actions}
|
|
1297
|
+
${screen.footer ? `<footer style="margin-top:18px;font-size:11px;color:${colors.textFaint}">${esc(screen.footer)}</footer>` : ""}
|
|
1298
|
+
</body></html>`;
|
|
1438
1299
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1300
|
+
var ANSI = {
|
|
1301
|
+
ok: "\x1B[32m",
|
|
1302
|
+
info: "\x1B[38;5;209m",
|
|
1303
|
+
// Carrier flame, nearest 256-colour
|
|
1304
|
+
warn: "\x1B[33m",
|
|
1305
|
+
critical: "\x1B[31m",
|
|
1306
|
+
muted: "\x1B[90m",
|
|
1307
|
+
reset: "\x1B[0m",
|
|
1308
|
+
bold: "\x1B[1m",
|
|
1309
|
+
dim: "\x1B[2m"
|
|
1310
|
+
};
|
|
1311
|
+
function paint(text, code, color) {
|
|
1312
|
+
return color ? `${code}${text}${ANSI.reset}` : text;
|
|
1441
1313
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
"Content-Type": "application/json"
|
|
1449
|
-
},
|
|
1450
|
-
body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
|
|
1451
|
-
});
|
|
1452
|
-
const text = await res.text();
|
|
1453
|
-
let json;
|
|
1454
|
-
try {
|
|
1455
|
-
json = text ? JSON.parse(text) : void 0;
|
|
1456
|
-
} catch {
|
|
1457
|
-
json = void 0;
|
|
1458
|
-
}
|
|
1459
|
-
if (!res.ok) {
|
|
1460
|
-
return { ok: false, status: res.status, json, error: clerkError(json) ?? text.slice(0, 300) };
|
|
1461
|
-
}
|
|
1462
|
-
return { ok: true, status: res.status, json };
|
|
1463
|
-
} catch (e) {
|
|
1464
|
-
return { ok: false, status: 0, error: e instanceof Error ? e.message : String(e) };
|
|
1465
|
-
}
|
|
1314
|
+
function displayWidth(text) {
|
|
1315
|
+
return text.replace(/\[[0-9;]*m/g, "").length;
|
|
1316
|
+
}
|
|
1317
|
+
function pad(text, width, align) {
|
|
1318
|
+
const gap = Math.max(0, width - displayWidth(text));
|
|
1319
|
+
return align === "right" ? " ".repeat(gap) + text : text + " ".repeat(gap);
|
|
1466
1320
|
}
|
|
1467
|
-
function
|
|
1468
|
-
|
|
1469
|
-
const errors = json.errors;
|
|
1470
|
-
if (!Array.isArray(errors) || errors.length === 0) return void 0;
|
|
1471
|
-
const first = errors[0];
|
|
1472
|
-
return first.long_message ?? first.message;
|
|
1321
|
+
function heading(text, color) {
|
|
1322
|
+
return paint(text.toUpperCase(), ANSI.bold, color);
|
|
1473
1323
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1324
|
+
function metricsTui(items, width, color) {
|
|
1325
|
+
if (items.length === 0) return [];
|
|
1326
|
+
const labelWidth = Math.min(
|
|
1327
|
+
28,
|
|
1328
|
+
Math.max(...items.map((m) => m.label.length))
|
|
1329
|
+
);
|
|
1330
|
+
return items.map((m) => {
|
|
1331
|
+
const label = paint(pad(m.label, labelWidth, "left"), ANSI.muted, color);
|
|
1332
|
+
const value = paint(String(m.value), m.tone ? ANSI[m.tone] : ANSI.bold, color);
|
|
1333
|
+
const hint = m.hint ? paint(` ${m.hint}`, ANSI.dim, color) : "";
|
|
1334
|
+
return ` ${label} ${value}${hint}`.slice(0, width + 64);
|
|
1483
1335
|
});
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1336
|
+
}
|
|
1337
|
+
function barsTui(items, width, color, empty) {
|
|
1338
|
+
if (items.length === 0) return [` ${paint(empty ?? "Nothing to show.", ANSI.muted, color)}`];
|
|
1339
|
+
const ceiling = Math.max(...items.map((b) => b.max ?? b.value), 1);
|
|
1340
|
+
const labelWidth = Math.min(24, Math.max(...items.map((b) => b.label.length)));
|
|
1341
|
+
const barWidth = Math.max(10, Math.min(40, width - labelWidth - 22));
|
|
1342
|
+
return items.map((b) => {
|
|
1343
|
+
const ratio = b.value / (b.max ?? ceiling) || 0;
|
|
1344
|
+
const filled = Math.max(0, Math.min(barWidth, Math.round(ratio * barWidth)));
|
|
1345
|
+
const bar = paint("\u2588".repeat(filled), b.tone ? ANSI[b.tone] : ANSI.info, color) + paint("\u2591".repeat(barWidth - filled), ANSI.dim, color);
|
|
1346
|
+
const label = paint(pad(b.label, labelWidth, "left"), ANSI.muted, color);
|
|
1347
|
+
const value = paint(String(b.hint ?? b.value), ANSI.dim, color);
|
|
1348
|
+
return ` ${label} ${bar} ${value}`;
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
function tableTui(section, width, color) {
|
|
1352
|
+
if (section.rows.length === 0) {
|
|
1353
|
+
return [` ${paint(section.empty ?? "No rows.", ANSI.muted, color)}`];
|
|
1490
1354
|
}
|
|
1491
|
-
const
|
|
1492
|
-
const
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1355
|
+
const numeric = new Set(section.numeric ?? []);
|
|
1356
|
+
const cols = section.columns.length;
|
|
1357
|
+
const widths = Array.from(
|
|
1358
|
+
{ length: cols },
|
|
1359
|
+
(_, i) => Math.max(
|
|
1360
|
+
section.columns[i]?.length ?? 0,
|
|
1361
|
+
...section.rows.map((r) => String(r[i] ?? "").length)
|
|
1362
|
+
)
|
|
1363
|
+
);
|
|
1364
|
+
let total = widths.reduce((a, b) => a + b + 2, 2);
|
|
1365
|
+
while (total > width && Math.max(...widths) > 8) {
|
|
1366
|
+
const widest = widths.indexOf(Math.max(...widths));
|
|
1367
|
+
widths[widest] -= 1;
|
|
1368
|
+
total -= 1;
|
|
1501
1369
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
credentials: {
|
|
1506
|
-
publishableKey: instance.publishable_key,
|
|
1507
|
-
secretKey: instance.secret_key,
|
|
1508
|
-
tier: "platform",
|
|
1509
|
-
applicationId: payload?.application_id,
|
|
1510
|
-
instanceId: instance.instance_id
|
|
1511
|
-
}
|
|
1370
|
+
const clip = (cell, i) => {
|
|
1371
|
+
const text = String(cell ?? "");
|
|
1372
|
+
return text.length > widths[i] ? `${text.slice(0, Math.max(1, widths[i] - 1))}\u2026` : text;
|
|
1512
1373
|
};
|
|
1374
|
+
const header = " " + section.columns.map((c, i) => paint(pad(clip(c, i), widths[i], numeric.has(i) ? "right" : "left"), ANSI.muted, color)).join(" ");
|
|
1375
|
+
const rule = " " + paint(widths.map((w) => "\u2500".repeat(w)).join(" "), ANSI.dim, color);
|
|
1376
|
+
const body = section.rows.map(
|
|
1377
|
+
(row) => " " + row.map((cell, i) => pad(clip(cell, i), widths[i], numeric.has(i) ? "right" : "left")).join(" ")
|
|
1378
|
+
);
|
|
1379
|
+
return [header, rule, ...body];
|
|
1513
1380
|
}
|
|
1514
|
-
|
|
1515
|
-
if (
|
|
1516
|
-
|
|
1381
|
+
function keyValueTui(items, color) {
|
|
1382
|
+
if (items.length === 0) return [];
|
|
1383
|
+
const labelWidth = Math.min(30, Math.max(...items.map((kv) => kv.label.length)));
|
|
1384
|
+
return items.map(
|
|
1385
|
+
(kv) => ` ${paint(pad(kv.label, labelWidth, "left"), ANSI.muted, color)} ${paint(kv.value, kv.tone ? ANSI[kv.tone] : ANSI.reset, color)}`
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
var NOTE_PREFIX = {
|
|
1389
|
+
ok: "ok",
|
|
1390
|
+
info: "note",
|
|
1391
|
+
warn: "warning",
|
|
1392
|
+
critical: "critical",
|
|
1393
|
+
muted: "note"
|
|
1394
|
+
};
|
|
1395
|
+
function sectionTui(section, width, color) {
|
|
1396
|
+
const lines = [];
|
|
1397
|
+
if ("title" in section && section.title) lines.push(heading(section.title, color));
|
|
1398
|
+
switch (section.kind) {
|
|
1399
|
+
case "metrics":
|
|
1400
|
+
lines.push(...metricsTui(section.items, width, color));
|
|
1401
|
+
break;
|
|
1402
|
+
case "bars":
|
|
1403
|
+
lines.push(...barsTui(section.items, width, color, section.empty));
|
|
1404
|
+
break;
|
|
1405
|
+
case "table":
|
|
1406
|
+
lines.push(...tableTui(section, width, color));
|
|
1407
|
+
break;
|
|
1408
|
+
case "keyvalue":
|
|
1409
|
+
lines.push(...keyValueTui(section.items, color));
|
|
1410
|
+
break;
|
|
1411
|
+
case "note":
|
|
1412
|
+
lines.push(
|
|
1413
|
+
` ${paint(`${NOTE_PREFIX[section.tone]}:`, ANSI[section.tone], color)} ${section.text}`
|
|
1414
|
+
);
|
|
1415
|
+
break;
|
|
1517
1416
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1417
|
+
lines.push("");
|
|
1418
|
+
return lines;
|
|
1419
|
+
}
|
|
1420
|
+
function renderTui(screen, opts = {}) {
|
|
1421
|
+
const width = Math.max(40, Math.min(160, opts.width ?? 80));
|
|
1422
|
+
const color = opts.color ?? true;
|
|
1423
|
+
const lines = [];
|
|
1424
|
+
lines.push(paint(screen.title, ANSI.bold, color));
|
|
1425
|
+
if (screen.subtitle) lines.push(paint(screen.subtitle, ANSI.muted, color));
|
|
1426
|
+
lines.push(paint("\u2500".repeat(width), ANSI.dim, color));
|
|
1427
|
+
lines.push("");
|
|
1428
|
+
if (isEmptyScreen(screen)) {
|
|
1429
|
+
lines.push(
|
|
1430
|
+
` ${paint("No data came back for this view.", ANSI.warn, color)} That is a real result, not a`,
|
|
1431
|
+
" loading state \u2014 check the account scope and credentials.",
|
|
1432
|
+
""
|
|
1524
1433
|
);
|
|
1525
|
-
|
|
1526
|
-
const
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
ok: false,
|
|
1533
|
-
tier: "cli-keyless",
|
|
1534
|
-
reason: `Clerk CLI did not produce keys${tail ? `: ${tail}` : "."}`
|
|
1535
|
-
};
|
|
1536
|
-
}
|
|
1537
|
-
if (!looksPublishable(publishableKey) || !looksSecret(secretKey)) {
|
|
1538
|
-
return { ok: false, tier: "cli-keyless", reason: "Clerk CLI wrote keys in an unexpected format." };
|
|
1434
|
+
} else {
|
|
1435
|
+
for (const section of screen.sections) lines.push(...sectionTui(section, width, color));
|
|
1436
|
+
}
|
|
1437
|
+
if (screen.actions?.length) {
|
|
1438
|
+
lines.push(heading("next", color));
|
|
1439
|
+
for (const a of screen.actions) {
|
|
1440
|
+
lines.push(` ${paint(a.command, ANSI.info, color)} ${paint(a.label, ANSI.dim, color)}`);
|
|
1539
1441
|
}
|
|
1540
|
-
|
|
1541
|
-
ok: true,
|
|
1542
|
-
tier: "cli-keyless",
|
|
1543
|
-
credentials: { publishableKey, secretKey, tier: "cli-keyless" }
|
|
1544
|
-
};
|
|
1545
|
-
} finally {
|
|
1546
|
-
await restore();
|
|
1442
|
+
lines.push("");
|
|
1547
1443
|
}
|
|
1444
|
+
if (screen.footer) lines.push(paint(screen.footer, ANSI.dim, color));
|
|
1445
|
+
return lines.join("\n");
|
|
1548
1446
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
const secretKey = firstNonEmpty(env, SECRET_KEYS);
|
|
1557
|
-
if (!publishableKey || !secretKey) return void 0;
|
|
1558
|
-
if (!looksPublishable(publishableKey) || !looksSecret(secretKey)) return void 0;
|
|
1559
|
-
return { publishableKey, secretKey, tier: "discovered" };
|
|
1447
|
+
var pct = (part, total) => total > 0 ? `${(part / total * 100).toFixed(1)}%` : "0%";
|
|
1448
|
+
var money = (value, currency = "") => `${currency}${value.toFixed(2)}`.trim();
|
|
1449
|
+
function humanBytes(bytes) {
|
|
1450
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 MB";
|
|
1451
|
+
const gb = bytes / 1073741824;
|
|
1452
|
+
if (gb >= 1) return `${gb.toFixed(gb >= 10 ? 0 : 1)} GB`;
|
|
1453
|
+
return `${(bytes / 1048576).toFixed(0)} MB`;
|
|
1560
1454
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1455
|
+
function fleetScreen(input) {
|
|
1456
|
+
const total = input.active + input.suspended + input.inventory + input.other;
|
|
1457
|
+
const lowBalance = input.accounts.filter((a) => a.balance < 10);
|
|
1458
|
+
const criticals = lowBalance.filter((a) => a.packageOnly && a.balance === 0);
|
|
1459
|
+
const sections = [
|
|
1460
|
+
{
|
|
1461
|
+
kind: "metrics",
|
|
1462
|
+
items: [
|
|
1463
|
+
{ label: "Total eSIMs", value: total },
|
|
1464
|
+
{ label: "Active", value: input.active, hint: pct(input.active, total), tone: "ok" },
|
|
1465
|
+
{ label: "Inventory", value: input.inventory, hint: pct(input.inventory, total) },
|
|
1466
|
+
{
|
|
1467
|
+
label: "Suspended",
|
|
1468
|
+
value: input.suspended,
|
|
1469
|
+
hint: pct(input.suspended, total),
|
|
1470
|
+
tone: input.suspended > input.active * 0.1 ? "warn" : void 0
|
|
1471
|
+
},
|
|
1472
|
+
{ label: "Accounts", value: input.accounts.length },
|
|
1473
|
+
{
|
|
1474
|
+
label: "Low balance",
|
|
1475
|
+
value: lowBalance.length,
|
|
1476
|
+
tone: lowBalance.length > 0 ? "warn" : "ok"
|
|
1477
|
+
}
|
|
1478
|
+
]
|
|
1479
|
+
}
|
|
1480
|
+
];
|
|
1481
|
+
const hasPerAccountCounts = input.accounts.some(
|
|
1482
|
+
(a) => a.active + a.suspended + a.inventory + a.other > 0
|
|
1483
|
+
);
|
|
1484
|
+
if (hasPerAccountCounts) {
|
|
1485
|
+
sections.push({
|
|
1486
|
+
kind: "bars",
|
|
1487
|
+
title: "eSIMs by account",
|
|
1488
|
+
items: [...input.accounts].sort((a, b) => b.active + b.inventory - (a.active + a.inventory)).slice(0, 10).map((a) => ({
|
|
1489
|
+
label: a.name,
|
|
1490
|
+
value: a.active + a.suspended + a.inventory + a.other,
|
|
1491
|
+
hint: `${a.active} active`,
|
|
1492
|
+
tone: a.active > 0 ? "ok" : "muted"
|
|
1493
|
+
}))
|
|
1578
1494
|
});
|
|
1579
|
-
if (created.ok) return created;
|
|
1580
|
-
notes.push(created.reason ?? "Clerk Platform API call failed.");
|
|
1581
|
-
}
|
|
1582
|
-
const discovered = discoverClerkCredentials(opts.env);
|
|
1583
|
-
if (discovered) {
|
|
1584
|
-
return {
|
|
1585
|
-
ok: true,
|
|
1586
|
-
tier: "discovered",
|
|
1587
|
-
credentials: discovered,
|
|
1588
|
-
reason: notes.length ? notes.join(" ") : void 0
|
|
1589
|
-
};
|
|
1590
1495
|
}
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1496
|
+
sections.push({
|
|
1497
|
+
kind: "table",
|
|
1498
|
+
title: "Accounts",
|
|
1499
|
+
columns: ["Account", "Balance", "Active", "Inventory"],
|
|
1500
|
+
numeric: [1, 2, 3],
|
|
1501
|
+
empty: "No accounts under this reseller.",
|
|
1502
|
+
rows: input.accounts.map((a) => [a.name, money(a.balance), a.active, a.inventory])
|
|
1503
|
+
});
|
|
1504
|
+
if (criticals.length > 0) {
|
|
1505
|
+
sections.push({
|
|
1506
|
+
kind: "note",
|
|
1507
|
+
tone: "critical",
|
|
1508
|
+
text: `${criticals.length} package-only account(s) at zero balance \u2014 packages cannot be assigned until topped up: ${criticals.map((a) => a.name).join(", ")}.`
|
|
1509
|
+
});
|
|
1597
1510
|
}
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
"No CLERK_PLATFORM_API_KEY set, so a new Clerk application cannot be created through the Platform API (it is a partner surface, not self-serve)."
|
|
1601
|
-
);
|
|
1511
|
+
for (const missing of input.unavailable ?? []) {
|
|
1512
|
+
sections.push({ kind: "note", tone: "warn", text: `Unavailable: ${missing}` });
|
|
1602
1513
|
}
|
|
1603
1514
|
return {
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1515
|
+
id: "fleet-health",
|
|
1516
|
+
title: "Fleet health",
|
|
1517
|
+
subtitle: `${total} eSIMs across ${input.accounts.length} account(s) \xB7 ${pct(input.active, total)} utilisation`,
|
|
1518
|
+
sections,
|
|
1519
|
+
actions: [
|
|
1520
|
+
{ label: "Top up an account", command: "wallet_topup_checkout" },
|
|
1521
|
+
{ label: "Per-account eSIM status", command: "esim_status_per_account" }
|
|
1522
|
+
]
|
|
1608
1523
|
};
|
|
1609
1524
|
}
|
|
1610
|
-
|
|
1611
|
-
const
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1525
|
+
function subscribersScreen(rows, opts = {}) {
|
|
1526
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
1527
|
+
for (const r of rows) byStatus.set(r.status, (byStatus.get(r.status) ?? 0) + 1);
|
|
1528
|
+
return {
|
|
1529
|
+
id: "subscribers",
|
|
1530
|
+
title: "Subscribers",
|
|
1531
|
+
subtitle: opts.account ? `Account ${opts.account} \xB7 ${rows.length} shown` : `${rows.length} shown`,
|
|
1532
|
+
sections: [
|
|
1533
|
+
{
|
|
1534
|
+
kind: "metrics",
|
|
1535
|
+
items: [
|
|
1536
|
+
{ label: "Listed", value: rows.length },
|
|
1537
|
+
...[...byStatus.entries()].map(([status, count]) => ({
|
|
1538
|
+
label: status,
|
|
1539
|
+
value: count,
|
|
1540
|
+
tone: status.toLowerCase() === "active" ? "ok" : void 0
|
|
1541
|
+
}))
|
|
1542
|
+
]
|
|
1543
|
+
},
|
|
1544
|
+
{
|
|
1545
|
+
kind: "table",
|
|
1546
|
+
title: "Records",
|
|
1547
|
+
columns: ["ICCID", "MSISDN", "Status", "Account", "Data used"],
|
|
1548
|
+
numeric: [4],
|
|
1549
|
+
empty: "No subscribers matched. Widen the filter or check the account scope.",
|
|
1550
|
+
rows: rows.map((r) => [
|
|
1551
|
+
r.iccid,
|
|
1552
|
+
r.msisdn ?? "\u2014",
|
|
1553
|
+
r.status,
|
|
1554
|
+
r.account ?? "\u2014",
|
|
1555
|
+
r.dataUsedBytes === void 0 ? "\u2014" : humanBytes(r.dataUsedBytes)
|
|
1556
|
+
])
|
|
1557
|
+
}
|
|
1558
|
+
],
|
|
1559
|
+
actions: [
|
|
1560
|
+
{ label: "Diagnose one", command: "diagnose_subscriber" },
|
|
1561
|
+
{ label: "Usage detail", command: "subscriber_usage" }
|
|
1562
|
+
]
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
function usageScreen(input) {
|
|
1566
|
+
const total = input.totalBytes ?? input.timeline.reduce((sum, p) => sum + p.bytes, 0);
|
|
1567
|
+
const peak = input.timeline.reduce(
|
|
1568
|
+
(best, p) => p.bytes > best.bytes ? p : best,
|
|
1569
|
+
{ date: "\u2014", bytes: 0 }
|
|
1570
|
+
);
|
|
1571
|
+
const sections = [
|
|
1572
|
+
{
|
|
1573
|
+
kind: "metrics",
|
|
1574
|
+
items: [
|
|
1575
|
+
{ label: "Total", value: humanBytes(total) },
|
|
1576
|
+
{ label: "Days", value: input.timeline.length },
|
|
1577
|
+
{ label: "Peak day", value: humanBytes(peak.bytes), hint: peak.date },
|
|
1578
|
+
{
|
|
1579
|
+
label: "Daily average",
|
|
1580
|
+
value: humanBytes(input.timeline.length ? total / input.timeline.length : 0)
|
|
1581
|
+
}
|
|
1582
|
+
]
|
|
1583
|
+
},
|
|
1584
|
+
{
|
|
1585
|
+
kind: "bars",
|
|
1586
|
+
title: "Daily usage",
|
|
1587
|
+
empty: "No usage recorded in this window.",
|
|
1588
|
+
items: input.timeline.map((p) => ({
|
|
1589
|
+
label: p.date,
|
|
1590
|
+
value: p.bytes,
|
|
1591
|
+
hint: humanBytes(p.bytes)
|
|
1592
|
+
}))
|
|
1593
|
+
}
|
|
1594
|
+
];
|
|
1595
|
+
if (input.countries?.length) {
|
|
1596
|
+
sections.push({
|
|
1597
|
+
kind: "bars",
|
|
1598
|
+
title: "By country",
|
|
1599
|
+
items: input.countries.slice(0, 10).map((c) => ({ label: c.country, value: c.bytes, hint: humanBytes(c.bytes) }))
|
|
1618
1600
|
});
|
|
1619
|
-
if (res.ok) applied.push(`allowed_origins (${origins.length})`);
|
|
1620
|
-
else failed.push({ step: "allowed_origins", reason: res.error ?? `HTTP ${res.status}` });
|
|
1621
|
-
}
|
|
1622
|
-
for (const url of dedupe(opts.redirectUrls ?? [])) {
|
|
1623
|
-
const res = await clerkFetch("/redirect_urls", secretKey, { method: "POST", body: { url } });
|
|
1624
|
-
if (res.ok) applied.push(`redirect_url ${url}`);
|
|
1625
|
-
else failed.push({ step: `redirect_url ${url}`, reason: res.error ?? `HTTP ${res.status}` });
|
|
1626
1601
|
}
|
|
1627
|
-
return {
|
|
1602
|
+
return {
|
|
1603
|
+
id: "usage",
|
|
1604
|
+
title: "Usage",
|
|
1605
|
+
subtitle: input.subject,
|
|
1606
|
+
sections,
|
|
1607
|
+
actions: [{ label: "Project depletion", command: "usage_projection" }]
|
|
1608
|
+
};
|
|
1628
1609
|
}
|
|
1629
|
-
function
|
|
1630
|
-
return
|
|
1610
|
+
function packagesScreen(rows) {
|
|
1611
|
+
return {
|
|
1612
|
+
id: "packages",
|
|
1613
|
+
title: "Package catalog",
|
|
1614
|
+
subtitle: `${rows.length} template(s)`,
|
|
1615
|
+
sections: [
|
|
1616
|
+
{
|
|
1617
|
+
kind: "metrics",
|
|
1618
|
+
items: [
|
|
1619
|
+
{ label: "Templates", value: rows.length },
|
|
1620
|
+
{ label: "Recurring", value: rows.filter((r) => r.recurring).length }
|
|
1621
|
+
]
|
|
1622
|
+
},
|
|
1623
|
+
{
|
|
1624
|
+
kind: "table",
|
|
1625
|
+
title: "Templates",
|
|
1626
|
+
columns: ["Name", "ID", "Data", "Validity", "Price"],
|
|
1627
|
+
numeric: [2, 3, 4],
|
|
1628
|
+
empty: "No package templates visible to this account.",
|
|
1629
|
+
rows: rows.map((r) => [
|
|
1630
|
+
r.name,
|
|
1631
|
+
String(r.id),
|
|
1632
|
+
r.dataLimitBytes === void 0 ? "\u2014" : humanBytes(r.dataLimitBytes),
|
|
1633
|
+
r.validityDays === void 0 ? "\u2014" : `${r.validityDays}d`,
|
|
1634
|
+
r.price === void 0 ? "\u2014" : money(r.price)
|
|
1635
|
+
])
|
|
1636
|
+
}
|
|
1637
|
+
],
|
|
1638
|
+
actions: [{ label: "Assign to a subscriber", command: "assign_package" }]
|
|
1639
|
+
};
|
|
1631
1640
|
}
|
|
1632
|
-
function
|
|
1633
|
-
const
|
|
1634
|
-
deployedUrl?.replace(/\/$/, "") ?? "",
|
|
1635
|
-
domain ? `https://${domain.replace(/^https?:\/\//, "").replace(/\/$/, "")}` : "",
|
|
1636
|
-
"http://localhost:3000"
|
|
1637
|
-
]);
|
|
1641
|
+
function billingScreen(input) {
|
|
1642
|
+
const spend = input.events.reduce((sum, e) => sum + (e.amount > 0 ? e.amount : 0), 0);
|
|
1638
1643
|
return {
|
|
1639
|
-
|
|
1640
|
-
|
|
1644
|
+
id: "billing",
|
|
1645
|
+
title: "Billing",
|
|
1646
|
+
subtitle: `${input.events.length} recent event(s)`,
|
|
1647
|
+
sections: [
|
|
1648
|
+
{
|
|
1649
|
+
kind: "metrics",
|
|
1650
|
+
items: [
|
|
1651
|
+
{
|
|
1652
|
+
label: "Balance",
|
|
1653
|
+
value: input.balance === void 0 ? "\u2014" : money(input.balance, input.currency),
|
|
1654
|
+
tone: (input.balance ?? 0) < 10 ? "warn" : "ok"
|
|
1655
|
+
},
|
|
1656
|
+
{
|
|
1657
|
+
label: "Pending",
|
|
1658
|
+
value: input.pending === void 0 ? "\u2014" : money(input.pending, input.currency)
|
|
1659
|
+
},
|
|
1660
|
+
{ label: "Recent spend", value: money(spend, input.currency) }
|
|
1661
|
+
]
|
|
1662
|
+
},
|
|
1663
|
+
{
|
|
1664
|
+
kind: "table",
|
|
1665
|
+
title: "Recent events",
|
|
1666
|
+
columns: ["Date", "Description", "Amount"],
|
|
1667
|
+
numeric: [2],
|
|
1668
|
+
empty: "No billing events in this window.",
|
|
1669
|
+
rows: input.events.map((e) => [e.date, e.description, money(e.amount, input.currency)])
|
|
1670
|
+
}
|
|
1671
|
+
],
|
|
1672
|
+
actions: [{ label: "Check payouts", command: "stripe_connect_payouts" }]
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
function walletScreen(input) {
|
|
1676
|
+
const low = input.balance < (input.threshold ?? 10);
|
|
1677
|
+
return {
|
|
1678
|
+
id: "wallet",
|
|
1679
|
+
title: "Wallet",
|
|
1680
|
+
subtitle: input.autoTopupEnabled ? "Auto top-up is on" : "Auto top-up is off",
|
|
1681
|
+
sections: [
|
|
1682
|
+
{
|
|
1683
|
+
kind: "metrics",
|
|
1684
|
+
items: [
|
|
1685
|
+
{
|
|
1686
|
+
label: "Balance",
|
|
1687
|
+
value: money(input.balance, input.currency),
|
|
1688
|
+
tone: low ? "warn" : "ok"
|
|
1689
|
+
},
|
|
1690
|
+
...input.credits === void 0 ? [] : [{ label: "Credits", value: input.credits }]
|
|
1691
|
+
]
|
|
1692
|
+
},
|
|
1693
|
+
{
|
|
1694
|
+
kind: "keyvalue",
|
|
1695
|
+
title: "Settings",
|
|
1696
|
+
items: [
|
|
1697
|
+
{ label: "Auto top-up", value: input.autoTopupEnabled ? "enabled" : "disabled" },
|
|
1698
|
+
{
|
|
1699
|
+
label: "Threshold",
|
|
1700
|
+
value: input.threshold === void 0 ? "\u2014" : money(input.threshold, input.currency)
|
|
1701
|
+
}
|
|
1702
|
+
]
|
|
1703
|
+
},
|
|
1704
|
+
...low ? [
|
|
1705
|
+
{
|
|
1706
|
+
kind: "note",
|
|
1707
|
+
tone: "warn",
|
|
1708
|
+
text: "Balance is under the top-up threshold. Package assignment fails at zero on package-only accounts."
|
|
1709
|
+
}
|
|
1710
|
+
] : []
|
|
1711
|
+
],
|
|
1712
|
+
actions: [
|
|
1713
|
+
{ label: "Top up", command: "wallet_topup_checkout" },
|
|
1714
|
+
{ label: "Configure auto top-up", command: "wallet_auto_topup" }
|
|
1715
|
+
]
|
|
1641
1716
|
};
|
|
1642
1717
|
}
|
|
1643
|
-
|
|
1644
|
-
// src/cli/lib/clerk-cli.ts
|
|
1645
|
-
import { join as join4 } from "path";
|
|
1646
|
-
import { cp, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
1647
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
1648
|
-
var SNAPSHOT_PATHS = ["src", "package.json", "next.config.mjs", "middleware.ts"];
|
|
1649
|
-
function clerkCliDeps() {
|
|
1718
|
+
function greenzoneScreen(entries) {
|
|
1650
1719
|
return {
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
}
|
|
1663
|
-
},
|
|
1664
|
-
snapshot: async (storefront) => {
|
|
1665
|
-
const backup = await mkdtemp2(join4(tmpdir2(), "carrier-clerk-snap-"));
|
|
1666
|
-
const saved = [];
|
|
1667
|
-
for (const rel of SNAPSHOT_PATHS) {
|
|
1668
|
-
const src = join4(storefront, rel);
|
|
1669
|
-
if (!await exists(src)) continue;
|
|
1670
|
-
await cp(src, join4(backup, rel), { recursive: true });
|
|
1671
|
-
saved.push(rel);
|
|
1720
|
+
id: "greenzone",
|
|
1721
|
+
title: "Greenzone whitelist",
|
|
1722
|
+
subtitle: `${entries.length} entr${entries.length === 1 ? "y" : "ies"}`,
|
|
1723
|
+
sections: [
|
|
1724
|
+
{ kind: "metrics", items: [{ label: "Entries", value: entries.length }] },
|
|
1725
|
+
{
|
|
1726
|
+
kind: "table",
|
|
1727
|
+
title: "Whitelisted",
|
|
1728
|
+
columns: ["Value", "Note"],
|
|
1729
|
+
empty: "Whitelist is empty \u2014 every destination follows the default policy.",
|
|
1730
|
+
rows: entries.map((e) => [e.value, e.note ?? "\u2014"])
|
|
1672
1731
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
await cp(join4(backup, rel), target, { recursive: true });
|
|
1679
|
-
}
|
|
1680
|
-
} finally {
|
|
1681
|
-
await rm2(backup, { recursive: true, force: true });
|
|
1682
|
-
}
|
|
1683
|
-
};
|
|
1684
|
-
}
|
|
1732
|
+
],
|
|
1733
|
+
actions: [
|
|
1734
|
+
{ label: "Add an entry", command: "greenzone_whitelist_add" },
|
|
1735
|
+
{ label: "Remove an entry", command: "greenzone_whitelist_remove" }
|
|
1736
|
+
]
|
|
1685
1737
|
};
|
|
1686
1738
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
const root = by("/");
|
|
1711
|
-
const shop = by("/shop");
|
|
1712
|
-
const help = by("/help");
|
|
1713
|
-
if (probes.every((p) => p.ok && p.bodyOk !== false)) return "healthy";
|
|
1714
|
-
if (probes.every((p) => p.status === 0)) return "not-deployed";
|
|
1715
|
-
const catalogDown = root?.status === 500 && shop?.status === 500;
|
|
1716
|
-
if (catalogDown && help?.ok) {
|
|
1717
|
-
return needsCarrierKey(secrets) ? "carrier-key-missing" : "unclassified";
|
|
1718
|
-
}
|
|
1719
|
-
if (shop?.ok && shop.bodyOk === false) return "empty-catalog";
|
|
1720
|
-
if (root?.ok && shop?.ok && probes.some((p) => p.status >= 500)) return "clerk-keys-missing";
|
|
1721
|
-
if (probes.every((p) => p.status === 404)) return "not-deployed";
|
|
1722
|
-
return "unclassified";
|
|
1723
|
-
}
|
|
1724
|
-
var SUMMARIES = {
|
|
1725
|
-
healthy: "All probes returned a working page.",
|
|
1726
|
-
"carrier-key-missing": "Catalog pages return 500 while non-catalog pages work \u2014 CARRIER_API_KEY is missing or invalid on the host.",
|
|
1727
|
-
"clerk-keys-missing": "Public pages work but auth routes error \u2014 Clerk keys are missing on the host.",
|
|
1728
|
-
"empty-catalog": "/shop renders but no plans are in it \u2014 the Carrier catalog returned nothing sellable. Not a deploy problem.",
|
|
1729
|
-
"not-deployed": "Nothing served at the deployed URL \u2014 the deploy did not land, or the URL is wrong.",
|
|
1730
|
-
unclassified: "The storefront is not serving correctly and the symptom matches no known cause."
|
|
1731
|
-
};
|
|
1732
|
-
var REPAIRABLE = /* @__PURE__ */ new Set([
|
|
1733
|
-
"carrier-key-missing",
|
|
1734
|
-
"clerk-keys-missing"
|
|
1735
|
-
]);
|
|
1736
|
-
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1737
|
-
async function verifyStorefront(url, storefront, env = process.env, opts = {}) {
|
|
1738
|
-
const attempts = Math.max(1, opts.attempts ?? 4);
|
|
1739
|
-
const delayMs = opts.delayMs ?? 5e3;
|
|
1740
|
-
const secrets = await discoverSecrets(storefront, env);
|
|
1741
|
-
let probes = [];
|
|
1742
|
-
let diagnosis = "unclassified";
|
|
1743
|
-
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
1744
|
-
probes = [];
|
|
1745
|
-
for (const { path, expectBody } of PROBES) {
|
|
1746
|
-
probes.push(await probe(url, path, expectBody));
|
|
1739
|
+
function storefrontScreen(input) {
|
|
1740
|
+
const sections = [
|
|
1741
|
+
{
|
|
1742
|
+
kind: "metrics",
|
|
1743
|
+
items: [
|
|
1744
|
+
{
|
|
1745
|
+
label: "Status",
|
|
1746
|
+
value: input.verified === true ? "serving" : input.verified === false ? "broken" : "unknown",
|
|
1747
|
+
tone: input.verified === true ? "ok" : input.verified === false ? "critical" : "muted"
|
|
1748
|
+
},
|
|
1749
|
+
{ label: "Host", value: input.target ?? "\u2014" },
|
|
1750
|
+
{ label: "Plans", value: input.plans ?? "\u2014" },
|
|
1751
|
+
{ label: "Secrets", value: input.secretsStaged?.length ?? 0 }
|
|
1752
|
+
]
|
|
1753
|
+
},
|
|
1754
|
+
{
|
|
1755
|
+
kind: "keyvalue",
|
|
1756
|
+
title: "Deployment",
|
|
1757
|
+
items: [
|
|
1758
|
+
{ label: "Brand", value: input.brand },
|
|
1759
|
+
{ label: "URL", value: input.url ?? "not deployed" },
|
|
1760
|
+
...input.diagnosis ? [{ label: "Diagnosis", value: input.diagnosis, tone: "warn" }] : []
|
|
1761
|
+
]
|
|
1747
1762
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1763
|
+
];
|
|
1764
|
+
if (input.probes?.length) {
|
|
1765
|
+
sections.push({
|
|
1766
|
+
kind: "table",
|
|
1767
|
+
title: "Probes",
|
|
1768
|
+
columns: ["Path", "Status"],
|
|
1769
|
+
numeric: [1],
|
|
1770
|
+
rows: input.probes.map((p) => [p.path, p.status === 0 ? "unreachable" : p.status])
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
if (input.secretsMissing?.length) {
|
|
1774
|
+
sections.push({
|
|
1775
|
+
kind: "note",
|
|
1776
|
+
tone: "critical",
|
|
1777
|
+
text: `Missing runtime secret(s): ${input.secretsMissing.join(", ")}. The build and deploy both succeed without them and the site fails at request time.`
|
|
1778
|
+
});
|
|
1751
1779
|
}
|
|
1752
|
-
const ok = diagnosis === "healthy";
|
|
1753
1780
|
return {
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1781
|
+
id: "storefront",
|
|
1782
|
+
title: "Storefront",
|
|
1783
|
+
subtitle: input.url ?? input.brand,
|
|
1784
|
+
sections,
|
|
1785
|
+
actions: [
|
|
1786
|
+
{ label: "Deploy", command: "carrier site deploy" },
|
|
1787
|
+
{ label: "Check hosts", command: "carrier site targets" }
|
|
1788
|
+
]
|
|
1760
1789
|
};
|
|
1761
1790
|
}
|
|
1762
|
-
function formatProbes(probes) {
|
|
1763
|
-
return probes.map((p) => {
|
|
1764
|
-
const status = p.status === 0 ? "unreachable" : String(p.status);
|
|
1765
|
-
const body = p.bodyOk === false ? " (no plans)" : "";
|
|
1766
|
-
return `${p.path} ${status}${body}`;
|
|
1767
|
-
}).join(" ");
|
|
1768
|
-
}
|
|
1769
|
-
function repairPlanFor(diagnosis) {
|
|
1770
|
-
if (diagnosis === "carrier-key-missing") {
|
|
1771
|
-
return {
|
|
1772
|
-
diagnosis,
|
|
1773
|
-
runClerk: false,
|
|
1774
|
-
// CARRIER_API_KEY is read at request time, so re-staging and redeploying
|
|
1775
|
-
// is enough; no rebuild needed.
|
|
1776
|
-
rebuild: false,
|
|
1777
|
-
note: "Re-resolve CARRIER_API_KEY, stage it, and redeploy."
|
|
1778
|
-
};
|
|
1779
|
-
}
|
|
1780
|
-
if (diagnosis === "clerk-keys-missing") {
|
|
1781
|
-
return {
|
|
1782
|
-
diagnosis,
|
|
1783
|
-
runClerk: true,
|
|
1784
|
-
// The publishable key is inlined at build time, so this one must rebuild.
|
|
1785
|
-
rebuild: true,
|
|
1786
|
-
note: "Provision Clerk, rebuild so the publishable key is inlined, and redeploy."
|
|
1787
|
-
};
|
|
1788
|
-
}
|
|
1789
|
-
return void 0;
|
|
1790
|
-
}
|
|
1791
1791
|
|
|
1792
1792
|
// package.json
|
|
1793
1793
|
var package_default = {
|
|
1794
1794
|
name: "@carrierllc/mcp",
|
|
1795
|
-
version: "0.
|
|
1795
|
+
version: "0.7.0",
|
|
1796
1796
|
description: "Carrier MCP \u2014 natural-language control of MVNO/eSIM fleets via eSIMVault OCS. Stdio mode for direct integration with Claude Desktop, Cursor, Windsurf, and MCP-compatible clients. Ships the `carrier` CLI (plugin install + white-label eSIM storefront scaffold).",
|
|
1797
1797
|
license: "MIT",
|
|
1798
1798
|
author: "Carrier (Lifecycle Innovations Limited)",
|
|
@@ -1823,7 +1823,8 @@ var package_default = {
|
|
|
1823
1823
|
lint: "eslint src",
|
|
1824
1824
|
prepublishOnly: "pnpm run build",
|
|
1825
1825
|
test: "pnpm run build && node --test test/*.test.js",
|
|
1826
|
-
"check:pack": "node scripts/check-pack-size.mjs"
|
|
1826
|
+
"check:pack": "node scripts/check-pack-size.mjs",
|
|
1827
|
+
"generate:domains": "node scripts/generate-domains.mjs"
|
|
1827
1828
|
},
|
|
1828
1829
|
dependencies: {
|
|
1829
1830
|
"@clack/prompts": "^0.7.0",
|
|
@@ -2274,6 +2275,118 @@ function normalizePackageTemplateChanges(changes) {
|
|
|
2274
2275
|
return out;
|
|
2275
2276
|
}
|
|
2276
2277
|
|
|
2278
|
+
// ../../packages/ocs-client/dist/index.js
|
|
2279
|
+
function buildListSubscriberParams(args) {
|
|
2280
|
+
const params = {};
|
|
2281
|
+
if (args.imsi) params.imsiPrefix = args.imsi;
|
|
2282
|
+
if (args.iccid) params.iccidPrefix = args.iccid;
|
|
2283
|
+
if (args.activationCode) params.activationCode = args.activationCode;
|
|
2284
|
+
if (args.accountId !== void 0) params.accountId = args.accountId;
|
|
2285
|
+
if (args.msisdn) params.msisdnPrefix = args.msisdn;
|
|
2286
|
+
return params;
|
|
2287
|
+
}
|
|
2288
|
+
function extractSubscriberStatus(row) {
|
|
2289
|
+
if (row === null || typeof row !== "object") return null;
|
|
2290
|
+
const raw = row.status;
|
|
2291
|
+
if (typeof raw === "string" && raw.length > 0) return raw;
|
|
2292
|
+
if (Array.isArray(raw)) {
|
|
2293
|
+
for (const entry of raw) {
|
|
2294
|
+
if (entry && typeof entry === "object") {
|
|
2295
|
+
const name = entry.status;
|
|
2296
|
+
if (typeof name === "string" && name.length > 0) return name;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
return null;
|
|
2301
|
+
}
|
|
2302
|
+
function unwrapSubscriberList(raw) {
|
|
2303
|
+
if (Array.isArray(raw)) return { rows: raw, envelope: null };
|
|
2304
|
+
if (raw !== null && typeof raw === "object") {
|
|
2305
|
+
const obj = raw;
|
|
2306
|
+
if (Array.isArray(obj.subscriberList)) {
|
|
2307
|
+
return { rows: obj.subscriberList, envelope: obj };
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
return { rows: null, envelope: null };
|
|
2311
|
+
}
|
|
2312
|
+
function rewrapSubscriberList(raw, envelope, rows, filtered, matched) {
|
|
2313
|
+
if (rows === null || filtered === null) return raw;
|
|
2314
|
+
const total = matched ?? filtered.length;
|
|
2315
|
+
const dropped = total - filtered.length;
|
|
2316
|
+
if (envelope === null) return filtered;
|
|
2317
|
+
const out = {
|
|
2318
|
+
...envelope,
|
|
2319
|
+
subscriberList: filtered,
|
|
2320
|
+
nbFound: total,
|
|
2321
|
+
hasMore: filtered.length < total
|
|
2322
|
+
};
|
|
2323
|
+
if (dropped > 0) {
|
|
2324
|
+
out.truncated = true;
|
|
2325
|
+
out.note = `${dropped} of ${total} matching subscribers omitted by offset/limit. Narrow with \`status\`, \`accountId\` or \`iccid\`, or page with \`offset\`.`;
|
|
2326
|
+
}
|
|
2327
|
+
return out;
|
|
2328
|
+
}
|
|
2329
|
+
function applySubscriberFilters(raw, args) {
|
|
2330
|
+
const { rows, envelope } = unwrapSubscriberList(raw);
|
|
2331
|
+
let filtered = rows;
|
|
2332
|
+
if (filtered && args.status) {
|
|
2333
|
+
const wanted = String(args.status).trim().toLowerCase();
|
|
2334
|
+
filtered = filtered.filter((row) => {
|
|
2335
|
+
const status = extractSubscriberStatus(row);
|
|
2336
|
+
return status !== null && status.toLowerCase() === wanted;
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
const matched = filtered?.length;
|
|
2340
|
+
if (filtered && typeof args.offset === "number" && args.offset > 0) {
|
|
2341
|
+
filtered = filtered.slice(args.offset);
|
|
2342
|
+
}
|
|
2343
|
+
if (filtered && typeof args.limit === "number" && args.limit >= 0) {
|
|
2344
|
+
filtered = filtered.slice(0, args.limit);
|
|
2345
|
+
}
|
|
2346
|
+
return rewrapSubscriberList(raw, envelope, rows, filtered, matched);
|
|
2347
|
+
}
|
|
2348
|
+
var descriptions = /* @__PURE__ */ new Map();
|
|
2349
|
+
function recordToolDescription(name, description) {
|
|
2350
|
+
if (description) descriptions.set(name, description);
|
|
2351
|
+
}
|
|
2352
|
+
function getToolDescription(name) {
|
|
2353
|
+
return descriptions.get(name);
|
|
2354
|
+
}
|
|
2355
|
+
function routerDescription(full) {
|
|
2356
|
+
if (!full) return void 0;
|
|
2357
|
+
const sentences = full.split(/(?<=\.)\s+(?=[A-Z`'"])/).map((s) => s.trim()).filter(Boolean);
|
|
2358
|
+
const kept = [
|
|
2359
|
+
...sentences.slice(0, 2),
|
|
2360
|
+
...sentences.slice(2).filter((s) => /\bDo NOT\b/i.test(s))
|
|
2361
|
+
];
|
|
2362
|
+
return kept.length ? kept.join(" ") : full;
|
|
2363
|
+
}
|
|
2364
|
+
function buildRouterCatalog(opts) {
|
|
2365
|
+
const curatedByName = new Map(opts.curated.map((t) => [t.name, t]));
|
|
2366
|
+
const tools = [];
|
|
2367
|
+
for (const name of [...opts.toolNames].sort()) {
|
|
2368
|
+
if (name === "carrier_ask" || name === "carrier_clarify") continue;
|
|
2369
|
+
const curated = curatedByName.get(name);
|
|
2370
|
+
const real = routerDescription(getToolDescription(name));
|
|
2371
|
+
tools.push({
|
|
2372
|
+
name,
|
|
2373
|
+
description: real ?? curated?.description ?? `Carrier MCP tool \`${name}\`. Required scope: ${opts.getScope(name)}. Use when the user intent clearly matches this tool name or its domain.`,
|
|
2374
|
+
input_schema: curated?.input_schema ?? { type: "object", properties: {} }
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
return [opts.clarifyTool, ...tools];
|
|
2378
|
+
}
|
|
2379
|
+
var ROUTER_RULES = `Rules:
|
|
2380
|
+
1. Pick the single best-matching tool. Never pick carrier_ask (the router itself).
|
|
2381
|
+
2. Extract any parameters mentioned in the intent (ICCID, account IDs, amounts, etc.) as the tool's input.
|
|
2382
|
+
3. Use carrier_clarify when the intent is ambiguous between several tools, AND when no tool actually answers the question. Returning nothing useful is correct and expected; a near-miss is not.
|
|
2383
|
+
4. DESTRUCTIVE tools are flagged in their descriptions \u2014 still pick them if they match; the safety layer handles the confirm flow.
|
|
2384
|
+
5. Only include params explicitly mentioned in the intent.
|
|
2385
|
+
6. Context fields (iccid, account_id, reseller_id) from the routing context take precedence.
|
|
2386
|
+
7. A tool answers the question only if its description says it does. Do not infer capability from the tool's NAME: several names share words with unrelated questions, and picking on the name alone has produced confidently wrong answers.
|
|
2387
|
+
8. Read the "Do NOT use this to \u2026" steers in a description as hard exclusions. They exist because that tool is the common wrong answer for a neighbouring question, and they name the tool to use instead.
|
|
2388
|
+
9. Prefer carrier_clarify over a tool that would return real data about a different question. Answering "which subscribers erode margin" when the user asked "what do my users' megabytes cost in total" is worse than admitting the gap, because the output looks like an answer.`;
|
|
2389
|
+
|
|
2277
2390
|
// src/lib/storefront-logo.ts
|
|
2278
2391
|
var OPENAI_KEY_NAMES = [
|
|
2279
2392
|
"OPENAI_API_KEY",
|
|
@@ -2411,16 +2524,11 @@ export {
|
|
|
2411
2524
|
esimStatusPerAccountParams,
|
|
2412
2525
|
normalizePackageTemplate,
|
|
2413
2526
|
normalizePackageTemplateChanges,
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
packagesScreen,
|
|
2420
|
-
billingScreen,
|
|
2421
|
-
walletScreen,
|
|
2422
|
-
greenzoneScreen,
|
|
2423
|
-
storefrontScreen,
|
|
2527
|
+
buildListSubscriberParams,
|
|
2528
|
+
applySubscriberFilters,
|
|
2529
|
+
recordToolDescription,
|
|
2530
|
+
buildRouterCatalog,
|
|
2531
|
+
ROUTER_RULES,
|
|
2424
2532
|
generateStorefrontLogo,
|
|
2425
2533
|
run,
|
|
2426
2534
|
which,
|
|
@@ -2447,6 +2555,16 @@ export {
|
|
|
2447
2555
|
verifyStorefront,
|
|
2448
2556
|
formatProbes,
|
|
2449
2557
|
repairPlanFor,
|
|
2558
|
+
renderHtml,
|
|
2559
|
+
renderTui,
|
|
2560
|
+
fleetScreen,
|
|
2561
|
+
subscribersScreen,
|
|
2562
|
+
usageScreen,
|
|
2563
|
+
packagesScreen,
|
|
2564
|
+
billingScreen,
|
|
2565
|
+
walletScreen,
|
|
2566
|
+
greenzoneScreen,
|
|
2567
|
+
storefrontScreen,
|
|
2450
2568
|
CARRIER_VERSION
|
|
2451
2569
|
};
|
|
2452
|
-
//# sourceMappingURL=chunk-
|
|
2570
|
+
//# sourceMappingURL=chunk-CQ6EOLA7.js.map
|