@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 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
+ }