@carrierllc/mcp 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-RBG4MKMW.js +2441 -0
- package/dist/chunk-RBG4MKMW.js.map +1 -0
- package/dist/chunk-SHKKVIIA.js +114 -0
- package/dist/chunk-SHKKVIIA.js.map +1 -0
- package/dist/cli.js +927 -371
- package/dist/cli.js.map +1 -1
- package/dist/fsx-BDDIQ3Y7.js +27 -0
- package/dist/fsx-BDDIQ3Y7.js.map +1 -0
- package/dist/index.js +585 -194
- package/dist/index.js.map +1 -1
- package/package.json +14 -4
- package/plugin/carrier/commands/storefront.md +55 -6
- package/templates/storefront/next-env.d.ts +7 -0
- package/templates/storefront/package-lock.json +0 -174
- package/templates/storefront/tsconfig.json +24 -6
- package/dist/chunk-TABEJ7SM.js +0 -461
- package/dist/chunk-TABEJ7SM.js.map +0 -1
|
@@ -0,0 +1,2441 @@
|
|
|
1
|
+
import {
|
|
2
|
+
exists,
|
|
3
|
+
readFile,
|
|
4
|
+
writeFile
|
|
5
|
+
} from "./chunk-SHKKVIIA.js";
|
|
6
|
+
|
|
7
|
+
// ../../packages/screens/dist/index.js
|
|
8
|
+
function isBlankValue(value) {
|
|
9
|
+
const text = String(value).trim();
|
|
10
|
+
return text === "" || text === "\u2014" || text === "-" || Number(text) === 0;
|
|
11
|
+
}
|
|
12
|
+
function isEmptyScreen(screen) {
|
|
13
|
+
return screen.sections.every((section) => {
|
|
14
|
+
switch (section.kind) {
|
|
15
|
+
case "metrics":
|
|
16
|
+
return section.items.every((m) => isBlankValue(m.value));
|
|
17
|
+
case "bars":
|
|
18
|
+
return section.items.every((b) => b.value === 0);
|
|
19
|
+
case "keyvalue":
|
|
20
|
+
return section.items.every((kv) => isBlankValue(kv.value));
|
|
21
|
+
case "table":
|
|
22
|
+
return section.rows.length === 0;
|
|
23
|
+
case "note":
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
var colors = {
|
|
29
|
+
/** Base page background — deepest ink */
|
|
30
|
+
backgroundDark: "#080C16",
|
|
31
|
+
backgroundDarkOklch: "oklch(0.09 0.02 260)",
|
|
32
|
+
/** Surface elevation ladder — alpha blends, no drop-shadows */
|
|
33
|
+
surface0: "#080C16",
|
|
34
|
+
surface1: "rgba(15, 20, 34, 0.80)",
|
|
35
|
+
surface2: "rgba(22, 28, 46, 0.90)",
|
|
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"
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var radius = {
|
|
103
|
+
sm: "0.375rem",
|
|
104
|
+
md: "0.5rem",
|
|
105
|
+
lg: "0.75rem",
|
|
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>`;
|
|
174
|
+
}
|
|
175
|
+
function noteHtml(tone, text) {
|
|
176
|
+
const c = toneColor(tone, colors.accentFlame);
|
|
177
|
+
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>`;
|
|
178
|
+
}
|
|
179
|
+
function emptyHtml(text) {
|
|
180
|
+
return `<div style="padding:14px;border:1px dashed ${colors.borderCard};border-radius:${radius.md};font-size:13px;color:${colors.textMuted}">${esc(text)}</div>`;
|
|
181
|
+
}
|
|
182
|
+
function sectionHtml(section) {
|
|
183
|
+
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>` : "";
|
|
184
|
+
let body;
|
|
185
|
+
switch (section.kind) {
|
|
186
|
+
case "metrics":
|
|
187
|
+
body = metricsHtml(section.items);
|
|
188
|
+
break;
|
|
189
|
+
case "bars":
|
|
190
|
+
body = barsHtml(section.items, section.empty);
|
|
191
|
+
break;
|
|
192
|
+
case "table":
|
|
193
|
+
body = tableHtml(section);
|
|
194
|
+
break;
|
|
195
|
+
case "keyvalue":
|
|
196
|
+
body = keyValueHtml(section.items);
|
|
197
|
+
break;
|
|
198
|
+
case "note":
|
|
199
|
+
body = noteHtml(section.tone, section.text);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
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
|
+
};
|
|
235
|
+
function paint(text, code, color) {
|
|
236
|
+
return color ? `${code}${text}${ANSI.reset}` : text;
|
|
237
|
+
}
|
|
238
|
+
function displayWidth(text) {
|
|
239
|
+
return text.replace(/\[[0-9;]*m/g, "").length;
|
|
240
|
+
}
|
|
241
|
+
function pad(text, width, align) {
|
|
242
|
+
const gap = Math.max(0, width - displayWidth(text));
|
|
243
|
+
return align === "right" ? " ".repeat(gap) + text : text + " ".repeat(gap);
|
|
244
|
+
}
|
|
245
|
+
function heading(text, color) {
|
|
246
|
+
return paint(text.toUpperCase(), ANSI.bold, color);
|
|
247
|
+
}
|
|
248
|
+
function metricsTui(items, width, color) {
|
|
249
|
+
if (items.length === 0) return [];
|
|
250
|
+
const labelWidth = Math.min(
|
|
251
|
+
28,
|
|
252
|
+
Math.max(...items.map((m) => m.label.length))
|
|
253
|
+
);
|
|
254
|
+
return items.map((m) => {
|
|
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
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function barsTui(items, width, color, empty) {
|
|
262
|
+
if (items.length === 0) return [` ${paint(empty ?? "Nothing to show.", ANSI.muted, color)}`];
|
|
263
|
+
const ceiling = Math.max(...items.map((b) => b.max ?? b.value), 1);
|
|
264
|
+
const labelWidth = Math.min(24, Math.max(...items.map((b) => b.label.length)));
|
|
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
|
+
});
|
|
274
|
+
}
|
|
275
|
+
function tableTui(section, width, color) {
|
|
276
|
+
if (section.rows.length === 0) {
|
|
277
|
+
return [` ${paint(section.empty ?? "No rows.", ANSI.muted, color)}`];
|
|
278
|
+
}
|
|
279
|
+
const numeric = new Set(section.numeric ?? []);
|
|
280
|
+
const cols = section.columns.length;
|
|
281
|
+
const widths = Array.from(
|
|
282
|
+
{ length: cols },
|
|
283
|
+
(_, i) => Math.max(
|
|
284
|
+
section.columns[i]?.length ?? 0,
|
|
285
|
+
...section.rows.map((r) => String(r[i] ?? "").length)
|
|
286
|
+
)
|
|
287
|
+
);
|
|
288
|
+
let total = widths.reduce((a, b) => a + b + 2, 2);
|
|
289
|
+
while (total > width && Math.max(...widths) > 8) {
|
|
290
|
+
const widest = widths.indexOf(Math.max(...widths));
|
|
291
|
+
widths[widest] -= 1;
|
|
292
|
+
total -= 1;
|
|
293
|
+
}
|
|
294
|
+
const clip = (cell, i) => {
|
|
295
|
+
const text = String(cell ?? "");
|
|
296
|
+
return text.length > widths[i] ? `${text.slice(0, Math.max(1, widths[i] - 1))}\u2026` : text;
|
|
297
|
+
};
|
|
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
|
+
}
|
|
305
|
+
function keyValueTui(items, color) {
|
|
306
|
+
if (items.length === 0) return [];
|
|
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
|
+
);
|
|
311
|
+
}
|
|
312
|
+
var NOTE_PREFIX = {
|
|
313
|
+
ok: "ok",
|
|
314
|
+
info: "note",
|
|
315
|
+
warn: "warning",
|
|
316
|
+
critical: "critical",
|
|
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;
|
|
343
|
+
}
|
|
344
|
+
function renderTui(screen, opts = {}) {
|
|
345
|
+
const width = Math.max(40, Math.min(160, opts.width ?? 80));
|
|
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");
|
|
370
|
+
}
|
|
371
|
+
var pct = (part, total) => total > 0 ? `${(part / total * 100).toFixed(1)}%` : "0%";
|
|
372
|
+
var money = (value, currency = "") => `${currency}${value.toFixed(2)}`.trim();
|
|
373
|
+
function humanBytes(bytes) {
|
|
374
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 MB";
|
|
375
|
+
const gb = bytes / 1073741824;
|
|
376
|
+
if (gb >= 1) return `${gb.toFixed(gb >= 10 ? 0 : 1)} GB`;
|
|
377
|
+
return `${(bytes / 1048576).toFixed(0)} MB`;
|
|
378
|
+
}
|
|
379
|
+
function fleetScreen(input) {
|
|
380
|
+
const total = input.active + input.suspended + input.inventory + input.other;
|
|
381
|
+
const lowBalance = input.accounts.filter((a) => a.balance < 10);
|
|
382
|
+
const criticals = lowBalance.filter((a) => a.packageOnly && a.balance === 0);
|
|
383
|
+
const sections = [
|
|
384
|
+
{
|
|
385
|
+
kind: "metrics",
|
|
386
|
+
items: [
|
|
387
|
+
{ label: "Total eSIMs", value: total },
|
|
388
|
+
{ label: "Active", value: input.active, hint: pct(input.active, total), tone: "ok" },
|
|
389
|
+
{ label: "Inventory", value: input.inventory, hint: pct(input.inventory, total) },
|
|
390
|
+
{
|
|
391
|
+
label: "Suspended",
|
|
392
|
+
value: input.suspended,
|
|
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])
|
|
427
|
+
});
|
|
428
|
+
if (criticals.length > 0) {
|
|
429
|
+
sections.push({
|
|
430
|
+
kind: "note",
|
|
431
|
+
tone: "critical",
|
|
432
|
+
text: `${criticals.length} package-only account(s) at zero balance \u2014 packages cannot be assigned until topped up: ${criticals.map((a) => a.name).join(", ")}.`
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
for (const missing of input.unavailable ?? []) {
|
|
436
|
+
sections.push({ kind: "note", tone: "warn", text: `Unavailable: ${missing}` });
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
id: "fleet-health",
|
|
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
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function usageScreen(input) {
|
|
490
|
+
const total = input.totalBytes ?? input.timeline.reduce((sum, p) => sum + p.bytes, 0);
|
|
491
|
+
const peak = input.timeline.reduce(
|
|
492
|
+
(best, p) => p.bytes > best.bytes ? p : best,
|
|
493
|
+
{ date: "\u2014", bytes: 0 }
|
|
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
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
id: "usage",
|
|
528
|
+
title: "Usage",
|
|
529
|
+
subtitle: input.subject,
|
|
530
|
+
sections,
|
|
531
|
+
actions: [{ label: "Project depletion", command: "usage_projection" }]
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function packagesScreen(rows) {
|
|
535
|
+
return {
|
|
536
|
+
id: "packages",
|
|
537
|
+
title: "Package catalog",
|
|
538
|
+
subtitle: `${rows.length} template(s)`,
|
|
539
|
+
sections: [
|
|
540
|
+
{
|
|
541
|
+
kind: "metrics",
|
|
542
|
+
items: [
|
|
543
|
+
{ label: "Templates", value: rows.length },
|
|
544
|
+
{ label: "Recurring", value: rows.filter((r) => r.recurring).length }
|
|
545
|
+
]
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
kind: "table",
|
|
549
|
+
title: "Templates",
|
|
550
|
+
columns: ["Name", "ID", "Data", "Validity", "Price"],
|
|
551
|
+
numeric: [2, 3, 4],
|
|
552
|
+
empty: "No package templates visible to this account.",
|
|
553
|
+
rows: rows.map((r) => [
|
|
554
|
+
r.name,
|
|
555
|
+
String(r.id),
|
|
556
|
+
r.dataLimitBytes === void 0 ? "\u2014" : humanBytes(r.dataLimitBytes),
|
|
557
|
+
r.validityDays === void 0 ? "\u2014" : `${r.validityDays}d`,
|
|
558
|
+
r.price === void 0 ? "\u2014" : money(r.price)
|
|
559
|
+
])
|
|
560
|
+
}
|
|
561
|
+
],
|
|
562
|
+
actions: [{ label: "Assign to a subscriber", command: "assign_package" }]
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function billingScreen(input) {
|
|
566
|
+
const spend = input.events.reduce((sum, e) => sum + (e.amount > 0 ? e.amount : 0), 0);
|
|
567
|
+
return {
|
|
568
|
+
id: "billing",
|
|
569
|
+
title: "Billing",
|
|
570
|
+
subtitle: `${input.events.length} recent event(s)`,
|
|
571
|
+
sections: [
|
|
572
|
+
{
|
|
573
|
+
kind: "metrics",
|
|
574
|
+
items: [
|
|
575
|
+
{
|
|
576
|
+
label: "Balance",
|
|
577
|
+
value: input.balance === void 0 ? "\u2014" : money(input.balance, input.currency),
|
|
578
|
+
tone: (input.balance ?? 0) < 10 ? "warn" : "ok"
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
label: "Pending",
|
|
582
|
+
value: input.pending === void 0 ? "\u2014" : money(input.pending, input.currency)
|
|
583
|
+
},
|
|
584
|
+
{ label: "Recent spend", value: money(spend, input.currency) }
|
|
585
|
+
]
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
kind: "table",
|
|
589
|
+
title: "Recent events",
|
|
590
|
+
columns: ["Date", "Description", "Amount"],
|
|
591
|
+
numeric: [2],
|
|
592
|
+
empty: "No billing events in this window.",
|
|
593
|
+
rows: input.events.map((e) => [e.date, e.description, money(e.amount, input.currency)])
|
|
594
|
+
}
|
|
595
|
+
],
|
|
596
|
+
actions: [{ label: "Check payouts", command: "stripe_connect_payouts" }]
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
function walletScreen(input) {
|
|
600
|
+
const low = input.balance < (input.threshold ?? 10);
|
|
601
|
+
return {
|
|
602
|
+
id: "wallet",
|
|
603
|
+
title: "Wallet",
|
|
604
|
+
subtitle: input.autoTopupEnabled ? "Auto top-up is on" : "Auto top-up is off",
|
|
605
|
+
sections: [
|
|
606
|
+
{
|
|
607
|
+
kind: "metrics",
|
|
608
|
+
items: [
|
|
609
|
+
{
|
|
610
|
+
label: "Balance",
|
|
611
|
+
value: money(input.balance, input.currency),
|
|
612
|
+
tone: low ? "warn" : "ok"
|
|
613
|
+
},
|
|
614
|
+
...input.credits === void 0 ? [] : [{ label: "Credits", value: input.credits }]
|
|
615
|
+
]
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
kind: "keyvalue",
|
|
619
|
+
title: "Settings",
|
|
620
|
+
items: [
|
|
621
|
+
{ label: "Auto top-up", value: input.autoTopupEnabled ? "enabled" : "disabled" },
|
|
622
|
+
{
|
|
623
|
+
label: "Threshold",
|
|
624
|
+
value: input.threshold === void 0 ? "\u2014" : money(input.threshold, input.currency)
|
|
625
|
+
}
|
|
626
|
+
]
|
|
627
|
+
},
|
|
628
|
+
...low ? [
|
|
629
|
+
{
|
|
630
|
+
kind: "note",
|
|
631
|
+
tone: "warn",
|
|
632
|
+
text: "Balance is under the top-up threshold. Package assignment fails at zero on package-only accounts."
|
|
633
|
+
}
|
|
634
|
+
] : []
|
|
635
|
+
],
|
|
636
|
+
actions: [
|
|
637
|
+
{ label: "Top up", command: "wallet_topup_checkout" },
|
|
638
|
+
{ label: "Configure auto top-up", command: "wallet_auto_topup" }
|
|
639
|
+
]
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function greenzoneScreen(entries) {
|
|
643
|
+
return {
|
|
644
|
+
id: "greenzone",
|
|
645
|
+
title: "Greenzone whitelist",
|
|
646
|
+
subtitle: `${entries.length} entr${entries.length === 1 ? "y" : "ies"}`,
|
|
647
|
+
sections: [
|
|
648
|
+
{ kind: "metrics", items: [{ label: "Entries", value: entries.length }] },
|
|
649
|
+
{
|
|
650
|
+
kind: "table",
|
|
651
|
+
title: "Whitelisted",
|
|
652
|
+
columns: ["Value", "Note"],
|
|
653
|
+
empty: "Whitelist is empty \u2014 every destination follows the default policy.",
|
|
654
|
+
rows: entries.map((e) => [e.value, e.note ?? "\u2014"])
|
|
655
|
+
}
|
|
656
|
+
],
|
|
657
|
+
actions: [
|
|
658
|
+
{ label: "Add an entry", command: "greenzone_whitelist_add" },
|
|
659
|
+
{ label: "Remove an entry", command: "greenzone_whitelist_remove" }
|
|
660
|
+
]
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function storefrontScreen(input) {
|
|
664
|
+
const sections = [
|
|
665
|
+
{
|
|
666
|
+
kind: "metrics",
|
|
667
|
+
items: [
|
|
668
|
+
{
|
|
669
|
+
label: "Status",
|
|
670
|
+
value: input.verified === true ? "serving" : input.verified === false ? "broken" : "unknown",
|
|
671
|
+
tone: input.verified === true ? "ok" : input.verified === false ? "critical" : "muted"
|
|
672
|
+
},
|
|
673
|
+
{ label: "Host", value: input.target ?? "\u2014" },
|
|
674
|
+
{ label: "Plans", value: input.plans ?? "\u2014" },
|
|
675
|
+
{ label: "Secrets", value: input.secretsStaged?.length ?? 0 }
|
|
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
|
+
]
|
|
686
|
+
}
|
|
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
|
+
});
|
|
703
|
+
}
|
|
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
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/cli/lib/deploy-targets.ts
|
|
717
|
+
import { join } from "path";
|
|
718
|
+
import { tmpdir } from "os";
|
|
719
|
+
import { mkdtemp, rm, writeFile as writeFileMode } from "fs/promises";
|
|
720
|
+
|
|
721
|
+
// src/cli/lib/exec.ts
|
|
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);
|
|
726
|
+
}
|
|
727
|
+
function run(cmd, args, opts = {}) {
|
|
728
|
+
if (!isSafeCommand(cmd)) {
|
|
729
|
+
return Promise.resolve({
|
|
730
|
+
ok: false,
|
|
731
|
+
code: null,
|
|
732
|
+
stdout: "",
|
|
733
|
+
stderr: `refusing to run unsafe command: ${cmd}`
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
return new Promise((resolve) => {
|
|
737
|
+
const child = spawn(cmd, args, { cwd: opts.cwd, shell: false });
|
|
738
|
+
if (opts.stdin !== void 0) {
|
|
739
|
+
child.stdin?.on("error", () => {
|
|
740
|
+
});
|
|
741
|
+
child.stdin?.end(opts.stdin);
|
|
742
|
+
}
|
|
743
|
+
let stdout = "";
|
|
744
|
+
let stderr = "";
|
|
745
|
+
let settled = false;
|
|
746
|
+
const finish = (result) => {
|
|
747
|
+
if (settled) return;
|
|
748
|
+
settled = true;
|
|
749
|
+
resolve(result);
|
|
750
|
+
};
|
|
751
|
+
let timer;
|
|
752
|
+
if (opts.timeoutMs && opts.timeoutMs > 0) {
|
|
753
|
+
timer = setTimeout(() => {
|
|
754
|
+
try {
|
|
755
|
+
child.kill("SIGTERM");
|
|
756
|
+
} catch {
|
|
757
|
+
}
|
|
758
|
+
finish({
|
|
759
|
+
ok: false,
|
|
760
|
+
code: null,
|
|
761
|
+
stdout,
|
|
762
|
+
stderr: stderr || `timeout after ${opts.timeoutMs}ms`
|
|
763
|
+
});
|
|
764
|
+
}, opts.timeoutMs);
|
|
765
|
+
}
|
|
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
|
+
}
|
|
787
|
+
return new Promise((resolve) => {
|
|
788
|
+
const child = spawn(cmd, args, { cwd: opts.cwd, shell: false, stdio: "inherit" });
|
|
789
|
+
child.on("error", () => resolve({ ok: false, code: null, stdout: "", stderr: "" }));
|
|
790
|
+
child.on("close", (code) => resolve({ ok: code === 0, code, stdout: "", stderr: "" }));
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
async function which(bin) {
|
|
794
|
+
const probe2 = process.platform === "win32" ? "where" : "which";
|
|
795
|
+
const r = await run(probe2, [bin]);
|
|
796
|
+
return r.ok && r.stdout.trim().length > 0;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/cli/lib/deploy-targets.ts
|
|
800
|
+
var TARGET_IDS = ["cloudflare", "vercel", "netlify", "fly"];
|
|
801
|
+
function isTargetId(v) {
|
|
802
|
+
return TARGET_IDS.includes(v);
|
|
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;
|
|
933
|
+
}
|
|
934
|
+
async function setCustomDomain(storefront, domain) {
|
|
935
|
+
const clean = domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
936
|
+
if (!clean || !clean.includes(".")) return false;
|
|
937
|
+
const path = join(storefront, "wrangler.jsonc");
|
|
938
|
+
if (!await exists(path)) return false;
|
|
939
|
+
const body = await readFileText(path);
|
|
940
|
+
if (body.includes(`"pattern": "${clean}"`)) return true;
|
|
941
|
+
const routes = ` "routes": [
|
|
942
|
+
{ "pattern": "${clean}", "custom_domain": true }
|
|
943
|
+
],
|
|
944
|
+
`;
|
|
945
|
+
const anchor = body.indexOf(`"main"`);
|
|
946
|
+
if (anchor === -1) return false;
|
|
947
|
+
const lineStart = body.lastIndexOf("\n", anchor) + 1;
|
|
948
|
+
const patched = body.slice(0, lineStart) + routes + body.slice(lineStart);
|
|
949
|
+
await writeFile(path, patched);
|
|
950
|
+
return true;
|
|
951
|
+
}
|
|
952
|
+
async function readFileText(path) {
|
|
953
|
+
const { readFile: readFile2 } = await import("./fsx-BDDIQ3Y7.js");
|
|
954
|
+
return readFile2(path, "utf8");
|
|
955
|
+
}
|
|
956
|
+
async function rollback(id, storefront, projectName) {
|
|
957
|
+
if (id !== "cloudflare") {
|
|
958
|
+
return { ok: false, reason: `${DEFS[id].label} has no one-command rollback \u2014 revert manually.` };
|
|
959
|
+
}
|
|
960
|
+
const def = DEFS[id];
|
|
961
|
+
const resolved = await resolveBin(def);
|
|
962
|
+
if (!resolved) return { ok: false, reason: "wrangler not found." };
|
|
963
|
+
const r = await run(
|
|
964
|
+
resolved.bin,
|
|
965
|
+
[...resolved.prefix, "rollback", "--name", projectName, "--yes"],
|
|
966
|
+
{ cwd: storefront, timeoutMs: 3e5 }
|
|
967
|
+
);
|
|
968
|
+
return r.ok ? { ok: true } : { ok: false, reason: `${r.stderr || r.stdout}`.trim().split("\n").slice(-2).join(" ").slice(0, 300) };
|
|
969
|
+
}
|
|
970
|
+
async function resolveBin(def) {
|
|
971
|
+
if (await which(def.bin)) return { bin: def.bin, prefix: [] };
|
|
972
|
+
if (def.npxPkg && await which("npx")) return { bin: "npx", prefix: [def.npxPkg] };
|
|
973
|
+
return void 0;
|
|
974
|
+
}
|
|
975
|
+
async function probeTarget(id, storefront) {
|
|
976
|
+
const def = DEFS[id];
|
|
977
|
+
let configured = false;
|
|
978
|
+
for (const marker of def.markers) {
|
|
979
|
+
if (await exists(join(storefront, marker))) {
|
|
980
|
+
configured = true;
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
const resolved = await resolveBin(def);
|
|
985
|
+
if (!resolved) {
|
|
986
|
+
return {
|
|
987
|
+
id,
|
|
988
|
+
label: def.label,
|
|
989
|
+
installed: false,
|
|
990
|
+
authenticated: false,
|
|
991
|
+
configured,
|
|
992
|
+
ready: false,
|
|
993
|
+
reason: def.npxPkg ? `${def.bin} not found \u2014 install it, or make npx available.` : `${def.bin} not found \u2014 install the Fly CLI (brew install flyctl).`
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
const who = await run(resolved.bin, [...resolved.prefix, ...def.whoami], {
|
|
997
|
+
cwd: storefront,
|
|
998
|
+
timeoutMs: 6e4
|
|
999
|
+
});
|
|
1000
|
+
if (!who.ok) {
|
|
1001
|
+
return {
|
|
1002
|
+
id,
|
|
1003
|
+
label: def.label,
|
|
1004
|
+
installed: true,
|
|
1005
|
+
authenticated: false,
|
|
1006
|
+
configured,
|
|
1007
|
+
ready: false,
|
|
1008
|
+
reason: `${def.bin} is not logged in \u2014 run \`${def.loginHint}\`.`
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
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
|
+
}
|
|
1021
|
+
async function probeAll(storefront) {
|
|
1022
|
+
return Promise.all(TARGET_IDS.map((id) => probeTarget(id, storefront)));
|
|
1023
|
+
}
|
|
1024
|
+
function rankTargets(statuses) {
|
|
1025
|
+
return statuses.filter((s) => s.ready).sort((a, b) => {
|
|
1026
|
+
if (a.configured !== b.configured) return a.configured ? -1 : 1;
|
|
1027
|
+
return TARGET_IDS.indexOf(a.id) - TARGET_IDS.indexOf(b.id);
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
function buildScriptFor(id) {
|
|
1031
|
+
return DEFS[id].buildScript;
|
|
1032
|
+
}
|
|
1033
|
+
async function artifactMissing(id, storefront) {
|
|
1034
|
+
const artifact = DEFS[id].artifact;
|
|
1035
|
+
if (!artifact) return false;
|
|
1036
|
+
return !await exists(join(storefront, artifact));
|
|
1037
|
+
}
|
|
1038
|
+
function parseDeployedUrl(output) {
|
|
1039
|
+
return output.match(
|
|
1040
|
+
/https:\/\/[^\s"']+\.(?:workers\.dev|vercel\.app|netlify\.app|fly\.dev)[^\s"']*/
|
|
1041
|
+
)?.[0];
|
|
1042
|
+
}
|
|
1043
|
+
async function putSecret(id, storefront, projectName, key, value) {
|
|
1044
|
+
const def = DEFS[id];
|
|
1045
|
+
const resolved = await resolveBin(def);
|
|
1046
|
+
if (!resolved) return false;
|
|
1047
|
+
const { args, stdin } = def.secretArgs(key, value, projectName);
|
|
1048
|
+
const r = await run(resolved.bin, [...resolved.prefix, ...args], {
|
|
1049
|
+
cwd: storefront,
|
|
1050
|
+
timeoutMs: 12e4,
|
|
1051
|
+
stdin
|
|
1052
|
+
});
|
|
1053
|
+
return r.ok;
|
|
1054
|
+
}
|
|
1055
|
+
async function stageSecrets(id, storefront, projectName, secrets) {
|
|
1056
|
+
const entries = Object.entries(secrets).filter(([, v]) => v?.trim());
|
|
1057
|
+
const noop = { staged: [], failed: [], cleanup: async () => {
|
|
1058
|
+
} };
|
|
1059
|
+
if (entries.length === 0) return noop;
|
|
1060
|
+
const def = DEFS[id];
|
|
1061
|
+
const resolved = await resolveBin(def);
|
|
1062
|
+
if (!resolved) return { staged: [], failed: entries.map(([k]) => k), cleanup: async () => {
|
|
1063
|
+
} };
|
|
1064
|
+
if (id === "cloudflare") {
|
|
1065
|
+
const dir = await mkdtemp(join(tmpdir(), "carrier-secrets-"));
|
|
1066
|
+
const file = join(dir, ".env");
|
|
1067
|
+
const body = entries.map(([k, v]) => `${k}=${v}`).join("\n");
|
|
1068
|
+
await writeFileMode(file, `${body}
|
|
1069
|
+
`, { mode: 384 });
|
|
1070
|
+
return {
|
|
1071
|
+
staged: entries.map(([k]) => k),
|
|
1072
|
+
failed: [],
|
|
1073
|
+
secretsFile: file,
|
|
1074
|
+
cleanup: async () => {
|
|
1075
|
+
await rm(dir, { recursive: true, force: true });
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
if (id === "fly") {
|
|
1080
|
+
const args = ["secrets", "set", "--stage", ...entries.map(([k, v]) => `${k}=${v}`)];
|
|
1081
|
+
const r = await run(resolved.bin, [...resolved.prefix, ...args], {
|
|
1082
|
+
cwd: storefront,
|
|
1083
|
+
timeoutMs: 18e4
|
|
1084
|
+
});
|
|
1085
|
+
return {
|
|
1086
|
+
staged: r.ok ? entries.map(([k]) => k) : [],
|
|
1087
|
+
failed: r.ok ? [] : entries.map(([k]) => k),
|
|
1088
|
+
cleanup: async () => {
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
const staged = [];
|
|
1093
|
+
const failed = [];
|
|
1094
|
+
for (const [key, value] of entries) {
|
|
1095
|
+
const ok = await putSecret(id, storefront, projectName, key, value);
|
|
1096
|
+
(ok ? staged : failed).push(key);
|
|
1097
|
+
}
|
|
1098
|
+
return { staged, failed, cleanup: async () => {
|
|
1099
|
+
} };
|
|
1100
|
+
}
|
|
1101
|
+
async function deployTo(id, storefront, projectName, opts = {}) {
|
|
1102
|
+
const def = DEFS[id];
|
|
1103
|
+
const resolved = await resolveBin(def);
|
|
1104
|
+
if (!resolved) {
|
|
1105
|
+
return { ok: false, target: id, projectName, reason: `${def.bin} not found.` };
|
|
1106
|
+
}
|
|
1107
|
+
await def.ensureConfig?.(storefront, projectName);
|
|
1108
|
+
if (await artifactMissing(id, storefront)) {
|
|
1109
|
+
return {
|
|
1110
|
+
ok: false,
|
|
1111
|
+
target: id,
|
|
1112
|
+
projectName,
|
|
1113
|
+
reason: `No ${def.artifact} build found \u2014 run the build step first.`
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
const args = [...resolved.prefix, ...def.deployArgs(projectName)];
|
|
1117
|
+
if (id === "cloudflare" && opts.secretsFile) args.push("--secrets-file", opts.secretsFile);
|
|
1118
|
+
const r = await run(resolved.bin, args, {
|
|
1119
|
+
cwd: storefront,
|
|
1120
|
+
timeoutMs: 9e5
|
|
1121
|
+
});
|
|
1122
|
+
const combined = `${r.stdout}${r.stderr}`;
|
|
1123
|
+
if (!r.ok) {
|
|
1124
|
+
const tail = combined.trim().split("\n").slice(-3).join(" ").slice(0, 400);
|
|
1125
|
+
return { ok: false, target: id, projectName, reason: tail || `${def.bin} deploy failed.` };
|
|
1126
|
+
}
|
|
1127
|
+
return { ok: true, target: id, projectName, url: parseDeployedUrl(combined) };
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// src/cli/lib/brand.ts
|
|
1131
|
+
var CARRIER_BRAND = {
|
|
1132
|
+
name: "Carrier",
|
|
1133
|
+
legalName: "Lifecycle Innovations Limited",
|
|
1134
|
+
tagline: "Programmable connectivity, on demand.",
|
|
1135
|
+
domain: "carrier.llc",
|
|
1136
|
+
supportEmail: "support@carrier.llc",
|
|
1137
|
+
supportUrl: "https://carrier.llc/help",
|
|
1138
|
+
supportWhatsapp: "+17864604829",
|
|
1139
|
+
colors: {
|
|
1140
|
+
bg: "#080C16",
|
|
1141
|
+
accent: "#FF6B35",
|
|
1142
|
+
accentDark: "#D9461C",
|
|
1143
|
+
text: "#F5F1EA"
|
|
1144
|
+
},
|
|
1145
|
+
social: {
|
|
1146
|
+
x: "@carrier_llc",
|
|
1147
|
+
instagram: "@carrier.llc",
|
|
1148
|
+
tiktok: "@carrier.llc"
|
|
1149
|
+
},
|
|
1150
|
+
carrierApiUrl: "https://api.carrier.llc"
|
|
1151
|
+
};
|
|
1152
|
+
var CARRIER_ACCENT_LIGHT = "#FFB088";
|
|
1153
|
+
var CARRIER_ACCENT_GRADIENT_START = "#FF7A45";
|
|
1154
|
+
function hexToRgb(hex) {
|
|
1155
|
+
const h = hex.replace(/^#/, "");
|
|
1156
|
+
if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
|
|
1157
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
1158
|
+
}
|
|
1159
|
+
function rgbToHex(r, g, b, lower = false) {
|
|
1160
|
+
const fmt = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
|
|
1161
|
+
const out = `#${fmt(r)}${fmt(g)}${fmt(b)}`;
|
|
1162
|
+
return lower ? out : out.toUpperCase();
|
|
1163
|
+
}
|
|
1164
|
+
function mixHexWithWhite(hex, whiteRatio) {
|
|
1165
|
+
const rgb = hexToRgb(hex);
|
|
1166
|
+
if (!rgb) return hex;
|
|
1167
|
+
const mix2 = (n) => n + (255 - n) * whiteRatio;
|
|
1168
|
+
return rgbToHex(mix2(rgb[0]), mix2(rgb[1]), mix2(rgb[2]));
|
|
1169
|
+
}
|
|
1170
|
+
function darkenHex(hex, factor) {
|
|
1171
|
+
const rgb = hexToRgb(hex);
|
|
1172
|
+
if (!rgb) return hex;
|
|
1173
|
+
const scale = (n) => n * factor;
|
|
1174
|
+
return rgbToHex(scale(rgb[0]), scale(rgb[1]), scale(rgb[2]), true);
|
|
1175
|
+
}
|
|
1176
|
+
function deriveAccentDark(accent, seed = CARRIER_BRAND) {
|
|
1177
|
+
if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return seed.colors.accentDark;
|
|
1178
|
+
const rgb = hexToRgb(accent);
|
|
1179
|
+
if (!rgb) return accent;
|
|
1180
|
+
const scale = (n) => Math.max(0, Math.min(255, Math.round(n * 0.75)));
|
|
1181
|
+
return rgbToHex(scale(rgb[0]), scale(rgb[1]), scale(rgb[2]));
|
|
1182
|
+
}
|
|
1183
|
+
function deriveAccentLight(accent, seed = CARRIER_BRAND) {
|
|
1184
|
+
if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return CARRIER_ACCENT_LIGHT;
|
|
1185
|
+
return mixHexWithWhite(accent, 0.45);
|
|
1186
|
+
}
|
|
1187
|
+
function deriveAccentGradientStart(accent, seed = CARRIER_BRAND) {
|
|
1188
|
+
if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return CARRIER_ACCENT_GRADIENT_START;
|
|
1189
|
+
return mixHexWithWhite(accent, 0.08);
|
|
1190
|
+
}
|
|
1191
|
+
function deriveAccentPaletteSubs(accent, accentDark) {
|
|
1192
|
+
if (accent.toUpperCase() === CARRIER_BRAND.colors.accent.toUpperCase()) return [];
|
|
1193
|
+
const accentLight = deriveAccentLight(accent);
|
|
1194
|
+
const gradientStart = deriveAccentGradientStart(accent);
|
|
1195
|
+
return [
|
|
1196
|
+
["#FF6B35", accent],
|
|
1197
|
+
["#D9461C", accentDark],
|
|
1198
|
+
["#FFB088", accentLight],
|
|
1199
|
+
["#FF7A45", gradientStart],
|
|
1200
|
+
["#fff4ef", mixHexWithWhite(accent, 0.94).toLowerCase()],
|
|
1201
|
+
["#ffe0d0", mixHexWithWhite(accent, 0.85).toLowerCase()],
|
|
1202
|
+
["#ffbfa0", mixHexWithWhite(accent, 0.7).toLowerCase()],
|
|
1203
|
+
["#ff9970", mixHexWithWhite(accent, 0.55).toLowerCase()],
|
|
1204
|
+
["#ff7d4d", mixHexWithWhite(accent, 0.4).toLowerCase()],
|
|
1205
|
+
["#b33a17", darkenHex(accentDark, 0.75)],
|
|
1206
|
+
["#8a2d12", darkenHex(accentDark, 0.58)],
|
|
1207
|
+
["#5e1e0c", darkenHex(accentDark, 0.4)],
|
|
1208
|
+
["#3a1107", darkenHex(accentDark, 0.25)]
|
|
1209
|
+
];
|
|
1210
|
+
}
|
|
1211
|
+
function renderEnv(brand) {
|
|
1212
|
+
return [
|
|
1213
|
+
`# Generated by @carrierllc/mcp`,
|
|
1214
|
+
`NEXT_PUBLIC_BRAND_NAME=${JSON.stringify(brand.name)}`,
|
|
1215
|
+
`NEXT_PUBLIC_CARRIER_API_URL=${JSON.stringify(brand.carrierApiUrl)}`,
|
|
1216
|
+
`# Public site origin. Used to build absolute URLs for guest checkout.`,
|
|
1217
|
+
`# \`carrier site deploy\` writes the deployed URL back here.`,
|
|
1218
|
+
`NEXT_PUBLIC_APP_URL=`,
|
|
1219
|
+
`# Required whenever NEXT_PUBLIC_CARRIER_API_URL points at a live origin:`,
|
|
1220
|
+
`# the catalog client throws without it, so / and /shop return 500.`,
|
|
1221
|
+
`# \`carrier site deploy\` fills this in and pushes it to the deploy target.`,
|
|
1222
|
+
`CARRIER_API_KEY=`,
|
|
1223
|
+
`# Guest checkout calls Stripe server-side. Without this, checkout fails at`,
|
|
1224
|
+
`# request time while every other page keeps working.`,
|
|
1225
|
+
`STRIPE_SECRET_KEY=`,
|
|
1226
|
+
`# Clerk \u2014 \`carrier site clerk\` fills these in, or paste them from dashboard.clerk.com`,
|
|
1227
|
+
`NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=`,
|
|
1228
|
+
`CLERK_SECRET_KEY=`,
|
|
1229
|
+
``
|
|
1230
|
+
].join("\n");
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// src/cli/lib/storefront-secrets.ts
|
|
1234
|
+
import { homedir } from "os";
|
|
1235
|
+
import { join as join2 } from "path";
|
|
1236
|
+
var TEMPLATE_ENV_KEYS = {
|
|
1237
|
+
NEXT_PUBLIC_BRAND_NAME: "public",
|
|
1238
|
+
NEXT_PUBLIC_CARRIER_API_URL: "public",
|
|
1239
|
+
NEXT_PUBLIC_APP_URL: "public",
|
|
1240
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: "public",
|
|
1241
|
+
CARRIER_API_KEY: "secret",
|
|
1242
|
+
CLERK_SECRET_KEY: "secret",
|
|
1243
|
+
STRIPE_SECRET_KEY: "secret"
|
|
1244
|
+
};
|
|
1245
|
+
function keysOfKind(kind) {
|
|
1246
|
+
return Object.keys(TEMPLATE_ENV_KEYS).filter(
|
|
1247
|
+
(k) => TEMPLATE_ENV_KEYS[k] === kind
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
var RUNTIME_SECRET_KEYS = keysOfKind("secret");
|
|
1251
|
+
var PUBLIC_ENV_KEYS = keysOfKind("public");
|
|
1252
|
+
function parseEnvFile(body) {
|
|
1253
|
+
const out = {};
|
|
1254
|
+
for (const raw of body.split("\n")) {
|
|
1255
|
+
const line = raw.trim();
|
|
1256
|
+
if (!line || line.startsWith("#")) continue;
|
|
1257
|
+
const eq = line.indexOf("=");
|
|
1258
|
+
if (eq <= 0) continue;
|
|
1259
|
+
const key = line.slice(0, eq).trim();
|
|
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;
|
|
1265
|
+
}
|
|
1266
|
+
return out;
|
|
1267
|
+
}
|
|
1268
|
+
async function readEnvFile(path) {
|
|
1269
|
+
if (!await exists(path)) return {};
|
|
1270
|
+
try {
|
|
1271
|
+
return parseEnvFile(await readFile(path, "utf8"));
|
|
1272
|
+
} catch {
|
|
1273
|
+
return {};
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
async function discoverSecrets(storefront, env = process.env, home = homedir()) {
|
|
1277
|
+
const local = await readEnvFile(join2(storefront, ".env.local"));
|
|
1278
|
+
const global = await readEnvFile(join2(home, ".env"));
|
|
1279
|
+
const merged = {};
|
|
1280
|
+
const keys = [...RUNTIME_SECRET_KEYS, ...PUBLIC_ENV_KEYS];
|
|
1281
|
+
for (const key of keys) {
|
|
1282
|
+
const value = local[key]?.trim() || env[key]?.trim() || global[key]?.trim();
|
|
1283
|
+
if (value) merged[key] = value;
|
|
1284
|
+
}
|
|
1285
|
+
return merged;
|
|
1286
|
+
}
|
|
1287
|
+
async function mergeEnvLocal(storefront, updates, opts = {}) {
|
|
1288
|
+
const path = join2(storefront, ".env.local");
|
|
1289
|
+
const overwrite = new Set(opts.overwrite ?? []);
|
|
1290
|
+
const original = await exists(path) ? await readFile(path, "utf8") : "";
|
|
1291
|
+
const lines = original ? original.split("\n") : [];
|
|
1292
|
+
const written = [];
|
|
1293
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
1294
|
+
if (!value) continue;
|
|
1295
|
+
const index = lines.findIndex((l) => l.trim().startsWith(`${key}=`));
|
|
1296
|
+
if (index === -1) {
|
|
1297
|
+
lines.push(`${key}=${value}`);
|
|
1298
|
+
written.push(key);
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
const current = lines[index].slice(lines[index].indexOf("=") + 1).trim();
|
|
1302
|
+
const isEmpty = current === "" || current === '""' || current === "''";
|
|
1303
|
+
if (isEmpty || overwrite.has(key)) {
|
|
1304
|
+
lines[index] = `${key}=${value}`;
|
|
1305
|
+
written.push(key);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
if (written.length === 0) return [];
|
|
1309
|
+
const body = lines.join("\n").replace(/\n{3,}$/, "\n");
|
|
1310
|
+
await writeFile(path, body.endsWith("\n") ? body : `${body}
|
|
1311
|
+
`);
|
|
1312
|
+
return written;
|
|
1313
|
+
}
|
|
1314
|
+
function needsCarrierKey(secrets) {
|
|
1315
|
+
const url = secrets.NEXT_PUBLIC_CARRIER_API_URL?.trim();
|
|
1316
|
+
return Boolean(url) && !secrets.CARRIER_API_KEY?.trim();
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// src/cli/lib/site.ts
|
|
1320
|
+
import { join as join3 } from "path";
|
|
1321
|
+
function slug(s) {
|
|
1322
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "storefront";
|
|
1323
|
+
}
|
|
1324
|
+
async function installDeps(target) {
|
|
1325
|
+
const pkgMgr = await which("pnpm") ? "pnpm" : "npm";
|
|
1326
|
+
const r = await runInherit(pkgMgr, ["install"], { cwd: target });
|
|
1327
|
+
return r.ok;
|
|
1328
|
+
}
|
|
1329
|
+
async function buildSite(target, forTarget = "cloudflare") {
|
|
1330
|
+
const pkgMgr = await which("pnpm") ? "pnpm" : "npm";
|
|
1331
|
+
const r = await runInherit(pkgMgr, ["run", buildScriptFor(forTarget)], { cwd: target });
|
|
1332
|
+
return r.ok;
|
|
1333
|
+
}
|
|
1334
|
+
async function loadStorefrontBrand(target, overrides) {
|
|
1335
|
+
const configPath = join3(target, "src", "brand.config.ts");
|
|
1336
|
+
if (!await exists(configPath)) {
|
|
1337
|
+
return { ...CARRIER_BRAND, ...overrides };
|
|
1338
|
+
}
|
|
1339
|
+
const src = await readFile(configPath, "utf8");
|
|
1340
|
+
const pick = (field, fallback) => {
|
|
1341
|
+
const m = src.match(new RegExp(`\\b${field}:\\s*(?:[^"\\n]*\\?\\?\\s*)?"([^"]*)"`));
|
|
1342
|
+
return m?.[1] ?? fallback;
|
|
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
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
async function deploySite(target, brand, opts = {}) {
|
|
1363
|
+
const statuses = await probeAll(target);
|
|
1364
|
+
let chosen;
|
|
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
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// src/cli/lib/clerk.ts
|
|
1423
|
+
var CLERK_API_BASE = "https://api.clerk.com/v1";
|
|
1424
|
+
var PUBLISHABLE_KEYS = [
|
|
1425
|
+
"CLERK_PUBLISHABLE_KEY",
|
|
1426
|
+
"NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY"
|
|
1427
|
+
];
|
|
1428
|
+
var SECRET_KEYS = ["CLERK_SECRET_KEY"];
|
|
1429
|
+
function firstNonEmpty(env, names) {
|
|
1430
|
+
for (const name of names) {
|
|
1431
|
+
const value = env[name]?.trim();
|
|
1432
|
+
if (value) return value;
|
|
1433
|
+
}
|
|
1434
|
+
return void 0;
|
|
1435
|
+
}
|
|
1436
|
+
function looksPublishable(key) {
|
|
1437
|
+
return /^pk_(test|live)_/.test(key);
|
|
1438
|
+
}
|
|
1439
|
+
function looksSecret(key) {
|
|
1440
|
+
return /^sk_(test|live)_/.test(key);
|
|
1441
|
+
}
|
|
1442
|
+
async function clerkFetch(path, token, init = {}) {
|
|
1443
|
+
try {
|
|
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
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
function clerkError(json) {
|
|
1468
|
+
if (typeof json !== "object" || json === null) return void 0;
|
|
1469
|
+
const errors = json.errors;
|
|
1470
|
+
if (!Array.isArray(errors) || errors.length === 0) return void 0;
|
|
1471
|
+
const first = errors[0];
|
|
1472
|
+
return first.long_message ?? first.message;
|
|
1473
|
+
}
|
|
1474
|
+
async function createClerkApplication(platformToken, opts) {
|
|
1475
|
+
const body = {
|
|
1476
|
+
name: opts.name,
|
|
1477
|
+
environment_types: opts.production ? ["development", "production"] : ["development"]
|
|
1478
|
+
};
|
|
1479
|
+
if (opts.domain) body.domain = opts.domain;
|
|
1480
|
+
const res = await clerkFetch("/platform/applications", platformToken, {
|
|
1481
|
+
method: "POST",
|
|
1482
|
+
body
|
|
1483
|
+
});
|
|
1484
|
+
if (!res.ok) {
|
|
1485
|
+
return {
|
|
1486
|
+
ok: false,
|
|
1487
|
+
tier: "platform",
|
|
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
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
const payload = res.json;
|
|
1492
|
+
const instances = payload?.instances ?? [];
|
|
1493
|
+
const wanted = opts.production ? "production" : "development";
|
|
1494
|
+
const instance = instances.find((i) => i.environment_type === wanted && i.secret_key && i.publishable_key) ?? instances.find((i) => i.secret_key && i.publishable_key);
|
|
1495
|
+
if (!instance?.secret_key || !instance.publishable_key) {
|
|
1496
|
+
return {
|
|
1497
|
+
ok: false,
|
|
1498
|
+
tier: "platform",
|
|
1499
|
+
reason: "Clerk created the application but returned no instance keys. Read them with GET /platform/applications?include_secret_keys=true."
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
return {
|
|
1503
|
+
ok: true,
|
|
1504
|
+
tier: "platform",
|
|
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
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
async function mintKeylessClerkApp(storefront, deps) {
|
|
1515
|
+
if (!await deps.which("npx")) {
|
|
1516
|
+
return { ok: false, tier: "cli-keyless", reason: "npx not found \u2014 cannot run the Clerk CLI." };
|
|
1517
|
+
}
|
|
1518
|
+
const restore = await deps.snapshot(storefront);
|
|
1519
|
+
try {
|
|
1520
|
+
const r = await deps.run(
|
|
1521
|
+
"npx",
|
|
1522
|
+
["--yes", "clerk@latest", "init", "--framework", "next", "--keyless", "--no-skills", "-y"],
|
|
1523
|
+
{ cwd: storefront, timeoutMs: 6e5 }
|
|
1524
|
+
);
|
|
1525
|
+
const envPath = `${storefront}/.env.local`;
|
|
1526
|
+
const body = await deps.readFileIfExists(envPath) ?? "";
|
|
1527
|
+
const publishableKey = matchEnv(body, "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY");
|
|
1528
|
+
const secretKey = matchEnv(body, "CLERK_SECRET_KEY");
|
|
1529
|
+
if (!publishableKey || !secretKey) {
|
|
1530
|
+
const tail = `${r.stdout}${r.stderr}`.trim().split("\n").slice(-2).join(" ").slice(0, 300);
|
|
1531
|
+
return {
|
|
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." };
|
|
1539
|
+
}
|
|
1540
|
+
return {
|
|
1541
|
+
ok: true,
|
|
1542
|
+
tier: "cli-keyless",
|
|
1543
|
+
credentials: { publishableKey, secretKey, tier: "cli-keyless" }
|
|
1544
|
+
};
|
|
1545
|
+
} finally {
|
|
1546
|
+
await restore();
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
function matchEnv(body, key) {
|
|
1550
|
+
const line = body.split("\n").find((l) => l.trim().startsWith(`${key}=`));
|
|
1551
|
+
const value = line?.slice(line.indexOf("=") + 1).trim();
|
|
1552
|
+
return value || void 0;
|
|
1553
|
+
}
|
|
1554
|
+
function discoverClerkCredentials(env) {
|
|
1555
|
+
const publishableKey = firstNonEmpty(env, PUBLISHABLE_KEYS);
|
|
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" };
|
|
1560
|
+
}
|
|
1561
|
+
var MANUAL_STEPS = [
|
|
1562
|
+
"Open https://dashboard.clerk.com and create an application.",
|
|
1563
|
+
"Copy the Publishable key (pk_...) and Secret key (sk_...) from API keys.",
|
|
1564
|
+
"Put them in the storefront's .env.local as NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY.",
|
|
1565
|
+
"Re-run `carrier site deploy` \u2014 the keys are pushed to the deploy target automatically."
|
|
1566
|
+
];
|
|
1567
|
+
async function provisionClerk(opts) {
|
|
1568
|
+
const platformToken = firstNonEmpty(opts.env, [
|
|
1569
|
+
"CLERK_PLATFORM_API_KEY",
|
|
1570
|
+
"CLERK_PLATFORM_TOKEN"
|
|
1571
|
+
]);
|
|
1572
|
+
const notes = [];
|
|
1573
|
+
if (platformToken && !opts.noCreate) {
|
|
1574
|
+
const created = await createClerkApplication(platformToken, {
|
|
1575
|
+
name: opts.name,
|
|
1576
|
+
domain: opts.domain,
|
|
1577
|
+
production: opts.production
|
|
1578
|
+
});
|
|
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
|
+
}
|
|
1591
|
+
if (opts.cli && opts.storefront && !opts.noCreate) {
|
|
1592
|
+
const minted = await mintKeylessClerkApp(opts.storefront, opts.cli);
|
|
1593
|
+
if (minted.ok) {
|
|
1594
|
+
return { ...minted, reason: notes.length ? notes.join(" ") : void 0 };
|
|
1595
|
+
}
|
|
1596
|
+
notes.push(minted.reason ?? "Clerk CLI keyless provisioning failed.");
|
|
1597
|
+
}
|
|
1598
|
+
if (!platformToken) {
|
|
1599
|
+
notes.push(
|
|
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
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
return {
|
|
1604
|
+
ok: false,
|
|
1605
|
+
tier: "manual",
|
|
1606
|
+
reason: notes.join(" "),
|
|
1607
|
+
guidance: MANUAL_STEPS
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
async function configureClerkInstance(secretKey, opts) {
|
|
1611
|
+
const applied = [];
|
|
1612
|
+
const failed = [];
|
|
1613
|
+
const origins = dedupe(opts.allowedOrigins ?? []);
|
|
1614
|
+
if (origins.length > 0) {
|
|
1615
|
+
const res = await clerkFetch("/instance", secretKey, {
|
|
1616
|
+
method: "PATCH",
|
|
1617
|
+
body: { allowed_origins: origins }
|
|
1618
|
+
});
|
|
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
|
+
}
|
|
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
|
+
return {
|
|
1639
|
+
allowedOrigins: bases,
|
|
1640
|
+
redirectUrls: bases.flatMap((b) => [b, `${b}/checkout/success`, `${b}/dashboard`])
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
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() {
|
|
1650
|
+
return {
|
|
1651
|
+
which,
|
|
1652
|
+
run: async (cmd, args, opts) => {
|
|
1653
|
+
const r = await run(cmd, args, opts);
|
|
1654
|
+
return { ok: r.ok, stdout: r.stdout, stderr: r.stderr };
|
|
1655
|
+
},
|
|
1656
|
+
readFileIfExists: async (path) => {
|
|
1657
|
+
if (!await exists(path)) return void 0;
|
|
1658
|
+
try {
|
|
1659
|
+
return await readFile(path, "utf8");
|
|
1660
|
+
} catch {
|
|
1661
|
+
return void 0;
|
|
1662
|
+
}
|
|
1663
|
+
},
|
|
1664
|
+
snapshot: async (storefront) => {
|
|
1665
|
+
const backup = await mkdtemp2(join4(tmpdir2(), "carrier-clerk-snap-"));
|
|
1666
|
+
const saved = [];
|
|
1667
|
+
for (const rel of SNAPSHOT_PATHS) {
|
|
1668
|
+
const src = join4(storefront, rel);
|
|
1669
|
+
if (!await exists(src)) continue;
|
|
1670
|
+
await cp(src, join4(backup, rel), { recursive: true });
|
|
1671
|
+
saved.push(rel);
|
|
1672
|
+
}
|
|
1673
|
+
return async () => {
|
|
1674
|
+
try {
|
|
1675
|
+
for (const rel of saved) {
|
|
1676
|
+
const target = join4(storefront, rel);
|
|
1677
|
+
await rm2(target, { recursive: true, force: true });
|
|
1678
|
+
await cp(join4(backup, rel), target, { recursive: true });
|
|
1679
|
+
}
|
|
1680
|
+
} finally {
|
|
1681
|
+
await rm2(backup, { recursive: true, force: true });
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
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";
|
|
1753
|
+
return {
|
|
1754
|
+
ok,
|
|
1755
|
+
url,
|
|
1756
|
+
probes,
|
|
1757
|
+
diagnosis,
|
|
1758
|
+
summary: SUMMARIES[diagnosis],
|
|
1759
|
+
repairable: !ok && REPAIRABLE.has(diagnosis)
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
function formatProbes(probes) {
|
|
1763
|
+
return probes.map((p) => {
|
|
1764
|
+
const status = p.status === 0 ? "unreachable" : String(p.status);
|
|
1765
|
+
const body = p.bodyOk === false ? " (no plans)" : "";
|
|
1766
|
+
return `${p.path} ${status}${body}`;
|
|
1767
|
+
}).join(" ");
|
|
1768
|
+
}
|
|
1769
|
+
function repairPlanFor(diagnosis) {
|
|
1770
|
+
if (diagnosis === "carrier-key-missing") {
|
|
1771
|
+
return {
|
|
1772
|
+
diagnosis,
|
|
1773
|
+
runClerk: false,
|
|
1774
|
+
// CARRIER_API_KEY is read at request time, so re-staging and redeploying
|
|
1775
|
+
// is enough; no rebuild needed.
|
|
1776
|
+
rebuild: false,
|
|
1777
|
+
note: "Re-resolve CARRIER_API_KEY, stage it, and redeploy."
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
if (diagnosis === "clerk-keys-missing") {
|
|
1781
|
+
return {
|
|
1782
|
+
diagnosis,
|
|
1783
|
+
runClerk: true,
|
|
1784
|
+
// The publishable key is inlined at build time, so this one must rebuild.
|
|
1785
|
+
rebuild: true,
|
|
1786
|
+
note: "Provision Clerk, rebuild so the publishable key is inlined, and redeploy."
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
return void 0;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// package.json
|
|
1793
|
+
var package_default = {
|
|
1794
|
+
name: "@carrierllc/mcp",
|
|
1795
|
+
version: "0.4.0",
|
|
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
|
+
license: "MIT",
|
|
1798
|
+
author: "Carrier (Lifecycle Innovations Limited)",
|
|
1799
|
+
homepage: "https://mcp.carrier.llc",
|
|
1800
|
+
repository: {
|
|
1801
|
+
type: "git",
|
|
1802
|
+
url: "https://github.com/Lifecycle-Innovations-Limited/carrier.llc"
|
|
1803
|
+
},
|
|
1804
|
+
type: "module",
|
|
1805
|
+
main: "./dist/index.js",
|
|
1806
|
+
files: [
|
|
1807
|
+
"dist",
|
|
1808
|
+
"plugin",
|
|
1809
|
+
"README.md",
|
|
1810
|
+
"templates",
|
|
1811
|
+
"!templates/**/node_modules",
|
|
1812
|
+
"!templates/**/.open-next",
|
|
1813
|
+
"!templates/**/.next",
|
|
1814
|
+
"!templates/**/.turbo",
|
|
1815
|
+
"!templates/**/.vercel",
|
|
1816
|
+
"!templates/**/.wrangler",
|
|
1817
|
+
"!templates/**/*.tsbuildinfo",
|
|
1818
|
+
"!templates/**/.env.local"
|
|
1819
|
+
],
|
|
1820
|
+
scripts: {
|
|
1821
|
+
build: "tsup",
|
|
1822
|
+
"type-check": "tsc --noEmit",
|
|
1823
|
+
lint: "eslint src",
|
|
1824
|
+
prepublishOnly: "pnpm run build",
|
|
1825
|
+
test: "pnpm run build && node --test test/*.test.js",
|
|
1826
|
+
"check:pack": "node scripts/check-pack-size.mjs"
|
|
1827
|
+
},
|
|
1828
|
+
dependencies: {
|
|
1829
|
+
"@clack/prompts": "^0.7.0",
|
|
1830
|
+
"@clerk/backend": "^3.14.0",
|
|
1831
|
+
"@modelcontextprotocol/ext-apps": "^1.7.5",
|
|
1832
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
1833
|
+
"@noble/hashes": "^1.8.0",
|
|
1834
|
+
"@sentry/cloudflare": "^10.65.0",
|
|
1835
|
+
aws4fetch: "^1.0.20",
|
|
1836
|
+
commander: "^15.0.0",
|
|
1837
|
+
picocolors: "^1.1.1",
|
|
1838
|
+
zod: "^4.4.3"
|
|
1839
|
+
},
|
|
1840
|
+
devDependencies: {
|
|
1841
|
+
"@cloudflare/workers-types": "^4.20260521.1",
|
|
1842
|
+
"@types/node": "^26.1.2",
|
|
1843
|
+
"@typescript-eslint/eslint-plugin": "^8.59.4",
|
|
1844
|
+
"@typescript-eslint/parser": "^8.67.0",
|
|
1845
|
+
eslint: "^9.39.4",
|
|
1846
|
+
tsup: "^8.5.1",
|
|
1847
|
+
typescript: "^5.9.3",
|
|
1848
|
+
"@carrier/ocs-client": "workspace:*",
|
|
1849
|
+
"@carrier/ocs-spec": "workspace:*",
|
|
1850
|
+
"@carrier/screens": "workspace:*"
|
|
1851
|
+
},
|
|
1852
|
+
keywords: [
|
|
1853
|
+
"mcp",
|
|
1854
|
+
"model-context-protocol",
|
|
1855
|
+
"esim",
|
|
1856
|
+
"mvno",
|
|
1857
|
+
"carrier",
|
|
1858
|
+
"fleet-management",
|
|
1859
|
+
"esimvault"
|
|
1860
|
+
],
|
|
1861
|
+
engines: {
|
|
1862
|
+
node: ">=22"
|
|
1863
|
+
},
|
|
1864
|
+
publishConfig: {
|
|
1865
|
+
access: "public"
|
|
1866
|
+
},
|
|
1867
|
+
bin: {
|
|
1868
|
+
"carrier-mcp": "dist/index.js",
|
|
1869
|
+
carrier: "dist/cli.js"
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
1872
|
+
|
|
1873
|
+
// src/version.ts
|
|
1874
|
+
var CARRIER_VERSION = package_default.version;
|
|
1875
|
+
|
|
1876
|
+
// ../../packages/ocs-client/dist/chunk-Y2LVKWI7.js
|
|
1877
|
+
var ENDPOINT_LIMITS_PER_MIN = {
|
|
1878
|
+
// Global sentinel (keyed as "__global__")
|
|
1879
|
+
__global__: 600,
|
|
1880
|
+
// 30/min — tightest endpoint
|
|
1881
|
+
subscriberNetworkEventsOverPeriod: 30,
|
|
1882
|
+
// 100/min
|
|
1883
|
+
subscriberUsageOverPeriod: 100,
|
|
1884
|
+
// 150/min
|
|
1885
|
+
listLocationZoneElement: 150,
|
|
1886
|
+
sendMtSms: 150,
|
|
1887
|
+
// 300/min (explicit; also the fallback default)
|
|
1888
|
+
modifySubscriberMobilePlan: 300,
|
|
1889
|
+
affectRecurringPackageToSubscriber: 300,
|
|
1890
|
+
affectSubscriberFakePhoneNumber: 300,
|
|
1891
|
+
affectSubscriberRealPhoneNumber: 300,
|
|
1892
|
+
esimStatusPerAccount: 300,
|
|
1893
|
+
getSubscriberActivePeriod: 300,
|
|
1894
|
+
hlrGetBitrate: 300,
|
|
1895
|
+
hlrSetBitrate: 300,
|
|
1896
|
+
listSubscriber: 300,
|
|
1897
|
+
listSubscriberPrepaidPackages: 300,
|
|
1898
|
+
moveSubscriberRangeToAccount: 300,
|
|
1899
|
+
pushSteeringToSubs: 300,
|
|
1900
|
+
setSubscriberTrafficRestrictions: 300,
|
|
1901
|
+
getSingleSubscriber: 300,
|
|
1902
|
+
getSubscriberLocationByCellId: 300
|
|
1903
|
+
};
|
|
1904
|
+
var DEFAULT_LIMIT_PER_MIN = 300;
|
|
1905
|
+
var WINDOW_MS2 = 6e4;
|
|
1906
|
+
var METRIC_EMIT_MIN_INTERVAL_MS = 1e4;
|
|
1907
|
+
var BATCH_FRACTION = 0.2;
|
|
1908
|
+
function resellerKeyHash(resellerKey) {
|
|
1909
|
+
if (!resellerKey) return "00000000";
|
|
1910
|
+
let h = 5381;
|
|
1911
|
+
for (let i = 0; i < resellerKey.length; i++) {
|
|
1912
|
+
h = ((h << 5) + h ^ resellerKey.charCodeAt(i)) >>> 0;
|
|
1913
|
+
}
|
|
1914
|
+
return h.toString(16).padStart(8, "0");
|
|
1915
|
+
}
|
|
1916
|
+
function emitMetric(endpoint, resellerKey, callsInWindow, limitPerMin) {
|
|
1917
|
+
const pct2 = Math.round(callsInWindow / limitPerMin * 100);
|
|
1918
|
+
const reseller_hash = resellerKeyHash(resellerKey);
|
|
1919
|
+
console.log(
|
|
1920
|
+
JSON.stringify({
|
|
1921
|
+
metric: "carrier_ocs_calls_per_min",
|
|
1922
|
+
endpoint,
|
|
1923
|
+
reseller_hash,
|
|
1924
|
+
calls_in_window: callsInWindow,
|
|
1925
|
+
limit_per_min: limitPerMin,
|
|
1926
|
+
utilisation_pct: pct2,
|
|
1927
|
+
alert: pct2 >= 80,
|
|
1928
|
+
ts: Date.now()
|
|
1929
|
+
})
|
|
1930
|
+
);
|
|
1931
|
+
if (pct2 >= 80) {
|
|
1932
|
+
console.warn(
|
|
1933
|
+
`[rate-governor] ALERT: ${endpoint} at ${pct2}% capacity (${callsInWindow}/${limitPerMin} per min) reseller=${reseller_hash}`
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
var _endpointWindows = /* @__PURE__ */ new Map();
|
|
1938
|
+
function _getEndpointState(key) {
|
|
1939
|
+
let s = _endpointWindows.get(key);
|
|
1940
|
+
if (!s) {
|
|
1941
|
+
s = {
|
|
1942
|
+
log: [],
|
|
1943
|
+
batchLog: [],
|
|
1944
|
+
gate: Promise.resolve(),
|
|
1945
|
+
lastMetricEmitAt: 0,
|
|
1946
|
+
metricBelowHighUtil: true
|
|
1947
|
+
};
|
|
1948
|
+
_endpointWindows.set(key, s);
|
|
1949
|
+
}
|
|
1950
|
+
return s;
|
|
1951
|
+
}
|
|
1952
|
+
function _prune(log, windowStart) {
|
|
1953
|
+
let i = 0;
|
|
1954
|
+
while (i < log.length && log[i] <= windowStart) i++;
|
|
1955
|
+
return i > 0 ? log.slice(i) : log;
|
|
1956
|
+
}
|
|
1957
|
+
async function acquireEndpointSlot(resellerId, endpoint, priority = "interactive") {
|
|
1958
|
+
const proc = globalThis["process"];
|
|
1959
|
+
if (proc?.env?.["RATE_FLOOR_DISABLED"] === "true" || globalThis["RATE_FLOOR_DISABLED"] === "true") {
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
await _acquireOneSlot(resellerId, "__global__", priority);
|
|
1963
|
+
await _acquireOneSlot(resellerId, endpoint, priority);
|
|
1964
|
+
}
|
|
1965
|
+
async function _acquireOneSlot(resellerId, endpoint, priority) {
|
|
1966
|
+
const bucketKey = `${resellerId}:${endpoint}`;
|
|
1967
|
+
const limitPerMin = ENDPOINT_LIMITS_PER_MIN[endpoint] ?? DEFAULT_LIMIT_PER_MIN;
|
|
1968
|
+
const batchCap = Math.floor(limitPerMin * BATCH_FRACTION);
|
|
1969
|
+
const state = _getEndpointState(bucketKey);
|
|
1970
|
+
const ticket = state.gate.then(async () => {
|
|
1971
|
+
while (true) {
|
|
1972
|
+
const now = Date.now();
|
|
1973
|
+
const windowStart = now - WINDOW_MS2;
|
|
1974
|
+
state.log = _prune(state.log, windowStart);
|
|
1975
|
+
state.batchLog = _prune(state.batchLog, windowStart);
|
|
1976
|
+
const totalInWindow = state.log.length;
|
|
1977
|
+
const batchInWindow = state.batchLog.length;
|
|
1978
|
+
if (totalInWindow > 0 && endpoint !== "__global__") {
|
|
1979
|
+
const utilPct = Math.round(totalInWindow / limitPerMin * 100);
|
|
1980
|
+
const intervalOk = now - state.lastMetricEmitAt >= METRIC_EMIT_MIN_INTERVAL_MS;
|
|
1981
|
+
const enteredHighUtil = utilPct >= 80 && state.metricBelowHighUtil;
|
|
1982
|
+
if (intervalOk || enteredHighUtil) {
|
|
1983
|
+
emitMetric(endpoint, resellerId, totalInWindow, limitPerMin);
|
|
1984
|
+
state.lastMetricEmitAt = now;
|
|
1985
|
+
if (utilPct >= 80) {
|
|
1986
|
+
state.metricBelowHighUtil = false;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
if (utilPct < 80) {
|
|
1990
|
+
state.metricBelowHighUtil = true;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
const globalFull = totalInWindow >= limitPerMin;
|
|
1994
|
+
const batchFull = priority === "batch" && batchInWindow >= batchCap;
|
|
1995
|
+
if (!globalFull && !batchFull) {
|
|
1996
|
+
state.log.push(now);
|
|
1997
|
+
if (priority === "batch") {
|
|
1998
|
+
state.batchLog.push(now);
|
|
1999
|
+
}
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
let waitMs;
|
|
2003
|
+
if (globalFull) {
|
|
2004
|
+
const oldest = state.log[0];
|
|
2005
|
+
waitMs = WINDOW_MS2 - (now - oldest) + 1;
|
|
2006
|
+
} else {
|
|
2007
|
+
const oldestBatch = state.batchLog[0];
|
|
2008
|
+
waitMs = WINDOW_MS2 - (now - oldestBatch) + 1;
|
|
2009
|
+
}
|
|
2010
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
2011
|
+
}
|
|
2012
|
+
});
|
|
2013
|
+
state.gate = ticket;
|
|
2014
|
+
return ticket;
|
|
2015
|
+
}
|
|
2016
|
+
function getLimitForEndpoint(endpoint) {
|
|
2017
|
+
return ENDPOINT_LIMITS_PER_MIN[endpoint] ?? DEFAULT_LIMIT_PER_MIN;
|
|
2018
|
+
}
|
|
2019
|
+
function getRateLimitWindowCounts(resellerKey, endpoint) {
|
|
2020
|
+
const bucketKey = `${resellerKey}:${endpoint}`;
|
|
2021
|
+
const state = _endpointWindows.get(bucketKey);
|
|
2022
|
+
if (!state) {
|
|
2023
|
+
return { calls_in_window: 0, batch_calls_in_window: 0 };
|
|
2024
|
+
}
|
|
2025
|
+
const windowStart = Date.now() - WINDOW_MS2;
|
|
2026
|
+
let calls_in_window = 0;
|
|
2027
|
+
for (const ts of state.log) {
|
|
2028
|
+
if (ts > windowStart) calls_in_window++;
|
|
2029
|
+
}
|
|
2030
|
+
let batch_calls_in_window = 0;
|
|
2031
|
+
for (const ts of state.batchLog) {
|
|
2032
|
+
if (ts > windowStart) batch_calls_in_window++;
|
|
2033
|
+
}
|
|
2034
|
+
return { calls_in_window, batch_calls_in_window };
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// ../../packages/ocs-client/dist/chunk-UVXJX6UZ.js
|
|
2038
|
+
var OCS_MAX_USAGE_WINDOW_DAYS = 7;
|
|
2039
|
+
function clampUsagePeriod(start, end, maxDays = OCS_MAX_USAGE_WINDOW_DAYS) {
|
|
2040
|
+
const endMs = Date.parse(`${end}T00:00:00Z`);
|
|
2041
|
+
const startMs = Date.parse(`${start}T00:00:00Z`);
|
|
2042
|
+
if (Number.isNaN(endMs) || Number.isNaN(startMs) || startMs > endMs) {
|
|
2043
|
+
return { start, end };
|
|
2044
|
+
}
|
|
2045
|
+
const minStartMs = endMs - (maxDays - 1) * 864e5;
|
|
2046
|
+
if (startMs < minStartMs) {
|
|
2047
|
+
return { start: new Date(minStartMs).toISOString().slice(0, 10), end };
|
|
2048
|
+
}
|
|
2049
|
+
return { start, end };
|
|
2050
|
+
}
|
|
2051
|
+
function lastNDaysPeriod(days = OCS_MAX_USAGE_WINDOW_DAYS, now = /* @__PURE__ */ new Date()) {
|
|
2052
|
+
const end = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
2053
|
+
const start = new Date(end);
|
|
2054
|
+
start.setUTCDate(end.getUTCDate() - (days - 1));
|
|
2055
|
+
return {
|
|
2056
|
+
start: start.toISOString().slice(0, 10),
|
|
2057
|
+
end: end.toISOString().slice(0, 10)
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
function locationParams(iccid) {
|
|
2061
|
+
return { iccid };
|
|
2062
|
+
}
|
|
2063
|
+
function subscriberIdParams(iccid) {
|
|
2064
|
+
return { iccid };
|
|
2065
|
+
}
|
|
2066
|
+
function networkEventsOverPeriodParams(iccid, start, end) {
|
|
2067
|
+
return usageOverPeriodParams(iccid, start, end);
|
|
2068
|
+
}
|
|
2069
|
+
function usageOverPeriodParams(iccid, start, end) {
|
|
2070
|
+
const period = clampUsagePeriod(start, end);
|
|
2071
|
+
return { subscriber: { iccid }, period };
|
|
2072
|
+
}
|
|
2073
|
+
function imsiFromSubscriberRecord(record) {
|
|
2074
|
+
if (!record || typeof record !== "object") return void 0;
|
|
2075
|
+
const rec = record;
|
|
2076
|
+
const inner = rec.getSingleSubscriber && typeof rec.getSingleSubscriber === "object" ? rec.getSingleSubscriber : rec;
|
|
2077
|
+
const direct = inner.imsi;
|
|
2078
|
+
if (typeof direct === "string" && direct.length > 0) return direct;
|
|
2079
|
+
for (const key of ["imsiList", "multiImsi"]) {
|
|
2080
|
+
const list = inner[key];
|
|
2081
|
+
if (!Array.isArray(list)) continue;
|
|
2082
|
+
for (const entry of list) {
|
|
2083
|
+
if (!entry || typeof entry !== "object") continue;
|
|
2084
|
+
const value = entry.imsi;
|
|
2085
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
return void 0;
|
|
2089
|
+
}
|
|
2090
|
+
function emptyEsimStatusCounts() {
|
|
2091
|
+
return { active: 0, suspended: 0, inventory: 0, other: 0, total: 0 };
|
|
2092
|
+
}
|
|
2093
|
+
function addEsimStatusRow(acc, rec) {
|
|
2094
|
+
const count = Number(rec.count ?? 0);
|
|
2095
|
+
if (!Number.isFinite(count) || count === 0) return;
|
|
2096
|
+
const str = String(rec.statusStr ?? rec.name ?? rec.status ?? "").toUpperCase();
|
|
2097
|
+
if (str === "ACTIVATED" || str === "ACTIVE" || str === "AFFECTED") {
|
|
2098
|
+
acc.active += count;
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
if (str === "SUSPENDED") {
|
|
2102
|
+
acc.suspended += count;
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
if (str === "FREE" || str === "INVENTORY" || str === "NOT_ACTIVATED" || str === "AVAILABLE") {
|
|
2106
|
+
acc.inventory += count;
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
const num = rec.statusNum;
|
|
2110
|
+
if (typeof num === "number") {
|
|
2111
|
+
if (num === 1) acc.active += count;
|
|
2112
|
+
else if (num === 0) acc.inventory += count;
|
|
2113
|
+
else if (num === 2) acc.suspended += count;
|
|
2114
|
+
else acc.other += count;
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
acc.other += count;
|
|
2118
|
+
}
|
|
2119
|
+
function extractEsimStatusCounts(json) {
|
|
2120
|
+
const acc = emptyEsimStatusCounts();
|
|
2121
|
+
const root = json ?? {};
|
|
2122
|
+
const payload = root.data ?? root.esimStatusPerAccount ?? root;
|
|
2123
|
+
const accounts = Array.isArray(payload) ? payload : Array.isArray(payload.account) ? payload.account : [];
|
|
2124
|
+
for (const account of accounts) {
|
|
2125
|
+
const rec = account ?? {};
|
|
2126
|
+
const sponsors = Array.isArray(rec.sponsor) ? rec.sponsor : [];
|
|
2127
|
+
if (sponsors.length > 0) {
|
|
2128
|
+
for (const sp of sponsors) {
|
|
2129
|
+
const esim = sp?.esim ?? {};
|
|
2130
|
+
const statuses = Array.isArray(esim.status) ? esim.status : [];
|
|
2131
|
+
for (const row of statuses) addEsimStatusRow(acc, row ?? {});
|
|
2132
|
+
}
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
acc.active += Number(rec.active ?? 0);
|
|
2136
|
+
acc.suspended += Number(rec.suspended ?? 0);
|
|
2137
|
+
acc.inventory += Number(rec.inventory ?? rec.notActivated ?? rec.free ?? 0);
|
|
2138
|
+
acc.other += Number(rec.other ?? rec.terminated ?? 0);
|
|
2139
|
+
}
|
|
2140
|
+
acc.total = acc.active + acc.suspended + acc.inventory + acc.other;
|
|
2141
|
+
return acc;
|
|
2142
|
+
}
|
|
2143
|
+
function esimStatusPerAccountParams(args) {
|
|
2144
|
+
if (typeof args === "number") return { accountId: args };
|
|
2145
|
+
if (args.accountId !== void 0) return { accountId: args.accountId };
|
|
2146
|
+
if (args.resellerId !== void 0) return { resellerId: args.resellerId };
|
|
2147
|
+
throw new Error("esimStatusPerAccountParams needs an accountId or a resellerId");
|
|
2148
|
+
}
|
|
2149
|
+
var UNLIMITED_DATA_BYTES = 100 * 1024 * 1024 * 1024;
|
|
2150
|
+
var BYTES_PER_MB = 1048576;
|
|
2151
|
+
var TEMPLATE_DATA_KEYS = ["databyte", "dataBytes", "dataLimit"];
|
|
2152
|
+
function readTemplateDataBytes(t) {
|
|
2153
|
+
for (const key of TEMPLATE_DATA_KEYS) {
|
|
2154
|
+
const v = t[key];
|
|
2155
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
2156
|
+
}
|
|
2157
|
+
return void 0;
|
|
2158
|
+
}
|
|
2159
|
+
function throttlePercentFromMb(mb, totalBytes = UNLIMITED_DATA_BYTES) {
|
|
2160
|
+
if (!Number.isFinite(mb) || mb <= 0 || totalBytes <= 0) return 100;
|
|
2161
|
+
const pct2 = Math.round(mb * BYTES_PER_MB / totalBytes * 100);
|
|
2162
|
+
return Math.min(100, Math.max(1, pct2));
|
|
2163
|
+
}
|
|
2164
|
+
var OCS_THROTTLE_KBPS = [
|
|
2165
|
+
128,
|
|
2166
|
+
256,
|
|
2167
|
+
384,
|
|
2168
|
+
512,
|
|
2169
|
+
1024,
|
|
2170
|
+
3072,
|
|
2171
|
+
5120,
|
|
2172
|
+
7680,
|
|
2173
|
+
10240,
|
|
2174
|
+
20480,
|
|
2175
|
+
51200,
|
|
2176
|
+
102400
|
|
2177
|
+
];
|
|
2178
|
+
var OCS_PERIODICITY = { DAILY: 0, WEEKLY: 1, MONTHLY: 2 };
|
|
2179
|
+
var OCS_PERIODICITY_VALUES = [0, 1, 2];
|
|
2180
|
+
var CEILING_MULTIPLIERS = [20, 25, 50, 100];
|
|
2181
|
+
var SECONDS_PER_DAY = 86400;
|
|
2182
|
+
function throttledBytesPerPeriod(kbps, periodDays = 1) {
|
|
2183
|
+
if (!Number.isFinite(kbps) || kbps <= 0) return 0;
|
|
2184
|
+
return kbps * 1e3 * SECONDS_PER_DAY * Math.max(1, periodDays) / 8;
|
|
2185
|
+
}
|
|
2186
|
+
function unlimitedCeilingFor(windowMb, throttleKbps, periodDays = 1) {
|
|
2187
|
+
const windowBytes = windowMb * BYTES_PER_MB;
|
|
2188
|
+
const unreachableAbove = windowBytes + throttledBytesPerPeriod(throttleKbps ?? 0, periodDays);
|
|
2189
|
+
for (const multiplier of CEILING_MULTIPLIERS) {
|
|
2190
|
+
const ceilingBytes = windowBytes * multiplier;
|
|
2191
|
+
if (ceilingBytes > unreachableAbove) {
|
|
2192
|
+
return { ceilingBytes, percent: Math.round(100 / multiplier) };
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
const last = CEILING_MULTIPLIERS[CEILING_MULTIPLIERS.length - 1];
|
|
2196
|
+
return { ceilingBytes: windowBytes * last, percent: Math.round(100 / last) };
|
|
2197
|
+
}
|
|
2198
|
+
function normalizePackageTemplate(template) {
|
|
2199
|
+
const out = { ...template };
|
|
2200
|
+
const unlimitedFlag = out.unlimited === true;
|
|
2201
|
+
const current = readTemplateDataBytes(out);
|
|
2202
|
+
const isUnlimited = unlimitedFlag || current === void 0 || current === 0;
|
|
2203
|
+
const throttleAfterMb = typeof out.throttleAfterMb === "number" ? out.throttleAfterMb : void 0;
|
|
2204
|
+
const throttleKbps = typeof out.throttleKbps === "number" ? out.throttleKbps : void 0;
|
|
2205
|
+
const periodDays = typeof out.perioddays === "number" ? out.perioddays : 1;
|
|
2206
|
+
if (throttleKbps !== void 0 && !OCS_THROTTLE_KBPS.includes(throttleKbps)) {
|
|
2207
|
+
throw new Error(
|
|
2208
|
+
`throttleKbps ${throttleKbps} is not an OCS bitrate. Allowed: ${OCS_THROTTLE_KBPS.join(", ")}. This field is a SPEED in Kbps, not a data allowance.`
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
const periodicity = out.recurringPeriodicityType;
|
|
2212
|
+
if (periodicity !== void 0 && !OCS_PERIODICITY_VALUES.includes(periodicity)) {
|
|
2213
|
+
throw new Error(
|
|
2214
|
+
`recurringPeriodicityType ${periodicity} is out of range. Allowed: 0=Daily, 1=Weekly, 2=Monthly. (The live v2 Mango templates use 3, which is not a valid value.)`
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
if (out.recurring === true && (periodicity === OCS_PERIODICITY.DAILY || periodicity === OCS_PERIODICITY.WEEKLY) && out.nbOccurrence === void 0) {
|
|
2218
|
+
throw new Error(
|
|
2219
|
+
"nbOccurrence is required for a daily or weekly recurring template; OCS only treats it as optional for monthly templates."
|
|
2220
|
+
);
|
|
2221
|
+
}
|
|
2222
|
+
if (throttleKbps !== void 0) out.throttlingThreshold1Limit = throttleKbps;
|
|
2223
|
+
delete out.throttleKbps;
|
|
2224
|
+
if (isUnlimited) {
|
|
2225
|
+
let ceiling = UNLIMITED_DATA_BYTES;
|
|
2226
|
+
if (throttleAfterMb !== void 0 && throttleAfterMb > 0) {
|
|
2227
|
+
const sized = unlimitedCeilingFor(throttleAfterMb, throttleKbps, periodDays);
|
|
2228
|
+
ceiling = sized.ceilingBytes;
|
|
2229
|
+
out.throttlingThreshold1Perc = sized.percent;
|
|
2230
|
+
}
|
|
2231
|
+
for (const key of TEMPLATE_DATA_KEYS) {
|
|
2232
|
+
if (key in out) out[key] = ceiling;
|
|
2233
|
+
}
|
|
2234
|
+
if (!TEMPLATE_DATA_KEYS.some((k) => k in out)) out.databyte = ceiling;
|
|
2235
|
+
}
|
|
2236
|
+
delete out.unlimited;
|
|
2237
|
+
if (throttleAfterMb !== void 0) {
|
|
2238
|
+
if (!isUnlimited) {
|
|
2239
|
+
const total = readTemplateDataBytes(out) ?? UNLIMITED_DATA_BYTES;
|
|
2240
|
+
out.throttlingThreshold1Perc = throttlePercentFromMb(throttleAfterMb, total);
|
|
2241
|
+
}
|
|
2242
|
+
out.throttlingActive = out.throttlingActive ?? true;
|
|
2243
|
+
}
|
|
2244
|
+
delete out.throttleAfterMb;
|
|
2245
|
+
return out;
|
|
2246
|
+
}
|
|
2247
|
+
function normalizePackageTemplateChanges(changes) {
|
|
2248
|
+
const out = { ...changes };
|
|
2249
|
+
const explicitlyUnlimited = out.unlimited === true;
|
|
2250
|
+
for (const key of TEMPLATE_DATA_KEYS) {
|
|
2251
|
+
if (!(key in out)) continue;
|
|
2252
|
+
if (out[key] === 0 || explicitlyUnlimited) out[key] = UNLIMITED_DATA_BYTES;
|
|
2253
|
+
}
|
|
2254
|
+
if (explicitlyUnlimited && !TEMPLATE_DATA_KEYS.some((k) => k in out)) {
|
|
2255
|
+
out.databyte = UNLIMITED_DATA_BYTES;
|
|
2256
|
+
}
|
|
2257
|
+
delete out.unlimited;
|
|
2258
|
+
const throttleAfterMb = out.throttleAfterMb;
|
|
2259
|
+
if (typeof throttleAfterMb === "number") {
|
|
2260
|
+
const total = readTemplateDataBytes(out) ?? UNLIMITED_DATA_BYTES;
|
|
2261
|
+
out.throttlingThreshold1Perc = throttlePercentFromMb(throttleAfterMb, total);
|
|
2262
|
+
}
|
|
2263
|
+
delete out.throttleAfterMb;
|
|
2264
|
+
return out;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
// src/lib/storefront-logo.ts
|
|
2268
|
+
var OPENAI_KEY_NAMES = [
|
|
2269
|
+
"OPENAI_API_KEY",
|
|
2270
|
+
"OPENAI_KEY",
|
|
2271
|
+
"CARRIER_OPENAI_API_KEY"
|
|
2272
|
+
];
|
|
2273
|
+
function firstImageKey(env = process.env) {
|
|
2274
|
+
for (const name of OPENAI_KEY_NAMES) {
|
|
2275
|
+
const v = env[name];
|
|
2276
|
+
if (v && v.trim() && !v.startsWith("__") && !v.includes("PLACEHOLD")) return v.trim();
|
|
2277
|
+
}
|
|
2278
|
+
return void 0;
|
|
2279
|
+
}
|
|
2280
|
+
function clampHex(hex, fallback) {
|
|
2281
|
+
const h = hex.trim();
|
|
2282
|
+
return /^#[0-9a-fA-F]{6}$/.test(h) ? h.toUpperCase() : fallback;
|
|
2283
|
+
}
|
|
2284
|
+
function hashName(name) {
|
|
2285
|
+
let h = 2166136261;
|
|
2286
|
+
for (let i = 0; i < name.length; i++) {
|
|
2287
|
+
h ^= name.charCodeAt(i);
|
|
2288
|
+
h = Math.imul(h, 16777619);
|
|
2289
|
+
}
|
|
2290
|
+
return h >>> 0;
|
|
2291
|
+
}
|
|
2292
|
+
function mix(hex, toward, t) {
|
|
2293
|
+
const parse = (x) => [
|
|
2294
|
+
parseInt(x.slice(1, 3), 16),
|
|
2295
|
+
parseInt(x.slice(3, 5), 16),
|
|
2296
|
+
parseInt(x.slice(5, 7), 16)
|
|
2297
|
+
];
|
|
2298
|
+
const a = parse(hex);
|
|
2299
|
+
const b = parse(toward);
|
|
2300
|
+
const ch = (i) => Math.round(a[i] + (b[i] - a[i]) * t).toString(16).padStart(2, "0");
|
|
2301
|
+
return `#${ch(0)}${ch(1)}${ch(2)}`;
|
|
2302
|
+
}
|
|
2303
|
+
function generateStorefrontLogoSvg(name, accent = "#FF6B35") {
|
|
2304
|
+
const brand = (name.trim() || "C").slice(0, 24);
|
|
2305
|
+
const letter = brand[0].toUpperCase();
|
|
2306
|
+
const accentHex = clampHex(accent, "#FF6B35");
|
|
2307
|
+
const deep = mix(accentHex, "#1A1812", 0.45);
|
|
2308
|
+
const light = mix(accentHex, "#FFF8EB", 0.35);
|
|
2309
|
+
const h = hashName(brand);
|
|
2310
|
+
const style = h % 4;
|
|
2311
|
+
const uid = `lg${(h % 1e6).toString(16)}`;
|
|
2312
|
+
let seal = "";
|
|
2313
|
+
if (style === 0) {
|
|
2314
|
+
seal = `<rect x="56" y="56" width="400" height="400" rx="96" fill="url(#${uid}g)"/>`;
|
|
2315
|
+
} else if (style === 1) {
|
|
2316
|
+
seal = `<circle cx="256" cy="256" r="196" fill="url(#${uid}g)"/>`;
|
|
2317
|
+
} else if (style === 2) {
|
|
2318
|
+
seal = `<path d="M256 48 L432 152 V360 L256 464 L80 360 V152 Z" fill="url(#${uid}g)"/>`;
|
|
2319
|
+
} else {
|
|
2320
|
+
seal = `<path d="M256 64 C320 64 400 96 424 176 C448 256 424 352 344 416 C280 464 232 464 168 416 C88 352 64 256 88 176 C112 96 192 64 256 64 Z" fill="url(#${uid}g)"/>`;
|
|
2321
|
+
}
|
|
2322
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2323
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512" role="img" aria-label="${escapeXml(brand)} logo">
|
|
2324
|
+
<defs>
|
|
2325
|
+
<linearGradient id="${uid}g" x1="0" y1="0" x2="1" y2="1">
|
|
2326
|
+
<stop offset="0%" stop-color="${light}"/>
|
|
2327
|
+
<stop offset="55%" stop-color="${accentHex}"/>
|
|
2328
|
+
<stop offset="100%" stop-color="${deep}"/>
|
|
2329
|
+
</linearGradient>
|
|
2330
|
+
</defs>
|
|
2331
|
+
<rect width="512" height="512" rx="96" fill="#FAFAF7"/>
|
|
2332
|
+
${seal}
|
|
2333
|
+
<text x="256" y="300" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-size="220" font-weight="700" fill="#FFF8EB">${escapeXml(letter)}</text>
|
|
2334
|
+
</svg>
|
|
2335
|
+
`;
|
|
2336
|
+
}
|
|
2337
|
+
function escapeXml(s) {
|
|
2338
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2339
|
+
}
|
|
2340
|
+
function logoPrompt(name, accent, tagline) {
|
|
2341
|
+
return [
|
|
2342
|
+
`App icon / brand mark for "${name}", a travel eSIM storefront.`,
|
|
2343
|
+
tagline ? `Tagline: ${tagline}.` : "",
|
|
2344
|
+
`Flat vector, single centered symbol, no letters unless they are a refined monogram of "${name[0] ?? "C"}".`,
|
|
2345
|
+
`Color: ${accent} on warm cream #FAFAF7. Soft squircle icon, premium, simple, high contrast.`,
|
|
2346
|
+
`No photorealism, no mockups, no shadows of devices, no watermarks.`
|
|
2347
|
+
].filter(Boolean).join(" ");
|
|
2348
|
+
}
|
|
2349
|
+
async function tryOpenAiLogoPng(name, accent, apiKey, tagline) {
|
|
2350
|
+
try {
|
|
2351
|
+
const res = await fetch("https://api.openai.com/v1/images/generations", {
|
|
2352
|
+
method: "POST",
|
|
2353
|
+
headers: {
|
|
2354
|
+
Authorization: `Bearer ${apiKey}`,
|
|
2355
|
+
"Content-Type": "application/json"
|
|
2356
|
+
},
|
|
2357
|
+
body: JSON.stringify({
|
|
2358
|
+
model: "dall-e-3",
|
|
2359
|
+
prompt: logoPrompt(name, accent, tagline),
|
|
2360
|
+
size: "1024x1024",
|
|
2361
|
+
quality: "hd",
|
|
2362
|
+
response_format: "b64_json",
|
|
2363
|
+
n: 1
|
|
2364
|
+
})
|
|
2365
|
+
});
|
|
2366
|
+
if (!res.ok) return void 0;
|
|
2367
|
+
const json = await res.json();
|
|
2368
|
+
return json.data?.[0]?.b64_json;
|
|
2369
|
+
} catch {
|
|
2370
|
+
return void 0;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
async function generateStorefrontLogo(input) {
|
|
2374
|
+
const name = input.name.trim() || "Carrier";
|
|
2375
|
+
const accent = clampHex(input.accent ?? "#FF6B35", "#FF6B35");
|
|
2376
|
+
const svg = generateStorefrontLogoSvg(name, accent);
|
|
2377
|
+
const key = firstImageKey(input.env ?? (typeof process !== "undefined" ? process.env : {}));
|
|
2378
|
+
if (key) {
|
|
2379
|
+
const png = await tryOpenAiLogoPng(name, accent, key, input.tagline);
|
|
2380
|
+
if (png) {
|
|
2381
|
+
return { svg, filename: "logo.svg", source: "openai", pngBase64: png };
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
return { svg, filename: "logo.svg", source: "svg" };
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
export {
|
|
2388
|
+
acquireEndpointSlot,
|
|
2389
|
+
getLimitForEndpoint,
|
|
2390
|
+
getRateLimitWindowCounts,
|
|
2391
|
+
OCS_MAX_USAGE_WINDOW_DAYS,
|
|
2392
|
+
clampUsagePeriod,
|
|
2393
|
+
lastNDaysPeriod,
|
|
2394
|
+
locationParams,
|
|
2395
|
+
subscriberIdParams,
|
|
2396
|
+
networkEventsOverPeriodParams,
|
|
2397
|
+
usageOverPeriodParams,
|
|
2398
|
+
imsiFromSubscriberRecord,
|
|
2399
|
+
extractEsimStatusCounts,
|
|
2400
|
+
esimStatusPerAccountParams,
|
|
2401
|
+
normalizePackageTemplate,
|
|
2402
|
+
normalizePackageTemplateChanges,
|
|
2403
|
+
renderHtml,
|
|
2404
|
+
renderTui,
|
|
2405
|
+
fleetScreen,
|
|
2406
|
+
subscribersScreen,
|
|
2407
|
+
usageScreen,
|
|
2408
|
+
packagesScreen,
|
|
2409
|
+
billingScreen,
|
|
2410
|
+
walletScreen,
|
|
2411
|
+
greenzoneScreen,
|
|
2412
|
+
storefrontScreen,
|
|
2413
|
+
generateStorefrontLogo,
|
|
2414
|
+
run,
|
|
2415
|
+
which,
|
|
2416
|
+
TARGET_IDS,
|
|
2417
|
+
isTargetId,
|
|
2418
|
+
rollback,
|
|
2419
|
+
probeAll,
|
|
2420
|
+
rankTargets,
|
|
2421
|
+
CARRIER_BRAND,
|
|
2422
|
+
deriveAccentDark,
|
|
2423
|
+
deriveAccentLight,
|
|
2424
|
+
deriveAccentPaletteSubs,
|
|
2425
|
+
renderEnv,
|
|
2426
|
+
mergeEnvLocal,
|
|
2427
|
+
slug,
|
|
2428
|
+
installDeps,
|
|
2429
|
+
buildSite,
|
|
2430
|
+
loadStorefrontBrand,
|
|
2431
|
+
deploySite,
|
|
2432
|
+
provisionClerk,
|
|
2433
|
+
configureClerkInstance,
|
|
2434
|
+
storefrontClerkUrls,
|
|
2435
|
+
clerkCliDeps,
|
|
2436
|
+
verifyStorefront,
|
|
2437
|
+
formatProbes,
|
|
2438
|
+
repairPlanFor,
|
|
2439
|
+
CARRIER_VERSION
|
|
2440
|
+
};
|
|
2441
|
+
//# sourceMappingURL=chunk-RBG4MKMW.js.map
|