@switchboard.spot/cli 0.2.1 → 0.2.3

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/README.md CHANGED
@@ -7,13 +7,13 @@ tests.
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- npm install -g @switchboard.spot/cli
10
+ npm install -g @switchboard.spot/cli@latest
11
11
  ```
12
12
 
13
13
  You can also run commands without a global install:
14
14
 
15
15
  ```bash
16
- npx @switchboard.spot/cli auth login
16
+ npx @switchboard.spot/cli@latest auth login
17
17
  ```
18
18
 
19
19
  ## Build for local testing
@@ -54,21 +54,32 @@ If a first publish fails, `npm view @switchboard.spot/cli` will continue to retu
54
54
 
55
55
  ```bash
56
56
  switchboard auth login
57
- switchboard setup --target client --json
58
- switchboard chat test --json
57
+ switchboard projects create --name "My App" --slug my-app
58
+ switchboard setup project --origin http://localhost:5173 --json
59
+ switchboard verify setup
60
+ switchboard launch prepare --production-origin https://app.example.com --end-user-terms-url https://app.example.com/terms --end-user-privacy-url https://app.example.com/privacy --support-email support@example.com --contact-email owner@example.com --use-case "Browser chat for signed-in customers"
61
+ switchboard verify publish
59
62
  ```
60
63
 
61
64
  Use `--json` for automation, CI, and coding agents.
62
65
 
63
- CLI account login does not create Client Gateway end-user sessions. End-user sign-up, sign-in, and refresh require a real browser/mobile challenge flow; use `@switchboard.spot/sdk` in the app, or use trusted-server/account APIs for automation.
66
+ `setup project --origin <origin>` writes public Client Gateway environment values such as `VITE_SWITCHBOARD_CLIENT_URL`, configures the exact local or preview origin, enables Client Gateway, and provisions Switchboard-managed Turnstile. It reports browser chat verification as a manual SDK check.
64
67
 
65
- Project-owned browser challenge keys are managed through the CLI:
68
+ CLI account login does not create Client Gateway end-user sessions. End-user sign-up, sign-in, and refresh require the SDK-managed browser challenge; curl, Node scripts, CI, and CLI account login cannot mint browser sessions without running that real browser/mobile flow.
69
+
70
+ For local Switchboard development, `switchboard verify setup --client-url http://localhost:4000/m/<slug>/v1` uses the built-in `dev_browser_challenge` token unless a scenario supplies an explicit `browserChallengeToken`. Hosted Switchboard should use managed Turnstile and the SDK-managed real browser challenge. A local sandbox smoke path is: configure allowed origin plus legal/support fields, create an anonymous session through the SDK or browser verification flow, then call Client Gateway chat.
71
+
72
+ Model discovery is global. Use `GET /v1/models` for OpenAI-compatible discovery or `GET /v1/catalog/models` for catalog/pricing metadata; Client Gateway chat is project-scoped at `/m/<slug>/v1/chat/completions`.
73
+
74
+ Switchboard-managed Turnstile is the default production path. Developers do not paste Cloudflare secrets into the CLI or repo files:
66
75
 
67
76
  ```bash
68
- switchboard projects turnstile <project-id> --site-key <site-key> --secret-key <secret-key>
77
+ switchboard setup project --origin <origin> --json
69
78
  switchboard projects turnstile <project-id> --clear
70
79
  ```
71
80
 
81
+ `switchboard projects provision-turnstile` remains available as a low-level admin/debug command. If its help is missing, upgrade with `npm install -g @switchboard.spot/cli@latest` before trying dashboard automation or manual Cloudflare keys.
82
+
72
83
  ## Configuration
73
84
 
74
85
  The CLI stores non-secret settings in `~/.switchboard/config.json` by default.
@@ -18,11 +18,13 @@ import { registerBillingCommands } from "../lib/commands/billing.js";
18
18
  import { registerEnvCommands } from "../lib/commands/env.js";
19
19
  import { registerUsageCommands } from "../lib/commands/usage.js";
20
20
  import { registerIntegrationCommands } from "../lib/commands/integration.js";
21
+ import { registerDocsCommands } from "../lib/commands/docs.js";
21
22
  import { registerInitCommand } from "../lib/commands/init.js";
22
23
  import { registerSetupCommand } from "../lib/commands/setup.js";
23
24
  import { registerHealthCommand } from "../lib/commands/health.js";
24
25
  import { registerDoctorCommand } from "../lib/commands/doctor.js";
25
26
  import { registerVerifyCommands } from "../lib/commands/verify.js";
27
+ import { registerLaunchCommands } from "../lib/commands/launch.js";
26
28
 
27
29
  const program = new Command();
28
30
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -42,6 +44,7 @@ registerSetupCommand(program);
42
44
  registerHealthCommand(program);
43
45
  registerDoctorCommand(program);
44
46
  registerVerifyCommands(program);
47
+ registerLaunchCommands(program);
45
48
  registerAuth(program);
46
49
  registerAccountCommands(program);
47
50
  registerWorkspacesCommands(program);
@@ -53,8 +56,9 @@ registerBillingCommands(program);
53
56
  registerEnvCommands(program);
54
57
  registerUsageCommands(program);
55
58
  registerIntegrationCommands(program);
59
+ registerDocsCommands(program);
56
60
 
57
- program.parse();
61
+ await program.parseAsync();
58
62
 
59
63
  if (!process.argv.slice(2).length) {
60
64
  program.outputHelp();
@@ -317,9 +317,11 @@ function waitForCallback(callbackServer, timeoutSeconds, json) {
317
317
  });
318
318
  }
319
319
 
320
- function callbackPage(title, message, tone) {
321
- const accent = tone === "success" ? "#28d17c" : "#ff6b6b";
322
- const mark = tone === "success" ? "" : "!";
320
+ export function callbackPage(title, message, tone) {
321
+ const success = tone === "success";
322
+ const accent = success ? "#14b8a6" : "#ef4444";
323
+ const accentSoft = success ? "#ccfbf1" : "#fee2e2";
324
+ const mark = success ? "✓" : "!";
323
325
 
324
326
  return `<!doctype html>
325
327
  <html lang="en">
@@ -329,74 +331,181 @@ function callbackPage(title, message, tone) {
329
331
  <title>${escapeHtml(title)}</title>
330
332
  <style>
331
333
  :root {
332
- color-scheme: dark;
334
+ color-scheme: light dark;
333
335
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
334
- background: #05070d;
335
- color: #f7f7fb;
336
+ background: #ffffff;
337
+ color: #1f2937;
336
338
  }
339
+ * { box-sizing: border-box; }
337
340
  body {
338
341
  min-height: 100vh;
339
342
  margin: 0;
340
- display: grid;
341
- place-items: center;
343
+ color: #1f2937;
342
344
  background:
343
- radial-gradient(circle at 20% 20%, rgba(68, 109, 255, 0.22), transparent 32rem),
344
- radial-gradient(circle at 80% 10%, rgba(40, 209, 124, 0.16), transparent 26rem),
345
- linear-gradient(135deg, #05070d 0%, #101526 100%);
345
+ repeating-linear-gradient(125deg, transparent, transparent 6px, #e8e8e8 6px, #e8e8e8 7px),
346
+ #ffffff;
347
+ }
348
+ .page {
349
+ min-height: 100vh;
350
+ width: min(100%, 80rem);
351
+ margin: 0 auto;
352
+ display: flex;
353
+ flex-direction: column;
354
+ border-inline: 1px solid #e5e7eb;
355
+ background:
356
+ repeating-linear-gradient(125deg, transparent, transparent 6px, #e8e8e8 6px, #e8e8e8 7px),
357
+ #ffffff;
358
+ }
359
+ header {
360
+ height: 4.5rem;
361
+ display: flex;
362
+ align-items: center;
363
+ justify-content: space-between;
364
+ padding: 0 1.5rem;
365
+ border-bottom: 1px solid #e5e7eb;
366
+ background: rgba(255, 255, 255, 0.88);
367
+ backdrop-filter: blur(12px);
368
+ }
369
+ .logo {
370
+ color: #1f2937;
371
+ font-size: 0.95rem;
372
+ font-weight: 800;
373
+ letter-spacing: 0;
374
+ }
375
+ .pill {
376
+ border: 1px solid #e5e7eb;
377
+ border-radius: 999px;
378
+ padding: 0.45rem 0.8rem;
379
+ background: #ffffff;
380
+ color: #4b5563;
381
+ font-size: 0.82rem;
382
+ font-weight: 600;
346
383
  }
347
384
  main {
348
- width: min(92vw, 34rem);
349
- border: 1px solid rgba(255, 255, 255, 0.12);
350
- border-radius: 1.5rem;
351
- padding: 2rem;
352
- background: rgba(10, 14, 27, 0.82);
353
- box-shadow: 0 2rem 6rem rgba(0, 0, 0, 0.38);
385
+ flex: 1;
386
+ display: grid;
387
+ place-items: center;
388
+ padding: 5rem 1.5rem;
389
+ }
390
+ .panel {
391
+ width: min(100%, 34rem);
392
+ border: 1px solid #e5e7eb;
393
+ border-radius: 0.5rem;
394
+ padding: clamp(1.5rem, 5vw, 2.25rem);
395
+ background: rgba(255, 255, 255, 0.94);
396
+ box-shadow:
397
+ 0 1.34368px 0.537473px -0.625px rgba(0, 0, 0, 0.09),
398
+ 0 15.5969px 6.23877px -3.125px rgba(0, 0, 0, 0.07),
399
+ 0 43.962px 17.5848px -4.375px rgba(0, 0, 0, 0.04);
354
400
  text-align: center;
355
- backdrop-filter: blur(18px);
356
401
  }
357
- .brand {
358
- margin: 0 0 1.5rem;
359
- color: rgba(247, 247, 251, 0.58);
402
+ .eyebrow {
403
+ margin: 0 0 1.25rem;
404
+ color: #4b5563;
360
405
  font-size: 0.78rem;
361
406
  font-weight: 700;
362
- letter-spacing: 0.18em;
407
+ letter-spacing: 0.12em;
363
408
  text-transform: uppercase;
364
409
  }
365
410
  .mark {
366
- width: 3.5rem;
367
- height: 3.5rem;
411
+ width: 4rem;
412
+ height: 4rem;
368
413
  margin: 0 auto 1.25rem;
369
414
  display: grid;
370
415
  place-items: center;
371
- border-radius: 999px;
416
+ border: 1px solid ${accent};
417
+ border-radius: 0.5rem;
372
418
  background: ${accent};
373
- color: #05070d;
374
- font-size: 1.8rem;
419
+ color: #111827;
420
+ font-size: 1.9rem;
375
421
  font-weight: 900;
376
- box-shadow: 0 0 2.5rem ${accent}55;
422
+ box-shadow: 0 18px 40px -20px ${accent};
377
423
  }
378
424
  h1 {
379
425
  margin: 0;
380
- font-size: clamp(1.75rem, 5vw, 2.45rem);
381
- line-height: 1.05;
382
- letter-spacing: -0.04em;
426
+ color: #1f2937;
427
+ font-size: clamp(2rem, 7vw, 3.5rem);
428
+ line-height: 0.98;
429
+ letter-spacing: 0;
383
430
  }
384
431
  p {
385
432
  margin: 1rem auto 0;
386
433
  max-width: 26rem;
387
- color: rgba(247, 247, 251, 0.66);
434
+ color: #4b5563;
388
435
  font-size: 1rem;
389
436
  line-height: 1.65;
390
437
  }
438
+ .status {
439
+ display: inline-flex;
440
+ align-items: center;
441
+ gap: 0.5rem;
442
+ margin-top: 1.5rem;
443
+ border: 1px solid ${accent};
444
+ border-radius: 999px;
445
+ padding: 0.5rem 0.8rem;
446
+ background: ${accentSoft};
447
+ color: #1f2937;
448
+ font-size: 0.86rem;
449
+ font-weight: 700;
450
+ }
451
+ @media (prefers-color-scheme: dark) {
452
+ :root {
453
+ background: #020617;
454
+ color: #f3f4f6;
455
+ }
456
+ body {
457
+ color: #f3f4f6;
458
+ background:
459
+ repeating-linear-gradient(125deg, transparent, transparent 6px, rgba(55, 65, 81, 0.6) 6px, rgba(55, 65, 81, 0.6) 7px),
460
+ #020617;
461
+ }
462
+ .page {
463
+ border-color: #1f2937;
464
+ background:
465
+ repeating-linear-gradient(125deg, transparent, transparent 6px, rgba(55, 65, 81, 0.6) 6px, rgba(55, 65, 81, 0.6) 7px),
466
+ #020617;
467
+ }
468
+ header,
469
+ .panel {
470
+ border-color: #1f2937;
471
+ background: rgba(2, 6, 23, 0.94);
472
+ }
473
+ .logo,
474
+ h1 {
475
+ color: #f3f4f6;
476
+ }
477
+ .pill,
478
+ .eyebrow,
479
+ p {
480
+ color: #d1d5db;
481
+ }
482
+ .pill {
483
+ border-color: #1f2937;
484
+ background: #111827;
485
+ }
486
+ .mark,
487
+ .status {
488
+ color: #020617;
489
+ }
490
+ }
391
491
  </style>
392
492
  </head>
393
493
  <body>
394
- <main>
395
- <p class="brand">Switchboard CLI</p>
396
- <div class="mark" aria-hidden="true">${mark}</div>
397
- <h1>${escapeHtml(title)}</h1>
398
- <p>${escapeHtml(message)}</p>
399
- </main>
494
+ <div class="page">
495
+ <header>
496
+ <div class="logo">Switchboard</div>
497
+ <div class="pill">CLI session</div>
498
+ </header>
499
+ <main>
500
+ <section class="panel" aria-labelledby="callback-title">
501
+ <p class="eyebrow">Switchboard CLI</p>
502
+ <div class="mark" aria-hidden="true">${mark}</div>
503
+ <h1 id="callback-title">${escapeHtml(title)}</h1>
504
+ <p>${escapeHtml(message)}</p>
505
+ <div class="status">${success ? "Session approved" : "Session not approved"}</div>
506
+ </section>
507
+ </main>
508
+ </div>
400
509
  </body>
401
510
  </html>`;
402
511
  }
@@ -2,6 +2,7 @@
2
2
  * Developer billing commands.
3
3
  */
4
4
 
5
+ import { spawn } from "node:child_process";
5
6
  import { accountRequest } from "../client.js";
6
7
  import { emit, globalFlags, printList } from "../output.js";
7
8
 
@@ -51,6 +52,40 @@ export function registerBillingCommands(program) {
51
52
  emit(flags.json ? data : `Created top-up ${data.id}`, flags);
52
53
  });
53
54
 
55
+ billing
56
+ .command("checkout")
57
+ .description("Create a Stripe-hosted developer billing checkout URL")
58
+ .requiredOption("--success-url <url>")
59
+ .requiredOption("--cancel-url <url>")
60
+ .option("--open", "Open checkout URL in the default browser")
61
+ .action(async (opts, cmd) => {
62
+ const flags = globalFlags(cmd);
63
+ const { data } = await accountRequest("POST", "/billing/checkout", {
64
+ body: {
65
+ success_url: opts.successUrl,
66
+ cancel_url: opts.cancelUrl,
67
+ },
68
+ json: flags.json,
69
+ });
70
+ if (opts.open) openUrl(data.checkout_url);
71
+ emit(flags.json ? data : data.checkout_url, flags);
72
+ });
73
+
74
+ billing
75
+ .command("portal")
76
+ .description("Create a Stripe-hosted developer billing portal URL")
77
+ .requiredOption("--return-url <url>")
78
+ .option("--open", "Open portal URL in the default browser")
79
+ .action(async (opts, cmd) => {
80
+ const flags = globalFlags(cmd);
81
+ const { data } = await accountRequest("POST", "/billing/portal", {
82
+ body: { return_url: opts.returnUrl },
83
+ json: flags.json,
84
+ });
85
+ if (opts.open) openUrl(data.url);
86
+ emit(flags.json ? data : data.url, flags);
87
+ });
88
+
54
89
  billing
55
90
  .command("prepaid")
56
91
  .description("Update prepaid wallet settings")
@@ -85,3 +120,11 @@ export function registerBillingCommands(program) {
85
120
  emit(flags.json ? data : "Updated prepaid settings", flags);
86
121
  });
87
122
  }
123
+
124
+ function openUrl(url) {
125
+ const command =
126
+ process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
127
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
128
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
129
+ child.unref();
130
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Hosted docs and embedded MCP commands.
3
+ */
4
+
5
+ import {
6
+ docsCapabilities,
7
+ listDocs,
8
+ readDoc,
9
+ searchDocs,
10
+ DocsClientError,
11
+ } from "../docsClient.js";
12
+ import { runMcpServer } from "../mcpServer.js";
13
+ import { emit, fail, globalFlags, redactSecrets } from "../output.js";
14
+
15
+ export function registerDocsCommands(program) {
16
+ const docs = program.command("docs").description("Public docs and MCP server");
17
+
18
+ docs
19
+ .command("list")
20
+ .description("List public Switchboard docs")
21
+ .option("--json", "Output JSON to stdout")
22
+ .action(async (_opts, cmd) => {
23
+ await runDocsAction(cmd, async (flags) => {
24
+ const result = await listDocs();
25
+ if (jsonMode(cmd)) return emit(result, { json: true });
26
+
27
+ for (const doc of result.data || []) {
28
+ emit(`${doc.id}\t${doc.title}\t${doc.url}`, flags);
29
+ }
30
+ });
31
+ });
32
+
33
+ docs
34
+ .command("read")
35
+ .description("Read one public Switchboard doc")
36
+ .argument("<id>", "Stable public doc id")
37
+ .option("--json", "Output JSON to stdout")
38
+ .action(async (id, _opts, cmd) => {
39
+ await runDocsAction(cmd, async (flags) => {
40
+ const result = await readDoc(id);
41
+ if (jsonMode(cmd)) return emit(result, { json: true });
42
+ emit(result.data?.content || "", flags);
43
+ });
44
+ });
45
+
46
+ docs
47
+ .command("search")
48
+ .description("Search public Switchboard docs")
49
+ .argument("<query>", "Search query")
50
+ .option("--limit <count>", "Maximum number of snippets", parseInteger)
51
+ .option("--json", "Output JSON to stdout")
52
+ .action(async (query, opts, cmd) => {
53
+ await runDocsAction(cmd, async (flags) => {
54
+ const result = await searchDocs(query, { limit: opts.limit });
55
+ if (jsonMode(cmd)) return emit(result, { json: true });
56
+
57
+ for (const match of result.data || []) {
58
+ emit(`${match.id}\t${match.title}\n${match.snippet}\n`, flags);
59
+ }
60
+ });
61
+ });
62
+
63
+ docs
64
+ .command("mcp")
65
+ .description("Start the embedded Switchboard MCP stdio server")
66
+ .action(async () => {
67
+ await runMcpServer();
68
+ });
69
+
70
+ docs
71
+ .command("mcp-config")
72
+ .description("Print MCP client configuration")
73
+ .requiredOption("--client <client>", "MCP client: codex, claude, or cursor")
74
+ .option("--json", "Output JSON to stdout")
75
+ .action(async (opts, cmd) => {
76
+ const client = opts.client.toLowerCase();
77
+ const config = mcpConfig(client);
78
+
79
+ if (!config) {
80
+ fail("Unsupported MCP client. Expected one of: codex, claude, cursor.", 1, jsonMode(cmd));
81
+ }
82
+
83
+ if (jsonMode(cmd)) {
84
+ emit(config, { json: true });
85
+ } else {
86
+ emit(formatConfig(client, config), globalFlags(cmd));
87
+ }
88
+ });
89
+
90
+ docs
91
+ .command("capabilities")
92
+ .description("Print hosted MCP capability metadata")
93
+ .option("--json", "Output JSON to stdout")
94
+ .action(async (_opts, cmd) => {
95
+ await runDocsAction(cmd, async (flags) => {
96
+ const result = await docsCapabilities();
97
+ emit(result, { json: jsonMode(cmd), quiet: flags.quiet });
98
+ });
99
+ });
100
+ }
101
+
102
+ async function runDocsAction(cmd, callback) {
103
+ const flags = globalFlags(cmd);
104
+
105
+ try {
106
+ await callback(flags);
107
+ } catch (error) {
108
+ if (error instanceof DocsClientError) {
109
+ fail(error.message, exitCode(error.status), jsonMode(cmd), error.type);
110
+ }
111
+
112
+ fail(error.message || "Switchboard docs command failed.", 1, jsonMode(cmd));
113
+ }
114
+ }
115
+
116
+ function jsonMode(cmd) {
117
+ return Boolean(cmd.opts?.().json || globalFlags(cmd).json);
118
+ }
119
+
120
+ function exitCode(status) {
121
+ if (status === 401 || status === 403) return 2;
122
+ if (status >= 500 || status === 3) return 3;
123
+ return 1;
124
+ }
125
+
126
+ function parseInteger(value) {
127
+ const parsed = Number.parseInt(value, 10);
128
+ if (!Number.isInteger(parsed)) {
129
+ throw new Error("Limit must be an integer.");
130
+ }
131
+ return parsed;
132
+ }
133
+
134
+ function mcpConfig(client) {
135
+ const command = "switchboard";
136
+ const args = ["docs", "mcp"];
137
+
138
+ switch (client) {
139
+ case "codex":
140
+ return {
141
+ mcp_servers: {
142
+ switchboard: {
143
+ command,
144
+ args,
145
+ env: {
146
+ SWITCHBOARD_BASE_URL: "https://switchboard.spot",
147
+ },
148
+ },
149
+ },
150
+ };
151
+ case "claude":
152
+ case "cursor":
153
+ return {
154
+ mcpServers: {
155
+ switchboard: {
156
+ command,
157
+ args,
158
+ env: {
159
+ SWITCHBOARD_BASE_URL: "https://switchboard.spot",
160
+ },
161
+ },
162
+ },
163
+ };
164
+ default:
165
+ return null;
166
+ }
167
+ }
168
+
169
+ function formatConfig(client, config) {
170
+ return [
171
+ `# ${client} MCP config`,
172
+ "```json",
173
+ JSON.stringify(redactSecrets(config), null, 2),
174
+ "```",
175
+ ].join("\n");
176
+ }