@switchboard.spot/cli 0.2.0 → 0.2.2
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 +27 -7
- package/bin/switchboard.js +5 -1
- package/lib/commands/auth.js +222 -49
- package/lib/commands/billing.js +43 -0
- package/lib/commands/docs.js +176 -0
- package/lib/commands/doctor.js +49 -0
- package/lib/commands/env.js +3 -1
- package/lib/commands/init.js +7 -4
- package/lib/commands/launch.js +192 -0
- package/lib/commands/projects.js +50 -4
- package/lib/commands/setup.js +7 -4
- package/lib/config.js +36 -5
- package/lib/credentialStore.js +68 -0
- package/lib/docsClient.js +157 -0
- package/lib/mcpServer.js +193 -0
- package/lib/output.js +65 -9
- package/lib/verify/index.js +32 -4
- package/package.json +4 -2
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,34 @@ 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 projects create --name "My App" --slug my-app
|
|
57
58
|
switchboard setup --target client --json
|
|
58
|
-
switchboard
|
|
59
|
+
switchboard projects update <project-id> --allowed-origins http://localhost:5173 --virtual-microservice-enabled true
|
|
60
|
+
switchboard projects provision-turnstile <project-id>
|
|
61
|
+
switchboard verify setup
|
|
62
|
+
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"
|
|
63
|
+
switchboard verify publish
|
|
59
64
|
```
|
|
60
65
|
|
|
61
66
|
Use `--json` for automation, CI, and coding agents.
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
`setup --target client` writes public Client Gateway environment values such as `VITE_SWITCHBOARD_CLIENT_URL`; it does not prove chat or session readiness. Before SDK browser testing, configure the exact local or preview origin and provision Switchboard-managed Turnstile.
|
|
64
69
|
|
|
65
|
-
|
|
70
|
+
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.
|
|
71
|
+
|
|
72
|
+
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.
|
|
73
|
+
|
|
74
|
+
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`.
|
|
75
|
+
|
|
76
|
+
Switchboard-managed Turnstile is the default production path. Developers do not paste Cloudflare secrets into the CLI or repo files:
|
|
66
77
|
|
|
67
78
|
```bash
|
|
68
|
-
switchboard projects turnstile <project-id>
|
|
79
|
+
switchboard projects provision-turnstile <project-id>
|
|
69
80
|
switchboard projects turnstile <project-id> --clear
|
|
70
81
|
```
|
|
71
82
|
|
|
83
|
+
If `switchboard projects provision-turnstile --help` is missing, upgrade with `npm install -g @switchboard.spot/cli@latest` before trying dashboard automation or manual Cloudflare keys.
|
|
84
|
+
|
|
72
85
|
## Configuration
|
|
73
86
|
|
|
74
87
|
The CLI stores non-secret settings in `~/.switchboard/config.json` by default.
|
|
@@ -82,14 +95,21 @@ Environment variables:
|
|
|
82
95
|
| `SWITCHBOARD_PROJECT_ID` | Project context for project-scoped commands. |
|
|
83
96
|
| `SWITCHBOARD_API_KEY` | Secret project key for trusted-server gateway smoke tests. |
|
|
84
97
|
| `SWITCHBOARD_CLIENT_URL` | Public Client Gateway URL for browser/mobile end-user auth and chat. |
|
|
98
|
+
| `VITE_SWITCHBOARD_CLIENT_URL` | Vite-safe public Client Gateway URL for browser apps. |
|
|
85
99
|
| `SWITCHBOARD_END_USER_SESSION` | Existing end-user session for Client Gateway checks; the CLI cannot mint one without browser challenge execution. |
|
|
86
100
|
| `SWITCHBOARD_CONFIG_DIR` | Alternate CLI config directory. |
|
|
87
101
|
|
|
88
102
|
Account sessions are stored in the OS keychain and are not read from
|
|
89
103
|
environment variables.
|
|
90
104
|
|
|
105
|
+
For isolated agent automation, `switchboard auth login --token-store config-dir`
|
|
106
|
+
stores the account session in `SWITCHBOARD_CONFIG_DIR/account-session.json` with
|
|
107
|
+
0600 permissions. The default remains keychain-only. `switchboard auth logout`
|
|
108
|
+
clears both keychain and config-dir account sessions.
|
|
109
|
+
|
|
91
110
|
## Credential safety
|
|
92
111
|
|
|
93
112
|
Do not paste `sb_sess_`, `sb_test_`, `sb_live_`, provider keys, private keys, or
|
|
94
113
|
webhook secrets into frontend, mobile, or public code. Browser and mobile code
|
|
95
|
-
should use `
|
|
114
|
+
should use `VITE_SWITCHBOARD_CLIENT_URL` in Vite apps or `SWITCHBOARD_CLIENT_URL`
|
|
115
|
+
in non-Vite tooling, plus end-user sessions.
|
package/bin/switchboard.js
CHANGED
|
@@ -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.
|
|
61
|
+
await program.parseAsync();
|
|
58
62
|
|
|
59
63
|
if (!process.argv.slice(2).length) {
|
|
60
64
|
program.outputHelp();
|
package/lib/commands/auth.js
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import { accountPublicRequest, accountRequest } from "../client.js";
|
|
6
6
|
import { accountApiUrl, resolveAccountConfig, resolveConfig, saveConfig } from "../config.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
deleteAccountToken,
|
|
9
|
+
deleteConfigDirAccountToken,
|
|
10
|
+
setAccountToken,
|
|
11
|
+
setConfigDirAccountToken,
|
|
12
|
+
} from "../credentialStore.js";
|
|
8
13
|
import { emit, fail, globalFlags } from "../output.js";
|
|
9
14
|
import crypto from "node:crypto";
|
|
10
15
|
import http from "node:http";
|
|
@@ -33,10 +38,15 @@ export function registerCommand(program) {
|
|
|
33
38
|
|
|
34
39
|
auth
|
|
35
40
|
.command("login")
|
|
36
|
-
.description("Sign in through the browser and save
|
|
41
|
+
.description("Sign in through the browser and save an account session")
|
|
37
42
|
.option("--email <email>")
|
|
38
43
|
.option("--password <password>")
|
|
39
44
|
.option("--timeout-seconds <seconds>", "Seconds to wait for browser login", "120")
|
|
45
|
+
.option(
|
|
46
|
+
"--token-store <store>",
|
|
47
|
+
"Where to save the session: keychain, config-dir, or both",
|
|
48
|
+
"keychain",
|
|
49
|
+
)
|
|
40
50
|
.action(async (opts, cmd) => {
|
|
41
51
|
const flags = globalFlags(cmd);
|
|
42
52
|
|
|
@@ -49,9 +59,18 @@ export function registerCommand(program) {
|
|
|
49
59
|
fail("timeout-seconds must be a positive integer", 1, flags.json);
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
const
|
|
62
|
+
const tokenStore = normalizeTokenStore(opts.tokenStore, flags.json);
|
|
63
|
+
const data = await browserLogin(flags, timeoutSeconds, tokenStore);
|
|
64
|
+
const sessionLocation =
|
|
65
|
+
tokenStore === "keychain"
|
|
66
|
+
? "the OS keychain"
|
|
67
|
+
: tokenStore === "config-dir"
|
|
68
|
+
? "the Switchboard config directory"
|
|
69
|
+
: "the OS keychain and Switchboard config directory";
|
|
53
70
|
emit(
|
|
54
|
-
flags.json
|
|
71
|
+
flags.json
|
|
72
|
+
? data
|
|
73
|
+
: `Logged in as ${data.user.email}. Session saved in ${sessionLocation}.`,
|
|
55
74
|
flags,
|
|
56
75
|
);
|
|
57
76
|
});
|
|
@@ -61,8 +80,9 @@ export function registerCommand(program) {
|
|
|
61
80
|
.description("Revoke current session and clear saved token")
|
|
62
81
|
.action(async (_opts, cmd) => {
|
|
63
82
|
const flags = globalFlags(cmd);
|
|
64
|
-
await
|
|
65
|
-
await
|
|
83
|
+
await revokeSavedSession();
|
|
84
|
+
await deleteKeychainTokenIfAvailable();
|
|
85
|
+
deleteConfigDirAccountToken();
|
|
66
86
|
saveConfig({
|
|
67
87
|
projectId: null,
|
|
68
88
|
apiKey: null,
|
|
@@ -91,8 +111,30 @@ export function registerCommand(program) {
|
|
|
91
111
|
});
|
|
92
112
|
}
|
|
93
113
|
|
|
94
|
-
async function
|
|
95
|
-
|
|
114
|
+
async function deleteKeychainTokenIfAvailable() {
|
|
115
|
+
try {
|
|
116
|
+
await deleteAccountToken();
|
|
117
|
+
} catch {
|
|
118
|
+
/* Local logout should still clear config-dir credentials without a keychain backend. */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeTokenStore(tokenStore, json) {
|
|
123
|
+
if (tokenStore === "keychain" || tokenStore === "config-dir" || tokenStore === "both") {
|
|
124
|
+
return tokenStore;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fail("--token-store must be keychain, config-dir, or both", 1, json);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function revokeSavedSession() {
|
|
131
|
+
let config;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
config = await resolveAccountConfig();
|
|
135
|
+
} catch {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
96
138
|
|
|
97
139
|
if (!config.accountToken) {
|
|
98
140
|
return;
|
|
@@ -114,7 +156,7 @@ async function revokeKeychainSession() {
|
|
|
114
156
|
/**
|
|
115
157
|
* Starts the loopback callback server, opens the browser handoff URL, and saves the exchanged session.
|
|
116
158
|
*/
|
|
117
|
-
async function browserLogin(flags, timeoutSeconds) {
|
|
159
|
+
async function browserLogin(flags, timeoutSeconds, tokenStore) {
|
|
118
160
|
const state = randomState();
|
|
119
161
|
const server = await startCallbackServer();
|
|
120
162
|
const callbackUrl = `http://127.0.0.1:${server.port}/callback`;
|
|
@@ -133,6 +175,7 @@ async function browserLogin(flags, timeoutSeconds) {
|
|
|
133
175
|
code: callback.code,
|
|
134
176
|
state: callback.state,
|
|
135
177
|
expectedState: state,
|
|
178
|
+
tokenStore,
|
|
136
179
|
json: flags.json,
|
|
137
180
|
});
|
|
138
181
|
|
|
@@ -149,10 +192,11 @@ export async function exchangeCliLogin({
|
|
|
149
192
|
code,
|
|
150
193
|
state,
|
|
151
194
|
expectedState,
|
|
195
|
+
tokenStore = "keychain",
|
|
152
196
|
json,
|
|
153
197
|
request = accountPublicRequest,
|
|
154
198
|
save = saveConfig,
|
|
155
|
-
credentials = { setAccountToken },
|
|
199
|
+
credentials = { setAccountToken, setConfigDirAccountToken },
|
|
156
200
|
}) {
|
|
157
201
|
if (!code) {
|
|
158
202
|
fail("Browser login did not return a code. Run `switchboard auth login` again.", 2, json);
|
|
@@ -167,7 +211,7 @@ export async function exchangeCliLogin({
|
|
|
167
211
|
json,
|
|
168
212
|
});
|
|
169
213
|
|
|
170
|
-
await
|
|
214
|
+
await storeAccountToken(data.token, tokenStore, credentials);
|
|
171
215
|
|
|
172
216
|
save({
|
|
173
217
|
projectId: null,
|
|
@@ -178,6 +222,26 @@ export async function exchangeCliLogin({
|
|
|
178
222
|
return sanitizeSession(data);
|
|
179
223
|
}
|
|
180
224
|
|
|
225
|
+
async function storeAccountToken(token, tokenStore, credentials) {
|
|
226
|
+
if (tokenStore === "keychain") {
|
|
227
|
+
await credentials.setAccountToken(token);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (tokenStore === "config-dir") {
|
|
232
|
+
await credentials.setConfigDirAccountToken(token);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (tokenStore === "both") {
|
|
237
|
+
await credentials.setAccountToken(token);
|
|
238
|
+
await credentials.setConfigDirAccountToken(token);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
throw new Error("Unsupported Switchboard account token store");
|
|
243
|
+
}
|
|
244
|
+
|
|
181
245
|
function sanitizeSession(data) {
|
|
182
246
|
const rest = { ...data };
|
|
183
247
|
delete rest.token;
|
|
@@ -253,9 +317,11 @@ function waitForCallback(callbackServer, timeoutSeconds, json) {
|
|
|
253
317
|
});
|
|
254
318
|
}
|
|
255
319
|
|
|
256
|
-
function callbackPage(title, message, tone) {
|
|
257
|
-
const
|
|
258
|
-
const
|
|
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 ? "✓" : "!";
|
|
259
325
|
|
|
260
326
|
return `<!doctype html>
|
|
261
327
|
<html lang="en">
|
|
@@ -265,74 +331,181 @@ function callbackPage(title, message, tone) {
|
|
|
265
331
|
<title>${escapeHtml(title)}</title>
|
|
266
332
|
<style>
|
|
267
333
|
:root {
|
|
268
|
-
color-scheme: dark;
|
|
334
|
+
color-scheme: light dark;
|
|
269
335
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
270
|
-
background: #
|
|
271
|
-
color: #
|
|
336
|
+
background: #ffffff;
|
|
337
|
+
color: #1f2937;
|
|
272
338
|
}
|
|
339
|
+
* { box-sizing: border-box; }
|
|
273
340
|
body {
|
|
274
341
|
min-height: 100vh;
|
|
275
342
|
margin: 0;
|
|
276
|
-
|
|
277
|
-
place-items: center;
|
|
343
|
+
color: #1f2937;
|
|
278
344
|
background:
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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;
|
|
282
383
|
}
|
|
283
384
|
main {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
padding:
|
|
288
|
-
|
|
289
|
-
|
|
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);
|
|
290
400
|
text-align: center;
|
|
291
|
-
backdrop-filter: blur(18px);
|
|
292
401
|
}
|
|
293
|
-
.
|
|
294
|
-
margin: 0 0 1.
|
|
295
|
-
color:
|
|
402
|
+
.eyebrow {
|
|
403
|
+
margin: 0 0 1.25rem;
|
|
404
|
+
color: #4b5563;
|
|
296
405
|
font-size: 0.78rem;
|
|
297
406
|
font-weight: 700;
|
|
298
|
-
letter-spacing: 0.
|
|
407
|
+
letter-spacing: 0.12em;
|
|
299
408
|
text-transform: uppercase;
|
|
300
409
|
}
|
|
301
410
|
.mark {
|
|
302
|
-
width:
|
|
303
|
-
height:
|
|
411
|
+
width: 4rem;
|
|
412
|
+
height: 4rem;
|
|
304
413
|
margin: 0 auto 1.25rem;
|
|
305
414
|
display: grid;
|
|
306
415
|
place-items: center;
|
|
307
|
-
border
|
|
416
|
+
border: 1px solid ${accent};
|
|
417
|
+
border-radius: 0.5rem;
|
|
308
418
|
background: ${accent};
|
|
309
|
-
color: #
|
|
310
|
-
font-size: 1.
|
|
419
|
+
color: #111827;
|
|
420
|
+
font-size: 1.9rem;
|
|
311
421
|
font-weight: 900;
|
|
312
|
-
box-shadow: 0
|
|
422
|
+
box-shadow: 0 18px 40px -20px ${accent};
|
|
313
423
|
}
|
|
314
424
|
h1 {
|
|
315
425
|
margin: 0;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
426
|
+
color: #1f2937;
|
|
427
|
+
font-size: clamp(2rem, 7vw, 3.5rem);
|
|
428
|
+
line-height: 0.98;
|
|
429
|
+
letter-spacing: 0;
|
|
319
430
|
}
|
|
320
431
|
p {
|
|
321
432
|
margin: 1rem auto 0;
|
|
322
433
|
max-width: 26rem;
|
|
323
|
-
color:
|
|
434
|
+
color: #4b5563;
|
|
324
435
|
font-size: 1rem;
|
|
325
436
|
line-height: 1.65;
|
|
326
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
|
+
}
|
|
327
491
|
</style>
|
|
328
492
|
</head>
|
|
329
493
|
<body>
|
|
330
|
-
<
|
|
331
|
-
<
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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>
|
|
336
509
|
</body>
|
|
337
510
|
</html>`;
|
|
338
511
|
}
|
package/lib/commands/billing.js
CHANGED
|
@@ -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
|
+
}
|