@carrierllc/mcp 0.2.15 → 0.2.17
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/cli.js +644 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +1264 -243
- package/dist/index.js.map +1 -1
- package/package.json +21 -14
- package/plugin/.claude-plugin/marketplace.json +31 -0
- package/plugin/carrier/.claude-plugin/plugin.json +19 -0
- package/plugin/carrier/.mcp.json +8 -0
- package/plugin/carrier/README.md +48 -0
- package/plugin/carrier/agents/carrier-billing-auditor.md +16 -0
- package/plugin/carrier/agents/carrier-fleet-ops.md +15 -0
- package/plugin/carrier/agents/carrier-storefront-builder.md +17 -0
- package/plugin/carrier/commands/billing.md +16 -0
- package/plugin/carrier/commands/churn.md +15 -0
- package/plugin/carrier/commands/credits.md +15 -0
- package/plugin/carrier/commands/esim-status.md +15 -0
- package/plugin/carrier/commands/fleet.md +17 -0
- package/plugin/carrier/commands/greenzone.md +16 -0
- package/plugin/carrier/commands/onboard.md +17 -0
- package/plugin/carrier/commands/packages.md +18 -0
- package/plugin/carrier/commands/provision.md +20 -0
- package/plugin/carrier/commands/sms.md +18 -0
- package/plugin/carrier/commands/status.md +15 -0
- package/plugin/carrier/commands/storefront.md +21 -0
- package/plugin/carrier/commands/subscribers.md +18 -0
- package/plugin/carrier/commands/usage.md +16 -0
- package/plugin/carrier/skills/carrier-operations/SKILL.md +36 -0
- package/templates/storefront/eslint.config.mjs +15 -0
- package/templates/storefront/next.config.ts +22 -0
- package/templates/storefront/open-next.config.ts +3 -0
- package/templates/storefront/package.json +38 -0
- package/templates/storefront/pnpm-lock.yaml +4020 -0
- package/templates/storefront/postcss.config.mjs +7 -0
- package/templates/storefront/src/app/activate/[orderId]/page.tsx +28 -0
- package/templates/storefront/src/app/apple-icon.tsx +29 -0
- package/templates/storefront/src/app/checkout/[templateId]/CheckoutClient.tsx +52 -0
- package/templates/storefront/src/app/checkout/[templateId]/page.tsx +24 -0
- package/templates/storefront/src/app/contact/page.tsx +35 -0
- package/templates/storefront/src/app/dashboard/page.tsx +26 -0
- package/templates/storefront/src/app/globals.css +99 -0
- package/templates/storefront/src/app/help/page.tsx +25 -0
- package/templates/storefront/src/app/icon.tsx +29 -0
- package/templates/storefront/src/app/layout.tsx +56 -0
- package/templates/storefront/src/app/legal/acceptable-use/page.tsx +21 -0
- package/templates/storefront/src/app/legal/privacy/page.tsx +27 -0
- package/templates/storefront/src/app/legal/terms/page.tsx +25 -0
- package/templates/storefront/src/app/manifest.ts +18 -0
- package/templates/storefront/src/app/page.tsx +31 -0
- package/templates/storefront/src/app/robots.ts +9 -0
- package/templates/storefront/src/app/shop/ShopClient.tsx +27 -0
- package/templates/storefront/src/app/shop/page.tsx +9 -0
- package/templates/storefront/src/app/sign-in/[[...sign-in]]/page.tsx +24 -0
- package/templates/storefront/src/app/sign-up/[[...sign-up]]/page.tsx +13 -0
- package/templates/storefront/src/app/sitemap.ts +14 -0
- package/templates/storefront/src/brand.config.ts +28 -0
- package/templates/storefront/src/components/Providers.tsx +16 -0
- package/templates/storefront/src/components/faq/FaqSection.tsx +80 -0
- package/templates/storefront/src/components/footer/StorefrontFooter.tsx +61 -0
- package/templates/storefront/src/components/landing/HeroSection.tsx +52 -0
- package/templates/storefront/src/components/landing/PlanCard.tsx +61 -0
- package/templates/storefront/src/components/nav/StorefrontNav.tsx +69 -0
- package/templates/storefront/src/components/theme/clerk-appearance.ts +51 -0
- package/templates/storefront/src/components/theme/theme-provider.tsx +87 -0
- package/templates/storefront/src/components/theme/theme-script.tsx +24 -0
- package/templates/storefront/src/lib/conversion-events.ts +36 -0
- package/templates/storefront/src/lib/sanitize-auth-redirect.ts +16 -0
- package/templates/storefront/src/middleware.ts +25 -0
- package/templates/storefront/src/vendor/carrier/client.ts +95 -0
- package/templates/storefront/src/vendor/carrier/index.ts +2 -0
- package/templates/storefront/src/vendor/carrier/types.ts +21 -0
- package/templates/storefront/src/vendor/config/index.ts +2 -0
- package/templates/storefront/src/vendor/geo/index.ts +32 -0
- package/templates/storefront/src/vendor/ui/brand.ts +68 -0
- package/templates/storefront/src/vendor/ui/cn.ts +7 -0
- package/templates/storefront/src/vendor/ui/countryImagery.ts +46 -0
- package/templates/storefront/src/vendor/ui/index.ts +3 -0
- package/templates/storefront/tsconfig.json +23 -0
- package/templates/storefront/wrangler.jsonc +11 -0
- package/dist/.metadata_never_index +0 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
import { resolve as resolve2 } from "path";
|
|
8
|
+
|
|
9
|
+
// src/cli/lib/brand.ts
|
|
10
|
+
var CARRIER_BRAND = {
|
|
11
|
+
name: "Carrier",
|
|
12
|
+
legalName: "Lifecycle Innovations Limited",
|
|
13
|
+
tagline: "Programmable connectivity, on demand.",
|
|
14
|
+
domain: "carrier.llc",
|
|
15
|
+
supportEmail: "support@carrier.llc",
|
|
16
|
+
supportUrl: "https://carrier.llc/help",
|
|
17
|
+
supportWhatsapp: "+17864604829",
|
|
18
|
+
colors: {
|
|
19
|
+
bg: "#080C16",
|
|
20
|
+
accent: "#FF6B35",
|
|
21
|
+
accentDark: "#D9461C",
|
|
22
|
+
text: "#F5F1EA"
|
|
23
|
+
},
|
|
24
|
+
social: {
|
|
25
|
+
x: "@carrier_llc",
|
|
26
|
+
instagram: "@carrier.llc",
|
|
27
|
+
tiktok: "@carrier.llc"
|
|
28
|
+
},
|
|
29
|
+
carrierApiUrl: "https://api.carrier.llc"
|
|
30
|
+
};
|
|
31
|
+
var CARRIER_ACCENT_LIGHT = "#FFB088";
|
|
32
|
+
var CARRIER_ACCENT_GRADIENT_START = "#FF7A45";
|
|
33
|
+
function hexToRgb(hex) {
|
|
34
|
+
const h = hex.replace(/^#/, "");
|
|
35
|
+
if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
|
|
36
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
37
|
+
}
|
|
38
|
+
function rgbToHex(r, g, b, lower = false) {
|
|
39
|
+
const fmt = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
|
|
40
|
+
const out = `#${fmt(r)}${fmt(g)}${fmt(b)}`;
|
|
41
|
+
return lower ? out : out.toUpperCase();
|
|
42
|
+
}
|
|
43
|
+
function mixHexWithWhite(hex, whiteRatio) {
|
|
44
|
+
const rgb = hexToRgb(hex);
|
|
45
|
+
if (!rgb) return hex;
|
|
46
|
+
const mix = (n) => n + (255 - n) * whiteRatio;
|
|
47
|
+
return rgbToHex(mix(rgb[0]), mix(rgb[1]), mix(rgb[2]));
|
|
48
|
+
}
|
|
49
|
+
function darkenHex(hex, factor) {
|
|
50
|
+
const rgb = hexToRgb(hex);
|
|
51
|
+
if (!rgb) return hex;
|
|
52
|
+
const scale = (n) => n * factor;
|
|
53
|
+
return rgbToHex(scale(rgb[0]), scale(rgb[1]), scale(rgb[2]), true);
|
|
54
|
+
}
|
|
55
|
+
function deriveAccentDark(accent, seed = CARRIER_BRAND) {
|
|
56
|
+
if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return seed.colors.accentDark;
|
|
57
|
+
const rgb = hexToRgb(accent);
|
|
58
|
+
if (!rgb) return accent;
|
|
59
|
+
const scale = (n) => Math.max(0, Math.min(255, Math.round(n * 0.75)));
|
|
60
|
+
return rgbToHex(scale(rgb[0]), scale(rgb[1]), scale(rgb[2]));
|
|
61
|
+
}
|
|
62
|
+
function deriveAccentLight(accent, seed = CARRIER_BRAND) {
|
|
63
|
+
if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return CARRIER_ACCENT_LIGHT;
|
|
64
|
+
return mixHexWithWhite(accent, 0.45);
|
|
65
|
+
}
|
|
66
|
+
function deriveAccentGradientStart(accent, seed = CARRIER_BRAND) {
|
|
67
|
+
if (accent.toUpperCase() === seed.colors.accent.toUpperCase()) return CARRIER_ACCENT_GRADIENT_START;
|
|
68
|
+
return mixHexWithWhite(accent, 0.08);
|
|
69
|
+
}
|
|
70
|
+
function deriveAccentPaletteSubs(accent, accentDark) {
|
|
71
|
+
if (accent.toUpperCase() === CARRIER_BRAND.colors.accent.toUpperCase()) return [];
|
|
72
|
+
const accentLight = deriveAccentLight(accent);
|
|
73
|
+
const gradientStart = deriveAccentGradientStart(accent);
|
|
74
|
+
return [
|
|
75
|
+
["#FF6B35", accent],
|
|
76
|
+
["#D9461C", accentDark],
|
|
77
|
+
["#FFB088", accentLight],
|
|
78
|
+
["#FF7A45", gradientStart],
|
|
79
|
+
["#fff4ef", mixHexWithWhite(accent, 0.94).toLowerCase()],
|
|
80
|
+
["#ffe0d0", mixHexWithWhite(accent, 0.85).toLowerCase()],
|
|
81
|
+
["#ffbfa0", mixHexWithWhite(accent, 0.7).toLowerCase()],
|
|
82
|
+
["#ff9970", mixHexWithWhite(accent, 0.55).toLowerCase()],
|
|
83
|
+
["#ff7d4d", mixHexWithWhite(accent, 0.4).toLowerCase()],
|
|
84
|
+
["#b33a17", darkenHex(accentDark, 0.75)],
|
|
85
|
+
["#8a2d12", darkenHex(accentDark, 0.58)],
|
|
86
|
+
["#5e1e0c", darkenHex(accentDark, 0.4)],
|
|
87
|
+
["#3a1107", darkenHex(accentDark, 0.25)]
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
function renderEnv(brand) {
|
|
91
|
+
return [
|
|
92
|
+
`# Generated by @carrierllc/mcp`,
|
|
93
|
+
`NEXT_PUBLIC_BRAND_NAME=${JSON.stringify(brand.name)}`,
|
|
94
|
+
`NEXT_PUBLIC_CARRIER_API_URL=${JSON.stringify(brand.carrierApiUrl)}`,
|
|
95
|
+
`# Clerk (fill in from your Clerk dashboard to enable auth + checkout)`,
|
|
96
|
+
`NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=`,
|
|
97
|
+
`CLERK_SECRET_KEY=`,
|
|
98
|
+
``
|
|
99
|
+
].join("\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/cli/lib/plugin.ts
|
|
103
|
+
import { join as join3 } from "path";
|
|
104
|
+
|
|
105
|
+
// src/cli/lib/paths.ts
|
|
106
|
+
import { fileURLToPath } from "url";
|
|
107
|
+
import { dirname, join, resolve } from "path";
|
|
108
|
+
import { existsSync } from "fs";
|
|
109
|
+
import { homedir } from "os";
|
|
110
|
+
function packageRoot() {
|
|
111
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
112
|
+
for (const candidate of [resolve(here, ".."), resolve(here, "..", "..")]) {
|
|
113
|
+
if (existsSync(join(candidate, "plugin")) && existsSync(join(candidate, "templates"))) {
|
|
114
|
+
return candidate;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return resolve(here, "..");
|
|
118
|
+
}
|
|
119
|
+
function pluginSourceDir() {
|
|
120
|
+
return join(packageRoot(), "plugin");
|
|
121
|
+
}
|
|
122
|
+
function pluginManifestDir() {
|
|
123
|
+
return join(packageRoot(), "plugin", "carrier");
|
|
124
|
+
}
|
|
125
|
+
function storefrontTemplateDir() {
|
|
126
|
+
return join(packageRoot(), "templates", "storefront");
|
|
127
|
+
}
|
|
128
|
+
function claudeHome() {
|
|
129
|
+
return process.env.CLAUDE_CONFIG_DIR ? resolve(process.env.CLAUDE_CONFIG_DIR) : join(homedir(), ".claude");
|
|
130
|
+
}
|
|
131
|
+
function claudePluginsDir() {
|
|
132
|
+
return join(claudeHome(), "plugins");
|
|
133
|
+
}
|
|
134
|
+
function installedPluginDir() {
|
|
135
|
+
return join(claudePluginsDir(), "carrier");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/cli/lib/fsx.ts
|
|
139
|
+
import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "fs/promises";
|
|
140
|
+
import { existsSync as existsSync2 } from "fs";
|
|
141
|
+
import { join as join2 } from "path";
|
|
142
|
+
var SKIP = /* @__PURE__ */ new Set([
|
|
143
|
+
"node_modules",
|
|
144
|
+
".next",
|
|
145
|
+
".turbo",
|
|
146
|
+
".vercel",
|
|
147
|
+
"dist",
|
|
148
|
+
".git",
|
|
149
|
+
"test-results",
|
|
150
|
+
"playwright-report"
|
|
151
|
+
]);
|
|
152
|
+
var cpFilter = (s) => {
|
|
153
|
+
const base = s.split("/").pop() ?? "";
|
|
154
|
+
return !SKIP.has(base);
|
|
155
|
+
};
|
|
156
|
+
async function copyTree(src, dest) {
|
|
157
|
+
await mkdir(dest, { recursive: true });
|
|
158
|
+
for (const entry of await readdir(src, { withFileTypes: true })) {
|
|
159
|
+
if (SKIP.has(entry.name)) continue;
|
|
160
|
+
await cp(join2(src, entry.name), join2(dest, entry.name), {
|
|
161
|
+
recursive: true,
|
|
162
|
+
filter: cpFilter
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function pruneTree(src, dest) {
|
|
167
|
+
if (!await isDir(dest)) return;
|
|
168
|
+
for (const entry of await readdir(dest, { withFileTypes: true })) {
|
|
169
|
+
if (SKIP.has(entry.name)) continue;
|
|
170
|
+
const srcPath = join2(src, entry.name);
|
|
171
|
+
const destPath = join2(dest, entry.name);
|
|
172
|
+
if (!existsSync2(srcPath)) {
|
|
173
|
+
await rm(destPath, { recursive: true, force: true });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (entry.isDirectory() && await isDir(srcPath)) await pruneTree(srcPath, destPath);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function* walk(dir) {
|
|
180
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
181
|
+
if (SKIP.has(entry.name)) continue;
|
|
182
|
+
const full = join2(dir, entry.name);
|
|
183
|
+
if (entry.isDirectory()) yield* walk(full);
|
|
184
|
+
else yield full;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
var TEXT_EXT = /* @__PURE__ */ new Set([
|
|
188
|
+
".ts",
|
|
189
|
+
".tsx",
|
|
190
|
+
".js",
|
|
191
|
+
".jsx",
|
|
192
|
+
".mjs",
|
|
193
|
+
".cjs",
|
|
194
|
+
".json",
|
|
195
|
+
".css",
|
|
196
|
+
".md",
|
|
197
|
+
".mdx",
|
|
198
|
+
".html",
|
|
199
|
+
".txt",
|
|
200
|
+
".env",
|
|
201
|
+
".example",
|
|
202
|
+
".yml",
|
|
203
|
+
".yaml",
|
|
204
|
+
".toml"
|
|
205
|
+
]);
|
|
206
|
+
function isTextFile(path) {
|
|
207
|
+
const dot = path.lastIndexOf(".");
|
|
208
|
+
if (dot === -1) return false;
|
|
209
|
+
return TEXT_EXT.has(path.slice(dot));
|
|
210
|
+
}
|
|
211
|
+
async function replaceInTree(dir, subs) {
|
|
212
|
+
let touched = 0;
|
|
213
|
+
for await (const file of walk(dir)) {
|
|
214
|
+
if (!isTextFile(file)) continue;
|
|
215
|
+
const before = await readFile(file, "utf8");
|
|
216
|
+
let after = before;
|
|
217
|
+
for (const [from, to] of subs) {
|
|
218
|
+
after = typeof from === "string" ? after.split(from).join(to) : after.replace(from, to);
|
|
219
|
+
}
|
|
220
|
+
if (after !== before) {
|
|
221
|
+
await writeFile(file, after);
|
|
222
|
+
touched++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return touched;
|
|
226
|
+
}
|
|
227
|
+
async function exists(p2) {
|
|
228
|
+
return existsSync2(p2);
|
|
229
|
+
}
|
|
230
|
+
async function isDir(p2) {
|
|
231
|
+
try {
|
|
232
|
+
return (await stat(p2)).isDirectory();
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/cli/lib/exec.ts
|
|
239
|
+
import { spawn } from "child_process";
|
|
240
|
+
function run(cmd, args, opts = {}) {
|
|
241
|
+
return new Promise((resolve3) => {
|
|
242
|
+
const child = spawn(cmd, args, { cwd: opts.cwd, shell: false });
|
|
243
|
+
let stdout = "";
|
|
244
|
+
let stderr = "";
|
|
245
|
+
child.stdout?.on("data", (d) => stdout += d.toString());
|
|
246
|
+
child.stderr?.on("data", (d) => stderr += d.toString());
|
|
247
|
+
child.on("error", () => resolve3({ ok: false, code: null, stdout, stderr }));
|
|
248
|
+
child.on("close", (code) => resolve3({ ok: code === 0, code, stdout, stderr }));
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function runInherit(cmd, args, opts = {}) {
|
|
252
|
+
return new Promise((resolve3) => {
|
|
253
|
+
const child = spawn(cmd, args, { cwd: opts.cwd, shell: false, stdio: "inherit" });
|
|
254
|
+
child.on("error", () => resolve3({ ok: false, code: null, stdout: "", stderr: "" }));
|
|
255
|
+
child.on("close", (code) => resolve3({ ok: code === 0, code, stdout: "", stderr: "" }));
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async function which(bin) {
|
|
259
|
+
const probe = process.platform === "win32" ? "where" : "which";
|
|
260
|
+
const r = await run(probe, [bin]);
|
|
261
|
+
return r.ok && r.stdout.trim().length > 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/cli/lib/plugin.ts
|
|
265
|
+
var MCP_NAME = "carrier";
|
|
266
|
+
var MCP_URL = "https://mcp.carrier.llc/mcp";
|
|
267
|
+
async function installPlugin() {
|
|
268
|
+
const marketplace = pluginSourceDir();
|
|
269
|
+
const pluginRoot = pluginManifestDir();
|
|
270
|
+
const dest = installedPluginDir();
|
|
271
|
+
const notes = [];
|
|
272
|
+
if (!await exists(pluginRoot)) {
|
|
273
|
+
throw new Error(`Bundled plugin not found at ${pluginRoot}. Reinstall @carrierllc/mcp.`);
|
|
274
|
+
}
|
|
275
|
+
await copyTree(pluginRoot, dest);
|
|
276
|
+
const claudeFound = await which("claude");
|
|
277
|
+
let marketplaceAdded = false;
|
|
278
|
+
let pluginInstalled = false;
|
|
279
|
+
let mcpAdded = false;
|
|
280
|
+
if (claudeFound) {
|
|
281
|
+
const mkt = await run("claude", ["plugin", "marketplace", "add", marketplace]);
|
|
282
|
+
marketplaceAdded = mkt.ok && !/✘|failed|error/i.test(mkt.stdout + mkt.stderr) || /already/i.test(mkt.stderr + mkt.stdout);
|
|
283
|
+
if (!marketplaceAdded) notes.push(`marketplace add: ${(mkt.stderr || mkt.stdout).trim().slice(0, 200)}`);
|
|
284
|
+
const inst = await run("claude", ["plugin", "install", "carrier@carrier"]);
|
|
285
|
+
const instOut = inst.stdout + inst.stderr;
|
|
286
|
+
pluginInstalled = /✔|success(fully)? installed|already (installed|enabled)/i.test(instOut) && !/✘|failed/i.test(instOut);
|
|
287
|
+
if (!pluginInstalled) notes.push(`plugin install: ${instOut.replace(/\s+/g, " ").trim().slice(0, 200)}`);
|
|
288
|
+
const mcp = await run("claude", ["mcp", "add", "--transport", "http", MCP_NAME, MCP_URL]);
|
|
289
|
+
mcpAdded = mcp.ok || /already|exists/i.test(mcp.stderr + mcp.stdout);
|
|
290
|
+
if (!mcpAdded) notes.push(`mcp add: ${(mcp.stderr || mcp.stdout).trim().slice(0, 200)}`);
|
|
291
|
+
} else {
|
|
292
|
+
notes.push("`claude` CLI not on PATH \u2014 plugin copied; finish wiring with the manual commands below.");
|
|
293
|
+
}
|
|
294
|
+
return { copiedTo: dest, marketplaceAdded, pluginInstalled, mcpAdded, claudeFound, notes };
|
|
295
|
+
}
|
|
296
|
+
async function pluginStatus() {
|
|
297
|
+
const dir = installedPluginDir();
|
|
298
|
+
const manifest = join3(dir, ".claude-plugin", "plugin.json");
|
|
299
|
+
if (!await exists(manifest)) return { installed: false, dir };
|
|
300
|
+
try {
|
|
301
|
+
const json = JSON.parse(await readFile(manifest, "utf8"));
|
|
302
|
+
return { installed: true, version: json.version, dir };
|
|
303
|
+
} catch {
|
|
304
|
+
return { installed: true, dir };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function manualCommands() {
|
|
308
|
+
return [
|
|
309
|
+
`claude plugin marketplace add ${pluginSourceDir()}`,
|
|
310
|
+
`claude plugin install carrier@carrier`,
|
|
311
|
+
`claude mcp add --transport http ${MCP_NAME} ${MCP_URL}`
|
|
312
|
+
];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/cli/lib/whitelabel.ts
|
|
316
|
+
import { join as join4 } from "path";
|
|
317
|
+
function preserveClerkFromEnv(existing, fresh) {
|
|
318
|
+
let out = fresh;
|
|
319
|
+
for (const key of ["NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", "CLERK_SECRET_KEY"]) {
|
|
320
|
+
const line = existing.split("\n").find((l) => l.startsWith(`${key}=`));
|
|
321
|
+
if (!line) continue;
|
|
322
|
+
const value = line.slice(key.length + 1);
|
|
323
|
+
if (!value.trim()) continue;
|
|
324
|
+
out = out.replace(new RegExp(`^${key}=.*$`, "m"), `${key}=${value}`);
|
|
325
|
+
}
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
async function scaffoldStorefront(target, brand) {
|
|
329
|
+
const template = storefrontTemplateDir();
|
|
330
|
+
if (!await exists(template)) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Storefront template not found at ${template}. The @carrierllc/mcp package may be corrupt \u2014 reinstall it.`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
const envPath = join4(target, ".env.local");
|
|
336
|
+
const existingEnv = await exists(envPath) ? await readFile(envPath, "utf8") : void 0;
|
|
337
|
+
await copyTree(template, target);
|
|
338
|
+
await pruneTree(template, target);
|
|
339
|
+
const subs = [
|
|
340
|
+
["Mango", brand.name],
|
|
341
|
+
["\u{1F96D}", ""]
|
|
342
|
+
];
|
|
343
|
+
subs.push(...deriveAccentPaletteSubs(brand.colors.accent, brand.colors.accentDark));
|
|
344
|
+
if (brand.colors.bg.toUpperCase() !== "#080C16") subs.push(["#080C16", brand.colors.bg]);
|
|
345
|
+
if (brand.colors.text.toUpperCase() !== "#F5F1EA") subs.push(["#F5F1EA", brand.colors.text]);
|
|
346
|
+
await replaceInTree(target, subs);
|
|
347
|
+
await patchBrandConfig(join4(target, "src", "brand.config.ts"), brand);
|
|
348
|
+
const envBody = existingEnv ? preserveClerkFromEnv(existingEnv, renderEnv(brand)) : renderEnv(brand);
|
|
349
|
+
await writeFile(envPath, envBody);
|
|
350
|
+
await writeFile(join4(target, ".npmrc"), "ignore-workspace-root-check=true\n");
|
|
351
|
+
}
|
|
352
|
+
async function patchBrandConfig(path, brand) {
|
|
353
|
+
if (!await exists(path)) return;
|
|
354
|
+
let src = await readFile(path, "utf8");
|
|
355
|
+
const setField = (field, value) => {
|
|
356
|
+
const re = new RegExp(`(\\b${field}:\\s*(?:[^"\\n]*\\?\\?\\s*)?)"[^"]*"`);
|
|
357
|
+
src = src.replace(re, `$1${JSON.stringify(value)}`);
|
|
358
|
+
};
|
|
359
|
+
setField("name", brand.name);
|
|
360
|
+
setField("legalName", brand.legalName);
|
|
361
|
+
setField("tagline", brand.tagline);
|
|
362
|
+
setField("domain", brand.domain);
|
|
363
|
+
setField("supportUrl", brand.supportUrl);
|
|
364
|
+
setField("supportEmail", brand.supportEmail);
|
|
365
|
+
setField("supportWhatsapp", brand.supportWhatsapp);
|
|
366
|
+
setField("accent", brand.colors.accent);
|
|
367
|
+
setField("accentDark", brand.colors.accentDark);
|
|
368
|
+
setField("accentLight", deriveAccentLight(brand.colors.accent));
|
|
369
|
+
setField("bg", brand.colors.bg);
|
|
370
|
+
const socialEntries = Object.entries(brand.social).filter(([, v]) => v);
|
|
371
|
+
const socialBody = socialEntries.map(([k, v]) => ` ${k}: ${JSON.stringify(v)},`).join("\n");
|
|
372
|
+
src = src.replace(
|
|
373
|
+
/social:\s*\{[^}]*\}/s,
|
|
374
|
+
socialEntries.length ? `social: {
|
|
375
|
+
${socialBody}
|
|
376
|
+
}` : "social: {}"
|
|
377
|
+
);
|
|
378
|
+
await writeFile(path, src);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/cli/lib/site.ts
|
|
382
|
+
import { join as join5 } from "path";
|
|
383
|
+
function slug(s) {
|
|
384
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "storefront";
|
|
385
|
+
}
|
|
386
|
+
async function installDeps(target) {
|
|
387
|
+
const pkgMgr = await which("pnpm") ? "pnpm" : "npm";
|
|
388
|
+
const r = await runInherit(pkgMgr, ["install"], { cwd: target });
|
|
389
|
+
return r.ok;
|
|
390
|
+
}
|
|
391
|
+
async function buildSite(target) {
|
|
392
|
+
const pkgMgr = await which("pnpm") ? "pnpm" : "npm";
|
|
393
|
+
const r = await runInherit(pkgMgr, ["run", "cf:build"], { cwd: target });
|
|
394
|
+
return r.ok;
|
|
395
|
+
}
|
|
396
|
+
async function loadStorefrontBrand(target, overrides) {
|
|
397
|
+
const configPath = join5(target, "src", "brand.config.ts");
|
|
398
|
+
if (!await exists(configPath)) {
|
|
399
|
+
return { ...CARRIER_BRAND, ...overrides };
|
|
400
|
+
}
|
|
401
|
+
const src = await readFile(configPath, "utf8");
|
|
402
|
+
const pick = (field, fallback) => {
|
|
403
|
+
const m = src.match(new RegExp(`\\b${field}:\\s*(?:[^"\\n]*\\?\\?\\s*)?"([^"]*)"`));
|
|
404
|
+
return m?.[1] ?? fallback;
|
|
405
|
+
};
|
|
406
|
+
return {
|
|
407
|
+
...CARRIER_BRAND,
|
|
408
|
+
name: overrides?.name ?? pick("name", CARRIER_BRAND.name),
|
|
409
|
+
domain: pick("domain", CARRIER_BRAND.domain),
|
|
410
|
+
supportEmail: pick("supportEmail", CARRIER_BRAND.supportEmail),
|
|
411
|
+
supportUrl: pick("supportUrl", CARRIER_BRAND.supportUrl),
|
|
412
|
+
tagline: pick("tagline", CARRIER_BRAND.tagline),
|
|
413
|
+
legalName: pick("legalName", CARRIER_BRAND.legalName),
|
|
414
|
+
colors: {
|
|
415
|
+
...CARRIER_BRAND.colors,
|
|
416
|
+
accent: pick("accent", CARRIER_BRAND.colors.accent),
|
|
417
|
+
accentDark: pick("accentDark", CARRIER_BRAND.colors.accentDark),
|
|
418
|
+
bg: pick("bg", CARRIER_BRAND.colors.bg),
|
|
419
|
+
text: pick("text", CARRIER_BRAND.colors.text)
|
|
420
|
+
},
|
|
421
|
+
carrierApiUrl: CARRIER_BRAND.carrierApiUrl
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
async function deploySite(target, brand) {
|
|
425
|
+
const projectName = slug(brand.name);
|
|
426
|
+
const workerBundle = join5(target, ".open-next", "worker.js");
|
|
427
|
+
if (!await exists(workerBundle)) {
|
|
428
|
+
return {
|
|
429
|
+
ok: false,
|
|
430
|
+
projectName,
|
|
431
|
+
reason: "No OpenNext build found \u2014 run `carrier site deploy` (build step) or `pnpm run build` in the storefront first."
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (!await which("wrangler") && !await which("npx")) {
|
|
435
|
+
return { ok: false, projectName, reason: "wrangler/npx not found \u2014 install wrangler to deploy." };
|
|
436
|
+
}
|
|
437
|
+
const bin = await which("wrangler") ? "wrangler" : "npx";
|
|
438
|
+
const args = bin === "wrangler" ? ["deploy", "--name", projectName] : ["wrangler", "deploy", "--name", projectName];
|
|
439
|
+
const r = await runInherit(bin, args, { cwd: target });
|
|
440
|
+
return r.ok ? { ok: true, projectName } : { ok: false, projectName, reason: "wrangler deploy failed \u2014 run `wrangler login` then retry `carrier site deploy`." };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/cli/index.ts
|
|
444
|
+
var VERSION = "0.2.17";
|
|
445
|
+
function header() {
|
|
446
|
+
p.intro(`${pc.bold(pc.yellow("\u25C6 carrier"))} ${pc.dim("\xB7 the Stripe of telecom \u2014 CLI v" + VERSION)}`);
|
|
447
|
+
}
|
|
448
|
+
function ok(msg) {
|
|
449
|
+
p.log.success(pc.green(msg));
|
|
450
|
+
}
|
|
451
|
+
function info(msg) {
|
|
452
|
+
p.log.info(msg);
|
|
453
|
+
}
|
|
454
|
+
async function promptBrand(seed) {
|
|
455
|
+
const name = await p.text({
|
|
456
|
+
message: "Brand name",
|
|
457
|
+
placeholder: seed.name,
|
|
458
|
+
defaultValue: seed.name
|
|
459
|
+
});
|
|
460
|
+
if (p.isCancel(name)) process.exit(0);
|
|
461
|
+
const domain = await p.text({
|
|
462
|
+
message: "Domain",
|
|
463
|
+
placeholder: seed.domain,
|
|
464
|
+
defaultValue: seed.domain
|
|
465
|
+
});
|
|
466
|
+
if (p.isCancel(domain)) process.exit(0);
|
|
467
|
+
const accent = await p.text({
|
|
468
|
+
message: "Accent color (hex)",
|
|
469
|
+
placeholder: seed.colors.accent,
|
|
470
|
+
defaultValue: seed.colors.accent
|
|
471
|
+
});
|
|
472
|
+
if (p.isCancel(accent)) process.exit(0);
|
|
473
|
+
const supportEmail = await p.text({
|
|
474
|
+
message: "Support email",
|
|
475
|
+
placeholder: `support@${domain}`,
|
|
476
|
+
defaultValue: `support@${domain}`
|
|
477
|
+
});
|
|
478
|
+
if (p.isCancel(supportEmail)) process.exit(0);
|
|
479
|
+
const accentDark = deriveAccentDark(accent, seed);
|
|
480
|
+
const isCarrier = name === CARRIER_BRAND.name && domain === CARRIER_BRAND.domain;
|
|
481
|
+
return {
|
|
482
|
+
...seed,
|
|
483
|
+
name,
|
|
484
|
+
legalName: name,
|
|
485
|
+
tagline: isCarrier ? seed.tagline : `Mobile connectivity by ${name}.`,
|
|
486
|
+
domain,
|
|
487
|
+
supportEmail,
|
|
488
|
+
supportUrl: `https://${domain}/help`,
|
|
489
|
+
supportWhatsapp: isCarrier ? seed.supportWhatsapp : "",
|
|
490
|
+
social: isCarrier ? seed.social : {},
|
|
491
|
+
colors: { ...seed.colors, accent, accentDark }
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
async function doPluginInstall() {
|
|
495
|
+
const s = p.spinner();
|
|
496
|
+
s.start("Installing Carrier Claude Code plugin");
|
|
497
|
+
const r = await installPlugin();
|
|
498
|
+
s.stop("Plugin staged");
|
|
499
|
+
ok(`Plugin \u2192 ${r.copiedTo}`);
|
|
500
|
+
info(
|
|
501
|
+
`MCP: ${r.mcpAdded ? pc.green("registered") : pc.yellow("manual")} \xB7 marketplace: ${r.marketplaceAdded ? pc.green("added") : pc.yellow("manual")} \xB7 install: ${r.pluginInstalled ? pc.green("done") : pc.yellow("manual")}`
|
|
502
|
+
);
|
|
503
|
+
if (!r.claudeFound || r.notes.length) {
|
|
504
|
+
p.note(manualCommands().join("\n"), "Finish wiring (run in your terminal)");
|
|
505
|
+
if (r.notes.length) info(pc.dim(r.notes.join("\n")));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async function doSiteCreate(target, brand) {
|
|
509
|
+
const s = p.spinner();
|
|
510
|
+
s.start(`Scaffolding ${brand.name} storefront \u2192 ${target}`);
|
|
511
|
+
await scaffoldStorefront(target, brand);
|
|
512
|
+
s.stop("Storefront scaffolded");
|
|
513
|
+
ok(`Created ${target} (white-labeled: ${brand.name}, accent ${brand.colors.accent})`);
|
|
514
|
+
}
|
|
515
|
+
async function maybeBuildDeploy(target, brand, opts) {
|
|
516
|
+
if (opts.install) {
|
|
517
|
+
const s = p.spinner();
|
|
518
|
+
s.start("Installing storefront dependencies");
|
|
519
|
+
const oki = await installDeps(target);
|
|
520
|
+
s.stop(oki ? "Dependencies installed" : "Dependency install reported errors");
|
|
521
|
+
if (!oki) return;
|
|
522
|
+
}
|
|
523
|
+
if (opts.build) {
|
|
524
|
+
const s = p.spinner();
|
|
525
|
+
s.start("Building storefront (next build)");
|
|
526
|
+
const okb = await buildSite(target);
|
|
527
|
+
s.stop(okb ? "Build succeeded" : "Build failed \u2014 see output above");
|
|
528
|
+
if (!okb) return;
|
|
529
|
+
}
|
|
530
|
+
if (opts.deploy) {
|
|
531
|
+
const s = p.spinner();
|
|
532
|
+
s.start("Deploying to Cloudflare Workers");
|
|
533
|
+
const r = await deploySite(target, brand);
|
|
534
|
+
s.stop(r.ok ? `Deployed: ${r.projectName}` : "Deploy skipped");
|
|
535
|
+
if (!r.ok && r.reason) info(pc.yellow(r.reason));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
var program = new Command();
|
|
539
|
+
program.name("carrier").description("Carrier CLI \u2014 install the Claude Code plugin and roll out a white-labeled eSIM storefront.").version(VERSION);
|
|
540
|
+
program.command("init").description("Interactive: install the plugin AND scaffold/deploy a storefront.").option("--dir <path>", "Storefront output directory", "./storefront").option("--yes", "Use Carrier defaults, no prompts").action(async (o) => {
|
|
541
|
+
header();
|
|
542
|
+
await doPluginInstall();
|
|
543
|
+
const brand = o.yes ? CARRIER_BRAND : await promptBrand(CARRIER_BRAND);
|
|
544
|
+
const target = resolve2(o.dir);
|
|
545
|
+
if (await exists(target)) {
|
|
546
|
+
const go = await p.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
|
|
547
|
+
if (p.isCancel(go) || !go) {
|
|
548
|
+
p.outro("Stopped. Re-run with --dir <new path>.");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
await doSiteCreate(target, brand);
|
|
553
|
+
let install = !!o.yes;
|
|
554
|
+
let build = !!o.yes;
|
|
555
|
+
let deploy = false;
|
|
556
|
+
if (!o.yes) {
|
|
557
|
+
const next = await p.select({
|
|
558
|
+
message: "Roll it out now?",
|
|
559
|
+
options: [
|
|
560
|
+
{ value: "build", label: "Install deps + build" },
|
|
561
|
+
{ value: "deploy", label: "Install + build + deploy to Cloudflare Workers" },
|
|
562
|
+
{ value: "none", label: "Just scaffold \u2014 I'll build later" }
|
|
563
|
+
],
|
|
564
|
+
initialValue: "build"
|
|
565
|
+
});
|
|
566
|
+
if (p.isCancel(next)) process.exit(0);
|
|
567
|
+
install = next !== "none";
|
|
568
|
+
build = next !== "none";
|
|
569
|
+
deploy = next === "deploy";
|
|
570
|
+
}
|
|
571
|
+
await maybeBuildDeploy(target, brand, { install, build, deploy });
|
|
572
|
+
p.note(
|
|
573
|
+
[
|
|
574
|
+
`cd ${o.dir}`,
|
|
575
|
+
`Edit src/brand.config.ts to re-brand anytime`,
|
|
576
|
+
`pnpm dev # local preview`,
|
|
577
|
+
`MCP endpoint: ${MCP_URL}`
|
|
578
|
+
].join("\n"),
|
|
579
|
+
"Next"
|
|
580
|
+
);
|
|
581
|
+
p.outro(pc.green("Done. Your connectivity business is wired."));
|
|
582
|
+
});
|
|
583
|
+
var plugin = program.command("plugin").description("Manage the Carrier Claude Code plugin.");
|
|
584
|
+
plugin.command("install").description("Install/register the Carrier plugin + MCP into Claude Code.").action(async () => {
|
|
585
|
+
header();
|
|
586
|
+
await doPluginInstall();
|
|
587
|
+
p.outro(pc.green("Plugin ready. Restart Claude Code to load it."));
|
|
588
|
+
});
|
|
589
|
+
plugin.command("status").description("Show plugin install status.").action(async () => {
|
|
590
|
+
const st = await pluginStatus();
|
|
591
|
+
header();
|
|
592
|
+
info(st.installed ? pc.green(`Installed v${st.version ?? "?"} \u2192 ${st.dir}`) : pc.yellow(`Not installed (${st.dir})`));
|
|
593
|
+
p.outro("");
|
|
594
|
+
});
|
|
595
|
+
var site = program.command("site").description("Scaffold and deploy a white-labeled storefront.");
|
|
596
|
+
site.command("create [dir]").description("Scaffold a white-labeled storefront from the Mango template.").option("--name <name>", "Brand name").option("--domain <domain>", "Domain").option("--accent <hex>", "Accent color").option("--yes", "Carrier defaults, no prompts").action(async (dir, o) => {
|
|
597
|
+
header();
|
|
598
|
+
let brand = { ...CARRIER_BRAND };
|
|
599
|
+
if (o.name || o.domain || o.accent) {
|
|
600
|
+
const name = o.name ?? brand.name;
|
|
601
|
+
const domain = o.domain ?? (o.name ? `${slug(o.name)}.com` : brand.domain);
|
|
602
|
+
const isCarrier = name === CARRIER_BRAND.name && domain === CARRIER_BRAND.domain;
|
|
603
|
+
brand = {
|
|
604
|
+
...brand,
|
|
605
|
+
name,
|
|
606
|
+
domain,
|
|
607
|
+
legalName: o.name ?? brand.legalName,
|
|
608
|
+
tagline: o.name ? `Mobile connectivity by ${o.name}.` : brand.tagline,
|
|
609
|
+
supportEmail: `support@${domain}`,
|
|
610
|
+
supportUrl: `https://${domain}/help`,
|
|
611
|
+
supportWhatsapp: isCarrier ? brand.supportWhatsapp : "",
|
|
612
|
+
social: isCarrier ? brand.social : {},
|
|
613
|
+
colors: {
|
|
614
|
+
...brand.colors,
|
|
615
|
+
accent: o.accent ?? brand.colors.accent,
|
|
616
|
+
accentDark: o.accent ? deriveAccentDark(o.accent) : brand.colors.accentDark
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
} else if (!o.yes) {
|
|
620
|
+
brand = await promptBrand(CARRIER_BRAND);
|
|
621
|
+
}
|
|
622
|
+
const target = resolve2(dir ?? "./storefront");
|
|
623
|
+
if (await exists(target)) {
|
|
624
|
+
const go = await p.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
|
|
625
|
+
if (p.isCancel(go) || !go) {
|
|
626
|
+
p.outro("Stopped. Re-run with a different directory.");
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
await doSiteCreate(target, brand);
|
|
631
|
+
p.outro(pc.green(`Scaffolded. cd ${dir ?? "storefront"} && pnpm install && pnpm dev`));
|
|
632
|
+
});
|
|
633
|
+
site.command("deploy [dir]").description("Build + deploy a storefront to Cloudflare Workers.").option("--name <name>", "Cloudflare Worker name (defaults from brand)").action(async (dir, o) => {
|
|
634
|
+
header();
|
|
635
|
+
const target = resolve2(dir ?? "./storefront");
|
|
636
|
+
const brand = await loadStorefrontBrand(target, o.name ? { name: o.name } : void 0);
|
|
637
|
+
await maybeBuildDeploy(target, brand, { install: true, build: true, deploy: true });
|
|
638
|
+
p.outro("");
|
|
639
|
+
});
|
|
640
|
+
program.parseAsync(process.argv).catch((e) => {
|
|
641
|
+
console.error(pc.red(String(e instanceof Error ? e.message : e)));
|
|
642
|
+
process.exit(1);
|
|
643
|
+
});
|
|
644
|
+
//# sourceMappingURL=cli.js.map
|