@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
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { DEFAULT_HOST, DEFAULT_LOG_TAIL_LINES, DEFAULT_PORT, DEFAULT_SESSION_IDLE_MINUTES, DEFAULT_SESSION_MAX_HOURS, defaultRootPath, resolveAuthPath, resolveConfigPath, resolveDataDir, resolveStateDir, } from './config.mjs';
|
|
5
|
+
const CONFIG_VERSION = 1;
|
|
6
|
+
const SUPPORTED_KEYS = [
|
|
7
|
+
'server.host',
|
|
8
|
+
'server.port',
|
|
9
|
+
'root.path',
|
|
10
|
+
'auth.publicMode',
|
|
11
|
+
'auth.password',
|
|
12
|
+
'auth.sessionIdleMinutes',
|
|
13
|
+
'auth.sessionMaxHours',
|
|
14
|
+
'system.openCommand',
|
|
15
|
+
'logs.tailLines',
|
|
16
|
+
];
|
|
17
|
+
function isObject(value) {
|
|
18
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
async function readJsonIfExists(filePath) {
|
|
21
|
+
try {
|
|
22
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
23
|
+
return JSON.parse(raw);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function writeJson(filePath, value) {
|
|
33
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
34
|
+
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
35
|
+
}
|
|
36
|
+
function trimToNull(value) {
|
|
37
|
+
if (value == null)
|
|
38
|
+
return null;
|
|
39
|
+
const text = String(value).trim();
|
|
40
|
+
return text ? text : null;
|
|
41
|
+
}
|
|
42
|
+
function asPositiveInteger(value, key) {
|
|
43
|
+
const number = typeof value === 'number' ? value : Number.parseInt(String(value), 10);
|
|
44
|
+
if (!Number.isInteger(number) || number <= 0) {
|
|
45
|
+
throw new Error(`invalid_${key}`);
|
|
46
|
+
}
|
|
47
|
+
return number;
|
|
48
|
+
}
|
|
49
|
+
function asPort(value) {
|
|
50
|
+
const number = typeof value === 'number' ? value : Number.parseInt(String(value), 10);
|
|
51
|
+
if (!Number.isInteger(number) || number <= 0 || number > 65535) {
|
|
52
|
+
throw new Error('invalid_server_port');
|
|
53
|
+
}
|
|
54
|
+
return number;
|
|
55
|
+
}
|
|
56
|
+
function asBoolean(value, key) {
|
|
57
|
+
if (typeof value === 'boolean')
|
|
58
|
+
return value;
|
|
59
|
+
const normalized = String(value).trim().toLowerCase();
|
|
60
|
+
if (['1', 'true', 'on', 'yes', 'enabled'].includes(normalized))
|
|
61
|
+
return true;
|
|
62
|
+
if (['0', 'false', 'off', 'no', 'disabled'].includes(normalized))
|
|
63
|
+
return false;
|
|
64
|
+
throw new Error(`invalid_${key}`);
|
|
65
|
+
}
|
|
66
|
+
function expandHome(value) {
|
|
67
|
+
if (!value || !value.startsWith('~'))
|
|
68
|
+
return value;
|
|
69
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
70
|
+
if (!home)
|
|
71
|
+
return value;
|
|
72
|
+
if (value === '~')
|
|
73
|
+
return home;
|
|
74
|
+
if (value.startsWith('~/') || value.startsWith('~\\')) {
|
|
75
|
+
return path.join(home, value.slice(2));
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
function coerceRootPath(value) {
|
|
80
|
+
if (value == null)
|
|
81
|
+
return null;
|
|
82
|
+
const text = trimToNull(value);
|
|
83
|
+
if (!text)
|
|
84
|
+
return null;
|
|
85
|
+
return path.resolve(expandHome(text));
|
|
86
|
+
}
|
|
87
|
+
function sanitizeCliConfig(raw) {
|
|
88
|
+
const config = isObject(raw) ? raw : {};
|
|
89
|
+
const logs = isObject(config.logs) ? config.logs : {};
|
|
90
|
+
const system = isObject(config.system) ? config.system : {};
|
|
91
|
+
const tailLines = Number.isInteger(logs.tailLines) && logs.tailLines > 0
|
|
92
|
+
? logs.tailLines
|
|
93
|
+
: DEFAULT_LOG_TAIL_LINES;
|
|
94
|
+
return {
|
|
95
|
+
version: CONFIG_VERSION,
|
|
96
|
+
system: {
|
|
97
|
+
openCommand: trimToNull(system.openCommand),
|
|
98
|
+
},
|
|
99
|
+
logs: {
|
|
100
|
+
tailLines,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function sanitizeAuthConfig(raw) {
|
|
105
|
+
const file = isObject(raw) ? raw : {};
|
|
106
|
+
const legacyRoots = Array.isArray(file.allowedRoots) ? file.allowedRoots : [];
|
|
107
|
+
const configuredRoot = trimToNull(file.rootPath) || trimToNull(legacyRoots[0]) || null;
|
|
108
|
+
return {
|
|
109
|
+
version: CONFIG_VERSION,
|
|
110
|
+
publicMode: typeof file.publicMode === 'boolean' ? file.publicMode : true,
|
|
111
|
+
password: typeof file.password === 'string' ? file.password.trim() : '',
|
|
112
|
+
rootPath: configuredRoot,
|
|
113
|
+
bindHost: trimToNull(file.bindHost) || DEFAULT_HOST,
|
|
114
|
+
bindPort: Number.isInteger(file.bindPort) && file.bindPort > 0 ? file.bindPort : DEFAULT_PORT,
|
|
115
|
+
sessionIdleMinutes: Number.isInteger(file.sessionIdleMinutes) && file.sessionIdleMinutes > 0
|
|
116
|
+
? file.sessionIdleMinutes
|
|
117
|
+
: DEFAULT_SESSION_IDLE_MINUTES,
|
|
118
|
+
sessionMaxHours: Number.isInteger(file.sessionMaxHours) && file.sessionMaxHours > 0
|
|
119
|
+
? file.sessionMaxHours
|
|
120
|
+
: DEFAULT_SESSION_MAX_HOURS,
|
|
121
|
+
sessions: Array.isArray(file.sessions) ? file.sessions : [],
|
|
122
|
+
raw: isObject(raw) ? raw : {},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function buildDefaultAuthConfig() {
|
|
126
|
+
return {
|
|
127
|
+
version: CONFIG_VERSION,
|
|
128
|
+
publicMode: true,
|
|
129
|
+
password: '',
|
|
130
|
+
rootPath: defaultRootPath(),
|
|
131
|
+
bindHost: DEFAULT_HOST,
|
|
132
|
+
bindPort: DEFAULT_PORT,
|
|
133
|
+
sessionIdleMinutes: DEFAULT_SESSION_IDLE_MINUTES,
|
|
134
|
+
sessionMaxHours: DEFAULT_SESSION_MAX_HOURS,
|
|
135
|
+
sessions: [],
|
|
136
|
+
raw: {},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function snapshotFromParts(paths, cliConfig, authConfig) {
|
|
140
|
+
const passwordConfigured = authConfig.password.trim().length > 0;
|
|
141
|
+
return {
|
|
142
|
+
paths,
|
|
143
|
+
values: {
|
|
144
|
+
server: {
|
|
145
|
+
host: authConfig.bindHost,
|
|
146
|
+
port: authConfig.bindPort,
|
|
147
|
+
},
|
|
148
|
+
root: {
|
|
149
|
+
path: authConfig.rootPath,
|
|
150
|
+
},
|
|
151
|
+
auth: {
|
|
152
|
+
publicMode: authConfig.publicMode,
|
|
153
|
+
passwordConfigured,
|
|
154
|
+
sessionIdleMinutes: authConfig.sessionIdleMinutes,
|
|
155
|
+
sessionMaxHours: authConfig.sessionMaxHours,
|
|
156
|
+
},
|
|
157
|
+
system: {
|
|
158
|
+
openCommand: cliConfig.system.openCommand,
|
|
159
|
+
},
|
|
160
|
+
logs: {
|
|
161
|
+
tailLines: cliConfig.logs.tailLines,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
secrets: {
|
|
165
|
+
password: authConfig.password,
|
|
166
|
+
},
|
|
167
|
+
raw: {
|
|
168
|
+
cli: cliConfig,
|
|
169
|
+
auth: authConfig,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export function resolveConfigFiles(input = {}) {
|
|
174
|
+
const stateDir = input.stateDir || resolveStateDir(input.env, input.platform);
|
|
175
|
+
const dataDir = input.dataDir || resolveDataDir(stateDir, input.env);
|
|
176
|
+
return {
|
|
177
|
+
stateDir,
|
|
178
|
+
dataDir,
|
|
179
|
+
configPath: resolveConfigPath(stateDir),
|
|
180
|
+
authPath: resolveAuthPath(dataDir),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export async function loadLocalConfig(input = {}) {
|
|
184
|
+
const paths = resolveConfigFiles(input);
|
|
185
|
+
const cliRaw = await readJsonIfExists(paths.configPath);
|
|
186
|
+
const authRaw = await readJsonIfExists(paths.authPath);
|
|
187
|
+
const cliConfig = sanitizeCliConfig(cliRaw);
|
|
188
|
+
const authConfig = authRaw ? sanitizeAuthConfig(authRaw) : buildDefaultAuthConfig();
|
|
189
|
+
return snapshotFromParts(paths, cliConfig, authConfig);
|
|
190
|
+
}
|
|
191
|
+
export function listConfigKeys() {
|
|
192
|
+
return [...SUPPORTED_KEYS];
|
|
193
|
+
}
|
|
194
|
+
export function isRuntimeConfigKey(key) {
|
|
195
|
+
return [
|
|
196
|
+
'server.host',
|
|
197
|
+
'server.port',
|
|
198
|
+
'root.path',
|
|
199
|
+
'auth.publicMode',
|
|
200
|
+
'auth.password',
|
|
201
|
+
'auth.sessionIdleMinutes',
|
|
202
|
+
'auth.sessionMaxHours',
|
|
203
|
+
].includes(key);
|
|
204
|
+
}
|
|
205
|
+
export function isCliConfigKey(key) {
|
|
206
|
+
return ['system.openCommand', 'logs.tailLines'].includes(key);
|
|
207
|
+
}
|
|
208
|
+
export function normalizeConfigValue(key, rawValue) {
|
|
209
|
+
switch (key) {
|
|
210
|
+
case 'server.host': {
|
|
211
|
+
const host = trimToNull(rawValue);
|
|
212
|
+
if (!host)
|
|
213
|
+
throw new Error('invalid_server_host');
|
|
214
|
+
return host;
|
|
215
|
+
}
|
|
216
|
+
case 'server.port':
|
|
217
|
+
return asPort(rawValue);
|
|
218
|
+
case 'root.path':
|
|
219
|
+
return coerceRootPath(rawValue);
|
|
220
|
+
case 'auth.publicMode':
|
|
221
|
+
return asBoolean(rawValue, 'auth_public_mode');
|
|
222
|
+
case 'auth.password':
|
|
223
|
+
return rawValue == null ? '' : String(rawValue).trim();
|
|
224
|
+
case 'auth.sessionIdleMinutes':
|
|
225
|
+
return asPositiveInteger(rawValue, 'auth_session_idle_minutes');
|
|
226
|
+
case 'auth.sessionMaxHours':
|
|
227
|
+
return asPositiveInteger(rawValue, 'auth_session_max_hours');
|
|
228
|
+
case 'system.openCommand':
|
|
229
|
+
return trimToNull(rawValue);
|
|
230
|
+
case 'logs.tailLines':
|
|
231
|
+
return asPositiveInteger(rawValue, 'logs_tail_lines');
|
|
232
|
+
default:
|
|
233
|
+
throw new Error(`unsupported_config_key:${key}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
export function defaultValueForKey(key) {
|
|
237
|
+
switch (key) {
|
|
238
|
+
case 'server.host':
|
|
239
|
+
return DEFAULT_HOST;
|
|
240
|
+
case 'server.port':
|
|
241
|
+
return DEFAULT_PORT;
|
|
242
|
+
case 'root.path':
|
|
243
|
+
return null;
|
|
244
|
+
case 'auth.publicMode':
|
|
245
|
+
return true;
|
|
246
|
+
case 'auth.password':
|
|
247
|
+
return '';
|
|
248
|
+
case 'auth.sessionIdleMinutes':
|
|
249
|
+
return DEFAULT_SESSION_IDLE_MINUTES;
|
|
250
|
+
case 'auth.sessionMaxHours':
|
|
251
|
+
return DEFAULT_SESSION_MAX_HOURS;
|
|
252
|
+
case 'system.openCommand':
|
|
253
|
+
return null;
|
|
254
|
+
case 'logs.tailLines':
|
|
255
|
+
return DEFAULT_LOG_TAIL_LINES;
|
|
256
|
+
default:
|
|
257
|
+
throw new Error(`unsupported_config_key:${key}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
export function getPublicConfigValue(snapshot, key) {
|
|
261
|
+
switch (key) {
|
|
262
|
+
case 'server.host':
|
|
263
|
+
return snapshot.values.server.host;
|
|
264
|
+
case 'server.port':
|
|
265
|
+
return snapshot.values.server.port;
|
|
266
|
+
case 'root.path':
|
|
267
|
+
return snapshot.values.root.path;
|
|
268
|
+
case 'auth.publicMode':
|
|
269
|
+
return snapshot.values.auth.publicMode;
|
|
270
|
+
case 'auth.password':
|
|
271
|
+
return snapshot.values.auth.passwordConfigured ? '(configured)' : '(not configured)';
|
|
272
|
+
case 'auth.sessionIdleMinutes':
|
|
273
|
+
return snapshot.values.auth.sessionIdleMinutes;
|
|
274
|
+
case 'auth.sessionMaxHours':
|
|
275
|
+
return snapshot.values.auth.sessionMaxHours;
|
|
276
|
+
case 'system.openCommand':
|
|
277
|
+
return snapshot.values.system.openCommand;
|
|
278
|
+
case 'logs.tailLines':
|
|
279
|
+
return snapshot.values.logs.tailLines;
|
|
280
|
+
default:
|
|
281
|
+
throw new Error(`unsupported_config_key:${key}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
export function flattenPublicConfig(snapshot) {
|
|
285
|
+
return {
|
|
286
|
+
'server.host': snapshot.values.server.host,
|
|
287
|
+
'server.port': snapshot.values.server.port,
|
|
288
|
+
'root.path': snapshot.values.root.path,
|
|
289
|
+
'auth.publicMode': snapshot.values.auth.publicMode,
|
|
290
|
+
'auth.password': snapshot.values.auth.passwordConfigured ? '(configured)' : '(not configured)',
|
|
291
|
+
'auth.sessionIdleMinutes': snapshot.values.auth.sessionIdleMinutes,
|
|
292
|
+
'auth.sessionMaxHours': snapshot.values.auth.sessionMaxHours,
|
|
293
|
+
'system.openCommand': snapshot.values.system.openCommand,
|
|
294
|
+
'logs.tailLines': snapshot.values.logs.tailLines,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function runtimeViewFromSnapshot(snapshot) {
|
|
298
|
+
return {
|
|
299
|
+
server: {
|
|
300
|
+
host: snapshot.values.server.host,
|
|
301
|
+
port: snapshot.values.server.port,
|
|
302
|
+
},
|
|
303
|
+
root: {
|
|
304
|
+
path: snapshot.values.root.path,
|
|
305
|
+
},
|
|
306
|
+
auth: {
|
|
307
|
+
publicMode: snapshot.values.auth.publicMode,
|
|
308
|
+
passwordConfigured: snapshot.values.auth.passwordConfigured,
|
|
309
|
+
sessionIdleMinutes: snapshot.values.auth.sessionIdleMinutes,
|
|
310
|
+
sessionMaxHours: snapshot.values.auth.sessionMaxHours,
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
export function mergeRuntimeConfigView(snapshot, runtimeView = null) {
|
|
315
|
+
if (!runtimeView) {
|
|
316
|
+
return snapshot;
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
...snapshot,
|
|
320
|
+
values: {
|
|
321
|
+
...snapshot.values,
|
|
322
|
+
server: {
|
|
323
|
+
host: runtimeView.server?.host ?? snapshot.values.server.host,
|
|
324
|
+
port: runtimeView.server?.port ?? snapshot.values.server.port,
|
|
325
|
+
},
|
|
326
|
+
root: {
|
|
327
|
+
path: runtimeView.root?.path ?? snapshot.values.root.path,
|
|
328
|
+
},
|
|
329
|
+
auth: {
|
|
330
|
+
publicMode: runtimeView.auth?.publicMode ?? snapshot.values.auth.publicMode,
|
|
331
|
+
passwordConfigured: runtimeView.auth?.passwordConfigured ?? snapshot.values.auth.passwordConfigured,
|
|
332
|
+
sessionIdleMinutes: runtimeView.auth?.sessionIdleMinutes ?? snapshot.values.auth.sessionIdleMinutes,
|
|
333
|
+
sessionMaxHours: runtimeView.auth?.sessionMaxHours ?? snapshot.values.auth.sessionMaxHours,
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function buildCliFile(snapshot) {
|
|
339
|
+
return {
|
|
340
|
+
version: CONFIG_VERSION,
|
|
341
|
+
system: {
|
|
342
|
+
openCommand: snapshot.values.system.openCommand,
|
|
343
|
+
},
|
|
344
|
+
logs: {
|
|
345
|
+
tailLines: snapshot.values.logs.tailLines,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function buildAuthFile(snapshot) {
|
|
350
|
+
const raw = isObject(snapshot.raw.auth.raw) ? { ...snapshot.raw.auth.raw } : {};
|
|
351
|
+
const next = {
|
|
352
|
+
...raw,
|
|
353
|
+
version: CONFIG_VERSION,
|
|
354
|
+
publicMode: snapshot.values.auth.publicMode,
|
|
355
|
+
password: snapshot.secrets.password,
|
|
356
|
+
bindHost: snapshot.values.server.host,
|
|
357
|
+
bindPort: snapshot.values.server.port,
|
|
358
|
+
sessionIdleMinutes: snapshot.values.auth.sessionIdleMinutes,
|
|
359
|
+
sessionMaxHours: snapshot.values.auth.sessionMaxHours,
|
|
360
|
+
sessions: Array.isArray(snapshot.raw.auth.sessions) ? snapshot.raw.auth.sessions : [],
|
|
361
|
+
};
|
|
362
|
+
if (snapshot.values.root.path) {
|
|
363
|
+
next.rootPath = snapshot.values.root.path;
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
delete next.rootPath;
|
|
367
|
+
}
|
|
368
|
+
delete next.allowedRoots;
|
|
369
|
+
return next;
|
|
370
|
+
}
|
|
371
|
+
export async function updateLocalConfig(input = {}, updates = {}, { unset = false } = {}) {
|
|
372
|
+
const current = await loadLocalConfig(input);
|
|
373
|
+
const next = structuredClone(current);
|
|
374
|
+
const changedKeys = [];
|
|
375
|
+
let restartRequired = false;
|
|
376
|
+
let sessionsReset = false;
|
|
377
|
+
for (const key of Object.keys(updates)) {
|
|
378
|
+
if (!SUPPORTED_KEYS.includes(key)) {
|
|
379
|
+
throw new Error(`unsupported_config_key:${key}`);
|
|
380
|
+
}
|
|
381
|
+
const value = unset ? defaultValueForKey(key) : normalizeConfigValue(key, updates[key]);
|
|
382
|
+
switch (key) {
|
|
383
|
+
case 'server.host':
|
|
384
|
+
if (next.values.server.host !== value) {
|
|
385
|
+
next.values.server.host = value;
|
|
386
|
+
changedKeys.push(key);
|
|
387
|
+
restartRequired = true;
|
|
388
|
+
}
|
|
389
|
+
break;
|
|
390
|
+
case 'server.port':
|
|
391
|
+
if (next.values.server.port !== value) {
|
|
392
|
+
next.values.server.port = value;
|
|
393
|
+
changedKeys.push(key);
|
|
394
|
+
restartRequired = true;
|
|
395
|
+
}
|
|
396
|
+
break;
|
|
397
|
+
case 'root.path':
|
|
398
|
+
if (next.values.root.path !== value) {
|
|
399
|
+
next.values.root.path = value;
|
|
400
|
+
changedKeys.push(key);
|
|
401
|
+
}
|
|
402
|
+
break;
|
|
403
|
+
case 'auth.publicMode':
|
|
404
|
+
if (next.values.auth.publicMode !== value) {
|
|
405
|
+
next.values.auth.publicMode = value;
|
|
406
|
+
changedKeys.push(key);
|
|
407
|
+
sessionsReset = true;
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
case 'auth.password':
|
|
411
|
+
if (next.secrets.password !== value) {
|
|
412
|
+
next.secrets.password = value;
|
|
413
|
+
next.values.auth.passwordConfigured = value.length > 0;
|
|
414
|
+
changedKeys.push(key);
|
|
415
|
+
sessionsReset = true;
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
418
|
+
case 'auth.sessionIdleMinutes':
|
|
419
|
+
if (next.values.auth.sessionIdleMinutes !== value) {
|
|
420
|
+
next.values.auth.sessionIdleMinutes = value;
|
|
421
|
+
changedKeys.push(key);
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
case 'auth.sessionMaxHours':
|
|
425
|
+
if (next.values.auth.sessionMaxHours !== value) {
|
|
426
|
+
next.values.auth.sessionMaxHours = value;
|
|
427
|
+
changedKeys.push(key);
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
case 'system.openCommand':
|
|
431
|
+
if (next.values.system.openCommand !== value) {
|
|
432
|
+
next.values.system.openCommand = value;
|
|
433
|
+
changedKeys.push(key);
|
|
434
|
+
}
|
|
435
|
+
break;
|
|
436
|
+
case 'logs.tailLines':
|
|
437
|
+
if (next.values.logs.tailLines !== value) {
|
|
438
|
+
next.values.logs.tailLines = value;
|
|
439
|
+
changedKeys.push(key);
|
|
440
|
+
}
|
|
441
|
+
break;
|
|
442
|
+
default:
|
|
443
|
+
throw new Error(`unsupported_config_key:${key}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (changedKeys.length === 0) {
|
|
447
|
+
return {
|
|
448
|
+
changedKeys,
|
|
449
|
+
restartRequired,
|
|
450
|
+
sessionsReset,
|
|
451
|
+
snapshot: current,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
if (next.values.root.path) {
|
|
455
|
+
await fs.mkdir(next.values.root.path, { recursive: true });
|
|
456
|
+
}
|
|
457
|
+
if (sessionsReset) {
|
|
458
|
+
next.raw.auth.sessions = [];
|
|
459
|
+
}
|
|
460
|
+
await writeJson(current.paths.configPath, buildCliFile(next));
|
|
461
|
+
await writeJson(current.paths.authPath, buildAuthFile(next));
|
|
462
|
+
return {
|
|
463
|
+
changedKeys,
|
|
464
|
+
restartRequired,
|
|
465
|
+
sessionsReset,
|
|
466
|
+
snapshot: await loadLocalConfig(input),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
export function validateConfigSnapshot(snapshot) {
|
|
470
|
+
const errors = [];
|
|
471
|
+
const warnings = [];
|
|
472
|
+
const flat = runtimeViewFromSnapshot(snapshot);
|
|
473
|
+
if (!trimToNull(flat.server.host)) {
|
|
474
|
+
errors.push('server.host must not be empty');
|
|
475
|
+
}
|
|
476
|
+
if (!Number.isInteger(flat.server.port) || flat.server.port <= 0 || flat.server.port > 65535) {
|
|
477
|
+
errors.push('server.port must be an integer between 1 and 65535');
|
|
478
|
+
}
|
|
479
|
+
if (!Number.isInteger(flat.auth.sessionIdleMinutes) || flat.auth.sessionIdleMinutes <= 0) {
|
|
480
|
+
errors.push('auth.sessionIdleMinutes must be a positive integer');
|
|
481
|
+
}
|
|
482
|
+
if (!Number.isInteger(flat.auth.sessionMaxHours) || flat.auth.sessionMaxHours <= 0) {
|
|
483
|
+
errors.push('auth.sessionMaxHours must be a positive integer');
|
|
484
|
+
}
|
|
485
|
+
if (!Number.isInteger(snapshot.values.logs.tailLines) || snapshot.values.logs.tailLines <= 0) {
|
|
486
|
+
errors.push('logs.tailLines must be a positive integer');
|
|
487
|
+
}
|
|
488
|
+
if (flat.auth.publicMode && !flat.root.path) {
|
|
489
|
+
errors.push('root.path is required when auth.publicMode is enabled');
|
|
490
|
+
}
|
|
491
|
+
if (flat.auth.publicMode && !flat.auth.passwordConfigured) {
|
|
492
|
+
warnings.push('auth.password is not configured; public-mode login will stay unavailable until a password is set');
|
|
493
|
+
}
|
|
494
|
+
if (flat.root.path) {
|
|
495
|
+
try {
|
|
496
|
+
const resolved = path.resolve(flat.root.path);
|
|
497
|
+
if (resolved !== flat.root.path) {
|
|
498
|
+
warnings.push(`root.path will resolve to ${resolved}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
errors.push('root.path is not a valid path');
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (snapshot.values.system.openCommand && !trimToNull(snapshot.values.system.openCommand)) {
|
|
506
|
+
warnings.push('system.openCommand is empty and will be ignored');
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
ok: errors.length === 0,
|
|
510
|
+
errors,
|
|
511
|
+
warnings,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
export function buildConfigPathsReport(snapshot) {
|
|
515
|
+
return {
|
|
516
|
+
stateDir: snapshot.paths.stateDir,
|
|
517
|
+
dataDir: snapshot.paths.dataDir,
|
|
518
|
+
configPath: snapshot.paths.configPath,
|
|
519
|
+
authPath: snapshot.paths.authPath,
|
|
520
|
+
};
|
|
521
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spencer-kit/coder-studio",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CLI runtime manager for Coder Studio.",
|
|
6
|
+
"bin": {
|
|
7
|
+
"coder-studio": "./bin/coder-studio.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"lib",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"optionalDependencies": {
|
|
15
|
+
"@spencer-kit/coder-studio-linux-x64": "0.1.3",
|
|
16
|
+
"@spencer-kit/coder-studio-darwin-arm64": "0.1.3",
|
|
17
|
+
"@spencer-kit/coder-studio-darwin-x64": "0.1.3",
|
|
18
|
+
"@spencer-kit/coder-studio-win32-x64": "0.1.3"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22"
|
|
25
|
+
}
|
|
26
|
+
}
|