codeharbor 0.1.10 → 0.1.11
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 +4 -3
- package/dist/cli.js +535 -467
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -353,9 +353,10 @@ Note: `PUT /api/admin/config/global` writes to `.env` and marks changes as resta
|
|
|
353
353
|
1. Start server: `codeharbor admin serve`.
|
|
354
354
|
2. Open `/settings/global`, set `Admin Token` (if enabled), then click `Save Auth`.
|
|
355
355
|
3. Adjust global fields and click `Save Global Config` (UI shows restart-required warning).
|
|
356
|
-
4.
|
|
357
|
-
5. Open `/
|
|
358
|
-
6. Open `/
|
|
356
|
+
4. Use `Restart Main Service` or `Restart Main + Admin` buttons for one-click restart from Admin UI (requires root-capable service context).
|
|
357
|
+
5. Open `/settings/rooms`, fill `Room ID + Workdir`, then `Save Room`.
|
|
358
|
+
6. Open `/health` to run connectivity checks (`codex` + Matrix).
|
|
359
|
+
7. Open `/audit` to verify config revisions (actor/summary/payload).
|
|
359
360
|
|
|
360
361
|
## Standalone Admin Deployment
|
|
361
362
|
|
package/dist/cli.js
CHANGED
|
@@ -30,14 +30,14 @@ var import_node_path12 = __toESM(require("path"));
|
|
|
30
30
|
var import_commander = require("commander");
|
|
31
31
|
|
|
32
32
|
// src/app.ts
|
|
33
|
-
var
|
|
33
|
+
var import_node_child_process4 = require("child_process");
|
|
34
34
|
var import_node_util2 = require("util");
|
|
35
35
|
|
|
36
36
|
// src/admin-server.ts
|
|
37
|
-
var
|
|
38
|
-
var
|
|
37
|
+
var import_node_child_process2 = require("child_process");
|
|
38
|
+
var import_node_fs4 = __toESM(require("fs"));
|
|
39
39
|
var import_node_http = __toESM(require("http"));
|
|
40
|
-
var
|
|
40
|
+
var import_node_path4 = __toESM(require("path"));
|
|
41
41
|
var import_node_util = require("util");
|
|
42
42
|
|
|
43
43
|
// src/init.ts
|
|
@@ -220,117 +220,413 @@ async function askYesNo(rl, question, defaultValue) {
|
|
|
220
220
|
return answer === "y" || answer === "yes";
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
// src/
|
|
224
|
-
var
|
|
225
|
-
var
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
223
|
+
// src/service-manager.ts
|
|
224
|
+
var import_node_child_process = require("child_process");
|
|
225
|
+
var import_node_fs3 = __toESM(require("fs"));
|
|
226
|
+
var import_node_os2 = __toESM(require("os"));
|
|
227
|
+
var import_node_path3 = __toESM(require("path"));
|
|
228
|
+
|
|
229
|
+
// src/runtime-home.ts
|
|
230
|
+
var import_node_fs2 = __toESM(require("fs"));
|
|
231
|
+
var import_node_os = __toESM(require("os"));
|
|
232
|
+
var import_node_path2 = __toESM(require("path"));
|
|
233
|
+
var LEGACY_RUNTIME_HOME = "/opt/codeharbor";
|
|
234
|
+
var USER_RUNTIME_HOME_DIR = ".codeharbor";
|
|
235
|
+
var DEFAULT_RUNTIME_HOME = import_node_path2.default.resolve(import_node_os.default.homedir(), USER_RUNTIME_HOME_DIR);
|
|
236
|
+
var RUNTIME_HOME_ENV_KEY = "CODEHARBOR_HOME";
|
|
237
|
+
function resolveRuntimeHome(env = process.env) {
|
|
238
|
+
const configured = env[RUNTIME_HOME_ENV_KEY]?.trim();
|
|
239
|
+
if (configured) {
|
|
240
|
+
return import_node_path2.default.resolve(configured);
|
|
230
241
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
logger;
|
|
235
|
-
stateStore;
|
|
236
|
-
configService;
|
|
237
|
-
host;
|
|
238
|
-
port;
|
|
239
|
-
adminToken;
|
|
240
|
-
adminIpAllowlist;
|
|
241
|
-
adminAllowedOrigins;
|
|
242
|
-
cwd;
|
|
243
|
-
checkCodex;
|
|
244
|
-
checkMatrix;
|
|
245
|
-
server = null;
|
|
246
|
-
address = null;
|
|
247
|
-
constructor(config, logger, stateStore, configService, options) {
|
|
248
|
-
this.config = config;
|
|
249
|
-
this.logger = logger;
|
|
250
|
-
this.stateStore = stateStore;
|
|
251
|
-
this.configService = configService;
|
|
252
|
-
this.host = options.host;
|
|
253
|
-
this.port = options.port;
|
|
254
|
-
this.adminToken = options.adminToken;
|
|
255
|
-
this.adminIpAllowlist = normalizeAllowlist(options.adminIpAllowlist ?? []);
|
|
256
|
-
this.adminAllowedOrigins = normalizeOriginAllowlist(options.adminAllowedOrigins ?? []);
|
|
257
|
-
this.cwd = options.cwd ?? process.cwd();
|
|
258
|
-
this.checkCodex = options.checkCodex ?? defaultCheckCodex;
|
|
259
|
-
this.checkMatrix = options.checkMatrix ?? defaultCheckMatrix;
|
|
242
|
+
const legacyEnvPath = import_node_path2.default.resolve(LEGACY_RUNTIME_HOME, ".env");
|
|
243
|
+
if (import_node_fs2.default.existsSync(legacyEnvPath)) {
|
|
244
|
+
return LEGACY_RUNTIME_HOME;
|
|
260
245
|
}
|
|
261
|
-
|
|
262
|
-
|
|
246
|
+
return resolveUserRuntimeHome(env);
|
|
247
|
+
}
|
|
248
|
+
function resolveUserRuntimeHome(env = process.env) {
|
|
249
|
+
const home = env.HOME?.trim() || import_node_os.default.homedir();
|
|
250
|
+
return import_node_path2.default.resolve(home, USER_RUNTIME_HOME_DIR);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/service-manager.ts
|
|
254
|
+
var SYSTEMD_DIR = "/etc/systemd/system";
|
|
255
|
+
var MAIN_SERVICE_NAME = "codeharbor.service";
|
|
256
|
+
var ADMIN_SERVICE_NAME = "codeharbor-admin.service";
|
|
257
|
+
function resolveDefaultRunUser(env = process.env) {
|
|
258
|
+
const sudoUser = env.SUDO_USER?.trim();
|
|
259
|
+
if (sudoUser) {
|
|
260
|
+
return sudoUser;
|
|
263
261
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
262
|
+
const user = env.USER?.trim();
|
|
263
|
+
if (user) {
|
|
264
|
+
return user;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
return import_node_os2.default.userInfo().username;
|
|
268
|
+
} catch {
|
|
269
|
+
return "root";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function resolveRuntimeHomeForUser(runUser, env = process.env, explicitRuntimeHome) {
|
|
273
|
+
const configuredRuntimeHome = explicitRuntimeHome?.trim() || env[RUNTIME_HOME_ENV_KEY]?.trim();
|
|
274
|
+
if (configuredRuntimeHome) {
|
|
275
|
+
return import_node_path3.default.resolve(configuredRuntimeHome);
|
|
276
|
+
}
|
|
277
|
+
const userHome = resolveUserHome(runUser);
|
|
278
|
+
if (userHome) {
|
|
279
|
+
return import_node_path3.default.resolve(userHome, USER_RUNTIME_HOME_DIR);
|
|
280
|
+
}
|
|
281
|
+
return import_node_path3.default.resolve(import_node_os2.default.homedir(), USER_RUNTIME_HOME_DIR);
|
|
282
|
+
}
|
|
283
|
+
function buildMainServiceUnit(options) {
|
|
284
|
+
validateUnitOptions(options);
|
|
285
|
+
const runtimeHome2 = import_node_path3.default.resolve(options.runtimeHome);
|
|
286
|
+
return [
|
|
287
|
+
"[Unit]",
|
|
288
|
+
"Description=CodeHarbor main service",
|
|
289
|
+
"After=network-online.target",
|
|
290
|
+
"Wants=network-online.target",
|
|
291
|
+
"",
|
|
292
|
+
"[Service]",
|
|
293
|
+
"Type=simple",
|
|
294
|
+
`User=${options.runUser}`,
|
|
295
|
+
`WorkingDirectory=${runtimeHome2}`,
|
|
296
|
+
`Environment=CODEHARBOR_HOME=${runtimeHome2}`,
|
|
297
|
+
`ExecStart=${import_node_path3.default.resolve(options.nodeBinPath)} ${import_node_path3.default.resolve(options.cliScriptPath)} start`,
|
|
298
|
+
"Restart=always",
|
|
299
|
+
"RestartSec=3",
|
|
300
|
+
"NoNewPrivileges=true",
|
|
301
|
+
"PrivateTmp=true",
|
|
302
|
+
"ProtectSystem=full",
|
|
303
|
+
"ProtectHome=false",
|
|
304
|
+
`ReadWritePaths=${runtimeHome2}`,
|
|
305
|
+
"",
|
|
306
|
+
"[Install]",
|
|
307
|
+
"WantedBy=multi-user.target",
|
|
308
|
+
""
|
|
309
|
+
].join("\n");
|
|
310
|
+
}
|
|
311
|
+
function buildAdminServiceUnit(options) {
|
|
312
|
+
validateUnitOptions(options);
|
|
313
|
+
const runtimeHome2 = import_node_path3.default.resolve(options.runtimeHome);
|
|
314
|
+
return [
|
|
315
|
+
"[Unit]",
|
|
316
|
+
"Description=CodeHarbor admin service",
|
|
317
|
+
"After=network-online.target",
|
|
318
|
+
"Wants=network-online.target",
|
|
319
|
+
"",
|
|
320
|
+
"[Service]",
|
|
321
|
+
"Type=simple",
|
|
322
|
+
`User=${options.runUser}`,
|
|
323
|
+
`WorkingDirectory=${runtimeHome2}`,
|
|
324
|
+
`Environment=CODEHARBOR_HOME=${runtimeHome2}`,
|
|
325
|
+
`ExecStart=${import_node_path3.default.resolve(options.nodeBinPath)} ${import_node_path3.default.resolve(options.cliScriptPath)} admin serve`,
|
|
326
|
+
"Restart=always",
|
|
327
|
+
"RestartSec=3",
|
|
328
|
+
"NoNewPrivileges=true",
|
|
329
|
+
"PrivateTmp=true",
|
|
330
|
+
"ProtectSystem=full",
|
|
331
|
+
"ProtectHome=false",
|
|
332
|
+
`ReadWritePaths=${runtimeHome2}`,
|
|
333
|
+
"",
|
|
334
|
+
"[Install]",
|
|
335
|
+
"WantedBy=multi-user.target",
|
|
336
|
+
""
|
|
337
|
+
].join("\n");
|
|
338
|
+
}
|
|
339
|
+
function installSystemdServices(options) {
|
|
340
|
+
assertLinuxWithSystemd();
|
|
341
|
+
assertRootPrivileges();
|
|
342
|
+
const output = options.output ?? process.stdout;
|
|
343
|
+
const runUser = options.runUser.trim();
|
|
344
|
+
const runtimeHome2 = import_node_path3.default.resolve(options.runtimeHome);
|
|
345
|
+
validateSimpleValue(runUser, "runUser");
|
|
346
|
+
validateSimpleValue(runtimeHome2, "runtimeHome");
|
|
347
|
+
validateSimpleValue(options.nodeBinPath, "nodeBinPath");
|
|
348
|
+
validateSimpleValue(options.cliScriptPath, "cliScriptPath");
|
|
349
|
+
ensureUserExists(runUser);
|
|
350
|
+
const runGroup = resolveUserGroup(runUser);
|
|
351
|
+
import_node_fs3.default.mkdirSync(runtimeHome2, { recursive: true });
|
|
352
|
+
runCommand("chown", ["-R", `${runUser}:${runGroup}`, runtimeHome2]);
|
|
353
|
+
const mainPath = import_node_path3.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
|
|
354
|
+
const adminPath = import_node_path3.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
|
|
355
|
+
const unitOptions = {
|
|
356
|
+
runUser,
|
|
357
|
+
runtimeHome: runtimeHome2,
|
|
358
|
+
nodeBinPath: options.nodeBinPath,
|
|
359
|
+
cliScriptPath: options.cliScriptPath
|
|
360
|
+
};
|
|
361
|
+
import_node_fs3.default.writeFileSync(mainPath, buildMainServiceUnit(unitOptions), "utf8");
|
|
362
|
+
if (options.installAdmin) {
|
|
363
|
+
import_node_fs3.default.writeFileSync(adminPath, buildAdminServiceUnit(unitOptions), "utf8");
|
|
364
|
+
}
|
|
365
|
+
runSystemctl(["daemon-reload"]);
|
|
366
|
+
if (options.startNow) {
|
|
367
|
+
runSystemctl(["enable", "--now", MAIN_SERVICE_NAME]);
|
|
368
|
+
if (options.installAdmin) {
|
|
369
|
+
runSystemctl(["enable", "--now", ADMIN_SERVICE_NAME]);
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
runSystemctl(["enable", MAIN_SERVICE_NAME]);
|
|
373
|
+
if (options.installAdmin) {
|
|
374
|
+
runSystemctl(["enable", ADMIN_SERVICE_NAME]);
|
|
267
375
|
}
|
|
268
|
-
this.server = import_node_http.default.createServer((req, res) => {
|
|
269
|
-
void this.handleRequest(req, res);
|
|
270
|
-
});
|
|
271
|
-
await new Promise((resolve, reject) => {
|
|
272
|
-
if (!this.server) {
|
|
273
|
-
reject(new Error("admin server is not initialized"));
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
this.server.once("error", reject);
|
|
277
|
-
this.server.listen(this.port, this.host, () => {
|
|
278
|
-
this.server?.removeListener("error", reject);
|
|
279
|
-
const address = this.server?.address();
|
|
280
|
-
if (!address || typeof address === "string") {
|
|
281
|
-
reject(new Error("failed to resolve admin server address"));
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
this.address = {
|
|
285
|
-
host: address.address,
|
|
286
|
-
port: address.port
|
|
287
|
-
};
|
|
288
|
-
resolve();
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
376
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
377
|
+
output.write(`Installed systemd unit: ${mainPath}
|
|
378
|
+
`);
|
|
379
|
+
if (options.installAdmin) {
|
|
380
|
+
output.write(`Installed systemd unit: ${adminPath}
|
|
381
|
+
`);
|
|
382
|
+
}
|
|
383
|
+
output.write("Done. Check status with: systemctl status codeharbor --no-pager\n");
|
|
384
|
+
}
|
|
385
|
+
function uninstallSystemdServices(options) {
|
|
386
|
+
assertLinuxWithSystemd();
|
|
387
|
+
assertRootPrivileges();
|
|
388
|
+
const output = options.output ?? process.stdout;
|
|
389
|
+
const mainPath = import_node_path3.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
|
|
390
|
+
const adminPath = import_node_path3.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
|
|
391
|
+
stopAndDisableIfPresent(MAIN_SERVICE_NAME);
|
|
392
|
+
if (import_node_fs3.default.existsSync(mainPath)) {
|
|
393
|
+
import_node_fs3.default.unlinkSync(mainPath);
|
|
394
|
+
}
|
|
395
|
+
if (options.removeAdmin) {
|
|
396
|
+
stopAndDisableIfPresent(ADMIN_SERVICE_NAME);
|
|
397
|
+
if (import_node_fs3.default.existsSync(adminPath)) {
|
|
398
|
+
import_node_fs3.default.unlinkSync(adminPath);
|
|
295
399
|
}
|
|
296
|
-
const server = this.server;
|
|
297
|
-
this.server = null;
|
|
298
|
-
this.address = null;
|
|
299
|
-
await new Promise((resolve, reject) => {
|
|
300
|
-
server.close((error) => {
|
|
301
|
-
if (error) {
|
|
302
|
-
reject(error);
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
resolve();
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
400
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
401
|
+
runSystemctl(["daemon-reload"]);
|
|
402
|
+
runSystemctlIgnoreFailure(["reset-failed"]);
|
|
403
|
+
output.write(`Removed systemd unit: ${mainPath}
|
|
404
|
+
`);
|
|
405
|
+
if (options.removeAdmin) {
|
|
406
|
+
output.write(`Removed systemd unit: ${adminPath}
|
|
407
|
+
`);
|
|
408
|
+
}
|
|
409
|
+
output.write("Done.\n");
|
|
410
|
+
}
|
|
411
|
+
function restartSystemdServices(options) {
|
|
412
|
+
assertLinuxWithSystemd();
|
|
413
|
+
assertRootPrivileges();
|
|
414
|
+
const output = options.output ?? process.stdout;
|
|
415
|
+
runSystemctl(["restart", MAIN_SERVICE_NAME]);
|
|
416
|
+
output.write(`Restarted service: ${MAIN_SERVICE_NAME}
|
|
417
|
+
`);
|
|
418
|
+
if (options.restartAdmin) {
|
|
419
|
+
runSystemctl(["restart", ADMIN_SERVICE_NAME]);
|
|
420
|
+
output.write(`Restarted service: ${ADMIN_SERVICE_NAME}
|
|
421
|
+
`);
|
|
422
|
+
}
|
|
423
|
+
output.write("Done.\n");
|
|
424
|
+
}
|
|
425
|
+
function resolveUserHome(runUser) {
|
|
426
|
+
try {
|
|
427
|
+
const passwdRaw = import_node_fs3.default.readFileSync("/etc/passwd", "utf8");
|
|
428
|
+
const line = passwdRaw.split(/\r?\n/).find((item) => item.startsWith(`${runUser}:`));
|
|
429
|
+
if (!line) {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
const fields = line.split(":");
|
|
433
|
+
return fields[5] ? fields[5].trim() : null;
|
|
434
|
+
} catch {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function validateUnitOptions(options) {
|
|
439
|
+
validateSimpleValue(options.runUser, "runUser");
|
|
440
|
+
validateSimpleValue(options.runtimeHome, "runtimeHome");
|
|
441
|
+
validateSimpleValue(options.nodeBinPath, "nodeBinPath");
|
|
442
|
+
validateSimpleValue(options.cliScriptPath, "cliScriptPath");
|
|
443
|
+
}
|
|
444
|
+
function validateSimpleValue(value, key) {
|
|
445
|
+
if (!value.trim()) {
|
|
446
|
+
throw new Error(`${key} cannot be empty.`);
|
|
447
|
+
}
|
|
448
|
+
if (/[\r\n]/.test(value)) {
|
|
449
|
+
throw new Error(`${key} contains invalid newline characters.`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function assertLinuxWithSystemd() {
|
|
453
|
+
if (process.platform !== "linux") {
|
|
454
|
+
throw new Error("Systemd service install only supports Linux.");
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
(0, import_node_child_process.execFileSync)("systemctl", ["--version"], { stdio: "ignore" });
|
|
458
|
+
} catch {
|
|
459
|
+
throw new Error("systemctl is required but not found.");
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function assertRootPrivileges() {
|
|
463
|
+
if (typeof process.getuid !== "function") {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (process.getuid() !== 0) {
|
|
467
|
+
throw new Error("Root privileges are required. Run with sudo.");
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function ensureUserExists(runUser) {
|
|
471
|
+
runCommand("id", ["-u", runUser]);
|
|
472
|
+
}
|
|
473
|
+
function resolveUserGroup(runUser) {
|
|
474
|
+
return runCommand("id", ["-gn", runUser]).trim();
|
|
475
|
+
}
|
|
476
|
+
function runSystemctl(args) {
|
|
477
|
+
runCommand("systemctl", args);
|
|
478
|
+
}
|
|
479
|
+
function stopAndDisableIfPresent(unitName) {
|
|
480
|
+
runSystemctlIgnoreFailure(["disable", "--now", unitName]);
|
|
481
|
+
}
|
|
482
|
+
function runSystemctlIgnoreFailure(args) {
|
|
483
|
+
try {
|
|
484
|
+
runCommand("systemctl", args);
|
|
485
|
+
} catch {
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function runCommand(file, args) {
|
|
489
|
+
try {
|
|
490
|
+
return (0, import_node_child_process.execFileSync)(file, args, {
|
|
491
|
+
encoding: "utf8",
|
|
492
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
493
|
+
});
|
|
494
|
+
} catch (error) {
|
|
495
|
+
throw new Error(formatCommandError(file, args, error), { cause: error });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function formatCommandError(file, args, error) {
|
|
499
|
+
const command = `${file} ${args.join(" ")}`.trim();
|
|
500
|
+
if (error && typeof error === "object") {
|
|
501
|
+
const maybeError = error;
|
|
502
|
+
const stderr = bufferToTrimmedString(maybeError.stderr);
|
|
503
|
+
const stdout = bufferToTrimmedString(maybeError.stdout);
|
|
504
|
+
const details = stderr || stdout || maybeError.message || "command failed";
|
|
505
|
+
return `Command failed: ${command}. ${details}`;
|
|
506
|
+
}
|
|
507
|
+
return `Command failed: ${command}. ${String(error)}`;
|
|
508
|
+
}
|
|
509
|
+
function bufferToTrimmedString(value) {
|
|
510
|
+
if (!value) {
|
|
511
|
+
return "";
|
|
512
|
+
}
|
|
513
|
+
const text = typeof value === "string" ? value : value.toString("utf8");
|
|
514
|
+
return text.trim();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/admin-server.ts
|
|
518
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
|
|
519
|
+
var HttpError = class extends Error {
|
|
520
|
+
statusCode;
|
|
521
|
+
constructor(statusCode, message) {
|
|
522
|
+
super(message);
|
|
523
|
+
this.statusCode = statusCode;
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
var AdminServer = class {
|
|
527
|
+
config;
|
|
528
|
+
logger;
|
|
529
|
+
stateStore;
|
|
530
|
+
configService;
|
|
531
|
+
host;
|
|
532
|
+
port;
|
|
533
|
+
adminToken;
|
|
534
|
+
adminIpAllowlist;
|
|
535
|
+
adminAllowedOrigins;
|
|
536
|
+
cwd;
|
|
537
|
+
checkCodex;
|
|
538
|
+
checkMatrix;
|
|
539
|
+
restartServices;
|
|
540
|
+
server = null;
|
|
541
|
+
address = null;
|
|
542
|
+
constructor(config, logger, stateStore, configService, options) {
|
|
543
|
+
this.config = config;
|
|
544
|
+
this.logger = logger;
|
|
545
|
+
this.stateStore = stateStore;
|
|
546
|
+
this.configService = configService;
|
|
547
|
+
this.host = options.host;
|
|
548
|
+
this.port = options.port;
|
|
549
|
+
this.adminToken = options.adminToken;
|
|
550
|
+
this.adminIpAllowlist = normalizeAllowlist(options.adminIpAllowlist ?? []);
|
|
551
|
+
this.adminAllowedOrigins = normalizeOriginAllowlist(options.adminAllowedOrigins ?? []);
|
|
552
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
553
|
+
this.checkCodex = options.checkCodex ?? defaultCheckCodex;
|
|
554
|
+
this.checkMatrix = options.checkMatrix ?? defaultCheckMatrix;
|
|
555
|
+
this.restartServices = options.restartServices ?? defaultRestartServices;
|
|
556
|
+
}
|
|
557
|
+
getAddress() {
|
|
558
|
+
return this.address;
|
|
559
|
+
}
|
|
560
|
+
async start() {
|
|
561
|
+
if (this.server) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
this.server = import_node_http.default.createServer((req, res) => {
|
|
565
|
+
void this.handleRequest(req, res);
|
|
566
|
+
});
|
|
567
|
+
await new Promise((resolve, reject) => {
|
|
568
|
+
if (!this.server) {
|
|
569
|
+
reject(new Error("admin server is not initialized"));
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
this.server.once("error", reject);
|
|
573
|
+
this.server.listen(this.port, this.host, () => {
|
|
574
|
+
this.server?.removeListener("error", reject);
|
|
575
|
+
const address = this.server?.address();
|
|
576
|
+
if (!address || typeof address === "string") {
|
|
577
|
+
reject(new Error("failed to resolve admin server address"));
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
this.address = {
|
|
581
|
+
host: address.address,
|
|
582
|
+
port: address.port
|
|
583
|
+
};
|
|
584
|
+
resolve();
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
async stop() {
|
|
589
|
+
if (!this.server) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const server = this.server;
|
|
593
|
+
this.server = null;
|
|
594
|
+
this.address = null;
|
|
595
|
+
await new Promise((resolve, reject) => {
|
|
596
|
+
server.close((error) => {
|
|
597
|
+
if (error) {
|
|
598
|
+
reject(error);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
resolve();
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
async handleRequest(req, res) {
|
|
606
|
+
try {
|
|
607
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
608
|
+
this.setSecurityHeaders(res);
|
|
609
|
+
const corsDecision = this.resolveCors(req);
|
|
610
|
+
this.setCorsHeaders(res, corsDecision);
|
|
611
|
+
if (!this.isClientAllowed(req)) {
|
|
612
|
+
this.sendJson(res, 403, {
|
|
613
|
+
ok: false,
|
|
614
|
+
error: "Forbidden by ADMIN_IP_ALLOWLIST."
|
|
615
|
+
});
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (url.pathname.startsWith("/api/admin/") && corsDecision.origin && !corsDecision.allowed) {
|
|
619
|
+
this.sendJson(res, 403, {
|
|
620
|
+
ok: false,
|
|
621
|
+
error: "Forbidden by ADMIN_ALLOWED_ORIGINS."
|
|
622
|
+
});
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (req.method === "OPTIONS") {
|
|
626
|
+
if (corsDecision.origin && !corsDecision.allowed) {
|
|
627
|
+
this.sendJson(res, 403, {
|
|
628
|
+
ok: false,
|
|
629
|
+
error: "Forbidden by ADMIN_ALLOWED_ORIGINS."
|
|
334
630
|
});
|
|
335
631
|
return;
|
|
336
632
|
}
|
|
@@ -420,6 +716,33 @@ var AdminServer = class {
|
|
|
420
716
|
});
|
|
421
717
|
return;
|
|
422
718
|
}
|
|
719
|
+
if (req.method === "POST" && url.pathname === "/api/admin/service/restart") {
|
|
720
|
+
const body = asObject(await readJsonBody(req), "service restart payload");
|
|
721
|
+
const restartAdmin = normalizeBoolean(body.withAdmin, false);
|
|
722
|
+
const actor = readActor(req);
|
|
723
|
+
try {
|
|
724
|
+
const result = await this.restartServices(restartAdmin);
|
|
725
|
+
this.stateStore.appendConfigRevision(
|
|
726
|
+
actor,
|
|
727
|
+
restartAdmin ? "restart services (main + admin)" : "restart service (main)",
|
|
728
|
+
JSON.stringify({
|
|
729
|
+
type: "service_restart",
|
|
730
|
+
restartAdmin,
|
|
731
|
+
restarted: result.restarted
|
|
732
|
+
})
|
|
733
|
+
);
|
|
734
|
+
this.sendJson(res, 200, {
|
|
735
|
+
ok: true,
|
|
736
|
+
restarted: result.restarted
|
|
737
|
+
});
|
|
738
|
+
return;
|
|
739
|
+
} catch (error) {
|
|
740
|
+
throw new HttpError(
|
|
741
|
+
500,
|
|
742
|
+
`Service restart failed: ${formatError(error)}. Ensure service has root privileges or run CLI command manually.`
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
423
746
|
this.sendJson(res, 404, {
|
|
424
747
|
ok: false,
|
|
425
748
|
error: `Not found: ${req.method ?? "GET"} ${url.pathname}`
|
|
@@ -450,7 +773,7 @@ var AdminServer = class {
|
|
|
450
773
|
updatedKeys.push("matrixCommandPrefix");
|
|
451
774
|
}
|
|
452
775
|
if ("codexWorkdir" in body) {
|
|
453
|
-
const workdir =
|
|
776
|
+
const workdir = import_node_path4.default.resolve(String(body.codexWorkdir ?? "").trim());
|
|
454
777
|
ensureDirectory(workdir, "codexWorkdir");
|
|
455
778
|
this.config.codexWorkdir = workdir;
|
|
456
779
|
envUpdates.CODEX_WORKDIR = workdir;
|
|
@@ -674,11 +997,11 @@ var AdminServer = class {
|
|
|
674
997
|
return this.adminIpAllowlist.includes(normalizedRemote);
|
|
675
998
|
}
|
|
676
999
|
persistEnvUpdates(updates) {
|
|
677
|
-
const envPath =
|
|
678
|
-
const examplePath =
|
|
679
|
-
const template =
|
|
1000
|
+
const envPath = import_node_path4.default.resolve(this.cwd, ".env");
|
|
1001
|
+
const examplePath = import_node_path4.default.resolve(this.cwd, ".env.example");
|
|
1002
|
+
const template = import_node_fs4.default.existsSync(envPath) ? import_node_fs4.default.readFileSync(envPath, "utf8") : import_node_fs4.default.existsSync(examplePath) ? import_node_fs4.default.readFileSync(examplePath, "utf8") : "";
|
|
680
1003
|
const next = applyEnvOverrides(template, updates);
|
|
681
|
-
|
|
1004
|
+
import_node_fs4.default.writeFileSync(envPath, next, "utf8");
|
|
682
1005
|
}
|
|
683
1006
|
resolveCors(req) {
|
|
684
1007
|
const origin = normalizeOriginHeader(req.headers.origin);
|
|
@@ -705,7 +1028,7 @@ var AdminServer = class {
|
|
|
705
1028
|
}
|
|
706
1029
|
res.setHeader("Access-Control-Allow-Origin", corsDecision.origin);
|
|
707
1030
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Admin-Token, X-Admin-Actor");
|
|
708
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, PUT, DELETE, OPTIONS");
|
|
1031
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS");
|
|
709
1032
|
appendVaryHeader(res, "Origin");
|
|
710
1033
|
}
|
|
711
1034
|
setSecurityHeaders(res) {
|
|
@@ -776,6 +1099,22 @@ function parseJsonLoose(raw) {
|
|
|
776
1099
|
return raw;
|
|
777
1100
|
}
|
|
778
1101
|
}
|
|
1102
|
+
async function defaultRestartServices(restartAdmin) {
|
|
1103
|
+
const outputChunks = [];
|
|
1104
|
+
const output = {
|
|
1105
|
+
write: (chunk) => {
|
|
1106
|
+
outputChunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
|
|
1107
|
+
return true;
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
restartSystemdServices({
|
|
1111
|
+
restartAdmin,
|
|
1112
|
+
output
|
|
1113
|
+
});
|
|
1114
|
+
return {
|
|
1115
|
+
restarted: restartAdmin ? ["codeharbor", "codeharbor-admin"] : ["codeharbor"]
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
779
1118
|
function isUiPath(pathname) {
|
|
780
1119
|
return pathname === "/" || pathname === "/index.html" || pathname === "/settings/global" || pathname === "/settings/rooms" || pathname === "/health" || pathname === "/audit";
|
|
781
1120
|
}
|
|
@@ -924,7 +1263,7 @@ function normalizeNonNegativeInt(value, fallback) {
|
|
|
924
1263
|
return normalizePositiveInt(value, fallback, 0, Number.MAX_SAFE_INTEGER);
|
|
925
1264
|
}
|
|
926
1265
|
function ensureDirectory(targetPath, fieldName) {
|
|
927
|
-
if (!
|
|
1266
|
+
if (!import_node_fs4.default.existsSync(targetPath) || !import_node_fs4.default.statSync(targetPath).isDirectory()) {
|
|
928
1267
|
throw new HttpError(400, `${fieldName} must be an existing directory: ${targetPath}`);
|
|
929
1268
|
}
|
|
930
1269
|
}
|
|
@@ -1266,6 +1605,8 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1266
1605
|
<div class="actions">
|
|
1267
1606
|
<button id="global-save-btn" type="button">Save Global Config</button>
|
|
1268
1607
|
<button id="global-reload-btn" type="button" class="secondary">Reload</button>
|
|
1608
|
+
<button id="global-restart-main-btn" type="button" class="secondary">Restart Main Service</button>
|
|
1609
|
+
<button id="global-restart-all-btn" type="button" class="secondary">Restart Main + Admin</button>
|
|
1269
1610
|
</div>
|
|
1270
1611
|
<p class="muted">Saving global config updates .env and requires restart to fully take effect.</p>
|
|
1271
1612
|
</section>
|
|
@@ -1408,6 +1749,12 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1408
1749
|
|
|
1409
1750
|
document.getElementById("global-save-btn").addEventListener("click", saveGlobal);
|
|
1410
1751
|
document.getElementById("global-reload-btn").addEventListener("click", loadGlobal);
|
|
1752
|
+
document.getElementById("global-restart-main-btn").addEventListener("click", function () {
|
|
1753
|
+
restartManagedServices(false);
|
|
1754
|
+
});
|
|
1755
|
+
document.getElementById("global-restart-all-btn").addEventListener("click", function () {
|
|
1756
|
+
restartManagedServices(true);
|
|
1757
|
+
});
|
|
1411
1758
|
document.getElementById("room-load-btn").addEventListener("click", loadRoom);
|
|
1412
1759
|
document.getElementById("room-save-btn").addEventListener("click", saveRoom);
|
|
1413
1760
|
document.getElementById("room-delete-btn").addEventListener("click", deleteRoom);
|
|
@@ -1611,6 +1958,19 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1611
1958
|
}
|
|
1612
1959
|
}
|
|
1613
1960
|
|
|
1961
|
+
async function restartManagedServices(withAdmin) {
|
|
1962
|
+
try {
|
|
1963
|
+
var response = await apiRequest("/api/admin/service/restart", "POST", {
|
|
1964
|
+
withAdmin: Boolean(withAdmin)
|
|
1965
|
+
});
|
|
1966
|
+
var restarted = Array.isArray(response.restarted) ? response.restarted.join(", ") : "codeharbor";
|
|
1967
|
+
var suffix = withAdmin ? " Admin page may reconnect during restart." : "";
|
|
1968
|
+
showNotice("warn", "Restart requested: " + restarted + "." + suffix);
|
|
1969
|
+
} catch (error) {
|
|
1970
|
+
showNotice("error", "Failed to restart service(s): " + error.message);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1614
1974
|
async function refreshRoomList() {
|
|
1615
1975
|
try {
|
|
1616
1976
|
var response = await apiRequest("/api/admin/config/rooms", "GET");
|
|
@@ -1836,8 +2196,8 @@ async function defaultCheckMatrix(homeserver, timeoutMs) {
|
|
|
1836
2196
|
|
|
1837
2197
|
// src/channels/matrix-channel.ts
|
|
1838
2198
|
var import_promises2 = __toESM(require("fs/promises"));
|
|
1839
|
-
var
|
|
1840
|
-
var
|
|
2199
|
+
var import_node_os3 = __toESM(require("os"));
|
|
2200
|
+
var import_node_path5 = __toESM(require("path"));
|
|
1841
2201
|
var import_matrix_js_sdk = require("matrix-js-sdk");
|
|
1842
2202
|
|
|
1843
2203
|
// src/utils/message.ts
|
|
@@ -2260,10 +2620,10 @@ var MatrixChannel = class {
|
|
|
2260
2620
|
}
|
|
2261
2621
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
2262
2622
|
const extension = resolveFileExtension(fileName, mimeType);
|
|
2263
|
-
const directory =
|
|
2623
|
+
const directory = import_node_path5.default.join(import_node_os3.default.tmpdir(), "codeharbor-media");
|
|
2264
2624
|
await import_promises2.default.mkdir(directory, { recursive: true });
|
|
2265
2625
|
const safeEventId = sanitizeFilename(eventId);
|
|
2266
|
-
const targetPath =
|
|
2626
|
+
const targetPath = import_node_path5.default.join(directory, `${safeEventId}-${index}${extension}`);
|
|
2267
2627
|
await import_promises2.default.writeFile(targetPath, bytes);
|
|
2268
2628
|
return targetPath;
|
|
2269
2629
|
}
|
|
@@ -2348,7 +2708,7 @@ function sanitizeFilename(value) {
|
|
|
2348
2708
|
return value.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
|
|
2349
2709
|
}
|
|
2350
2710
|
function resolveFileExtension(fileName, mimeType) {
|
|
2351
|
-
const ext =
|
|
2711
|
+
const ext = import_node_path5.default.extname(fileName).trim();
|
|
2352
2712
|
if (ext) {
|
|
2353
2713
|
return ext;
|
|
2354
2714
|
}
|
|
@@ -2365,14 +2725,14 @@ function resolveFileExtension(fileName, mimeType) {
|
|
|
2365
2725
|
}
|
|
2366
2726
|
|
|
2367
2727
|
// src/config-service.ts
|
|
2368
|
-
var
|
|
2369
|
-
var
|
|
2728
|
+
var import_node_fs5 = __toESM(require("fs"));
|
|
2729
|
+
var import_node_path6 = __toESM(require("path"));
|
|
2370
2730
|
var ConfigService = class {
|
|
2371
2731
|
stateStore;
|
|
2372
2732
|
defaultWorkdir;
|
|
2373
2733
|
constructor(stateStore, defaultWorkdir) {
|
|
2374
2734
|
this.stateStore = stateStore;
|
|
2375
|
-
this.defaultWorkdir =
|
|
2735
|
+
this.defaultWorkdir = import_node_path6.default.resolve(defaultWorkdir);
|
|
2376
2736
|
}
|
|
2377
2737
|
resolveRoomConfig(roomId, fallbackPolicy) {
|
|
2378
2738
|
const room = this.stateStore.getRoomSettings(roomId);
|
|
@@ -2443,8 +2803,8 @@ function normalizeRoomSettingsInput(input) {
|
|
|
2443
2803
|
if (!roomId) {
|
|
2444
2804
|
throw new Error("roomId is required.");
|
|
2445
2805
|
}
|
|
2446
|
-
const workdir =
|
|
2447
|
-
if (!
|
|
2806
|
+
const workdir = import_node_path6.default.resolve(input.workdir);
|
|
2807
|
+
if (!import_node_fs5.default.existsSync(workdir) || !import_node_fs5.default.statSync(workdir).isDirectory()) {
|
|
2448
2808
|
throw new Error(`workdir does not exist or is not a directory: ${workdir}`);
|
|
2449
2809
|
}
|
|
2450
2810
|
return {
|
|
@@ -2459,7 +2819,7 @@ function normalizeRoomSettingsInput(input) {
|
|
|
2459
2819
|
}
|
|
2460
2820
|
|
|
2461
2821
|
// src/executor/codex-executor.ts
|
|
2462
|
-
var
|
|
2822
|
+
var import_node_child_process3 = require("child_process");
|
|
2463
2823
|
var import_node_readline = __toESM(require("readline"));
|
|
2464
2824
|
var CodexExecutionCancelledError = class extends Error {
|
|
2465
2825
|
constructor(message = "codex execution cancelled") {
|
|
@@ -2477,7 +2837,7 @@ var CodexExecutor = class {
|
|
|
2477
2837
|
}
|
|
2478
2838
|
startExecution(prompt, sessionId, onProgress, startOptions) {
|
|
2479
2839
|
const args = buildCodexArgs(prompt, sessionId, this.options, startOptions);
|
|
2480
|
-
const child = (0,
|
|
2840
|
+
const child = (0, import_node_child_process3.spawn)(this.options.bin, args, {
|
|
2481
2841
|
cwd: startOptions?.workdir ?? this.options.workdir,
|
|
2482
2842
|
env: {
|
|
2483
2843
|
...process.env,
|
|
@@ -2713,20 +3073,20 @@ var import_async_mutex = require("async-mutex");
|
|
|
2713
3073
|
var import_promises3 = __toESM(require("fs/promises"));
|
|
2714
3074
|
|
|
2715
3075
|
// src/compat/cli-compat-recorder.ts
|
|
2716
|
-
var
|
|
2717
|
-
var
|
|
3076
|
+
var import_node_fs6 = __toESM(require("fs"));
|
|
3077
|
+
var import_node_path7 = __toESM(require("path"));
|
|
2718
3078
|
var CliCompatRecorder = class {
|
|
2719
3079
|
filePath;
|
|
2720
3080
|
chain = Promise.resolve();
|
|
2721
3081
|
constructor(filePath) {
|
|
2722
|
-
this.filePath =
|
|
2723
|
-
|
|
3082
|
+
this.filePath = import_node_path7.default.resolve(filePath);
|
|
3083
|
+
import_node_fs6.default.mkdirSync(import_node_path7.default.dirname(this.filePath), { recursive: true });
|
|
2724
3084
|
}
|
|
2725
3085
|
append(entry) {
|
|
2726
3086
|
const payload = `${JSON.stringify(entry)}
|
|
2727
3087
|
`;
|
|
2728
3088
|
this.chain = this.chain.then(async () => {
|
|
2729
|
-
await
|
|
3089
|
+
await import_node_fs6.default.promises.appendFile(this.filePath, payload, "utf8");
|
|
2730
3090
|
});
|
|
2731
3091
|
return this.chain;
|
|
2732
3092
|
}
|
|
@@ -4114,8 +4474,8 @@ ${result.review}
|
|
|
4114
4474
|
}
|
|
4115
4475
|
|
|
4116
4476
|
// src/store/state-store.ts
|
|
4117
|
-
var
|
|
4118
|
-
var
|
|
4477
|
+
var import_node_fs7 = __toESM(require("fs"));
|
|
4478
|
+
var import_node_path8 = __toESM(require("path"));
|
|
4119
4479
|
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4120
4480
|
var PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
4121
4481
|
var SQLITE_MODULE_ID = `node:${"sqlite"}`;
|
|
@@ -4141,7 +4501,7 @@ var StateStore = class {
|
|
|
4141
4501
|
this.maxProcessedEventsPerSession = maxProcessedEventsPerSession;
|
|
4142
4502
|
this.maxSessionAgeMs = maxSessionAgeDays * ONE_DAY_MS;
|
|
4143
4503
|
this.maxSessions = maxSessions;
|
|
4144
|
-
|
|
4504
|
+
import_node_fs7.default.mkdirSync(import_node_path8.default.dirname(this.dbPath), { recursive: true });
|
|
4145
4505
|
this.db = new DatabaseSync(this.dbPath);
|
|
4146
4506
|
this.initializeSchema();
|
|
4147
4507
|
this.importLegacyStateIfNeeded();
|
|
@@ -4386,7 +4746,7 @@ var StateStore = class {
|
|
|
4386
4746
|
`);
|
|
4387
4747
|
}
|
|
4388
4748
|
importLegacyStateIfNeeded() {
|
|
4389
|
-
if (!this.legacyJsonPath || !
|
|
4749
|
+
if (!this.legacyJsonPath || !import_node_fs7.default.existsSync(this.legacyJsonPath)) {
|
|
4390
4750
|
return;
|
|
4391
4751
|
}
|
|
4392
4752
|
const countRow = this.db.prepare("SELECT COUNT(*) AS count FROM sessions").get();
|
|
@@ -4469,7 +4829,7 @@ var StateStore = class {
|
|
|
4469
4829
|
};
|
|
4470
4830
|
function loadLegacyState(filePath) {
|
|
4471
4831
|
try {
|
|
4472
|
-
const raw =
|
|
4832
|
+
const raw = import_node_fs7.default.readFileSync(filePath, "utf8");
|
|
4473
4833
|
const parsed = JSON.parse(raw);
|
|
4474
4834
|
if (!parsed.sessions || typeof parsed.sessions !== "object") {
|
|
4475
4835
|
return null;
|
|
@@ -4512,7 +4872,7 @@ function boolToInt(value) {
|
|
|
4512
4872
|
}
|
|
4513
4873
|
|
|
4514
4874
|
// src/app.ts
|
|
4515
|
-
var execFileAsync2 = (0, import_node_util2.promisify)(
|
|
4875
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process4.execFile);
|
|
4516
4876
|
var CodeHarborApp = class {
|
|
4517
4877
|
config;
|
|
4518
4878
|
logger;
|
|
@@ -4661,8 +5021,8 @@ function isNonLoopbackHost(host) {
|
|
|
4661
5021
|
}
|
|
4662
5022
|
|
|
4663
5023
|
// src/config.ts
|
|
4664
|
-
var
|
|
4665
|
-
var
|
|
5024
|
+
var import_node_fs8 = __toESM(require("fs"));
|
|
5025
|
+
var import_node_path9 = __toESM(require("path"));
|
|
4666
5026
|
var import_dotenv2 = __toESM(require("dotenv"));
|
|
4667
5027
|
var import_zod = require("zod");
|
|
4668
5028
|
var configSchema = import_zod.z.object({
|
|
@@ -4723,7 +5083,7 @@ var configSchema = import_zod.z.object({
|
|
|
4723
5083
|
matrixCommandPrefix: v.MATRIX_COMMAND_PREFIX,
|
|
4724
5084
|
codexBin: v.CODEX_BIN,
|
|
4725
5085
|
codexModel: v.CODEX_MODEL?.trim() || null,
|
|
4726
|
-
codexWorkdir:
|
|
5086
|
+
codexWorkdir: import_node_path9.default.resolve(v.CODEX_WORKDIR),
|
|
4727
5087
|
codexDangerousBypass: v.CODEX_DANGEROUS_BYPASS,
|
|
4728
5088
|
codexExecTimeoutMs: v.CODEX_EXEC_TIMEOUT_MS,
|
|
4729
5089
|
codexSandboxMode: v.CODEX_SANDBOX_MODE?.trim() || null,
|
|
@@ -4734,8 +5094,8 @@ var configSchema = import_zod.z.object({
|
|
|
4734
5094
|
enabled: v.AGENT_WORKFLOW_ENABLED,
|
|
4735
5095
|
autoRepairMaxRounds: v.AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS
|
|
4736
5096
|
},
|
|
4737
|
-
stateDbPath:
|
|
4738
|
-
legacyStateJsonPath: v.STATE_PATH.trim() ?
|
|
5097
|
+
stateDbPath: import_node_path9.default.resolve(v.STATE_DB_PATH),
|
|
5098
|
+
legacyStateJsonPath: v.STATE_PATH.trim() ? import_node_path9.default.resolve(v.STATE_PATH) : null,
|
|
4739
5099
|
maxProcessedEventsPerSession: v.MAX_PROCESSED_EVENTS_PER_SESSION,
|
|
4740
5100
|
maxSessionAgeDays: v.MAX_SESSION_AGE_DAYS,
|
|
4741
5101
|
maxSessions: v.MAX_SESSIONS,
|
|
@@ -4766,7 +5126,7 @@ var configSchema = import_zod.z.object({
|
|
|
4766
5126
|
disableReplyChunkSplit: v.CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT,
|
|
4767
5127
|
progressThrottleMs: v.CLI_COMPAT_PROGRESS_THROTTLE_MS,
|
|
4768
5128
|
fetchMedia: v.CLI_COMPAT_FETCH_MEDIA,
|
|
4769
|
-
recordPath: v.CLI_COMPAT_RECORD_PATH.trim() ?
|
|
5129
|
+
recordPath: v.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path9.default.resolve(v.CLI_COMPAT_RECORD_PATH) : null
|
|
4770
5130
|
},
|
|
4771
5131
|
doctorHttpTimeoutMs: v.DOCTOR_HTTP_TIMEOUT_MS,
|
|
4772
5132
|
adminBindHost: v.ADMIN_BIND_HOST.trim() || "127.0.0.1",
|
|
@@ -4776,7 +5136,7 @@ var configSchema = import_zod.z.object({
|
|
|
4776
5136
|
adminAllowedOrigins: parseCsvList(v.ADMIN_ALLOWED_ORIGINS),
|
|
4777
5137
|
logLevel: v.LOG_LEVEL
|
|
4778
5138
|
}));
|
|
4779
|
-
function loadEnvFromFile(filePath =
|
|
5139
|
+
function loadEnvFromFile(filePath = import_node_path9.default.resolve(process.cwd(), ".env"), env = process.env) {
|
|
4780
5140
|
import_dotenv2.default.config({
|
|
4781
5141
|
path: filePath,
|
|
4782
5142
|
processEnv: env,
|
|
@@ -4789,9 +5149,9 @@ function loadConfig(env = process.env) {
|
|
|
4789
5149
|
const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "config"}: ${issue.message}`).join("; ");
|
|
4790
5150
|
throw new Error(`Invalid configuration: ${message}`);
|
|
4791
5151
|
}
|
|
4792
|
-
|
|
5152
|
+
import_node_fs8.default.mkdirSync(import_node_path9.default.dirname(parsed.data.stateDbPath), { recursive: true });
|
|
4793
5153
|
if (parsed.data.legacyStateJsonPath) {
|
|
4794
|
-
|
|
5154
|
+
import_node_fs8.default.mkdirSync(import_node_path9.default.dirname(parsed.data.legacyStateJsonPath), { recursive: true });
|
|
4795
5155
|
}
|
|
4796
5156
|
return parsed.data;
|
|
4797
5157
|
}
|
|
@@ -4864,8 +5224,8 @@ function parseCsvList(raw) {
|
|
|
4864
5224
|
}
|
|
4865
5225
|
|
|
4866
5226
|
// src/config-snapshot.ts
|
|
4867
|
-
var
|
|
4868
|
-
var
|
|
5227
|
+
var import_node_fs9 = __toESM(require("fs"));
|
|
5228
|
+
var import_node_path10 = __toESM(require("path"));
|
|
4869
5229
|
var import_zod2 = require("zod");
|
|
4870
5230
|
var CONFIG_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
4871
5231
|
var CONFIG_SNAPSHOT_ENV_KEYS = [
|
|
@@ -5052,9 +5412,9 @@ async function runConfigExportCommand(options = {}) {
|
|
|
5052
5412
|
const snapshot = buildConfigSnapshot(config, stateStore.listRoomSettings(), options.now ?? /* @__PURE__ */ new Date());
|
|
5053
5413
|
const serialized = serializeConfigSnapshot(snapshot);
|
|
5054
5414
|
if (options.outputPath) {
|
|
5055
|
-
const targetPath =
|
|
5056
|
-
|
|
5057
|
-
|
|
5415
|
+
const targetPath = import_node_path10.default.resolve(cwd, options.outputPath);
|
|
5416
|
+
import_node_fs9.default.mkdirSync(import_node_path10.default.dirname(targetPath), { recursive: true });
|
|
5417
|
+
import_node_fs9.default.writeFileSync(targetPath, serialized, "utf8");
|
|
5058
5418
|
output.write(`Exported config snapshot to ${targetPath}
|
|
5059
5419
|
`);
|
|
5060
5420
|
return;
|
|
@@ -5068,8 +5428,8 @@ async function runConfigImportCommand(options) {
|
|
|
5068
5428
|
const cwd = options.cwd ?? process.cwd();
|
|
5069
5429
|
const output = options.output ?? process.stdout;
|
|
5070
5430
|
const actor = options.actor?.trim() || "cli:config-import";
|
|
5071
|
-
const sourcePath =
|
|
5072
|
-
if (!
|
|
5431
|
+
const sourcePath = import_node_path10.default.resolve(cwd, options.filePath);
|
|
5432
|
+
if (!import_node_fs9.default.existsSync(sourcePath)) {
|
|
5073
5433
|
throw new Error(`Config snapshot file not found: ${sourcePath}`);
|
|
5074
5434
|
}
|
|
5075
5435
|
const snapshot = parseConfigSnapshot(parseJsonFile(sourcePath));
|
|
@@ -5099,7 +5459,7 @@ async function runConfigImportCommand(options) {
|
|
|
5099
5459
|
synchronizeRoomSettings(stateStore, normalizedRooms);
|
|
5100
5460
|
stateStore.appendConfigRevision(
|
|
5101
5461
|
actor,
|
|
5102
|
-
`import config snapshot from ${
|
|
5462
|
+
`import config snapshot from ${import_node_path10.default.basename(sourcePath)}`,
|
|
5103
5463
|
JSON.stringify({
|
|
5104
5464
|
type: "config_snapshot_import",
|
|
5105
5465
|
sourcePath,
|
|
@@ -5113,7 +5473,7 @@ async function runConfigImportCommand(options) {
|
|
|
5113
5473
|
output.write(
|
|
5114
5474
|
[
|
|
5115
5475
|
`Imported config snapshot from ${sourcePath}`,
|
|
5116
|
-
`- updated .env in ${
|
|
5476
|
+
`- updated .env in ${import_node_path10.default.resolve(cwd, ".env")}`,
|
|
5117
5477
|
`- synchronized room settings: ${normalizedRooms.length}`,
|
|
5118
5478
|
"- restart required: yes (global env settings are restart-scoped)"
|
|
5119
5479
|
].join("\n") + "\n"
|
|
@@ -5175,7 +5535,7 @@ function buildSnapshotEnv(config) {
|
|
|
5175
5535
|
}
|
|
5176
5536
|
function parseJsonFile(filePath) {
|
|
5177
5537
|
try {
|
|
5178
|
-
const raw =
|
|
5538
|
+
const raw = import_node_fs9.default.readFileSync(filePath, "utf8");
|
|
5179
5539
|
return JSON.parse(raw);
|
|
5180
5540
|
} catch (error) {
|
|
5181
5541
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -5187,10 +5547,10 @@ function parseJsonFile(filePath) {
|
|
|
5187
5547
|
function normalizeSnapshotEnv(env, cwd) {
|
|
5188
5548
|
return {
|
|
5189
5549
|
...env,
|
|
5190
|
-
CODEX_WORKDIR:
|
|
5191
|
-
STATE_DB_PATH:
|
|
5192
|
-
STATE_PATH: env.STATE_PATH.trim() ?
|
|
5193
|
-
CLI_COMPAT_RECORD_PATH: env.CLI_COMPAT_RECORD_PATH.trim() ?
|
|
5550
|
+
CODEX_WORKDIR: import_node_path10.default.resolve(cwd, env.CODEX_WORKDIR),
|
|
5551
|
+
STATE_DB_PATH: import_node_path10.default.resolve(cwd, env.STATE_DB_PATH),
|
|
5552
|
+
STATE_PATH: env.STATE_PATH.trim() ? import_node_path10.default.resolve(cwd, env.STATE_PATH) : "",
|
|
5553
|
+
CLI_COMPAT_RECORD_PATH: env.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path10.default.resolve(cwd, env.CLI_COMPAT_RECORD_PATH) : ""
|
|
5194
5554
|
};
|
|
5195
5555
|
}
|
|
5196
5556
|
function normalizeSnapshotRooms(rooms, cwd) {
|
|
@@ -5205,7 +5565,7 @@ function normalizeSnapshotRooms(rooms, cwd) {
|
|
|
5205
5565
|
throw new Error(`Duplicate roomId in snapshot: ${roomId}`);
|
|
5206
5566
|
}
|
|
5207
5567
|
seen.add(roomId);
|
|
5208
|
-
const workdir =
|
|
5568
|
+
const workdir = import_node_path10.default.resolve(cwd, room.workdir);
|
|
5209
5569
|
ensureDirectory2(workdir, `room workdir (${roomId})`);
|
|
5210
5570
|
normalized.push({
|
|
5211
5571
|
roomId,
|
|
@@ -5232,18 +5592,18 @@ function synchronizeRoomSettings(stateStore, rooms) {
|
|
|
5232
5592
|
}
|
|
5233
5593
|
}
|
|
5234
5594
|
function persistEnvSnapshot(cwd, env) {
|
|
5235
|
-
const envPath =
|
|
5236
|
-
const examplePath =
|
|
5237
|
-
const template =
|
|
5595
|
+
const envPath = import_node_path10.default.resolve(cwd, ".env");
|
|
5596
|
+
const examplePath = import_node_path10.default.resolve(cwd, ".env.example");
|
|
5597
|
+
const template = import_node_fs9.default.existsSync(envPath) ? import_node_fs9.default.readFileSync(envPath, "utf8") : import_node_fs9.default.existsSync(examplePath) ? import_node_fs9.default.readFileSync(examplePath, "utf8") : "";
|
|
5238
5598
|
const overrides = {};
|
|
5239
5599
|
for (const key of CONFIG_SNAPSHOT_ENV_KEYS) {
|
|
5240
5600
|
overrides[key] = env[key];
|
|
5241
5601
|
}
|
|
5242
5602
|
const next = applyEnvOverrides(template, overrides);
|
|
5243
|
-
|
|
5603
|
+
import_node_fs9.default.writeFileSync(envPath, next, "utf8");
|
|
5244
5604
|
}
|
|
5245
5605
|
function ensureDirectory2(dirPath, label) {
|
|
5246
|
-
if (!
|
|
5606
|
+
if (!import_node_fs9.default.existsSync(dirPath) || !import_node_fs9.default.statSync(dirPath).isDirectory()) {
|
|
5247
5607
|
throw new Error(`${label} does not exist or is not a directory: ${dirPath}`);
|
|
5248
5608
|
}
|
|
5249
5609
|
}
|
|
@@ -5296,20 +5656,20 @@ function jsonObjectStringSchema(key, allowEmpty) {
|
|
|
5296
5656
|
}
|
|
5297
5657
|
|
|
5298
5658
|
// src/preflight.ts
|
|
5299
|
-
var
|
|
5300
|
-
var
|
|
5301
|
-
var
|
|
5659
|
+
var import_node_child_process5 = require("child_process");
|
|
5660
|
+
var import_node_fs10 = __toESM(require("fs"));
|
|
5661
|
+
var import_node_path11 = __toESM(require("path"));
|
|
5302
5662
|
var import_node_util3 = require("util");
|
|
5303
|
-
var execFileAsync3 = (0, import_node_util3.promisify)(
|
|
5663
|
+
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process5.execFile);
|
|
5304
5664
|
var REQUIRED_ENV_KEYS = ["MATRIX_HOMESERVER", "MATRIX_USER_ID", "MATRIX_ACCESS_TOKEN"];
|
|
5305
5665
|
async function runStartupPreflight(options = {}) {
|
|
5306
5666
|
const env = options.env ?? process.env;
|
|
5307
5667
|
const cwd = options.cwd ?? process.cwd();
|
|
5308
5668
|
const checkCodexBinary = options.checkCodexBinary ?? defaultCheckCodexBinary;
|
|
5309
|
-
const fileExists = options.fileExists ??
|
|
5669
|
+
const fileExists = options.fileExists ?? import_node_fs10.default.existsSync;
|
|
5310
5670
|
const isDirectory = options.isDirectory ?? defaultIsDirectory;
|
|
5311
5671
|
const issues = [];
|
|
5312
|
-
const envPath =
|
|
5672
|
+
const envPath = import_node_path11.default.resolve(cwd, ".env");
|
|
5313
5673
|
if (!fileExists(envPath)) {
|
|
5314
5674
|
issues.push({
|
|
5315
5675
|
level: "warn",
|
|
@@ -5368,7 +5728,7 @@ async function runStartupPreflight(options = {}) {
|
|
|
5368
5728
|
});
|
|
5369
5729
|
}
|
|
5370
5730
|
const configuredWorkdir = readEnv(env, "CODEX_WORKDIR");
|
|
5371
|
-
const workdir =
|
|
5731
|
+
const workdir = import_node_path11.default.resolve(cwd, configuredWorkdir || cwd);
|
|
5372
5732
|
if (!fileExists(workdir) || !isDirectory(workdir)) {
|
|
5373
5733
|
issues.push({
|
|
5374
5734
|
level: "error",
|
|
@@ -5407,7 +5767,7 @@ async function defaultCheckCodexBinary(bin) {
|
|
|
5407
5767
|
}
|
|
5408
5768
|
function defaultIsDirectory(targetPath) {
|
|
5409
5769
|
try {
|
|
5410
|
-
return
|
|
5770
|
+
return import_node_fs10.default.statSync(targetPath).isDirectory();
|
|
5411
5771
|
} catch {
|
|
5412
5772
|
return false;
|
|
5413
5773
|
}
|
|
@@ -5416,298 +5776,6 @@ function readEnv(env, key) {
|
|
|
5416
5776
|
return env[key]?.trim() ?? "";
|
|
5417
5777
|
}
|
|
5418
5778
|
|
|
5419
|
-
// src/runtime-home.ts
|
|
5420
|
-
var import_node_fs9 = __toESM(require("fs"));
|
|
5421
|
-
var import_node_os2 = __toESM(require("os"));
|
|
5422
|
-
var import_node_path10 = __toESM(require("path"));
|
|
5423
|
-
var LEGACY_RUNTIME_HOME = "/opt/codeharbor";
|
|
5424
|
-
var USER_RUNTIME_HOME_DIR = ".codeharbor";
|
|
5425
|
-
var DEFAULT_RUNTIME_HOME = import_node_path10.default.resolve(import_node_os2.default.homedir(), USER_RUNTIME_HOME_DIR);
|
|
5426
|
-
var RUNTIME_HOME_ENV_KEY = "CODEHARBOR_HOME";
|
|
5427
|
-
function resolveRuntimeHome(env = process.env) {
|
|
5428
|
-
const configured = env[RUNTIME_HOME_ENV_KEY]?.trim();
|
|
5429
|
-
if (configured) {
|
|
5430
|
-
return import_node_path10.default.resolve(configured);
|
|
5431
|
-
}
|
|
5432
|
-
const legacyEnvPath = import_node_path10.default.resolve(LEGACY_RUNTIME_HOME, ".env");
|
|
5433
|
-
if (import_node_fs9.default.existsSync(legacyEnvPath)) {
|
|
5434
|
-
return LEGACY_RUNTIME_HOME;
|
|
5435
|
-
}
|
|
5436
|
-
return resolveUserRuntimeHome(env);
|
|
5437
|
-
}
|
|
5438
|
-
function resolveUserRuntimeHome(env = process.env) {
|
|
5439
|
-
const home = env.HOME?.trim() || import_node_os2.default.homedir();
|
|
5440
|
-
return import_node_path10.default.resolve(home, USER_RUNTIME_HOME_DIR);
|
|
5441
|
-
}
|
|
5442
|
-
|
|
5443
|
-
// src/service-manager.ts
|
|
5444
|
-
var import_node_child_process5 = require("child_process");
|
|
5445
|
-
var import_node_fs10 = __toESM(require("fs"));
|
|
5446
|
-
var import_node_os3 = __toESM(require("os"));
|
|
5447
|
-
var import_node_path11 = __toESM(require("path"));
|
|
5448
|
-
var SYSTEMD_DIR = "/etc/systemd/system";
|
|
5449
|
-
var MAIN_SERVICE_NAME = "codeharbor.service";
|
|
5450
|
-
var ADMIN_SERVICE_NAME = "codeharbor-admin.service";
|
|
5451
|
-
function resolveDefaultRunUser(env = process.env) {
|
|
5452
|
-
const sudoUser = env.SUDO_USER?.trim();
|
|
5453
|
-
if (sudoUser) {
|
|
5454
|
-
return sudoUser;
|
|
5455
|
-
}
|
|
5456
|
-
const user = env.USER?.trim();
|
|
5457
|
-
if (user) {
|
|
5458
|
-
return user;
|
|
5459
|
-
}
|
|
5460
|
-
try {
|
|
5461
|
-
return import_node_os3.default.userInfo().username;
|
|
5462
|
-
} catch {
|
|
5463
|
-
return "root";
|
|
5464
|
-
}
|
|
5465
|
-
}
|
|
5466
|
-
function resolveRuntimeHomeForUser(runUser, env = process.env, explicitRuntimeHome) {
|
|
5467
|
-
const configuredRuntimeHome = explicitRuntimeHome?.trim() || env[RUNTIME_HOME_ENV_KEY]?.trim();
|
|
5468
|
-
if (configuredRuntimeHome) {
|
|
5469
|
-
return import_node_path11.default.resolve(configuredRuntimeHome);
|
|
5470
|
-
}
|
|
5471
|
-
const userHome = resolveUserHome(runUser);
|
|
5472
|
-
if (userHome) {
|
|
5473
|
-
return import_node_path11.default.resolve(userHome, USER_RUNTIME_HOME_DIR);
|
|
5474
|
-
}
|
|
5475
|
-
return import_node_path11.default.resolve(import_node_os3.default.homedir(), USER_RUNTIME_HOME_DIR);
|
|
5476
|
-
}
|
|
5477
|
-
function buildMainServiceUnit(options) {
|
|
5478
|
-
validateUnitOptions(options);
|
|
5479
|
-
const runtimeHome2 = import_node_path11.default.resolve(options.runtimeHome);
|
|
5480
|
-
return [
|
|
5481
|
-
"[Unit]",
|
|
5482
|
-
"Description=CodeHarbor main service",
|
|
5483
|
-
"After=network-online.target",
|
|
5484
|
-
"Wants=network-online.target",
|
|
5485
|
-
"",
|
|
5486
|
-
"[Service]",
|
|
5487
|
-
"Type=simple",
|
|
5488
|
-
`User=${options.runUser}`,
|
|
5489
|
-
`WorkingDirectory=${runtimeHome2}`,
|
|
5490
|
-
`Environment=CODEHARBOR_HOME=${runtimeHome2}`,
|
|
5491
|
-
`ExecStart=${import_node_path11.default.resolve(options.nodeBinPath)} ${import_node_path11.default.resolve(options.cliScriptPath)} start`,
|
|
5492
|
-
"Restart=always",
|
|
5493
|
-
"RestartSec=3",
|
|
5494
|
-
"NoNewPrivileges=true",
|
|
5495
|
-
"PrivateTmp=true",
|
|
5496
|
-
"ProtectSystem=full",
|
|
5497
|
-
"ProtectHome=false",
|
|
5498
|
-
`ReadWritePaths=${runtimeHome2}`,
|
|
5499
|
-
"",
|
|
5500
|
-
"[Install]",
|
|
5501
|
-
"WantedBy=multi-user.target",
|
|
5502
|
-
""
|
|
5503
|
-
].join("\n");
|
|
5504
|
-
}
|
|
5505
|
-
function buildAdminServiceUnit(options) {
|
|
5506
|
-
validateUnitOptions(options);
|
|
5507
|
-
const runtimeHome2 = import_node_path11.default.resolve(options.runtimeHome);
|
|
5508
|
-
return [
|
|
5509
|
-
"[Unit]",
|
|
5510
|
-
"Description=CodeHarbor admin service",
|
|
5511
|
-
"After=network-online.target",
|
|
5512
|
-
"Wants=network-online.target",
|
|
5513
|
-
"",
|
|
5514
|
-
"[Service]",
|
|
5515
|
-
"Type=simple",
|
|
5516
|
-
`User=${options.runUser}`,
|
|
5517
|
-
`WorkingDirectory=${runtimeHome2}`,
|
|
5518
|
-
`Environment=CODEHARBOR_HOME=${runtimeHome2}`,
|
|
5519
|
-
`ExecStart=${import_node_path11.default.resolve(options.nodeBinPath)} ${import_node_path11.default.resolve(options.cliScriptPath)} admin serve`,
|
|
5520
|
-
"Restart=always",
|
|
5521
|
-
"RestartSec=3",
|
|
5522
|
-
"NoNewPrivileges=true",
|
|
5523
|
-
"PrivateTmp=true",
|
|
5524
|
-
"ProtectSystem=full",
|
|
5525
|
-
"ProtectHome=false",
|
|
5526
|
-
`ReadWritePaths=${runtimeHome2}`,
|
|
5527
|
-
"",
|
|
5528
|
-
"[Install]",
|
|
5529
|
-
"WantedBy=multi-user.target",
|
|
5530
|
-
""
|
|
5531
|
-
].join("\n");
|
|
5532
|
-
}
|
|
5533
|
-
function installSystemdServices(options) {
|
|
5534
|
-
assertLinuxWithSystemd();
|
|
5535
|
-
assertRootPrivileges();
|
|
5536
|
-
const output = options.output ?? process.stdout;
|
|
5537
|
-
const runUser = options.runUser.trim();
|
|
5538
|
-
const runtimeHome2 = import_node_path11.default.resolve(options.runtimeHome);
|
|
5539
|
-
validateSimpleValue(runUser, "runUser");
|
|
5540
|
-
validateSimpleValue(runtimeHome2, "runtimeHome");
|
|
5541
|
-
validateSimpleValue(options.nodeBinPath, "nodeBinPath");
|
|
5542
|
-
validateSimpleValue(options.cliScriptPath, "cliScriptPath");
|
|
5543
|
-
ensureUserExists(runUser);
|
|
5544
|
-
const runGroup = resolveUserGroup(runUser);
|
|
5545
|
-
import_node_fs10.default.mkdirSync(runtimeHome2, { recursive: true });
|
|
5546
|
-
runCommand("chown", ["-R", `${runUser}:${runGroup}`, runtimeHome2]);
|
|
5547
|
-
const mainPath = import_node_path11.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
|
|
5548
|
-
const adminPath = import_node_path11.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
|
|
5549
|
-
const unitOptions = {
|
|
5550
|
-
runUser,
|
|
5551
|
-
runtimeHome: runtimeHome2,
|
|
5552
|
-
nodeBinPath: options.nodeBinPath,
|
|
5553
|
-
cliScriptPath: options.cliScriptPath
|
|
5554
|
-
};
|
|
5555
|
-
import_node_fs10.default.writeFileSync(mainPath, buildMainServiceUnit(unitOptions), "utf8");
|
|
5556
|
-
if (options.installAdmin) {
|
|
5557
|
-
import_node_fs10.default.writeFileSync(adminPath, buildAdminServiceUnit(unitOptions), "utf8");
|
|
5558
|
-
}
|
|
5559
|
-
runSystemctl(["daemon-reload"]);
|
|
5560
|
-
if (options.startNow) {
|
|
5561
|
-
runSystemctl(["enable", "--now", MAIN_SERVICE_NAME]);
|
|
5562
|
-
if (options.installAdmin) {
|
|
5563
|
-
runSystemctl(["enable", "--now", ADMIN_SERVICE_NAME]);
|
|
5564
|
-
}
|
|
5565
|
-
} else {
|
|
5566
|
-
runSystemctl(["enable", MAIN_SERVICE_NAME]);
|
|
5567
|
-
if (options.installAdmin) {
|
|
5568
|
-
runSystemctl(["enable", ADMIN_SERVICE_NAME]);
|
|
5569
|
-
}
|
|
5570
|
-
}
|
|
5571
|
-
output.write(`Installed systemd unit: ${mainPath}
|
|
5572
|
-
`);
|
|
5573
|
-
if (options.installAdmin) {
|
|
5574
|
-
output.write(`Installed systemd unit: ${adminPath}
|
|
5575
|
-
`);
|
|
5576
|
-
}
|
|
5577
|
-
output.write("Done. Check status with: systemctl status codeharbor --no-pager\n");
|
|
5578
|
-
}
|
|
5579
|
-
function uninstallSystemdServices(options) {
|
|
5580
|
-
assertLinuxWithSystemd();
|
|
5581
|
-
assertRootPrivileges();
|
|
5582
|
-
const output = options.output ?? process.stdout;
|
|
5583
|
-
const mainPath = import_node_path11.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
|
|
5584
|
-
const adminPath = import_node_path11.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
|
|
5585
|
-
stopAndDisableIfPresent(MAIN_SERVICE_NAME);
|
|
5586
|
-
if (import_node_fs10.default.existsSync(mainPath)) {
|
|
5587
|
-
import_node_fs10.default.unlinkSync(mainPath);
|
|
5588
|
-
}
|
|
5589
|
-
if (options.removeAdmin) {
|
|
5590
|
-
stopAndDisableIfPresent(ADMIN_SERVICE_NAME);
|
|
5591
|
-
if (import_node_fs10.default.existsSync(adminPath)) {
|
|
5592
|
-
import_node_fs10.default.unlinkSync(adminPath);
|
|
5593
|
-
}
|
|
5594
|
-
}
|
|
5595
|
-
runSystemctl(["daemon-reload"]);
|
|
5596
|
-
runSystemctlIgnoreFailure(["reset-failed"]);
|
|
5597
|
-
output.write(`Removed systemd unit: ${mainPath}
|
|
5598
|
-
`);
|
|
5599
|
-
if (options.removeAdmin) {
|
|
5600
|
-
output.write(`Removed systemd unit: ${adminPath}
|
|
5601
|
-
`);
|
|
5602
|
-
}
|
|
5603
|
-
output.write("Done.\n");
|
|
5604
|
-
}
|
|
5605
|
-
function restartSystemdServices(options) {
|
|
5606
|
-
assertLinuxWithSystemd();
|
|
5607
|
-
assertRootPrivileges();
|
|
5608
|
-
const output = options.output ?? process.stdout;
|
|
5609
|
-
runSystemctl(["restart", MAIN_SERVICE_NAME]);
|
|
5610
|
-
output.write(`Restarted service: ${MAIN_SERVICE_NAME}
|
|
5611
|
-
`);
|
|
5612
|
-
if (options.restartAdmin) {
|
|
5613
|
-
runSystemctl(["restart", ADMIN_SERVICE_NAME]);
|
|
5614
|
-
output.write(`Restarted service: ${ADMIN_SERVICE_NAME}
|
|
5615
|
-
`);
|
|
5616
|
-
}
|
|
5617
|
-
output.write("Done.\n");
|
|
5618
|
-
}
|
|
5619
|
-
function resolveUserHome(runUser) {
|
|
5620
|
-
try {
|
|
5621
|
-
const passwdRaw = import_node_fs10.default.readFileSync("/etc/passwd", "utf8");
|
|
5622
|
-
const line = passwdRaw.split(/\r?\n/).find((item) => item.startsWith(`${runUser}:`));
|
|
5623
|
-
if (!line) {
|
|
5624
|
-
return null;
|
|
5625
|
-
}
|
|
5626
|
-
const fields = line.split(":");
|
|
5627
|
-
return fields[5] ? fields[5].trim() : null;
|
|
5628
|
-
} catch {
|
|
5629
|
-
return null;
|
|
5630
|
-
}
|
|
5631
|
-
}
|
|
5632
|
-
function validateUnitOptions(options) {
|
|
5633
|
-
validateSimpleValue(options.runUser, "runUser");
|
|
5634
|
-
validateSimpleValue(options.runtimeHome, "runtimeHome");
|
|
5635
|
-
validateSimpleValue(options.nodeBinPath, "nodeBinPath");
|
|
5636
|
-
validateSimpleValue(options.cliScriptPath, "cliScriptPath");
|
|
5637
|
-
}
|
|
5638
|
-
function validateSimpleValue(value, key) {
|
|
5639
|
-
if (!value.trim()) {
|
|
5640
|
-
throw new Error(`${key} cannot be empty.`);
|
|
5641
|
-
}
|
|
5642
|
-
if (/[\r\n]/.test(value)) {
|
|
5643
|
-
throw new Error(`${key} contains invalid newline characters.`);
|
|
5644
|
-
}
|
|
5645
|
-
}
|
|
5646
|
-
function assertLinuxWithSystemd() {
|
|
5647
|
-
if (process.platform !== "linux") {
|
|
5648
|
-
throw new Error("Systemd service install only supports Linux.");
|
|
5649
|
-
}
|
|
5650
|
-
try {
|
|
5651
|
-
(0, import_node_child_process5.execFileSync)("systemctl", ["--version"], { stdio: "ignore" });
|
|
5652
|
-
} catch {
|
|
5653
|
-
throw new Error("systemctl is required but not found.");
|
|
5654
|
-
}
|
|
5655
|
-
}
|
|
5656
|
-
function assertRootPrivileges() {
|
|
5657
|
-
if (typeof process.getuid !== "function") {
|
|
5658
|
-
return;
|
|
5659
|
-
}
|
|
5660
|
-
if (process.getuid() !== 0) {
|
|
5661
|
-
throw new Error("Root privileges are required. Run with sudo.");
|
|
5662
|
-
}
|
|
5663
|
-
}
|
|
5664
|
-
function ensureUserExists(runUser) {
|
|
5665
|
-
runCommand("id", ["-u", runUser]);
|
|
5666
|
-
}
|
|
5667
|
-
function resolveUserGroup(runUser) {
|
|
5668
|
-
return runCommand("id", ["-gn", runUser]).trim();
|
|
5669
|
-
}
|
|
5670
|
-
function runSystemctl(args) {
|
|
5671
|
-
runCommand("systemctl", args);
|
|
5672
|
-
}
|
|
5673
|
-
function stopAndDisableIfPresent(unitName) {
|
|
5674
|
-
runSystemctlIgnoreFailure(["disable", "--now", unitName]);
|
|
5675
|
-
}
|
|
5676
|
-
function runSystemctlIgnoreFailure(args) {
|
|
5677
|
-
try {
|
|
5678
|
-
runCommand("systemctl", args);
|
|
5679
|
-
} catch {
|
|
5680
|
-
}
|
|
5681
|
-
}
|
|
5682
|
-
function runCommand(file, args) {
|
|
5683
|
-
try {
|
|
5684
|
-
return (0, import_node_child_process5.execFileSync)(file, args, {
|
|
5685
|
-
encoding: "utf8",
|
|
5686
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5687
|
-
});
|
|
5688
|
-
} catch (error) {
|
|
5689
|
-
throw new Error(formatCommandError(file, args, error), { cause: error });
|
|
5690
|
-
}
|
|
5691
|
-
}
|
|
5692
|
-
function formatCommandError(file, args, error) {
|
|
5693
|
-
const command = `${file} ${args.join(" ")}`.trim();
|
|
5694
|
-
if (error && typeof error === "object") {
|
|
5695
|
-
const maybeError = error;
|
|
5696
|
-
const stderr = bufferToTrimmedString(maybeError.stderr);
|
|
5697
|
-
const stdout = bufferToTrimmedString(maybeError.stdout);
|
|
5698
|
-
const details = stderr || stdout || maybeError.message || "command failed";
|
|
5699
|
-
return `Command failed: ${command}. ${details}`;
|
|
5700
|
-
}
|
|
5701
|
-
return `Command failed: ${command}. ${String(error)}`;
|
|
5702
|
-
}
|
|
5703
|
-
function bufferToTrimmedString(value) {
|
|
5704
|
-
if (!value) {
|
|
5705
|
-
return "";
|
|
5706
|
-
}
|
|
5707
|
-
const text = typeof value === "string" ? value : value.toString("utf8");
|
|
5708
|
-
return text.trim();
|
|
5709
|
-
}
|
|
5710
|
-
|
|
5711
5779
|
// src/cli.ts
|
|
5712
5780
|
var runtimeHome = null;
|
|
5713
5781
|
var cliVersion = resolveCliVersion();
|