@switchboard.spot/cli 0.2.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/LICENSE +7 -0
- package/README.md +95 -0
- package/bin/switchboard.js +61 -0
- package/lib/client.js +150 -0
- package/lib/commands/account.js +81 -0
- package/lib/commands/auth.js +369 -0
- package/lib/commands/billing.js +87 -0
- package/lib/commands/doctor.js +74 -0
- package/lib/commands/endUsers.js +51 -0
- package/lib/commands/env.js +393 -0
- package/lib/commands/health.js +24 -0
- package/lib/commands/init.js +75 -0
- package/lib/commands/integration.js +38 -0
- package/lib/commands/keys.js +106 -0
- package/lib/commands/org.js +55 -0
- package/lib/commands/projects.js +197 -0
- package/lib/commands/setup.js +76 -0
- package/lib/commands/usage.js +52 -0
- package/lib/commands/verify.js +143 -0
- package/lib/commands/workspaces.js +92 -0
- package/lib/config.js +132 -0
- package/lib/credentialStore.js +312 -0
- package/lib/output.js +112 -0
- package/lib/verify/index.js +762 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Switchboard CLI Proprietary License Notice
|
|
2
|
+
|
|
3
|
+
Copyright (c) Switchboard.
|
|
4
|
+
|
|
5
|
+
This package is proprietary software. You may install and use it to access and
|
|
6
|
+
operate Switchboard services. No other rights are granted except by a separate
|
|
7
|
+
written agreement with Switchboard.
|
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# @switchboard.spot/cli
|
|
2
|
+
|
|
3
|
+
Command-line management for [Switchboard](https://switchboard.spot): account
|
|
4
|
+
auth, project setup, key rotation, integration setup, usage checks, and smoke
|
|
5
|
+
tests.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @switchboard.spot/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
You can also run commands without a global install:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @switchboard.spot/cli auth login
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Build for local testing
|
|
20
|
+
|
|
21
|
+
Build an installable npm tarball from the same package files used for publish:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm run build
|
|
25
|
+
npm install -g ./dist/switchboard-cli-*.tgz
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The build output is written to `dist/` and is safe to delete.
|
|
29
|
+
|
|
30
|
+
## Publish to npm
|
|
31
|
+
|
|
32
|
+
Before publishing, npm requires either an interactive account with 2FA enabled
|
|
33
|
+
or a granular access token with **bypass 2FA** enabled. For token-based
|
|
34
|
+
publishing, create a granular npm token with read/write access for
|
|
35
|
+
`@switchboard.spot/cli`, enable bypass 2FA, then expose it only for the publish
|
|
36
|
+
command:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
export NPM_TOKEN=npm_...
|
|
40
|
+
npmrc="$(mktemp)"
|
|
41
|
+
printf '//registry.npmjs.org/:_authToken=%s\n' "$NPM_TOKEN" > "$npmrc"
|
|
42
|
+
npm --userconfig "$npmrc" whoami
|
|
43
|
+
npm --userconfig "$npmrc" run publish:dry-run
|
|
44
|
+
npm --userconfig "$npmrc" run publish:npm
|
|
45
|
+
rm "$npmrc"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The package is scoped and public. `publishConfig.access = public` is set in
|
|
49
|
+
`package.json`, and the publish script also passes `--access public` explicitly.
|
|
50
|
+
If a first publish fails, `npm view @switchboard.spot/cli` will continue to return
|
|
51
|
+
404 until a publish succeeds.
|
|
52
|
+
|
|
53
|
+
## Auth workflow
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
switchboard auth login
|
|
57
|
+
switchboard setup --target client --json
|
|
58
|
+
switchboard chat test --json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use `--json` for automation, CI, and coding agents.
|
|
62
|
+
|
|
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/sdk` in the app, or use trusted-server/account APIs for automation.
|
|
64
|
+
|
|
65
|
+
Project-owned browser challenge keys are managed through the CLI:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
switchboard projects turnstile <project-id> --site-key <site-key> --secret-key <secret-key>
|
|
69
|
+
switchboard projects turnstile <project-id> --clear
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
The CLI stores non-secret settings in `~/.switchboard/config.json` by default.
|
|
75
|
+
Set `SWITCHBOARD_CONFIG_DIR` to use a different directory.
|
|
76
|
+
|
|
77
|
+
Environment variables:
|
|
78
|
+
|
|
79
|
+
| Variable | Purpose |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `SWITCHBOARD_BASE_URL` | Switchboard app origin. Defaults to `https://switchboard.spot`; set to `http://localhost:4000` for local development. |
|
|
82
|
+
| `SWITCHBOARD_PROJECT_ID` | Project context for project-scoped commands. |
|
|
83
|
+
| `SWITCHBOARD_API_KEY` | Secret project key for trusted-server gateway smoke tests. |
|
|
84
|
+
| `SWITCHBOARD_CLIENT_URL` | Public Client Gateway URL for browser/mobile end-user auth and chat. |
|
|
85
|
+
| `SWITCHBOARD_END_USER_SESSION` | Existing end-user session for Client Gateway checks; the CLI cannot mint one without browser challenge execution. |
|
|
86
|
+
| `SWITCHBOARD_CONFIG_DIR` | Alternate CLI config directory. |
|
|
87
|
+
|
|
88
|
+
Account sessions are stored in the OS keychain and are not read from
|
|
89
|
+
environment variables.
|
|
90
|
+
|
|
91
|
+
## Credential safety
|
|
92
|
+
|
|
93
|
+
Do not paste `sb_sess_`, `sb_test_`, `sb_live_`, provider keys, private keys, or
|
|
94
|
+
webhook secrets into frontend, mobile, or public code. Browser and mobile code
|
|
95
|
+
should use `SWITCHBOARD_CLIENT_URL` plus end-user sessions.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Switchboard CLI — full dashboard parity via the account management API.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { registerCommand as registerAuth } from "../lib/commands/auth.js";
|
|
11
|
+
import { registerAccountCommands } from "../lib/commands/account.js";
|
|
12
|
+
import { registerKeysCommands } from "../lib/commands/keys.js";
|
|
13
|
+
import { registerProjectsCommands } from "../lib/commands/projects.js";
|
|
14
|
+
import { registerWorkspacesCommands } from "../lib/commands/workspaces.js";
|
|
15
|
+
import { registerOrgCommands } from "../lib/commands/org.js";
|
|
16
|
+
import { registerEndUsersCommands } from "../lib/commands/endUsers.js";
|
|
17
|
+
import { registerBillingCommands } from "../lib/commands/billing.js";
|
|
18
|
+
import { registerEnvCommands } from "../lib/commands/env.js";
|
|
19
|
+
import { registerUsageCommands } from "../lib/commands/usage.js";
|
|
20
|
+
import { registerIntegrationCommands } from "../lib/commands/integration.js";
|
|
21
|
+
import { registerInitCommand } from "../lib/commands/init.js";
|
|
22
|
+
import { registerSetupCommand } from "../lib/commands/setup.js";
|
|
23
|
+
import { registerHealthCommand } from "../lib/commands/health.js";
|
|
24
|
+
import { registerDoctorCommand } from "../lib/commands/doctor.js";
|
|
25
|
+
import { registerVerifyCommands } from "../lib/commands/verify.js";
|
|
26
|
+
|
|
27
|
+
const program = new Command();
|
|
28
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const packageJson = JSON.parse(
|
|
30
|
+
fs.readFileSync(path.join(__dirname, "../package.json"), "utf8")
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
program
|
|
34
|
+
.name("switchboard")
|
|
35
|
+
.description("Switchboard CLI — manage your workspace from the terminal")
|
|
36
|
+
.option("--json", "Output JSON to stdout")
|
|
37
|
+
.option("-q, --quiet", "Suppress non-essential output")
|
|
38
|
+
.version(packageJson.version);
|
|
39
|
+
|
|
40
|
+
registerInitCommand(program);
|
|
41
|
+
registerSetupCommand(program);
|
|
42
|
+
registerHealthCommand(program);
|
|
43
|
+
registerDoctorCommand(program);
|
|
44
|
+
registerVerifyCommands(program);
|
|
45
|
+
registerAuth(program);
|
|
46
|
+
registerAccountCommands(program);
|
|
47
|
+
registerWorkspacesCommands(program);
|
|
48
|
+
registerKeysCommands(program);
|
|
49
|
+
registerProjectsCommands(program);
|
|
50
|
+
registerOrgCommands(program);
|
|
51
|
+
registerEndUsersCommands(program);
|
|
52
|
+
registerBillingCommands(program);
|
|
53
|
+
registerEnvCommands(program);
|
|
54
|
+
registerUsageCommands(program);
|
|
55
|
+
registerIntegrationCommands(program);
|
|
56
|
+
|
|
57
|
+
program.parse();
|
|
58
|
+
|
|
59
|
+
if (!process.argv.slice(2).length) {
|
|
60
|
+
program.outputHelp();
|
|
61
|
+
}
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for Switchboard account and gateway APIs.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { accountApiUrl, resolveAccountConfig, resolveConfig } from "./config.js";
|
|
6
|
+
import { emitHttpError, fail, normalizeError } from "./output.js";
|
|
7
|
+
|
|
8
|
+
const PROJECT_SCOPED_ACCOUNT_PATHS = [
|
|
9
|
+
/^\/keys(?:\/|$)/,
|
|
10
|
+
/^\/end_users(?:\/|$)/,
|
|
11
|
+
/^\/billing\/(?:ledger|top_up|prepaid)(?:\/|\?|$)/,
|
|
12
|
+
/^\/usage(?:\?|$)/,
|
|
13
|
+
/^\/integration_kit(?:\?|$)/,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Performs an authenticated account API request.
|
|
18
|
+
*/
|
|
19
|
+
export async function accountRequest(method, path, { body, json, config, projectId } = {}) {
|
|
20
|
+
const { url, init } = await accountRequestConfig(method, path, {
|
|
21
|
+
body,
|
|
22
|
+
json,
|
|
23
|
+
config,
|
|
24
|
+
projectId,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return handleResponse(await fetch(url, init), json);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function accountRequestConfig(
|
|
31
|
+
method,
|
|
32
|
+
path,
|
|
33
|
+
{ body, json, config, projectId, accept = "application/json", requireProjectScope = false } = {},
|
|
34
|
+
) {
|
|
35
|
+
const cfg = await loadAccountConfig(config, json);
|
|
36
|
+
const token = requireAccountToken(cfg, json);
|
|
37
|
+
|
|
38
|
+
const url = new URL(accountApiUrl(cfg) + path);
|
|
39
|
+
const selectedProjectId = projectId || cfg.projectId;
|
|
40
|
+
if (selectedProjectId) {
|
|
41
|
+
url.searchParams.set("project_id", selectedProjectId);
|
|
42
|
+
} else if (requireProjectScope || requiresProject(path)) {
|
|
43
|
+
fail(
|
|
44
|
+
"Specify a project before this command. Run: switchboard projects list, then switchboard projects use <id>.",
|
|
45
|
+
1,
|
|
46
|
+
json,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const headers = {
|
|
51
|
+
Authorization: `Bearer ${token}`,
|
|
52
|
+
Accept: accept,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (body) {
|
|
56
|
+
headers["Content-Type"] = "application/json";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
url,
|
|
61
|
+
init: {
|
|
62
|
+
method,
|
|
63
|
+
headers,
|
|
64
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Performs a public account API request (register, login).
|
|
71
|
+
*/
|
|
72
|
+
export async function accountPublicRequest(method, path, { body, json, config } = {}) {
|
|
73
|
+
const cfg = config || resolveConfig();
|
|
74
|
+
const url = accountApiUrl(cfg) + path;
|
|
75
|
+
|
|
76
|
+
const res = await fetch(url, {
|
|
77
|
+
method,
|
|
78
|
+
headers: {
|
|
79
|
+
"Content-Type": "application/json",
|
|
80
|
+
Accept: "application/json",
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify(body),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return handleResponse(res, json);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function requiresProject(path) {
|
|
89
|
+
return PROJECT_SCOPED_ACCOUNT_PATHS.some((pattern) => pattern.test(path));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function loadAccountConfig(config, json) {
|
|
93
|
+
try {
|
|
94
|
+
return await resolveAccountConfig(config);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
fail(
|
|
97
|
+
error.message || "Could not read Switchboard account session from the OS keychain",
|
|
98
|
+
2,
|
|
99
|
+
json,
|
|
100
|
+
"keychain_unavailable",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function requireAccountToken(cfg, json) {
|
|
106
|
+
if (!cfg.accountToken) {
|
|
107
|
+
fail(
|
|
108
|
+
"Not logged in. Run: switchboard auth login and complete sign-in in your browser.",
|
|
109
|
+
2,
|
|
110
|
+
json,
|
|
111
|
+
"authentication_required",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return cfg.accountToken;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* GET /health without authentication.
|
|
120
|
+
*/
|
|
121
|
+
export async function healthCheck(config) {
|
|
122
|
+
const cfg = config || resolveConfig();
|
|
123
|
+
const url = `${cfg.baseUrl.replace(/\/$/, "")}/health`;
|
|
124
|
+
const res = await fetch(url);
|
|
125
|
+
const text = await res.text();
|
|
126
|
+
let data;
|
|
127
|
+
try {
|
|
128
|
+
data = JSON.parse(text);
|
|
129
|
+
} catch {
|
|
130
|
+
data = { status: text };
|
|
131
|
+
}
|
|
132
|
+
return { ok: res.ok, status: res.status, data };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function handleResponse(res, jsonMode) {
|
|
136
|
+
const text = await res.text();
|
|
137
|
+
let data;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
data = text ? JSON.parse(text) : null;
|
|
141
|
+
} catch {
|
|
142
|
+
data = { raw: text };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
emitHttpError(normalizeError(data, text, res.status), res.status, { json: jsonMode });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { ok: true, status: res.status, data };
|
|
150
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authenticated account management commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { accountRequest } from "../client.js";
|
|
6
|
+
import { fail, emit, globalFlags } from "../output.js";
|
|
7
|
+
|
|
8
|
+
export function registerAccountCommands(program) {
|
|
9
|
+
const account = program.command("account").description("Account");
|
|
10
|
+
|
|
11
|
+
account
|
|
12
|
+
.command("show")
|
|
13
|
+
.description("Show the authenticated account")
|
|
14
|
+
.action(async (_opts, cmd) => {
|
|
15
|
+
const flags = globalFlags(cmd);
|
|
16
|
+
const { data } = await accountRequest("GET", "/me", { json: flags.json });
|
|
17
|
+
emit(flags.json ? data : JSON.stringify(data, null, 2), flags);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
account
|
|
21
|
+
.command("update-email")
|
|
22
|
+
.description("Request an account email change")
|
|
23
|
+
.requiredOption("--email <email>")
|
|
24
|
+
.requiredOption("--current-password <password>")
|
|
25
|
+
.action(async (opts, cmd) => {
|
|
26
|
+
const flags = globalFlags(cmd);
|
|
27
|
+
const { data } = await accountRequest("PATCH", "/me", {
|
|
28
|
+
body: {
|
|
29
|
+
email: opts.email,
|
|
30
|
+
current_password: opts.currentPassword,
|
|
31
|
+
},
|
|
32
|
+
json: flags.json,
|
|
33
|
+
});
|
|
34
|
+
emit(flags.json ? data : "Email confirmation sent", flags);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
account
|
|
38
|
+
.command("change-password")
|
|
39
|
+
.description("Change the account password")
|
|
40
|
+
.requiredOption("--current-password <password>")
|
|
41
|
+
.requiredOption("--password <password>")
|
|
42
|
+
.requiredOption("--password-confirmation <password>")
|
|
43
|
+
.action(async (opts, cmd) => {
|
|
44
|
+
const flags = globalFlags(cmd);
|
|
45
|
+
const { data } = await accountRequest("PATCH", "/me", {
|
|
46
|
+
body: {
|
|
47
|
+
current_password: opts.currentPassword,
|
|
48
|
+
password: opts.password,
|
|
49
|
+
password_confirmation: opts.passwordConfirmation,
|
|
50
|
+
},
|
|
51
|
+
json: flags.json,
|
|
52
|
+
});
|
|
53
|
+
emit(flags.json ? data : "Password changed", flags);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
account
|
|
57
|
+
.command("delete")
|
|
58
|
+
.description("Delete the authenticated account")
|
|
59
|
+
.requiredOption("--confirm <email>")
|
|
60
|
+
.action(async (opts, cmd) => {
|
|
61
|
+
const flags = globalFlags(cmd);
|
|
62
|
+
const { data: accountData } = await accountRequest("GET", "/me", { json: flags.json });
|
|
63
|
+
const email = accountData?.user?.email;
|
|
64
|
+
|
|
65
|
+
if (opts.confirm !== email) {
|
|
66
|
+
fail("Confirmation email does not match the authenticated account.", 2, flags.json);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { data } = await accountRequest("DELETE", "/me", { json: flags.json });
|
|
70
|
+
emit(flags.json ? data : `Deleted account ${email}`, flags);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
account
|
|
74
|
+
.command("restore")
|
|
75
|
+
.description("Restore the authenticated account")
|
|
76
|
+
.action(async (_opts, cmd) => {
|
|
77
|
+
const flags = globalFlags(cmd);
|
|
78
|
+
const { data } = await accountRequest("POST", "/me/restore", { json: flags.json });
|
|
79
|
+
emit(flags.json ? data : `Restored account ${data.user.email}`, flags);
|
|
80
|
+
});
|
|
81
|
+
}
|