@spencer-kit/coder-studio 0.1.3

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/lib/cli.mjs ADDED
@@ -0,0 +1,1190 @@
1
+ // @ts-nocheck
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { emitKeypressEvents } from 'node:readline';
5
+ import { generateCompletionScript, installCompletionScript, uninstallCompletionScript, SUPPORTED_COMPLETION_SHELLS, } from './completion.mjs';
6
+ import { resolveLogPath } from './config.mjs';
7
+ import { buildConfigPathsReport, flattenPublicConfig, getPublicConfigValue, isRuntimeConfigKey, listConfigKeys, loadLocalConfig, mergeRuntimeConfigView, normalizeConfigValue, updateLocalConfig, validateConfigSnapshot, } from './user-config.mjs';
8
+ import { sleep } from './process-utils.mjs';
9
+ import { fetchAdminAuthStatus, fetchAdminConfig, fetchAdminIpBlocks, patchAdminConfig, unblockAdminIp, } from './http.mjs';
10
+ import { doctorRuntime, getStatus, openRuntime, readRuntimeLogs, restartRuntime, startRuntime, stopRuntime, } from './runtime-controller.mjs';
11
+ import { readPackageVersion } from './state.mjs';
12
+ const EXIT_SUCCESS = 0;
13
+ const EXIT_FAILURE = 1;
14
+ const EXIT_USAGE = 2;
15
+ const RUNTIME_DB_FILENAME = 'coder-studio.db';
16
+ class CliError extends Error {
17
+ constructor(message, { exitCode = EXIT_FAILURE, helpTopic = null } = {}) {
18
+ super(message);
19
+ this.name = 'CliError';
20
+ this.exitCode = exitCode;
21
+ this.helpTopic = helpTopic;
22
+ }
23
+ }
24
+ function usageError(message, helpTopic = null) {
25
+ return new CliError(message, { exitCode: EXIT_USAGE, helpTopic });
26
+ }
27
+ function parseArgv(argv) {
28
+ const args = [...argv];
29
+ const command = args.shift() || 'help';
30
+ const flags = {};
31
+ const positionals = [];
32
+ for (let index = 0; index < args.length; index += 1) {
33
+ const token = args[index];
34
+ if (token === '--foreground')
35
+ flags.foreground = true;
36
+ else if (token === '--json')
37
+ flags.json = true;
38
+ else if (token === '--force')
39
+ flags.force = true;
40
+ else if (token === '--follow' || token === '-f')
41
+ flags.follow = true;
42
+ else if (token === '--help' || token === '-h')
43
+ flags.help = true;
44
+ else if (token === '--stdin')
45
+ flags.stdin = true;
46
+ else if (token === '--all')
47
+ flags.all = true;
48
+ else if (token === '--host')
49
+ flags.host = args[++index];
50
+ else if (token === '--port')
51
+ flags.port = Number(args[++index]);
52
+ else if (token === '--lines' || token === '-n')
53
+ flags.lines = Number(args[++index]);
54
+ else
55
+ positionals.push(token);
56
+ }
57
+ return { command, flags, positionals };
58
+ }
59
+ async function resolveCommandContext(flags) {
60
+ const config = await loadLocalConfig();
61
+ const host = flags.host || config.values.server.host;
62
+ const port = Number.isFinite(flags.port) ? flags.port : config.values.server.port;
63
+ const options = {
64
+ stateDir: config.paths.stateDir,
65
+ dataDir: config.paths.dataDir,
66
+ host,
67
+ port,
68
+ logPath: resolveLogPath(config.paths.stateDir),
69
+ tailLines: config.values.logs.tailLines,
70
+ openCommand: config.values.system.openCommand,
71
+ };
72
+ return { config, options };
73
+ }
74
+ function printJson(value) {
75
+ console.log(JSON.stringify(value, null, 2));
76
+ }
77
+ function printHelp() {
78
+ console.log(`Coder Studio CLI
79
+
80
+ Usage:
81
+ coder-studio help [command]
82
+ coder-studio start [--host 127.0.0.1] [--port 41033] [--foreground] [--json]
83
+ coder-studio stop [--json]
84
+ coder-studio restart [--json]
85
+ coder-studio status [--json]
86
+ coder-studio logs [-f] [-n 120]
87
+ coder-studio open [--json]
88
+ coder-studio doctor [--json]
89
+ coder-studio config <subcommand>
90
+ coder-studio auth <subcommand>
91
+ coder-studio completion <bash|zsh|fish>
92
+ coder-studio completion install <bash|zsh|fish> [--json] [--force]
93
+ coder-studio completion uninstall <bash|zsh|fish> [--json]
94
+ coder-studio --version
95
+
96
+ Global Flags:
97
+ --json machine-readable output
98
+ --host <host> override configured host for this invocation
99
+ --port <port> override configured port for this invocation
100
+ -h, --help show help
101
+
102
+ Exit Codes:
103
+ 0 success
104
+ 1 runtime or operation failure
105
+ 2 usage or argument error
106
+
107
+ Examples:
108
+ coder-studio help start
109
+ coder-studio help completion
110
+ coder-studio start
111
+ coder-studio config show --json
112
+ coder-studio config root set /srv/coder-studio/workspaces
113
+ coder-studio auth ip list
114
+ eval "$(coder-studio completion bash)"
115
+ coder-studio completion install bash
116
+ coder-studio completion uninstall bash
117
+
118
+ Run \`coder-studio config --help\`, \`coder-studio auth --help\`, or \`coder-studio help completion\` for detailed usage.
119
+ `);
120
+ }
121
+ function printStartHelp() {
122
+ console.log(`coder-studio start
123
+
124
+ Usage:
125
+ coder-studio start [--host <host>] [--port <port>] [--foreground] [--json]
126
+
127
+ Options:
128
+ --host <host> override configured host for this invocation
129
+ --port <port> override configured port for this invocation
130
+ --foreground keep the runtime in the foreground
131
+ --json machine-readable output
132
+
133
+ Examples:
134
+ coder-studio start
135
+ coder-studio start --foreground
136
+ coder-studio start --port 42033 --json
137
+ `);
138
+ }
139
+ function printStopHelp() {
140
+ console.log(`coder-studio stop
141
+
142
+ Usage:
143
+ coder-studio stop [--json]
144
+
145
+ Options:
146
+ --json machine-readable output
147
+
148
+ Examples:
149
+ coder-studio stop
150
+ coder-studio stop --json
151
+ `);
152
+ }
153
+ function printRestartHelp() {
154
+ console.log(`coder-studio restart
155
+
156
+ Usage:
157
+ coder-studio restart [--json]
158
+
159
+ Options:
160
+ --json machine-readable output
161
+
162
+ Examples:
163
+ coder-studio restart
164
+ coder-studio restart --json
165
+ `);
166
+ }
167
+ function printStatusHelp() {
168
+ console.log(`coder-studio status
169
+
170
+ Usage:
171
+ coder-studio status [--host <host>] [--port <port>] [--json]
172
+
173
+ Options:
174
+ --host <host> override configured host for this invocation
175
+ --port <port> override configured port for this invocation
176
+ --json machine-readable output
177
+
178
+ Examples:
179
+ coder-studio status
180
+ coder-studio status --json
181
+ `);
182
+ }
183
+ function printLogsHelp() {
184
+ console.log(`coder-studio logs
185
+
186
+ Usage:
187
+ coder-studio logs [-f] [-n <lines>]
188
+
189
+ Options:
190
+ -f, --follow follow the runtime log
191
+ -n, --lines <n> read the last <n> lines
192
+
193
+ Examples:
194
+ coder-studio logs
195
+ coder-studio logs -n 200
196
+ coder-studio logs -f
197
+ `);
198
+ }
199
+ function printOpenHelp() {
200
+ console.log(`coder-studio open
201
+
202
+ Usage:
203
+ coder-studio open [--host <host>] [--port <port>] [--json]
204
+
205
+ Options:
206
+ --host <host> override configured host for this invocation
207
+ --port <port> override configured port for this invocation
208
+ --json machine-readable output
209
+
210
+ Examples:
211
+ coder-studio open
212
+ coder-studio open --json
213
+ `);
214
+ }
215
+ function printDoctorHelp() {
216
+ console.log(`coder-studio doctor
217
+
218
+ Usage:
219
+ coder-studio doctor [--host <host>] [--port <port>] [--json]
220
+
221
+ Options:
222
+ --host <host> override configured host for this invocation
223
+ --port <port> override configured port for this invocation
224
+ --json machine-readable output
225
+
226
+ Examples:
227
+ coder-studio doctor
228
+ coder-studio doctor --json
229
+ `);
230
+ }
231
+ function printConfigHelp() {
232
+ console.log(`coder-studio config
233
+
234
+ Usage:
235
+ coder-studio config path
236
+ coder-studio config show [--json]
237
+ coder-studio config get <key> [--json]
238
+ coder-studio config set <key> <value>
239
+ coder-studio config unset <key>
240
+ coder-studio config validate [--json]
241
+ coder-studio config root show|set <path>|clear
242
+ coder-studio config password status|set <value>|set --stdin|clear
243
+ coder-studio config auth public-mode <on|off>
244
+ coder-studio config auth session-idle <minutes>
245
+ coder-studio config auth session-max <hours>
246
+
247
+ Supported keys:
248
+ ${listConfigKeys().join('\n ')}
249
+
250
+ Examples:
251
+ coder-studio config show
252
+ coder-studio config get server.port
253
+ coder-studio config set server.port 42033
254
+ coder-studio config root set /srv/coder-studio/workspaces
255
+ coder-studio config password set --stdin
256
+ `);
257
+ }
258
+ function printAuthHelp() {
259
+ console.log(`coder-studio auth
260
+
261
+ Usage:
262
+ coder-studio auth status [--json]
263
+ coder-studio auth ip list [--json]
264
+ coder-studio auth ip unblock <ip> [--json]
265
+ coder-studio auth ip unblock --all [--json]
266
+
267
+ Examples:
268
+ coder-studio auth status
269
+ coder-studio auth ip list
270
+ coder-studio auth ip unblock 203.0.113.10
271
+ `);
272
+ }
273
+ function printCompletionHelp() {
274
+ console.log(`coder-studio completion
275
+
276
+ Usage:
277
+ coder-studio completion <bash|zsh|fish>
278
+ coder-studio completion install <bash|zsh|fish> [--json] [--force]
279
+ coder-studio completion uninstall <bash|zsh|fish> [--json]
280
+
281
+ Description:
282
+ Print, install, or uninstall shell completion scripts.
283
+
284
+ Examples:
285
+ eval "$(coder-studio completion bash)"
286
+ source <(coder-studio completion zsh)
287
+ coder-studio completion fish | source
288
+ coder-studio completion install bash
289
+ coder-studio completion install bash --force
290
+ coder-studio completion install zsh --json
291
+ coder-studio completion uninstall bash
292
+ `);
293
+ }
294
+ function printCompletionInstall(result) {
295
+ console.log(`installed: ${result.shell}`);
296
+ console.log(`scriptPath: ${result.scriptPath}`);
297
+ console.log(`scriptUpdated: ${result.scriptUpdated ? 'yes' : 'no'}`);
298
+ if (result.profilePath) {
299
+ console.log(`profilePath: ${result.profilePath}`);
300
+ console.log(`profileUpdated: ${result.profileUpdated ? 'yes' : 'no'}`);
301
+ }
302
+ else {
303
+ console.log('profilePath: n/a');
304
+ console.log('profileUpdated: no');
305
+ }
306
+ console.log(`activationCommand: ${result.activationCommand}`);
307
+ console.log(`forced: ${result.forced ? 'yes' : 'no'}`);
308
+ }
309
+ function printCompletionUninstall(result) {
310
+ console.log(`uninstalled: ${result.shell}`);
311
+ console.log(`scriptPath: ${result.scriptPath}`);
312
+ console.log(`scriptRemoved: ${result.scriptRemoved ? 'yes' : 'no'}`);
313
+ if (result.profilePath) {
314
+ console.log(`profilePath: ${result.profilePath}`);
315
+ console.log(`profileUpdated: ${result.profileUpdated ? 'yes' : 'no'}`);
316
+ }
317
+ else {
318
+ console.log('profilePath: n/a');
319
+ console.log('profileUpdated: no');
320
+ }
321
+ }
322
+ function printHelpTopic(topic) {
323
+ switch (topic) {
324
+ case undefined:
325
+ case null:
326
+ case '':
327
+ case 'main':
328
+ printHelp();
329
+ return EXIT_SUCCESS;
330
+ case 'start':
331
+ printStartHelp();
332
+ return EXIT_SUCCESS;
333
+ case 'stop':
334
+ printStopHelp();
335
+ return EXIT_SUCCESS;
336
+ case 'restart':
337
+ printRestartHelp();
338
+ return EXIT_SUCCESS;
339
+ case 'status':
340
+ printStatusHelp();
341
+ return EXIT_SUCCESS;
342
+ case 'logs':
343
+ printLogsHelp();
344
+ return EXIT_SUCCESS;
345
+ case 'open':
346
+ printOpenHelp();
347
+ return EXIT_SUCCESS;
348
+ case 'doctor':
349
+ printDoctorHelp();
350
+ return EXIT_SUCCESS;
351
+ case 'config':
352
+ printConfigHelp();
353
+ return EXIT_SUCCESS;
354
+ case 'auth':
355
+ printAuthHelp();
356
+ return EXIT_SUCCESS;
357
+ case 'completion':
358
+ printCompletionHelp();
359
+ return EXIT_SUCCESS;
360
+ default:
361
+ throw usageError(`unsupported help topic: ${topic}`, 'main');
362
+ }
363
+ }
364
+ function printStatus(status) {
365
+ console.log(`status: ${status.status}`);
366
+ console.log(`managed: ${status.managed ? 'yes' : 'no'}`);
367
+ console.log(`endpoint: ${status.endpoint}`);
368
+ console.log(`pid: ${status.pid ?? 'n/a'}`);
369
+ console.log(`stateDir: ${status.stateDir}`);
370
+ console.log(`logPath: ${status.logPath}`);
371
+ if (status.health?.version) {
372
+ console.log(`version: ${status.health.version}`);
373
+ }
374
+ if (status.error) {
375
+ console.log(`error: ${status.error}`);
376
+ }
377
+ if (status.stale) {
378
+ console.log('note: stale runtime state was cleaned up');
379
+ }
380
+ }
381
+ async function printDoctor(report, asJson) {
382
+ if (asJson) {
383
+ printJson(report);
384
+ return;
385
+ }
386
+ console.log('doctor:');
387
+ console.log(`status: ${report.status.status}`);
388
+ console.log(`endpoint: ${report.status.endpoint}`);
389
+ console.log(`stateDir: ${report.stateDir}`);
390
+ console.log(`dataDir: ${report.dataDir}`);
391
+ console.log(`logPath: ${report.logPath}`);
392
+ console.log(`logExists: ${report.logExists ? 'yes' : 'no'}`);
393
+ if (report.bundle?.error) {
394
+ console.log(`bundleError: ${report.bundle.error}`);
395
+ }
396
+ else {
397
+ console.log(`runtimePackage: ${report.bundle.packageName}`);
398
+ console.log(`binaryPath: ${report.bundle.binaryPath}`);
399
+ console.log(`distDir: ${report.bundle.distDir}`);
400
+ }
401
+ if (report.runtime?.startedAt) {
402
+ console.log(`startedAt: ${report.runtime.startedAt}`);
403
+ }
404
+ if (report.status.error) {
405
+ console.log(`error: ${report.status.error}`);
406
+ }
407
+ }
408
+ async function followLogs(logPath, initialLines = 80) {
409
+ const initial = await readRuntimeLogs({ logPath, lines: initialLines });
410
+ if (initial) {
411
+ process.stdout.write(`${initial}\n`);
412
+ }
413
+ let cursor = 0;
414
+ try {
415
+ const stat = await fs.stat(logPath);
416
+ cursor = stat.size;
417
+ }
418
+ catch {
419
+ cursor = 0;
420
+ }
421
+ let active = true;
422
+ const stop = () => {
423
+ active = false;
424
+ };
425
+ process.on('SIGINT', stop);
426
+ process.on('SIGTERM', stop);
427
+ try {
428
+ while (active) {
429
+ try {
430
+ const stat = await fs.stat(logPath);
431
+ if (stat.size < cursor) {
432
+ cursor = 0;
433
+ }
434
+ if (stat.size > cursor) {
435
+ const handle = await fs.open(logPath, 'r');
436
+ const chunk = Buffer.alloc(stat.size - cursor);
437
+ await handle.read(chunk, 0, chunk.length, cursor);
438
+ await handle.close();
439
+ cursor = stat.size;
440
+ process.stdout.write(chunk.toString('utf8'));
441
+ }
442
+ }
443
+ catch {
444
+ // Ignore missing log between restarts.
445
+ }
446
+ await sleep(400);
447
+ }
448
+ }
449
+ finally {
450
+ process.off('SIGINT', stop);
451
+ process.off('SIGTERM', stop);
452
+ }
453
+ }
454
+ function runtimeIsActive(status) {
455
+ return status.status === 'running' || status.status === 'degraded';
456
+ }
457
+ async function loadLiveRuntimeView(context) {
458
+ const status = await getStatus(context.options);
459
+ if (!runtimeIsActive(status) || !status.managed) {
460
+ return { status, runtimeView: null, authStatus: null, ipBlocks: [], adminError: null };
461
+ }
462
+ try {
463
+ const [runtimeView, authStatus, ipBlocks] = await Promise.all([
464
+ fetchAdminConfig(status.endpoint),
465
+ fetchAdminAuthStatus(status.endpoint),
466
+ fetchAdminIpBlocks(status.endpoint),
467
+ ]);
468
+ return { status, runtimeView, authStatus, ipBlocks, adminError: null };
469
+ }
470
+ catch (error) {
471
+ return {
472
+ status,
473
+ runtimeView: null,
474
+ authStatus: null,
475
+ ipBlocks: [],
476
+ adminError: error instanceof Error ? error.message : String(error),
477
+ };
478
+ }
479
+ }
480
+ async function loadEffectiveConfig(context) {
481
+ const local = context.config;
482
+ const live = await loadLiveRuntimeView(context);
483
+ return {
484
+ ...live,
485
+ snapshot: mergeRuntimeConfigView(local, live.runtimeView),
486
+ };
487
+ }
488
+ function printFlatConfig(snapshot, { includePaths = true } = {}) {
489
+ if (includePaths) {
490
+ const paths = buildConfigPathsReport(snapshot);
491
+ console.log(`stateDir: ${paths.stateDir}`);
492
+ console.log(`dataDir: ${paths.dataDir}`);
493
+ console.log(`configPath: ${paths.configPath}`);
494
+ console.log(`authPath: ${paths.authPath}`);
495
+ }
496
+ const flat = flattenPublicConfig(snapshot);
497
+ for (const [key, value] of Object.entries(flat)) {
498
+ console.log(`${key}: ${value ?? 'null'}`);
499
+ }
500
+ }
501
+ function printRuntimeMetadata(status, adminError = null) {
502
+ console.log(`runtime.status: ${status.status}`);
503
+ console.log(`runtime.managed: ${status.managed ? 'yes' : 'no'}`);
504
+ console.log(`runtime.endpoint: ${status.endpoint}`);
505
+ if (adminError) {
506
+ console.log(`runtime.adminError: ${adminError}`);
507
+ }
508
+ }
509
+ function printConfigMutation(result, snapshot, key) {
510
+ if (result.changedKeys.length === 0) {
511
+ console.log(`unchanged: ${key}`);
512
+ }
513
+ else {
514
+ console.log(`updated: ${result.changedKeys.join(', ')}`);
515
+ }
516
+ console.log(`${key}: ${getPublicConfigValue(snapshot, key) ?? 'null'}`);
517
+ if (result.sessionsReset) {
518
+ console.log('note: active auth sessions were cleared');
519
+ }
520
+ if (result.restartRequired) {
521
+ console.log('note: restart the runtime to apply the new bind host/port');
522
+ }
523
+ }
524
+ async function readSecretFromStdin() {
525
+ const chunks = [];
526
+ for await (const chunk of process.stdin) {
527
+ chunks.push(Buffer.from(chunk));
528
+ }
529
+ return Buffer.concat(chunks).toString('utf8').trimEnd();
530
+ }
531
+ async function pathExists(filePath) {
532
+ try {
533
+ await fs.access(filePath);
534
+ return true;
535
+ }
536
+ catch {
537
+ return false;
538
+ }
539
+ }
540
+ async function promptHiddenInput(label) {
541
+ if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== 'function') {
542
+ throw new CliError('interactive password setup requires a TTY', { exitCode: EXIT_FAILURE });
543
+ }
544
+ const stdin = process.stdin;
545
+ const stdout = process.stdout;
546
+ const wasRaw = Boolean(stdin.isRaw);
547
+ emitKeypressEvents(stdin);
548
+ stdin.resume();
549
+ if (!wasRaw) {
550
+ stdin.setRawMode(true);
551
+ }
552
+ stdout.write(label);
553
+ return await new Promise((resolve, reject) => {
554
+ let value = '';
555
+ const cleanup = () => {
556
+ stdin.off('keypress', onKeypress);
557
+ if (!wasRaw) {
558
+ stdin.setRawMode(false);
559
+ }
560
+ stdout.write('\n');
561
+ };
562
+ const finish = (callback) => {
563
+ cleanup();
564
+ callback();
565
+ };
566
+ const onKeypress = (chunk, key = {}) => {
567
+ if (key.ctrl && (key.name === 'c' || key.name === 'd')) {
568
+ finish(() => reject(new CliError('initial password setup cancelled', { exitCode: EXIT_FAILURE })));
569
+ return;
570
+ }
571
+ if (key.name === 'return' || key.name === 'enter') {
572
+ finish(() => resolve(value));
573
+ return;
574
+ }
575
+ if (key.name === 'backspace' || key.name === 'delete') {
576
+ value = value.slice(0, -1);
577
+ return;
578
+ }
579
+ if (typeof chunk === 'string' && chunk.length > 0 && !key.ctrl && !key.meta) {
580
+ value += chunk;
581
+ }
582
+ };
583
+ stdin.on('keypress', onKeypress);
584
+ });
585
+ }
586
+ async function ensureInitialPasswordConfigured(context, flags) {
587
+ const status = await getStatus(context.options);
588
+ if (runtimeIsActive(status)) {
589
+ return context;
590
+ }
591
+ const needsPassword = context.config.values.auth.publicMode && !context.config.values.auth.passwordConfigured;
592
+ if (!needsPassword) {
593
+ return context;
594
+ }
595
+ const dbPath = path.join(context.config.paths.dataDir, RUNTIME_DB_FILENAME);
596
+ if (await pathExists(dbPath)) {
597
+ return context;
598
+ }
599
+ if (flags.json || !process.stdin.isTTY || !process.stdout.isTTY) {
600
+ throw new CliError('first launch requires configuring auth.password before start; run `coder-studio config password set --stdin` and retry', { exitCode: EXIT_FAILURE });
601
+ }
602
+ console.log('First launch detected. Set an access password before starting Coder Studio.');
603
+ while (true) {
604
+ const password = await promptHiddenInput('New password: ');
605
+ if (!password.trim()) {
606
+ console.log('Password cannot be empty.');
607
+ continue;
608
+ }
609
+ const confirmation = await promptHiddenInput('Confirm password: ');
610
+ if (password !== confirmation) {
611
+ console.log('Passwords do not match. Try again.');
612
+ continue;
613
+ }
614
+ await updateLocalConfig({ stateDir: context.config.paths.stateDir, dataDir: context.config.paths.dataDir }, { 'auth.password': password });
615
+ console.log('Password saved. Starting Coder Studio...');
616
+ return {
617
+ ...context,
618
+ config: await loadLocalConfig({
619
+ stateDir: context.config.paths.stateDir,
620
+ dataDir: context.config.paths.dataDir,
621
+ }),
622
+ };
623
+ }
624
+ }
625
+ async function applyConfigUpdate(context, key, rawValue, { unset = false } = {}) {
626
+ const status = await getStatus(context.options);
627
+ if (runtimeIsActive(status) && status.managed && isRuntimeConfigKey(key)) {
628
+ const updates = { [key]: unset ? null : normalizeConfigValue(key, rawValue) };
629
+ const result = await patchAdminConfig(status.endpoint, updates);
630
+ const local = await loadLocalConfig({ stateDir: context.config.paths.stateDir, dataDir: context.config.paths.dataDir });
631
+ const snapshot = mergeRuntimeConfigView(local, result.config);
632
+ return { result, snapshot };
633
+ }
634
+ const result = await updateLocalConfig({ stateDir: context.config.paths.stateDir, dataDir: context.config.paths.dataDir }, { [key]: rawValue }, { unset });
635
+ return { result, snapshot: result.snapshot };
636
+ }
637
+ function assertSupportedConfigKey(key) {
638
+ if (!listConfigKeys().includes(key)) {
639
+ throw usageError(`unsupported config key: ${key}`, 'config');
640
+ }
641
+ }
642
+ async function handleConfigCommand(positionals, flags, context) {
643
+ const [subcommand, ...rest] = positionals;
644
+ if (!subcommand || flags.help) {
645
+ printConfigHelp();
646
+ return EXIT_SUCCESS;
647
+ }
648
+ if (subcommand === 'path') {
649
+ const report = buildConfigPathsReport(context.config);
650
+ if (flags.json)
651
+ printJson(report);
652
+ else {
653
+ console.log(`stateDir: ${report.stateDir}`);
654
+ console.log(`dataDir: ${report.dataDir}`);
655
+ console.log(`configPath: ${report.configPath}`);
656
+ console.log(`authPath: ${report.authPath}`);
657
+ }
658
+ return EXIT_SUCCESS;
659
+ }
660
+ if (subcommand === 'show') {
661
+ const effective = await loadEffectiveConfig(context);
662
+ if (flags.json) {
663
+ printJson({
664
+ paths: buildConfigPathsReport(effective.snapshot),
665
+ values: flattenPublicConfig(effective.snapshot),
666
+ runtime: {
667
+ status: effective.status.status,
668
+ managed: effective.status.managed,
669
+ endpoint: effective.status.endpoint,
670
+ live: Boolean(effective.runtimeView),
671
+ adminError: effective.adminError,
672
+ },
673
+ });
674
+ }
675
+ else {
676
+ printFlatConfig(effective.snapshot);
677
+ printRuntimeMetadata(effective.status, effective.adminError);
678
+ console.log(`runtime.liveConfig: ${effective.runtimeView ? 'yes' : 'no'}`);
679
+ }
680
+ return EXIT_SUCCESS;
681
+ }
682
+ if (subcommand === 'get') {
683
+ const key = rest[0];
684
+ if (!key) {
685
+ throw usageError('config get requires <key>', 'config');
686
+ }
687
+ assertSupportedConfigKey(key);
688
+ const effective = await loadEffectiveConfig(context);
689
+ if (flags.json) {
690
+ if (key === 'auth.password') {
691
+ printJson({ key, configured: effective.snapshot.values.auth.passwordConfigured, hidden: true });
692
+ }
693
+ else {
694
+ printJson({ key, value: getPublicConfigValue(effective.snapshot, key) });
695
+ }
696
+ }
697
+ else if (key === 'auth.password') {
698
+ console.log(effective.snapshot.values.auth.passwordConfigured ? 'configured' : 'not configured');
699
+ }
700
+ else {
701
+ console.log(getPublicConfigValue(effective.snapshot, key) ?? 'null');
702
+ }
703
+ return EXIT_SUCCESS;
704
+ }
705
+ if (subcommand === 'set') {
706
+ const [key, ...valueParts] = rest;
707
+ if (!key || valueParts.length === 0) {
708
+ throw usageError('config set requires <key> <value>', 'config');
709
+ }
710
+ assertSupportedConfigKey(key);
711
+ const value = valueParts.join(' ');
712
+ const { result, snapshot } = await applyConfigUpdate(context, key, value);
713
+ if (flags.json)
714
+ printJson({ changedKeys: result.changedKeys, restartRequired: result.restartRequired, sessionsReset: result.sessionsReset, values: flattenPublicConfig(snapshot) });
715
+ else
716
+ printConfigMutation(result, snapshot, key);
717
+ return EXIT_SUCCESS;
718
+ }
719
+ if (subcommand === 'unset') {
720
+ const key = rest[0];
721
+ if (!key) {
722
+ throw usageError('config unset requires <key>', 'config');
723
+ }
724
+ assertSupportedConfigKey(key);
725
+ const { result, snapshot } = await applyConfigUpdate(context, key, null, { unset: true });
726
+ if (flags.json)
727
+ printJson({ changedKeys: result.changedKeys, restartRequired: result.restartRequired, sessionsReset: result.sessionsReset, values: flattenPublicConfig(snapshot) });
728
+ else
729
+ printConfigMutation(result, snapshot, key);
730
+ return EXIT_SUCCESS;
731
+ }
732
+ if (subcommand === 'validate') {
733
+ const effective = await loadEffectiveConfig(context);
734
+ const report = validateConfigSnapshot(effective.snapshot);
735
+ if (flags.json) {
736
+ printJson(report);
737
+ }
738
+ else {
739
+ console.log(`valid: ${report.ok ? 'yes' : 'no'}`);
740
+ if (report.errors.length > 0) {
741
+ console.log('errors:');
742
+ for (const error of report.errors)
743
+ console.log(`- ${error}`);
744
+ }
745
+ if (report.warnings.length > 0) {
746
+ console.log('warnings:');
747
+ for (const warning of report.warnings)
748
+ console.log(`- ${warning}`);
749
+ }
750
+ }
751
+ return report.ok ? EXIT_SUCCESS : EXIT_FAILURE;
752
+ }
753
+ if (subcommand === 'root') {
754
+ const [action, ...valueParts] = rest;
755
+ if (action === 'show') {
756
+ const effective = await loadEffectiveConfig(context);
757
+ const value = effective.snapshot.values.root.path;
758
+ if (flags.json)
759
+ printJson({ key: 'root.path', value });
760
+ else
761
+ console.log(value ?? 'null');
762
+ return EXIT_SUCCESS;
763
+ }
764
+ if (action === 'set') {
765
+ if (valueParts.length === 0)
766
+ throw usageError('config root set requires <path>', 'config');
767
+ const value = valueParts.join(' ');
768
+ const { result, snapshot } = await applyConfigUpdate(context, 'root.path', value);
769
+ if (flags.json)
770
+ printJson({ changedKeys: result.changedKeys, values: flattenPublicConfig(snapshot) });
771
+ else
772
+ printConfigMutation(result, snapshot, 'root.path');
773
+ return EXIT_SUCCESS;
774
+ }
775
+ if (action === 'clear') {
776
+ const { result, snapshot } = await applyConfigUpdate(context, 'root.path', null, { unset: true });
777
+ if (flags.json)
778
+ printJson({ changedKeys: result.changedKeys, values: flattenPublicConfig(snapshot) });
779
+ else
780
+ printConfigMutation(result, snapshot, 'root.path');
781
+ return EXIT_SUCCESS;
782
+ }
783
+ throw usageError(`unsupported config root subcommand: ${action || '(missing)'}`, 'config');
784
+ }
785
+ if (subcommand === 'password') {
786
+ const [action, ...valueParts] = rest;
787
+ if (action === 'status') {
788
+ const effective = await loadEffectiveConfig(context);
789
+ const configured = effective.snapshot.values.auth.passwordConfigured;
790
+ if (flags.json)
791
+ printJson({ configured });
792
+ else
793
+ console.log(configured ? 'configured' : 'not configured');
794
+ return EXIT_SUCCESS;
795
+ }
796
+ if (action === 'set') {
797
+ const value = flags.stdin ? await readSecretFromStdin() : valueParts.join(' ');
798
+ if (!value)
799
+ throw usageError('config password set requires <value> or --stdin', 'config');
800
+ const { result, snapshot } = await applyConfigUpdate(context, 'auth.password', value);
801
+ if (flags.json)
802
+ printJson({ changedKeys: result.changedKeys, configured: snapshot.values.auth.passwordConfigured, sessionsReset: result.sessionsReset });
803
+ else
804
+ printConfigMutation(result, snapshot, 'auth.password');
805
+ return EXIT_SUCCESS;
806
+ }
807
+ if (action === 'clear') {
808
+ const { result, snapshot } = await applyConfigUpdate(context, 'auth.password', null, { unset: true });
809
+ if (flags.json)
810
+ printJson({ changedKeys: result.changedKeys, configured: snapshot.values.auth.passwordConfigured, sessionsReset: result.sessionsReset });
811
+ else
812
+ printConfigMutation(result, snapshot, 'auth.password');
813
+ return EXIT_SUCCESS;
814
+ }
815
+ throw usageError(`unsupported config password subcommand: ${action || '(missing)'}`, 'config');
816
+ }
817
+ if (subcommand === 'auth') {
818
+ const [action, value] = rest;
819
+ if (action === 'public-mode') {
820
+ if (!value)
821
+ throw usageError('config auth public-mode requires <on|off>', 'config');
822
+ const normalized = normalizeConfigValue('auth.publicMode', value);
823
+ const { result, snapshot } = await applyConfigUpdate(context, 'auth.publicMode', normalized);
824
+ if (flags.json)
825
+ printJson({ changedKeys: result.changedKeys, values: flattenPublicConfig(snapshot), sessionsReset: result.sessionsReset });
826
+ else
827
+ printConfigMutation(result, snapshot, 'auth.publicMode');
828
+ return EXIT_SUCCESS;
829
+ }
830
+ if (action === 'session-idle') {
831
+ if (!value)
832
+ throw usageError('config auth session-idle requires <minutes>', 'config');
833
+ const { result, snapshot } = await applyConfigUpdate(context, 'auth.sessionIdleMinutes', value);
834
+ if (flags.json)
835
+ printJson({ changedKeys: result.changedKeys, values: flattenPublicConfig(snapshot) });
836
+ else
837
+ printConfigMutation(result, snapshot, 'auth.sessionIdleMinutes');
838
+ return EXIT_SUCCESS;
839
+ }
840
+ if (action === 'session-max') {
841
+ if (!value)
842
+ throw usageError('config auth session-max requires <hours>', 'config');
843
+ const { result, snapshot } = await applyConfigUpdate(context, 'auth.sessionMaxHours', value);
844
+ if (flags.json)
845
+ printJson({ changedKeys: result.changedKeys, values: flattenPublicConfig(snapshot) });
846
+ else
847
+ printConfigMutation(result, snapshot, 'auth.sessionMaxHours');
848
+ return EXIT_SUCCESS;
849
+ }
850
+ throw usageError(`unsupported config auth subcommand: ${action || '(missing)'}`, 'config');
851
+ }
852
+ throw usageError(`unsupported config subcommand: ${subcommand}`, 'config');
853
+ }
854
+ function printAuthStatus(report) {
855
+ console.log(`runtime: ${report.runtimeRunning ? 'running' : 'stopped'}`);
856
+ if (report.endpoint) {
857
+ console.log(`endpoint: ${report.endpoint}`);
858
+ }
859
+ if (typeof report.managed === 'boolean') {
860
+ console.log(`managed: ${report.managed ? 'yes' : 'no'}`);
861
+ }
862
+ console.log(`server.host: ${report.server.host}`);
863
+ console.log(`server.port: ${report.server.port}`);
864
+ console.log(`root.path: ${report.root.path ?? 'null'}`);
865
+ console.log(`auth.publicMode: ${report.auth.publicMode}`);
866
+ console.log(`auth.passwordConfigured: ${report.auth.passwordConfigured}`);
867
+ console.log(`auth.sessionIdleMinutes: ${report.auth.sessionIdleMinutes}`);
868
+ console.log(`auth.sessionMaxHours: ${report.auth.sessionMaxHours}`);
869
+ console.log(`blockedIpCount: ${report.blockedIpCount}`);
870
+ if (report.adminError) {
871
+ console.log(`adminError: ${report.adminError}`);
872
+ }
873
+ }
874
+ function printIpBlocks(entries) {
875
+ if (entries.length === 0) {
876
+ console.log('no blocked IPs');
877
+ return;
878
+ }
879
+ for (const entry of entries) {
880
+ console.log(`${entry.ip} blockedUntil=${entry.blockedUntil} failCount=${entry.failCount}`);
881
+ }
882
+ }
883
+ async function handleAuthCommand(positionals, flags, context) {
884
+ const [subcommand, ...rest] = positionals;
885
+ if (!subcommand || flags.help) {
886
+ printAuthHelp();
887
+ return EXIT_SUCCESS;
888
+ }
889
+ const live = await loadLiveRuntimeView(context);
890
+ if (subcommand === 'status') {
891
+ const report = live.authStatus
892
+ ? {
893
+ ...live.authStatus,
894
+ runtimeRunning: true,
895
+ managed: live.status.managed,
896
+ endpoint: live.status.endpoint,
897
+ adminError: live.adminError,
898
+ blockedIpCount: live.ipBlocks.length,
899
+ }
900
+ : {
901
+ runtimeRunning: false,
902
+ managed: live.status.managed,
903
+ endpoint: live.status.endpoint,
904
+ adminError: live.adminError,
905
+ server: {
906
+ host: context.config.values.server.host,
907
+ port: context.config.values.server.port,
908
+ },
909
+ root: {
910
+ path: context.config.values.root.path,
911
+ },
912
+ auth: {
913
+ publicMode: context.config.values.auth.publicMode,
914
+ passwordConfigured: context.config.values.auth.passwordConfigured,
915
+ sessionIdleMinutes: context.config.values.auth.sessionIdleMinutes,
916
+ sessionMaxHours: context.config.values.auth.sessionMaxHours,
917
+ },
918
+ blockedIpCount: 0,
919
+ };
920
+ if (flags.json)
921
+ printJson(report);
922
+ else
923
+ printAuthStatus(report);
924
+ return EXIT_SUCCESS;
925
+ }
926
+ if (subcommand === 'ip') {
927
+ const [action, value] = rest;
928
+ if (action === 'list') {
929
+ if (flags.json)
930
+ printJson({ running: runtimeIsActive(live.status), entries: live.ipBlocks });
931
+ else {
932
+ if (!runtimeIsActive(live.status))
933
+ console.log('runtime is not running; blocked IPs are memory-only');
934
+ printIpBlocks(live.ipBlocks);
935
+ }
936
+ return EXIT_SUCCESS;
937
+ }
938
+ if (action === 'unblock') {
939
+ if (!runtimeIsActive(live.status)) {
940
+ if (flags.json)
941
+ printJson({ running: false, removed: 0, entries: [] });
942
+ else
943
+ console.log('runtime is not running; nothing to unblock');
944
+ return EXIT_SUCCESS;
945
+ }
946
+ const payload = flags.all ? { all: true } : { ip: value };
947
+ if (!payload.all && !payload.ip) {
948
+ throw usageError('auth ip unblock requires <ip> or --all', 'auth');
949
+ }
950
+ const result = await unblockAdminIp(live.status.endpoint, payload);
951
+ if (flags.json)
952
+ printJson(result);
953
+ else {
954
+ console.log(`removed: ${result.removed}`);
955
+ printIpBlocks(result.entries);
956
+ }
957
+ return EXIT_SUCCESS;
958
+ }
959
+ }
960
+ throw usageError(`unsupported auth subcommand: ${subcommand}`, 'auth');
961
+ }
962
+ function normalizeCliError(error) {
963
+ if (error instanceof CliError) {
964
+ return error;
965
+ }
966
+ const message = error instanceof Error ? error.message : String(error);
967
+ const configPrefix = 'unsupported config key:';
968
+ if (message.startsWith('unsupported_config_key:')) {
969
+ return usageError(`unsupported config key: ${message.slice('unsupported_config_key:'.length)}`, 'config');
970
+ }
971
+ const messageMap = new Map([
972
+ ['invalid_server_host', 'invalid value for server.host'],
973
+ ['invalid_server_port', 'invalid value for server.port'],
974
+ ['invalid_auth_public_mode', 'invalid value for auth.publicMode; expected on/off or true/false'],
975
+ ['invalid_auth_password', 'invalid value for auth.password'],
976
+ ['invalid_auth_session_idle_minutes', 'invalid value for auth.sessionIdleMinutes'],
977
+ ['invalid_auth_session_max_hours', 'invalid value for auth.sessionMaxHours'],
978
+ ['invalid_logs_tail_lines', 'invalid value for logs.tailLines'],
979
+ ['invalid_system_open_command', 'invalid value for system.openCommand'],
980
+ ['invalid_root_path', 'invalid value for root.path'],
981
+ ['missing_ip', 'missing IP address'],
982
+ ['path_has_no_existing_parent', 'root.path must have an existing parent directory'],
983
+ ['empty_path', 'root.path must not be empty'],
984
+ ]);
985
+ if (messageMap.has(message)) {
986
+ return usageError(messageMap.get(message), 'config');
987
+ }
988
+ if (message.startsWith('invalid_')) {
989
+ return usageError(message.replace(/^invalid_/, 'invalid value: '), 'config');
990
+ }
991
+ if (message.startsWith(configPrefix)) {
992
+ return usageError(message, 'config');
993
+ }
994
+ return new CliError(message, { exitCode: EXIT_FAILURE });
995
+ }
996
+ function printCliError(error, flags) {
997
+ const normalized = normalizeCliError(error);
998
+ if (flags.json) {
999
+ printJson({
1000
+ ok: false,
1001
+ error: normalized.message,
1002
+ exitCode: normalized.exitCode,
1003
+ kind: normalized.exitCode === EXIT_USAGE ? 'usage' : 'runtime',
1004
+ helpTopic: normalized.helpTopic ?? undefined,
1005
+ });
1006
+ return normalized.exitCode;
1007
+ }
1008
+ console.error(`error: ${normalized.message}`);
1009
+ if (normalized.helpTopic === 'config') {
1010
+ console.error('hint: run `coder-studio config --help`');
1011
+ }
1012
+ else if (normalized.helpTopic === 'auth') {
1013
+ console.error('hint: run `coder-studio auth --help`');
1014
+ }
1015
+ else if (normalized.helpTopic === 'completion') {
1016
+ console.error('hint: run `coder-studio help completion`');
1017
+ }
1018
+ else if (normalized.helpTopic === 'main') {
1019
+ console.error('hint: run `coder-studio help`');
1020
+ }
1021
+ return normalized.exitCode;
1022
+ }
1023
+ export async function runCli(argv = process.argv.slice(2)) {
1024
+ const { command, flags, positionals } = parseArgv(argv);
1025
+ try {
1026
+ if (command === '--version' || command === '-v' || flags.version) {
1027
+ console.log(await readPackageVersion());
1028
+ return EXIT_SUCCESS;
1029
+ }
1030
+ if (command === 'help') {
1031
+ return printHelpTopic(positionals[0]);
1032
+ }
1033
+ if (flags.help) {
1034
+ return printHelpTopic(command);
1035
+ }
1036
+ if (command === 'completion') {
1037
+ const [modeOrShell, maybeShell, ...rest] = positionals;
1038
+ if (!modeOrShell) {
1039
+ printCompletionHelp();
1040
+ return EXIT_SUCCESS;
1041
+ }
1042
+ if (modeOrShell === 'install') {
1043
+ const shell = maybeShell;
1044
+ if (!shell) {
1045
+ throw usageError('completion install requires <bash|zsh|fish>', 'completion');
1046
+ }
1047
+ if (rest.length > 0) {
1048
+ throw usageError('completion install accepts exactly one <shell> argument', 'completion');
1049
+ }
1050
+ if (!SUPPORTED_COMPLETION_SHELLS.includes(shell)) {
1051
+ throw usageError(`unsupported completion shell: ${shell}`, 'completion');
1052
+ }
1053
+ const result = await installCompletionScript(shell, { force: Boolean(flags.force) });
1054
+ if (flags.json)
1055
+ printJson(result);
1056
+ else
1057
+ printCompletionInstall(result);
1058
+ return EXIT_SUCCESS;
1059
+ }
1060
+ if (modeOrShell === 'uninstall') {
1061
+ const shell = maybeShell;
1062
+ if (!shell) {
1063
+ throw usageError('completion uninstall requires <bash|zsh|fish>', 'completion');
1064
+ }
1065
+ if (rest.length > 0) {
1066
+ throw usageError('completion uninstall accepts exactly one <shell> argument', 'completion');
1067
+ }
1068
+ if (flags.force) {
1069
+ throw usageError('completion uninstall does not support --force', 'completion');
1070
+ }
1071
+ if (!SUPPORTED_COMPLETION_SHELLS.includes(shell)) {
1072
+ throw usageError(`unsupported completion shell: ${shell}`, 'completion');
1073
+ }
1074
+ const result = await uninstallCompletionScript(shell);
1075
+ if (flags.json)
1076
+ printJson(result);
1077
+ else
1078
+ printCompletionUninstall(result);
1079
+ return EXIT_SUCCESS;
1080
+ }
1081
+ if (flags.json) {
1082
+ throw usageError('completion does not support --json', 'completion');
1083
+ }
1084
+ if (flags.force) {
1085
+ throw usageError('completion does not support --force', 'completion');
1086
+ }
1087
+ if (maybeShell || rest.length > 0) {
1088
+ throw usageError('completion accepts exactly one <shell> argument', 'completion');
1089
+ }
1090
+ if (!SUPPORTED_COMPLETION_SHELLS.includes(modeOrShell)) {
1091
+ throw usageError(`unsupported completion shell: ${modeOrShell}`, 'completion');
1092
+ }
1093
+ process.stdout.write(generateCompletionScript(modeOrShell));
1094
+ return EXIT_SUCCESS;
1095
+ }
1096
+ if (command === 'config') {
1097
+ const context = await resolveCommandContext(flags);
1098
+ return await handleConfigCommand(positionals, flags, context);
1099
+ }
1100
+ if (command === 'auth') {
1101
+ const context = await resolveCommandContext(flags);
1102
+ return await handleAuthCommand(positionals, flags, context);
1103
+ }
1104
+ let context = await resolveCommandContext(flags);
1105
+ let options = context.options;
1106
+ if (command === 'start') {
1107
+ context = await ensureInitialPasswordConfigured(context, flags);
1108
+ options = context.options;
1109
+ const result = await startRuntime({
1110
+ ...options,
1111
+ foreground: Boolean(flags.foreground),
1112
+ onReady: async ({ endpoint, pid }) => {
1113
+ if (!flags.json) {
1114
+ console.log('coder-studio started');
1115
+ console.log(`endpoint: ${endpoint}`);
1116
+ console.log(`pid: ${pid}`);
1117
+ }
1118
+ }
1119
+ });
1120
+ if (flags.json) {
1121
+ printJson(result);
1122
+ }
1123
+ else if (!flags.foreground) {
1124
+ console.log(result.changed ? 'runtime is ready' : 'runtime already running');
1125
+ console.log(`endpoint: ${result.endpoint}`);
1126
+ console.log(`pid: ${result.pid ?? 'n/a'}`);
1127
+ console.log(`logPath: ${result.logPath}`);
1128
+ }
1129
+ return result.status === 'failed' ? EXIT_FAILURE : EXIT_SUCCESS;
1130
+ }
1131
+ if (command === 'stop') {
1132
+ const result = await stopRuntime(options);
1133
+ if (flags.json)
1134
+ printJson(result);
1135
+ else
1136
+ console.log(result.changed ? 'coder-studio stopped' : 'coder-studio already stopped');
1137
+ return EXIT_SUCCESS;
1138
+ }
1139
+ if (command === 'restart') {
1140
+ context = await ensureInitialPasswordConfigured(context, flags);
1141
+ options = context.options;
1142
+ const result = await restartRuntime(options);
1143
+ if (flags.json)
1144
+ printJson(result);
1145
+ else {
1146
+ console.log('coder-studio restarted');
1147
+ console.log(`endpoint: ${result.endpoint}`);
1148
+ console.log(`pid: ${result.pid ?? 'n/a'}`);
1149
+ }
1150
+ return EXIT_SUCCESS;
1151
+ }
1152
+ if (command === 'status') {
1153
+ const status = await getStatus(options);
1154
+ if (flags.json)
1155
+ printJson(status);
1156
+ else
1157
+ printStatus(status);
1158
+ return status.status === 'stopped' ? EXIT_FAILURE : EXIT_SUCCESS;
1159
+ }
1160
+ if (command === 'logs') {
1161
+ if (flags.follow) {
1162
+ await followLogs(context.options.logPath, Number.isFinite(flags.lines) ? flags.lines : context.config.values.logs.tailLines);
1163
+ return EXIT_SUCCESS;
1164
+ }
1165
+ const output = await readRuntimeLogs({ ...options, lines: Number.isFinite(flags.lines) ? flags.lines : context.config.values.logs.tailLines });
1166
+ if (output)
1167
+ console.log(output);
1168
+ return EXIT_SUCCESS;
1169
+ }
1170
+ if (command === 'open') {
1171
+ context = await ensureInitialPasswordConfigured(context, flags);
1172
+ options = context.options;
1173
+ const result = await openRuntime(options);
1174
+ if (flags.json)
1175
+ printJson(result);
1176
+ else
1177
+ console.log(`opened: ${result.endpoint}`);
1178
+ return EXIT_SUCCESS;
1179
+ }
1180
+ if (command === 'doctor') {
1181
+ const report = await doctorRuntime(options);
1182
+ await printDoctor(report, Boolean(flags.json));
1183
+ return report.status.status === 'running' || report.status.status === 'degraded' ? EXIT_SUCCESS : EXIT_FAILURE;
1184
+ }
1185
+ }
1186
+ catch (error) {
1187
+ return printCliError(error, flags);
1188
+ }
1189
+ return printCliError(usageError(`unsupported command: ${command}`, 'main'), flags);
1190
+ }