@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
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stores the account session token in the operating system credential store.
|
|
3
|
+
*
|
|
4
|
+
* The CLI intentionally never persists account tokens in ~/.switchboard/config.json
|
|
5
|
+
* or reads them from environment variables. Browser login writes the token here,
|
|
6
|
+
* and authenticated commands read it back only when they need to call the account
|
|
7
|
+
* API.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execFile } from "node:child_process";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { promisify } from "node:util";
|
|
13
|
+
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
const SERVICE = "Switchboard CLI";
|
|
16
|
+
const ACCOUNT = "account-session";
|
|
17
|
+
const PROJECT_SECRET_PREFIX = "project-secret";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reads the current account session token from the OS keychain.
|
|
21
|
+
*/
|
|
22
|
+
export async function getAccountToken() {
|
|
23
|
+
try {
|
|
24
|
+
if (process.platform === "darwin") {
|
|
25
|
+
const { stdout } = await execFileAsync("security", [
|
|
26
|
+
"find-generic-password",
|
|
27
|
+
"-a",
|
|
28
|
+
ACCOUNT,
|
|
29
|
+
"-s",
|
|
30
|
+
SERVICE,
|
|
31
|
+
"-w",
|
|
32
|
+
]);
|
|
33
|
+
return stdout.trim() || null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (process.platform === "linux") {
|
|
37
|
+
const { stdout } = await execFileAsync("secret-tool", [
|
|
38
|
+
"lookup",
|
|
39
|
+
"service",
|
|
40
|
+
SERVICE,
|
|
41
|
+
"account",
|
|
42
|
+
ACCOUNT,
|
|
43
|
+
]);
|
|
44
|
+
return stdout.trim() || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throw unsupportedPlatformError();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (credentialMissing(error)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
throw normalizeKeychainError(error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Writes the current account session token to the OS keychain.
|
|
59
|
+
*/
|
|
60
|
+
export async function setAccountToken(token) {
|
|
61
|
+
if (!token) {
|
|
62
|
+
throw new Error("Cannot store an empty Switchboard account token");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
if (process.platform === "darwin") {
|
|
67
|
+
await execFileAsync("security", [
|
|
68
|
+
"add-generic-password",
|
|
69
|
+
"-a",
|
|
70
|
+
ACCOUNT,
|
|
71
|
+
"-s",
|
|
72
|
+
SERVICE,
|
|
73
|
+
"-w",
|
|
74
|
+
token,
|
|
75
|
+
"-U",
|
|
76
|
+
]);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (process.platform === "linux") {
|
|
81
|
+
await execFileWithInput(
|
|
82
|
+
"secret-tool",
|
|
83
|
+
["store", "--label", SERVICE, "service", SERVICE, "account", ACCOUNT],
|
|
84
|
+
token,
|
|
85
|
+
);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw unsupportedPlatformError();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw normalizeKeychainError(error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Deletes the current account session token from the OS keychain.
|
|
97
|
+
*/
|
|
98
|
+
export async function deleteAccountToken() {
|
|
99
|
+
try {
|
|
100
|
+
if (process.platform === "darwin") {
|
|
101
|
+
await execFileAsync("security", [
|
|
102
|
+
"delete-generic-password",
|
|
103
|
+
"-a",
|
|
104
|
+
ACCOUNT,
|
|
105
|
+
"-s",
|
|
106
|
+
SERVICE,
|
|
107
|
+
]);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (process.platform === "linux") {
|
|
112
|
+
await execFileAsync("secret-tool", [
|
|
113
|
+
"clear",
|
|
114
|
+
"service",
|
|
115
|
+
SERVICE,
|
|
116
|
+
"account",
|
|
117
|
+
ACCOUNT,
|
|
118
|
+
]);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
throw unsupportedPlatformError();
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (credentialMissing(error)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
throw normalizeKeychainError(error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Reads a stored project secret key from the OS keychain.
|
|
134
|
+
*/
|
|
135
|
+
export async function getProjectSecretKey(projectId, mode = "sandbox") {
|
|
136
|
+
return getCredential(projectSecretAccount(projectId, mode));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Stores a project secret key in the OS keychain.
|
|
141
|
+
*/
|
|
142
|
+
export async function setProjectSecretKey(projectId, mode, token) {
|
|
143
|
+
if (!token) {
|
|
144
|
+
throw new Error("Cannot store an empty Switchboard project secret key");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return setCredential(projectSecretAccount(projectId, mode), token);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Deletes a stored project secret key from the OS keychain.
|
|
152
|
+
*/
|
|
153
|
+
export async function deleteProjectSecretKey(projectId, mode = "sandbox") {
|
|
154
|
+
return deleteCredential(projectSecretAccount(projectId, mode));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function projectSecretAccount(projectId, mode) {
|
|
158
|
+
if (!projectId) {
|
|
159
|
+
throw new Error("Project id is required for Switchboard project secret storage");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return `${PROJECT_SECRET_PREFIX}:${projectId}:${mode}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function getCredential(account) {
|
|
166
|
+
try {
|
|
167
|
+
if (process.platform === "darwin") {
|
|
168
|
+
const { stdout } = await execFileAsync("security", [
|
|
169
|
+
"find-generic-password",
|
|
170
|
+
"-a",
|
|
171
|
+
account,
|
|
172
|
+
"-s",
|
|
173
|
+
SERVICE,
|
|
174
|
+
"-w",
|
|
175
|
+
]);
|
|
176
|
+
return stdout.trim() || null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (process.platform === "linux") {
|
|
180
|
+
const { stdout } = await execFileAsync("secret-tool", [
|
|
181
|
+
"lookup",
|
|
182
|
+
"service",
|
|
183
|
+
SERVICE,
|
|
184
|
+
"account",
|
|
185
|
+
account,
|
|
186
|
+
]);
|
|
187
|
+
return stdout.trim() || null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
throw unsupportedPlatformError();
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (credentialMissing(error)) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
throw normalizeKeychainError(error);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function setCredential(account, token) {
|
|
201
|
+
try {
|
|
202
|
+
if (process.platform === "darwin") {
|
|
203
|
+
await execFileAsync("security", [
|
|
204
|
+
"add-generic-password",
|
|
205
|
+
"-a",
|
|
206
|
+
account,
|
|
207
|
+
"-s",
|
|
208
|
+
SERVICE,
|
|
209
|
+
"-w",
|
|
210
|
+
token,
|
|
211
|
+
"-U",
|
|
212
|
+
]);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (process.platform === "linux") {
|
|
217
|
+
await execFileWithInput(
|
|
218
|
+
"secret-tool",
|
|
219
|
+
["store", "--label", SERVICE, "service", SERVICE, "account", account],
|
|
220
|
+
token,
|
|
221
|
+
);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
throw unsupportedPlatformError();
|
|
226
|
+
} catch (error) {
|
|
227
|
+
throw normalizeKeychainError(error);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function deleteCredential(account) {
|
|
232
|
+
try {
|
|
233
|
+
if (process.platform === "darwin") {
|
|
234
|
+
await execFileAsync("security", [
|
|
235
|
+
"delete-generic-password",
|
|
236
|
+
"-a",
|
|
237
|
+
account,
|
|
238
|
+
"-s",
|
|
239
|
+
SERVICE,
|
|
240
|
+
]);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (process.platform === "linux") {
|
|
245
|
+
await execFileAsync("secret-tool", [
|
|
246
|
+
"clear",
|
|
247
|
+
"service",
|
|
248
|
+
SERVICE,
|
|
249
|
+
"account",
|
|
250
|
+
account,
|
|
251
|
+
]);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
throw unsupportedPlatformError();
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (credentialMissing(error)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
throw normalizeKeychainError(error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function credentialMissing(error) {
|
|
266
|
+
return error?.code === 44 || error?.code === 1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function execFileWithInput(command, args, input) {
|
|
270
|
+
return new Promise((resolve, reject) => {
|
|
271
|
+
const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
272
|
+
let stdout = "";
|
|
273
|
+
let stderr = "";
|
|
274
|
+
|
|
275
|
+
child.stdout.on("data", (chunk) => {
|
|
276
|
+
stdout += chunk;
|
|
277
|
+
});
|
|
278
|
+
child.stderr.on("data", (chunk) => {
|
|
279
|
+
stderr += chunk;
|
|
280
|
+
});
|
|
281
|
+
child.on("error", reject);
|
|
282
|
+
child.on("close", (code) => {
|
|
283
|
+
if (code === 0) {
|
|
284
|
+
resolve({ stdout, stderr });
|
|
285
|
+
} else {
|
|
286
|
+
const error = new Error(stderr || `${command} exited with ${code}`);
|
|
287
|
+
error.code = code;
|
|
288
|
+
error.stdout = stdout;
|
|
289
|
+
error.stderr = stderr;
|
|
290
|
+
reject(error);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
child.stdin.end(input);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function unsupportedPlatformError() {
|
|
299
|
+
return new Error(
|
|
300
|
+
"Switchboard account login requires an OS keychain. This CLI currently supports macOS Keychain and Linux Secret Service.",
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function normalizeKeychainError(error) {
|
|
305
|
+
if (error?.code === "ENOENT") {
|
|
306
|
+
return new Error(
|
|
307
|
+
"Switchboard account login requires an OS keychain command. On Linux, install libsecret's `secret-tool`; on macOS, ensure the `security` command is available.",
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return error;
|
|
312
|
+
}
|
package/lib/output.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable and JSON output helpers for the Switchboard CLI.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Prints data as JSON or a human message depending on global flags.
|
|
7
|
+
*/
|
|
8
|
+
export function emit(data, { json, quiet } = {}) {
|
|
9
|
+
if (json) {
|
|
10
|
+
console.log(JSON.stringify(data, null, 2));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (!quiet && typeof data === "string") {
|
|
14
|
+
console.log(data);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Reads global flags from a Commander command regardless of nesting depth.
|
|
20
|
+
*/
|
|
21
|
+
export function globalFlags(cmd) {
|
|
22
|
+
let current = cmd;
|
|
23
|
+
|
|
24
|
+
while (current?.parent) {
|
|
25
|
+
current = current.parent;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return current?.opts?.() ?? cmd?.opts?.() ?? {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Prints an error to stderr and exits with the given code.
|
|
33
|
+
*/
|
|
34
|
+
export function fail(message, code = 1, json = false, type = "invalid_request") {
|
|
35
|
+
if (json) {
|
|
36
|
+
console.log(
|
|
37
|
+
JSON.stringify(
|
|
38
|
+
{
|
|
39
|
+
ok: false,
|
|
40
|
+
error: {
|
|
41
|
+
type,
|
|
42
|
+
message,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
null,
|
|
46
|
+
2,
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
process.exit(code);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.error(message);
|
|
53
|
+
process.exit(code);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Maps HTTP status to CLI exit codes per agent contract.
|
|
58
|
+
*/
|
|
59
|
+
export function exitCodeForStatus(status) {
|
|
60
|
+
if (status === 401 || status === 403) return 2;
|
|
61
|
+
if (status >= 500) return 3;
|
|
62
|
+
if (status >= 400) return 1;
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function parseErrorResponse(res) {
|
|
67
|
+
const text = await res.text();
|
|
68
|
+
let data;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
data = text ? JSON.parse(text) : {};
|
|
72
|
+
} catch {
|
|
73
|
+
data = { raw: text };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return normalizeError(data, text, res.status);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function normalizeError(data, text, status) {
|
|
80
|
+
if (data?.error && typeof data.error === "object") {
|
|
81
|
+
return {
|
|
82
|
+
type: data.error.type || "error",
|
|
83
|
+
message: data.error.message || `HTTP ${status}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (typeof data?.error === "string") {
|
|
88
|
+
return { type: "error", message: data.error };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { type: "error", message: text || `HTTP ${status}` };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function emitHttpError(error, status, flags = {}) {
|
|
95
|
+
if (flags.json) {
|
|
96
|
+
console.log(JSON.stringify({ ok: false, status, error }, null, 2));
|
|
97
|
+
} else {
|
|
98
|
+
console.error(error.message);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
process.exit(exitCodeForStatus(status));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Formats a simple key-value listing for human output.
|
|
106
|
+
*/
|
|
107
|
+
export function printList(title, items, formatter) {
|
|
108
|
+
console.log(title);
|
|
109
|
+
for (const item of items) {
|
|
110
|
+
console.log(formatter(item));
|
|
111
|
+
}
|
|
112
|
+
}
|