@carrierllc/mcp 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-Y4JEN7M4.js → chunk-2TLBS4AR.js} +1698 -1654
- package/dist/chunk-2TLBS4AR.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +6120 -6114
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-Y4JEN7M4.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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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();
|
|
454
|
+
}
|
|
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]));
|
|
473
|
+
}
|
|
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);
|
|
477
|
+
}
|
|
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)]
|
|
500
|
+
];
|
|
598
501
|
}
|
|
599
|
-
function
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
};
|
|
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");
|
|
641
522
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
{ label: "Remove an entry", command: "greenzone_whitelist_remove" }
|
|
660
|
-
]
|
|
661
|
-
};
|
|
523
|
+
|
|
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
|
+
);
|
|
662
540
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
]
|
|
677
|
-
},
|
|
678
|
-
{
|
|
679
|
-
kind: "keyvalue",
|
|
680
|
-
title: "Deployment",
|
|
681
|
-
items: [
|
|
682
|
-
{ label: "Brand", value: input.brand },
|
|
683
|
-
{ label: "URL", value: input.url ?? "not deployed" },
|
|
684
|
-
...input.diagnosis ? [{ label: "Diagnosis", value: input.diagnosis, tone: "warn" }] : []
|
|
685
|
-
]
|
|
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);
|
|
686
554
|
}
|
|
687
|
-
|
|
688
|
-
if (input.probes?.length) {
|
|
689
|
-
sections.push({
|
|
690
|
-
kind: "table",
|
|
691
|
-
title: "Probes",
|
|
692
|
-
columns: ["Path", "Status"],
|
|
693
|
-
numeric: [1],
|
|
694
|
-
rows: input.probes.map((p) => [p.path, p.status === 0 ? "unreachable" : p.status])
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
if (input.secretsMissing?.length) {
|
|
698
|
-
sections.push({
|
|
699
|
-
kind: "note",
|
|
700
|
-
tone: "critical",
|
|
701
|
-
text: `Missing runtime secret(s): ${input.secretsMissing.join(", ")}. The build and deploy both succeed without them and the site fails at request time.`
|
|
702
|
-
});
|
|
555
|
+
if (value) out[key] = value;
|
|
703
556
|
}
|
|
704
|
-
return
|
|
705
|
-
id: "storefront",
|
|
706
|
-
title: "Storefront",
|
|
707
|
-
subtitle: input.url ?? input.brand,
|
|
708
|
-
sections,
|
|
709
|
-
actions: [
|
|
710
|
-
{ label: "Deploy", command: "carrier site deploy" },
|
|
711
|
-
{ label: "Check hosts", command: "carrier site targets" }
|
|
712
|
-
]
|
|
713
|
-
};
|
|
557
|
+
return out;
|
|
714
558
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
import { spawn } from "child_process";
|
|
723
|
-
var SAFE_COMMAND = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
724
|
-
function isSafeCommand(cmd) {
|
|
725
|
-
return SAFE_COMMAND.test(cmd);
|
|
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
|
+
}
|
|
726
566
|
}
|
|
727
|
-
function
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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;
|
|
735
575
|
}
|
|
736
|
-
return
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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;
|
|
742
591
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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);
|
|
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);
|
|
765
597
|
}
|
|
766
|
-
child.stdout?.on("data", (d) => stdout += d.toString());
|
|
767
|
-
child.stderr?.on("data", (d) => stderr += d.toString());
|
|
768
|
-
child.on("error", () => {
|
|
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
|
-
});
|
|
777
|
-
}
|
|
778
|
-
function runInherit(cmd, args, opts = {}) {
|
|
779
|
-
if (!isSafeCommand(cmd)) {
|
|
780
|
-
return Promise.resolve({
|
|
781
|
-
ok: false,
|
|
782
|
-
code: null,
|
|
783
|
-
stdout: "",
|
|
784
|
-
stderr: `refusing to run unsafe command: ${cmd}`
|
|
785
|
-
});
|
|
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
|
|
803
|
-
}
|
|
804
|
-
var DEFS = {
|
|
805
|
-
cloudflare: {
|
|
806
|
-
id: "cloudflare",
|
|
807
|
-
label: "Cloudflare Workers",
|
|
808
|
-
bin: "wrangler",
|
|
809
|
-
npxPkg: "wrangler",
|
|
810
|
-
markers: ["wrangler.jsonc", "wrangler.json", "wrangler.toml"],
|
|
811
|
-
buildScript: "cf:build",
|
|
812
|
-
artifact: join(".open-next", "worker.js"),
|
|
813
|
-
whoami: ["whoami"],
|
|
814
|
-
loginHint: "wrangler login",
|
|
815
|
-
// wrangler writes this mid-sentence, so the trailing period is not part of it.
|
|
816
|
-
parseAccount: (stdout, stderr) => `${stdout}${stderr}`.match(/associated with the email\s+(\S+?)[.,]?(?:\s|$)/)?.[1],
|
|
817
|
-
deployArgs: (name) => ["deploy", "--name", name],
|
|
818
|
-
secretArgs: (key, value, name) => ({ args: ["secret", "put", key, "--name", name], stdin: value })
|
|
819
|
-
},
|
|
820
|
-
vercel: {
|
|
821
|
-
id: "vercel",
|
|
822
|
-
label: "Vercel",
|
|
823
|
-
bin: "vercel",
|
|
824
|
-
npxPkg: "vercel",
|
|
825
|
-
markers: ["vercel.json", ".vercel"],
|
|
826
|
-
buildScript: "build",
|
|
827
|
-
whoami: ["whoami"],
|
|
828
|
-
loginHint: "vercel login",
|
|
829
|
-
// The username is the only thing on stdout; the version banner goes to stderr.
|
|
830
|
-
parseAccount: (stdout) => stdout.trim().split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith(">") && !/^Vercel CLI/i.test(l)).pop(),
|
|
831
|
-
deployArgs: () => ["deploy", "--prod", "--yes"],
|
|
832
|
-
secretArgs: (key, value) => ({ args: ["env", "add", key, "production", "--force"], stdin: value })
|
|
833
|
-
},
|
|
834
|
-
netlify: {
|
|
835
|
-
id: "netlify",
|
|
836
|
-
label: "Netlify",
|
|
837
|
-
bin: "netlify",
|
|
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
|
-
}
|
|
919
|
-
},
|
|
920
|
-
secretArgs: (key, value) => ({ args: ["secrets", "set", `${key}=${value}`] })
|
|
921
|
-
}
|
|
922
|
-
};
|
|
923
|
-
async function configuredProjectName(id, storefront) {
|
|
924
|
-
if (id !== "cloudflare") return void 0;
|
|
925
|
-
for (const marker of DEFS.cloudflare.markers) {
|
|
926
|
-
const path = join(storefront, marker);
|
|
927
|
-
if (!await exists(path)) continue;
|
|
928
|
-
const body = await readFileText(path);
|
|
929
|
-
const name = body.match(/^\s*"?name"?\s*:\s*"([^"]+)"/m)?.[1];
|
|
930
|
-
if (name) return name;
|
|
931
|
-
}
|
|
932
|
-
return void 0;
|
|
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";
|
|
933
614
|
}
|
|
934
|
-
async function
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
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;
|
|
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;
|
|
951
619
|
}
|
|
952
|
-
async function
|
|
953
|
-
const
|
|
954
|
-
|
|
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;
|
|
955
624
|
}
|
|
956
|
-
async function
|
|
957
|
-
|
|
958
|
-
|
|
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 };
|
|
959
629
|
}
|
|
960
|
-
const
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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)
|
|
649
|
+
},
|
|
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
|
+
};
|
|
982
665
|
}
|
|
666
|
+
chosen = wanted;
|
|
667
|
+
} else {
|
|
668
|
+
chosen = rankTargets(statuses)[0];
|
|
983
669
|
}
|
|
984
|
-
|
|
985
|
-
if (!resolved) {
|
|
670
|
+
if (!chosen) {
|
|
986
671
|
return {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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).`
|
|
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(" | ")
|
|
994
676
|
};
|
|
995
677
|
}
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
}
|
|
1000
|
-
|
|
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
|
+
}
|
|
1001
700
|
return {
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
reason: `${def.bin} is not logged in \u2014 run \`${def.loginHint}\`.`
|
|
701
|
+
ok: true,
|
|
702
|
+
projectName,
|
|
703
|
+
statuses,
|
|
704
|
+
target: chosen.id,
|
|
705
|
+
url: outcome.url,
|
|
706
|
+
secrets: { pushed: staged.staged, failed: staged.failed }
|
|
1009
707
|
};
|
|
708
|
+
} finally {
|
|
709
|
+
await staged.cleanup();
|
|
1010
710
|
}
|
|
1011
|
-
return {
|
|
1012
|
-
id,
|
|
1013
|
-
label: def.label,
|
|
1014
|
-
installed: true,
|
|
1015
|
-
authenticated: true,
|
|
1016
|
-
configured,
|
|
1017
|
-
ready: true,
|
|
1018
|
-
account: def.parseAccount?.(who.stdout, who.stderr)
|
|
1019
|
-
};
|
|
1020
711
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
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;
|
|
724
|
+
}
|
|
725
|
+
return void 0;
|
|
1023
726
|
}
|
|
1024
|
-
function
|
|
1025
|
-
return
|
|
1026
|
-
if (a.configured !== b.configured) return a.configured ? -1 : 1;
|
|
1027
|
-
return TARGET_IDS.indexOf(a.id) - TARGET_IDS.indexOf(b.id);
|
|
1028
|
-
});
|
|
727
|
+
function looksPublishable(key) {
|
|
728
|
+
return /^pk_(test|live)_/.test(key);
|
|
1029
729
|
}
|
|
1030
|
-
function
|
|
1031
|
-
return
|
|
730
|
+
function looksSecret(key) {
|
|
731
|
+
return /^sk_(test|live)_/.test(key);
|
|
1032
732
|
}
|
|
1033
|
-
async function
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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) };
|
|
756
|
+
}
|
|
1037
757
|
}
|
|
1038
|
-
function
|
|
1039
|
-
return
|
|
1040
|
-
|
|
1041
|
-
)
|
|
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;
|
|
1042
764
|
}
|
|
1043
|
-
async function
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
|
1052
774
|
});
|
|
1053
|
-
|
|
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 });
|
|
775
|
+
if (!res.ok) {
|
|
1070
776
|
return {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
cleanup: async () => {
|
|
1075
|
-
await rm(dir, { recursive: true, force: true });
|
|
1076
|
-
}
|
|
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}`}`
|
|
1077
780
|
};
|
|
1078
781
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
});
|
|
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) {
|
|
1085
787
|
return {
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
}
|
|
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."
|
|
1090
791
|
};
|
|
1091
792
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
793
|
+
return {
|
|
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
|
+
}
|
|
803
|
+
};
|
|
804
|
+
}
|
|
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
|
+
}
|
|
831
|
+
return {
|
|
832
|
+
ok: true,
|
|
833
|
+
tier: "cli-keyless",
|
|
834
|
+
credentials: { publishableKey, secretKey, tier: "cli-keyless" }
|
|
835
|
+
};
|
|
836
|
+
} finally {
|
|
837
|
+
await restore();
|
|
1097
838
|
}
|
|
1098
|
-
return { staged, failed, cleanup: async () => {
|
|
1099
|
-
} };
|
|
1100
839
|
}
|
|
1101
|
-
|
|
1102
|
-
const
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
|
|
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
|
|
869
|
+
});
|
|
870
|
+
if (created.ok) return created;
|
|
871
|
+
notes.push(created.reason ?? "Clerk Platform API call failed.");
|
|
1106
872
|
}
|
|
1107
|
-
|
|
1108
|
-
if (
|
|
873
|
+
const discovered = discoverClerkCredentials(opts.env);
|
|
874
|
+
if (discovered) {
|
|
1109
875
|
return {
|
|
1110
|
-
ok:
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
reason:
|
|
876
|
+
ok: true,
|
|
877
|
+
tier: "discovered",
|
|
878
|
+
credentials: discovered,
|
|
879
|
+
reason: notes.length ? notes.join(" ") : void 0
|
|
1114
880
|
};
|
|
1115
881
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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.` };
|
|
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.");
|
|
1126
888
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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]));
|
|
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
|
+
);
|
|
893
|
+
}
|
|
894
|
+
return {
|
|
895
|
+
ok: false,
|
|
896
|
+
tier: "manual",
|
|
897
|
+
reason: notes.join(" "),
|
|
898
|
+
guidance: MANUAL_STEPS
|
|
899
|
+
};
|
|
1182
900
|
}
|
|
1183
|
-
function
|
|
1184
|
-
|
|
1185
|
-
|
|
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}` });
|
|
912
|
+
}
|
|
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}` });
|
|
917
|
+
}
|
|
918
|
+
return { ok: failed.length === 0, applied, failed };
|
|
1186
919
|
}
|
|
1187
|
-
function
|
|
1188
|
-
|
|
1189
|
-
return mixHexWithWhite(accent, 0.08);
|
|
920
|
+
function dedupe(values) {
|
|
921
|
+
return [...new Set(values.map((v) => v.trim()).filter(Boolean))];
|
|
1190
922
|
}
|
|
1191
|
-
function
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
[
|
|
1200
|
-
|
|
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
|
-
];
|
|
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
|
+
};
|
|
1210
933
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
934
|
+
|
|
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
|
+
};
|
|
975
|
+
}
|
|
976
|
+
};
|
|
1231
977
|
}
|
|
1232
978
|
|
|
1233
|
-
// src/cli/lib/storefront
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
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()}`;
|
|
988
|
+
try {
|
|
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) };
|
|
997
|
+
}
|
|
1249
998
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
let value = line.slice(eq + 1).trim();
|
|
1261
|
-
if (value.startsWith('"') && value.endsWith('"') && value.length > 1 || value.startsWith("'") && value.endsWith("'") && value.length > 1) {
|
|
1262
|
-
value = value.slice(1, -1);
|
|
1263
|
-
}
|
|
1264
|
-
if (value) out[key] = value;
|
|
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";
|
|
1265
1009
|
}
|
|
1266
|
-
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";
|
|
1267
1014
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
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));
|
|
1038
|
+
}
|
|
1039
|
+
diagnosis = diagnose(probes, secrets);
|
|
1040
|
+
if (diagnosis !== "not-deployed" || attempt === attempts) break;
|
|
1041
|
+
await sleep(delayMs);
|
|
1274
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
|
+
};
|
|
1275
1052
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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
|
+
};
|
|
1284
1070
|
}
|
|
1285
|
-
|
|
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;
|
|
1286
1081
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
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"
|
|
1145
|
+
}
|
|
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"
|
|
1307
1176
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
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;
|
|
1313
|
+
}
|
|
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);
|
|
1441
1320
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
const res = await fetch(`${CLERK_API_BASE}${path}`, {
|
|
1445
|
-
method: init.method ?? "GET",
|
|
1446
|
-
headers: {
|
|
1447
|
-
Authorization: `Bearer ${token}`,
|
|
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
|
-
}
|
|
1321
|
+
function heading(text, color) {
|
|
1322
|
+
return paint(text.toUpperCase(), ANSI.bold, color);
|
|
1466
1323
|
}
|
|
1467
|
-
function
|
|
1468
|
-
if (
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
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);
|
|
1335
|
+
});
|
|
1473
1336
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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}`;
|
|
1483
1349
|
});
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
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}`}`
|
|
1489
|
-
};
|
|
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 { ok: failed.length === 0, applied, failed };
|
|
1628
|
-
}
|
|
1629
|
-
function dedupe(values) {
|
|
1630
|
-
return [...new Set(values.map((v) => v.trim()).filter(Boolean))];
|
|
1631
|
-
}
|
|
1632
|
-
function storefrontClerkUrls(deployedUrl, domain) {
|
|
1633
|
-
const bases = dedupe([
|
|
1634
|
-
deployedUrl?.replace(/\/$/, "") ?? "",
|
|
1635
|
-
domain ? `https://${domain.replace(/^https?:\/\//, "").replace(/\/$/, "")}` : "",
|
|
1636
|
-
"http://localhost:3000"
|
|
1637
|
-
]);
|
|
1638
1602
|
return {
|
|
1639
|
-
|
|
1640
|
-
|
|
1603
|
+
id: "usage",
|
|
1604
|
+
title: "Usage",
|
|
1605
|
+
subtitle: input.subject,
|
|
1606
|
+
sections,
|
|
1607
|
+
actions: [{ label: "Project depletion", command: "usage_projection" }]
|
|
1641
1608
|
};
|
|
1642
1609
|
}
|
|
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() {
|
|
1610
|
+
function packagesScreen(rows) {
|
|
1650
1611
|
return {
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
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
|
+
])
|
|
1662
1636
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1637
|
+
],
|
|
1638
|
+
actions: [{ label: "Assign to a subscriber", command: "assign_package" }]
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
function billingScreen(input) {
|
|
1642
|
+
const spend = input.events.reduce((sum, e) => sum + (e.amount > 0 ? e.amount : 0), 0);
|
|
1643
|
+
return {
|
|
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)])
|
|
1672
1670
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
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)
|
|
1679
1701
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
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."
|
|
1682
1709
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1710
|
+
] : []
|
|
1711
|
+
],
|
|
1712
|
+
actions: [
|
|
1713
|
+
{ label: "Top up", command: "wallet_topup_checkout" },
|
|
1714
|
+
{ label: "Configure auto top-up", command: "wallet_auto_topup" }
|
|
1715
|
+
]
|
|
1685
1716
|
};
|
|
1686
1717
|
}
|
|
1687
|
-
|
|
1688
|
-
// src/cli/lib/verify-storefront.ts
|
|
1689
|
-
var PROBES = [
|
|
1690
|
-
{ path: "/" },
|
|
1691
|
-
{ path: "/shop", expectBody: /·/ },
|
|
1692
|
-
// plan names render as "Visit · Calm"
|
|
1693
|
-
{ path: "/help" }
|
|
1694
|
-
];
|
|
1695
|
-
async function probe(base, path, expectBody) {
|
|
1696
|
-
const url = `${base.replace(/\/$/, "")}${path}${path.includes("?") ? "&" : "?"}_v=${Date.now()}`;
|
|
1697
|
-
try {
|
|
1698
|
-
const res = await fetch(url, { redirect: "manual" });
|
|
1699
|
-
const status = res.status;
|
|
1700
|
-
const ok = status < 400;
|
|
1701
|
-
if (!expectBody || !ok) return { path, status, ok };
|
|
1702
|
-
const body = await res.text();
|
|
1703
|
-
return { path, status, ok, bodyOk: expectBody.test(body) };
|
|
1704
|
-
} catch (e) {
|
|
1705
|
-
return { path, status: 0, ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
function diagnose(probes, secrets) {
|
|
1709
|
-
const by = (p) => probes.find((x) => x.path === p);
|
|
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));
|
|
1747
|
-
}
|
|
1748
|
-
diagnosis = diagnose(probes, secrets);
|
|
1749
|
-
if (diagnosis !== "not-deployed" || attempt === attempts) break;
|
|
1750
|
-
await sleep(delayMs);
|
|
1751
|
-
}
|
|
1752
|
-
const ok = diagnosis === "healthy";
|
|
1718
|
+
function greenzoneScreen(entries) {
|
|
1753
1719
|
return {
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
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"])
|
|
1731
|
+
}
|
|
1732
|
+
],
|
|
1733
|
+
actions: [
|
|
1734
|
+
{ label: "Add an entry", command: "greenzone_whitelist_add" },
|
|
1735
|
+
{ label: "Remove an entry", command: "greenzone_whitelist_remove" }
|
|
1736
|
+
]
|
|
1760
1737
|
};
|
|
1761
1738
|
}
|
|
1762
|
-
function
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
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
|
+
]
|
|
1762
|
+
}
|
|
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
|
+
});
|
|
1779
1772
|
}
|
|
1780
|
-
if (
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
note: "Provision Clerk, rebuild so the publishable key is inlined, and redeploy."
|
|
1787
|
-
};
|
|
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
|
+
});
|
|
1788
1779
|
}
|
|
1789
|
-
return
|
|
1780
|
+
return {
|
|
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
|
+
]
|
|
1789
|
+
};
|
|
1790
1790
|
}
|
|
1791
1791
|
|
|
1792
1792
|
// package.json
|
|
1793
1793
|
var package_default = {
|
|
1794
1794
|
name: "@carrierllc/mcp",
|
|
1795
|
-
version: "0.
|
|
1795
|
+
version: "0.7.1",
|
|
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)",
|
|
@@ -2345,6 +2345,47 @@ function applySubscriberFilters(raw, args) {
|
|
|
2345
2345
|
}
|
|
2346
2346
|
return rewrapSubscriberList(raw, envelope, rows, filtered, matched);
|
|
2347
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.`;
|
|
2348
2389
|
|
|
2349
2390
|
// src/lib/storefront-logo.ts
|
|
2350
2391
|
var OPENAI_KEY_NAMES = [
|
|
@@ -2485,16 +2526,9 @@ export {
|
|
|
2485
2526
|
normalizePackageTemplateChanges,
|
|
2486
2527
|
buildListSubscriberParams,
|
|
2487
2528
|
applySubscriberFilters,
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
subscribersScreen,
|
|
2492
|
-
usageScreen,
|
|
2493
|
-
packagesScreen,
|
|
2494
|
-
billingScreen,
|
|
2495
|
-
walletScreen,
|
|
2496
|
-
greenzoneScreen,
|
|
2497
|
-
storefrontScreen,
|
|
2529
|
+
recordToolDescription,
|
|
2530
|
+
buildRouterCatalog,
|
|
2531
|
+
ROUTER_RULES,
|
|
2498
2532
|
generateStorefrontLogo,
|
|
2499
2533
|
run,
|
|
2500
2534
|
which,
|
|
@@ -2521,6 +2555,16 @@ export {
|
|
|
2521
2555
|
verifyStorefront,
|
|
2522
2556
|
formatProbes,
|
|
2523
2557
|
repairPlanFor,
|
|
2558
|
+
renderHtml,
|
|
2559
|
+
renderTui,
|
|
2560
|
+
fleetScreen,
|
|
2561
|
+
subscribersScreen,
|
|
2562
|
+
usageScreen,
|
|
2563
|
+
packagesScreen,
|
|
2564
|
+
billingScreen,
|
|
2565
|
+
walletScreen,
|
|
2566
|
+
greenzoneScreen,
|
|
2567
|
+
storefrontScreen,
|
|
2524
2568
|
CARRIER_VERSION
|
|
2525
2569
|
};
|
|
2526
|
-
//# sourceMappingURL=chunk-
|
|
2570
|
+
//# sourceMappingURL=chunk-2TLBS4AR.js.map
|