@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/README.md +24 -0
- package/bin/coder-studio.mjs +10 -0
- package/lib/cli.mjs +1190 -0
- package/lib/completion.mjs +562 -0
- package/lib/config.mjs +59 -0
- package/lib/http.mjs +89 -0
- package/lib/platform.mjs +50 -0
- package/lib/process-utils.mjs +76 -0
- package/lib/runtime-controller.mjs +350 -0
- package/lib/state.mjs +75 -0
- package/lib/user-config.mjs +521 -0
- package/package.json +26 -0
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
|
+
}
|