@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,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-safe environment configuration commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { accountRequest } from "../client.js";
|
|
9
|
+
import { resolveAccountConfig, resolveConfig } from "../config.js";
|
|
10
|
+
import {
|
|
11
|
+
getProjectSecretKey,
|
|
12
|
+
setProjectSecretKey,
|
|
13
|
+
} from "../credentialStore.js";
|
|
14
|
+
import { emit, fail, globalFlags } from "../output.js";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_ENV_FILE = ".env.local";
|
|
17
|
+
const SECRET_ENV_NAMES = ["SWITCHBOARD_API_KEY"];
|
|
18
|
+
const SECRET_PATTERNS = [
|
|
19
|
+
/sb_test_[A-Za-z0-9_-]+/g,
|
|
20
|
+
/sb_live_[A-Za-z0-9_-]+/g,
|
|
21
|
+
/sb_sess_[A-Za-z0-9_-]+/g,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function registerEnvCommands(program) {
|
|
25
|
+
const env = program.command("env").description("Agent-safe environment setup");
|
|
26
|
+
|
|
27
|
+
env
|
|
28
|
+
.command("configure")
|
|
29
|
+
.description("Configure Switchboard environment values without printing secrets")
|
|
30
|
+
.option("--mode <mode>", "client, server, or both", "client")
|
|
31
|
+
.option("--file <path>", "Local env file to update", DEFAULT_ENV_FILE)
|
|
32
|
+
.option("--target <target>", "local or exec", "local")
|
|
33
|
+
.option("--secret-command <command>", "Command that stores secrets from stdin JSON")
|
|
34
|
+
.option("--project-id <id>", "Override selected project")
|
|
35
|
+
.option("--force", "Replace existing Switchboard-managed values")
|
|
36
|
+
.action(async (opts, cmd) => {
|
|
37
|
+
const flags = globalFlags(cmd);
|
|
38
|
+
const result = await configureEnvironment(opts, { json: flags.json });
|
|
39
|
+
emit(flags.json ? result : humanConfigureMessage(result), flags);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
env
|
|
43
|
+
.command("run")
|
|
44
|
+
.description("Run a command with Switchboard managed secrets injected")
|
|
45
|
+
.allowUnknownOption(true)
|
|
46
|
+
.argument("[command...]", "Command to run")
|
|
47
|
+
.action(async (command, _opts, cmd) => {
|
|
48
|
+
const flags = globalFlags(cmd);
|
|
49
|
+
await runWithEnvironment(command, { json: flags.json });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function configureEnvironment(
|
|
54
|
+
opts,
|
|
55
|
+
{
|
|
56
|
+
request = accountRequest,
|
|
57
|
+
getSecret = getProjectSecretKey,
|
|
58
|
+
setSecret = setProjectSecretKey,
|
|
59
|
+
cwd = process.cwd(),
|
|
60
|
+
json = false,
|
|
61
|
+
runSecretCommand = runSecretSinkCommand,
|
|
62
|
+
} = {},
|
|
63
|
+
) {
|
|
64
|
+
const mode = normalizeMode(opts.mode);
|
|
65
|
+
const target = normalizeTarget(opts.target);
|
|
66
|
+
const projectId = opts.projectId;
|
|
67
|
+
const force = Boolean(opts.force);
|
|
68
|
+
|
|
69
|
+
if (target === "exec" && !opts.secretCommand) {
|
|
70
|
+
fail("--secret-command is required when --target exec is used", 1, json);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { data: kit } = await request("GET", "/integration_kit", {
|
|
74
|
+
json,
|
|
75
|
+
projectId,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const selectedProjectId = String(projectId || kit.project_id || resolveConfig().projectId || "");
|
|
79
|
+
if (!selectedProjectId) {
|
|
80
|
+
fail("Could not determine the selected Switchboard project id", 1, json);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const changed = [];
|
|
84
|
+
const skipped = [];
|
|
85
|
+
const secrets = [];
|
|
86
|
+
const envFile = path.resolve(cwd, opts.file || DEFAULT_ENV_FILE);
|
|
87
|
+
|
|
88
|
+
const envUpdates = {};
|
|
89
|
+
|
|
90
|
+
if (mode === "client" || mode === "both") {
|
|
91
|
+
envUpdates.SWITCHBOARD_CLIENT_URL = kit.client_url || kit.virtual_microservice_url;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (mode === "server" || mode === "both") {
|
|
95
|
+
envUpdates.SWITCHBOARD_BASE_URL = kit.server_base_url || kit.base_url;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const fileResult = updateEnvFile(envFile, envUpdates, { force });
|
|
99
|
+
changed.push(...fileResult.changed);
|
|
100
|
+
skipped.push(...fileResult.skipped);
|
|
101
|
+
|
|
102
|
+
if (mode === "server" || mode === "both") {
|
|
103
|
+
const secretResult = await ensureServerSecret({
|
|
104
|
+
projectId: selectedProjectId,
|
|
105
|
+
force,
|
|
106
|
+
request,
|
|
107
|
+
getSecret,
|
|
108
|
+
setSecret,
|
|
109
|
+
target,
|
|
110
|
+
secretCommand: opts.secretCommand,
|
|
111
|
+
runSecretCommand,
|
|
112
|
+
json,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
changed.push(...secretResult.changed);
|
|
116
|
+
skipped.push(...secretResult.skipped);
|
|
117
|
+
secrets.push(secretResult.secret);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
project_id: selectedProjectId,
|
|
123
|
+
mode,
|
|
124
|
+
target,
|
|
125
|
+
env_file: envFile,
|
|
126
|
+
changed,
|
|
127
|
+
skipped,
|
|
128
|
+
secrets,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function runWithEnvironment(
|
|
133
|
+
command,
|
|
134
|
+
{
|
|
135
|
+
config,
|
|
136
|
+
getSecret = getProjectSecretKey,
|
|
137
|
+
env = process.env,
|
|
138
|
+
spawnProcess = spawn,
|
|
139
|
+
exitProcess = process.exit,
|
|
140
|
+
killProcess = process.kill,
|
|
141
|
+
json = false,
|
|
142
|
+
} = {},
|
|
143
|
+
) {
|
|
144
|
+
if (!command || command.length === 0) {
|
|
145
|
+
fail("Usage: switchboard env run -- <command>", 1, json);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const cfg = await resolveAccountConfig(config || resolveConfig());
|
|
149
|
+
if (!cfg.projectId) {
|
|
150
|
+
fail(
|
|
151
|
+
"Specify a project before this command. Run: switchboard projects list, then switchboard projects use <id>.",
|
|
152
|
+
1,
|
|
153
|
+
json,
|
|
154
|
+
"project_required",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const secret = await getSecret(cfg.projectId, "sandbox");
|
|
159
|
+
if (!secret) {
|
|
160
|
+
fail(
|
|
161
|
+
"No managed Switchboard server secret found. Run: switchboard env configure --mode server",
|
|
162
|
+
1,
|
|
163
|
+
json,
|
|
164
|
+
"secret_required",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const child = spawnProcess(command[0], command.slice(1), {
|
|
169
|
+
stdio: "inherit",
|
|
170
|
+
env: {
|
|
171
|
+
...env,
|
|
172
|
+
SWITCHBOARD_API_KEY: secret,
|
|
173
|
+
SWITCHBOARD_PROJECT_ID: cfg.projectId,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
child.on("exit", (code, signal) => {
|
|
179
|
+
try {
|
|
180
|
+
if (signal) {
|
|
181
|
+
killProcess(process.pid, signal);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
resolve(exitProcess(code ?? 0));
|
|
186
|
+
} catch (error) {
|
|
187
|
+
reject(error);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeMode(mode) {
|
|
194
|
+
if (mode === "client" || mode === "server" || mode === "both") {
|
|
195
|
+
return mode;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (mode === "virtual") {
|
|
199
|
+
return "client";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fail("--mode must be client, server, or both");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function normalizeTarget(target) {
|
|
206
|
+
if (target === "local" || target === "exec") {
|
|
207
|
+
return target;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fail("--target must be local or exec");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function updateEnvFile(file, updates, { force }) {
|
|
214
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
215
|
+
const parsed = parseEnvLines(existing);
|
|
216
|
+
const changed = [];
|
|
217
|
+
const skipped = [];
|
|
218
|
+
const nextLines = [...parsed.lines];
|
|
219
|
+
|
|
220
|
+
for (const [name, value] of Object.entries(updates)) {
|
|
221
|
+
if (!value) continue;
|
|
222
|
+
|
|
223
|
+
if (parsed.indexes.has(name)) {
|
|
224
|
+
if (!force) {
|
|
225
|
+
skipped.push(name);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
nextLines[parsed.indexes.get(name)] = `${name}=${value}`;
|
|
230
|
+
changed.push(name);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
nextLines.push(`${name}=${value}`);
|
|
235
|
+
changed.push(name);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (changed.length > 0) {
|
|
239
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
240
|
+
fs.writeFileSync(file, `${trimTrailingBlankLines(nextLines).join("\n")}\n`, {
|
|
241
|
+
mode: 0o600,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { changed, skipped };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function parseEnvLines(text) {
|
|
249
|
+
const lines = text ? text.replace(/\r\n/g, "\n").split("\n") : [];
|
|
250
|
+
if (lines.at(-1) === "") lines.pop();
|
|
251
|
+
|
|
252
|
+
const indexes = new Map();
|
|
253
|
+
lines.forEach((line, index) => {
|
|
254
|
+
const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
255
|
+
if (match) indexes.set(match[1], index);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return { lines, indexes };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function trimTrailingBlankLines(lines) {
|
|
262
|
+
const next = [...lines];
|
|
263
|
+
while (next.length > 0 && next.at(-1) === "") {
|
|
264
|
+
next.pop();
|
|
265
|
+
}
|
|
266
|
+
return next;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function ensureServerSecret({
|
|
270
|
+
projectId,
|
|
271
|
+
force,
|
|
272
|
+
request,
|
|
273
|
+
getSecret,
|
|
274
|
+
setSecret,
|
|
275
|
+
target,
|
|
276
|
+
secretCommand,
|
|
277
|
+
runSecretCommand,
|
|
278
|
+
json,
|
|
279
|
+
}) {
|
|
280
|
+
const existing = await getSecret(projectId, "sandbox");
|
|
281
|
+
const changed = [];
|
|
282
|
+
const skipped = [];
|
|
283
|
+
let plaintext = existing;
|
|
284
|
+
let metadata = null;
|
|
285
|
+
|
|
286
|
+
if (!plaintext || force) {
|
|
287
|
+
const { data } = await request("POST", "/keys", {
|
|
288
|
+
body: {
|
|
289
|
+
mode: "sandbox",
|
|
290
|
+
name: "Managed server secret",
|
|
291
|
+
key_type: "secret",
|
|
292
|
+
},
|
|
293
|
+
json,
|
|
294
|
+
projectId,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
plaintext = data.plaintext;
|
|
298
|
+
metadata = redactedKeyMetadata(data);
|
|
299
|
+
changed.push("SWITCHBOARD_API_KEY");
|
|
300
|
+
} else {
|
|
301
|
+
skipped.push("SWITCHBOARD_API_KEY");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!plaintext) {
|
|
305
|
+
fail("Switchboard did not return a one-time project secret key", 1, json);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (target === "local") {
|
|
309
|
+
await setSecret(projectId, "sandbox", plaintext);
|
|
310
|
+
} else {
|
|
311
|
+
await runSecretCommand(secretCommand, {
|
|
312
|
+
secrets: SECRET_ENV_NAMES.map((name) => ({
|
|
313
|
+
name,
|
|
314
|
+
value: plaintext,
|
|
315
|
+
metadata: metadata || { project_id: projectId, mode: "sandbox" },
|
|
316
|
+
})),
|
|
317
|
+
redactValues: [plaintext],
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
changed,
|
|
323
|
+
skipped,
|
|
324
|
+
secret: {
|
|
325
|
+
name: "SWITCHBOARD_API_KEY",
|
|
326
|
+
stored: target,
|
|
327
|
+
...metadata,
|
|
328
|
+
redacted: true,
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function redactedKeyMetadata(data) {
|
|
334
|
+
return {
|
|
335
|
+
key_id: data.id,
|
|
336
|
+
project_id: data.project_id != null ? String(data.project_id) : undefined,
|
|
337
|
+
mode: data.mode,
|
|
338
|
+
key_type: data.key_type,
|
|
339
|
+
last_four: data.last_four,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function runSecretSinkCommand(command, { secrets, redactValues }) {
|
|
344
|
+
const redactions = [...SECRET_PATTERNS, ...redactValues.map((value) => new RegExp(escapeRegExp(value), "g"))];
|
|
345
|
+
const payload = JSON.stringify({ secrets });
|
|
346
|
+
|
|
347
|
+
await new Promise((resolve, reject) => {
|
|
348
|
+
const child = spawn(command, {
|
|
349
|
+
shell: true,
|
|
350
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
let stdout = "";
|
|
354
|
+
let stderr = "";
|
|
355
|
+
|
|
356
|
+
child.stdout.on("data", (chunk) => {
|
|
357
|
+
stdout += redact(String(chunk), redactions);
|
|
358
|
+
});
|
|
359
|
+
child.stderr.on("data", (chunk) => {
|
|
360
|
+
stderr += redact(String(chunk), redactions);
|
|
361
|
+
});
|
|
362
|
+
child.on("error", reject);
|
|
363
|
+
child.on("close", (code) => {
|
|
364
|
+
if (code === 0) {
|
|
365
|
+
resolve();
|
|
366
|
+
} else {
|
|
367
|
+
const error = new Error(stderr || stdout || `Secret command exited with ${code}`);
|
|
368
|
+
error.code = code;
|
|
369
|
+
reject(error);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
child.stdin.end(payload);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function redact(text, redactions) {
|
|
378
|
+
return redactions.reduce((acc, pattern) => acc.replace(pattern, "[REDACTED]"), text);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function escapeRegExp(value) {
|
|
382
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function appOriginFromApiBase(baseUrl) {
|
|
386
|
+
return String(baseUrl || "").replace(/\/v1\/?$/, "");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function humanConfigureMessage(result) {
|
|
390
|
+
const changed = result.changed.length > 0 ? result.changed.join(", ") : "none";
|
|
391
|
+
const skipped = result.skipped.length > 0 ? ` Skipped existing: ${result.skipped.join(", ")}.` : "";
|
|
392
|
+
return `Configured Switchboard env (${result.mode}) for project ${result.project_id}. Changed: ${changed}.${skipped}`;
|
|
393
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health check command.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { healthCheck } from "../client.js";
|
|
6
|
+
import { resolveConfig } from "../config.js";
|
|
7
|
+
import { emit, globalFlags } from "../output.js";
|
|
8
|
+
|
|
9
|
+
export function registerHealthCommand(program) {
|
|
10
|
+
program
|
|
11
|
+
.command("health")
|
|
12
|
+
.description("Check API health")
|
|
13
|
+
.action(async (_opts, cmd) => {
|
|
14
|
+
const flags = globalFlags(cmd);
|
|
15
|
+
const result = await healthCheck(resolveConfig());
|
|
16
|
+
if (!result.ok) process.exit(3);
|
|
17
|
+
|
|
18
|
+
if (flags.json) {
|
|
19
|
+
emit(result.data, { json: true });
|
|
20
|
+
} else if (!flags.quiet) {
|
|
21
|
+
console.log(result.data?.status ?? "ok");
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project init and agent manifest commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { resolveConfig, gatewayApiUrl } from "../config.js";
|
|
8
|
+
import { emit } from "../output.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builds default frontend environment placeholders for Pro-bono Embed apps.
|
|
12
|
+
*
|
|
13
|
+
* The generated Pro-bono Embed URL is the only value a browser app needs by default.
|
|
14
|
+
*/
|
|
15
|
+
function envBlock(clientUrl) {
|
|
16
|
+
return `SWITCHBOARD_CLIENT_URL=${clientUrl}
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Builds the agent manifest for automated setup.
|
|
22
|
+
*/
|
|
23
|
+
export function agentManifest(config) {
|
|
24
|
+
const base = gatewayApiUrl(config);
|
|
25
|
+
const clientUrl = `${config.baseUrl.replace(/\/$/, "")}/m/<client-gateway-slug>/v1`;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
base_url: base,
|
|
29
|
+
client_url: clientUrl,
|
|
30
|
+
virtual_microservice_url: clientUrl,
|
|
31
|
+
account_api: `${config.baseUrl.replace(/\/$/, "")}/v1/account`,
|
|
32
|
+
env: envBlock(clientUrl),
|
|
33
|
+
checklist: [
|
|
34
|
+
"switchboard setup --target client --json",
|
|
35
|
+
"export SWITCHBOARD_CLIENT_URL=<client_url from integrations show>",
|
|
36
|
+
"Use mountSwitchboardWidget({ clientUrl, target }) in browser apps",
|
|
37
|
+
"Use createSwitchboardClient({ clientUrl, storage }) only for custom UI",
|
|
38
|
+
"switchboard billing top-up --amount-micros <micros>",
|
|
39
|
+
],
|
|
40
|
+
browser_smoke_test: `import { mountSwitchboardWidget } from "@switchboard/sdk";
|
|
41
|
+
|
|
42
|
+
mountSwitchboardWidget({
|
|
43
|
+
clientUrl: process.env.SWITCHBOARD_CLIENT_URL ?? "${clientUrl}",
|
|
44
|
+
target: "#switchboard",
|
|
45
|
+
});`,
|
|
46
|
+
automation_note:
|
|
47
|
+
"Pro-bono Embed auth is browser/mobile only and requires a real browser challenge. Use account/CLI APIs only for project configuration automation.",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function registerInitCommand(program) {
|
|
52
|
+
program
|
|
53
|
+
.command("init")
|
|
54
|
+
.description("Write .env.local with Switchboard placeholders")
|
|
55
|
+
.option("--agent", "Print agent manifest JSON")
|
|
56
|
+
.action((opts, cmd) => {
|
|
57
|
+
const flags = cmd.opts();
|
|
58
|
+
const config = resolveConfig();
|
|
59
|
+
const clientUrl = `${config.baseUrl.replace(/\/$/, "")}/m/<client-gateway-slug>/v1`;
|
|
60
|
+
const target = path.join(process.cwd(), ".env.local");
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(target)) {
|
|
63
|
+
fs.writeFileSync(target, envBlock(clientUrl));
|
|
64
|
+
if (!flags.quiet && !flags.json) {
|
|
65
|
+
console.log(`Wrote ${target}`);
|
|
66
|
+
}
|
|
67
|
+
} else if (!flags.quiet && !flags.json) {
|
|
68
|
+
console.log(`${target} already exists`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (opts.agent || flags.json) {
|
|
72
|
+
emit(agentManifest(config), { json: true });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration kit command.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { accountRequest } from "../client.js";
|
|
6
|
+
import { emit, globalFlags } from "../output.js";
|
|
7
|
+
|
|
8
|
+
export function registerIntegrationCommands(program) {
|
|
9
|
+
const integrations = program.command("integrations").description("Integration helpers");
|
|
10
|
+
|
|
11
|
+
integrations
|
|
12
|
+
.command("show")
|
|
13
|
+
.description("Fetch integration setup JSON")
|
|
14
|
+
.option("--stack <stack>", "node or python", "node")
|
|
15
|
+
.action(async (opts, cmd) => {
|
|
16
|
+
await showIntegration(opts, cmd);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const integration = program.command("integration").description("Legacy integration helpers");
|
|
20
|
+
|
|
21
|
+
integration
|
|
22
|
+
.command("kit")
|
|
23
|
+
.description("Legacy alias for integrations show")
|
|
24
|
+
.option("--stack <stack>", "node or python", "node")
|
|
25
|
+
.action(async (opts, cmd) => {
|
|
26
|
+
await showIntegration(opts, cmd);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function showIntegration(opts, cmd) {
|
|
31
|
+
const flags = globalFlags(cmd);
|
|
32
|
+
const { data } = await accountRequest(
|
|
33
|
+
"GET",
|
|
34
|
+
`/integration_kit?stack=${opts.stack}`,
|
|
35
|
+
{ json: flags.json },
|
|
36
|
+
);
|
|
37
|
+
emit(data, flags);
|
|
38
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key management commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { accountRequest } from "../client.js";
|
|
6
|
+
import { saveConfig } from "../config.js";
|
|
7
|
+
import { emit, globalFlags, printList } from "../output.js";
|
|
8
|
+
|
|
9
|
+
function saveProjectContext(data) {
|
|
10
|
+
const updates = {};
|
|
11
|
+
|
|
12
|
+
if (data.project_id != null) {
|
|
13
|
+
updates.projectId = String(data.project_id);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
updates.apiKey = null;
|
|
17
|
+
|
|
18
|
+
if (Object.keys(updates).length > 0) {
|
|
19
|
+
saveConfig(updates);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function registerKeysCommands(program) {
|
|
24
|
+
const keys = program.command("keys").description("Project API keys");
|
|
25
|
+
|
|
26
|
+
keys
|
|
27
|
+
.command("list")
|
|
28
|
+
.description("List API keys for the selected project")
|
|
29
|
+
.action(async (_opts, cmd) => {
|
|
30
|
+
const flags = globalFlags(cmd);
|
|
31
|
+
const { data } = await accountRequest("GET", "/keys", { json: flags.json });
|
|
32
|
+
if (flags.json) {
|
|
33
|
+
emit(data, flags);
|
|
34
|
+
} else {
|
|
35
|
+
printList("API keys:", data.data || [], (k) =>
|
|
36
|
+
` ${k.id} ${k.mode} ${k.key_type || "secret"} ${k.name} …${k.last_four}${k.revoked_at ? " (revoked)" : ""}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
keys
|
|
42
|
+
.command("create")
|
|
43
|
+
.description("Create a new API key")
|
|
44
|
+
.option("--mode <mode>", "sandbox or live", "sandbox")
|
|
45
|
+
.option("--name <name>", "Key label")
|
|
46
|
+
.action(async (opts, cmd) => {
|
|
47
|
+
const flags = globalFlags(cmd);
|
|
48
|
+
const body = { mode: opts.mode, name: opts.name };
|
|
49
|
+
|
|
50
|
+
const { data } = await accountRequest("POST", "/keys", {
|
|
51
|
+
body,
|
|
52
|
+
json: flags.json,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
saveProjectContext(data);
|
|
56
|
+
emit(
|
|
57
|
+
flags.json
|
|
58
|
+
? data
|
|
59
|
+
: `Created ${data.mode} key. Plaintext (shown once): ${data.plaintext}`,
|
|
60
|
+
flags,
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
keys
|
|
65
|
+
.command("regenerate-sandbox")
|
|
66
|
+
.description("Revoke sandbox secret keys and create a new one")
|
|
67
|
+
.action(async (_opts, cmd) => {
|
|
68
|
+
const flags = globalFlags(cmd);
|
|
69
|
+
const { data } = await accountRequest("POST", "/keys/sandbox/regenerate", {
|
|
70
|
+
json: flags.json,
|
|
71
|
+
});
|
|
72
|
+
saveProjectContext(data);
|
|
73
|
+
emit(
|
|
74
|
+
flags.json
|
|
75
|
+
? data
|
|
76
|
+
: `New sandbox key: ${data.plaintext}`,
|
|
77
|
+
flags,
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
keys
|
|
82
|
+
.command("revoke <id>")
|
|
83
|
+
.description("Revoke an API key")
|
|
84
|
+
.action(async (id, _opts, cmd) => {
|
|
85
|
+
const flags = globalFlags(cmd);
|
|
86
|
+
const { data } = await accountRequest("POST", `/keys/${id}/revoke`, {
|
|
87
|
+
json: flags.json,
|
|
88
|
+
});
|
|
89
|
+
emit(flags.json ? data : `Revoked key ${id}`, flags);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
keys
|
|
93
|
+
.command("rotate <id>")
|
|
94
|
+
.description("Rotate an API key")
|
|
95
|
+
.action(async (id, _opts, cmd) => {
|
|
96
|
+
const flags = globalFlags(cmd);
|
|
97
|
+
const { data } = await accountRequest("POST", `/keys/${id}/rotate`, {
|
|
98
|
+
json: flags.json,
|
|
99
|
+
});
|
|
100
|
+
saveProjectContext(data);
|
|
101
|
+
emit(
|
|
102
|
+
flags.json ? data : `Rotated key ${id}. New plaintext: ${data.plaintext}`,
|
|
103
|
+
flags,
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Organization invitation commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { accountRequest } from "../client.js";
|
|
6
|
+
import { emit, globalFlags, printList } from "../output.js";
|
|
7
|
+
|
|
8
|
+
export function registerOrgCommands(program) {
|
|
9
|
+
const org = program.command("org").description("Organization and team");
|
|
10
|
+
|
|
11
|
+
org
|
|
12
|
+
.command("invitations")
|
|
13
|
+
.description("List pending invitations")
|
|
14
|
+
.action(async (_opts, cmd) => {
|
|
15
|
+
const flags = globalFlags(cmd);
|
|
16
|
+
const { data } = await accountRequest("GET", "/invitations", { json: flags.json });
|
|
17
|
+
if (flags.json) {
|
|
18
|
+
emit(data, flags);
|
|
19
|
+
} else {
|
|
20
|
+
printList("Pending invitations:", data.data || [], (i) =>
|
|
21
|
+
` ${i.email} (${i.role}) token=${i.token}`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
org
|
|
27
|
+
.command("invite")
|
|
28
|
+
.description("Invite a member by email")
|
|
29
|
+
.requiredOption("--email <email>")
|
|
30
|
+
.action(async (opts, cmd) => {
|
|
31
|
+
const flags = globalFlags(cmd);
|
|
32
|
+
const { data } = await accountRequest("POST", "/invitations", {
|
|
33
|
+
body: { email: opts.email },
|
|
34
|
+
json: flags.json,
|
|
35
|
+
});
|
|
36
|
+
emit(
|
|
37
|
+
flags.json ? data : `Invited ${data.email}. Token: ${data.token}`,
|
|
38
|
+
flags,
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
org
|
|
43
|
+
.command("accept <token>")
|
|
44
|
+
.description("Accept an invitation")
|
|
45
|
+
.action(async (token, _opts, cmd) => {
|
|
46
|
+
const flags = globalFlags(cmd);
|
|
47
|
+
const { data } = await accountRequest("POST", `/invitations/${token}/accept`, {
|
|
48
|
+
json: flags.json,
|
|
49
|
+
});
|
|
50
|
+
emit(
|
|
51
|
+
flags.json ? data : `Joined organization ${data.name}`,
|
|
52
|
+
flags,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
}
|