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.
Files changed (3) hide show
  1. package/README.md +4 -3
  2. package/dist/cli.js +535 -467
  3. 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. Open `/settings/rooms`, fill `Room ID + Workdir`, then `Save Room`.
357
- 5. Open `/health` to run connectivity checks (`codex` + Matrix).
358
- 6. Open `/audit` to verify config revisions (actor/summary/payload).
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 import_node_child_process3 = require("child_process");
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 import_node_child_process = require("child_process");
38
- var import_node_fs2 = __toESM(require("fs"));
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 import_node_path2 = __toESM(require("path"));
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/admin-server.ts
224
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
225
- var HttpError = class extends Error {
226
- statusCode;
227
- constructor(statusCode, message) {
228
- super(message);
229
- this.statusCode = statusCode;
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
- var AdminServer = class {
233
- config;
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
- getAddress() {
262
- return this.address;
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
- async start() {
265
- if (this.server) {
266
- return;
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
- async stop() {
293
- if (!this.server) {
294
- return;
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
- async handleRequest(req, res) {
310
- try {
311
- const url = new URL(req.url ?? "/", "http://localhost");
312
- this.setSecurityHeaders(res);
313
- const corsDecision = this.resolveCors(req);
314
- this.setCorsHeaders(res, corsDecision);
315
- if (!this.isClientAllowed(req)) {
316
- this.sendJson(res, 403, {
317
- ok: false,
318
- error: "Forbidden by ADMIN_IP_ALLOWLIST."
319
- });
320
- return;
321
- }
322
- if (url.pathname.startsWith("/api/admin/") && corsDecision.origin && !corsDecision.allowed) {
323
- this.sendJson(res, 403, {
324
- ok: false,
325
- error: "Forbidden by ADMIN_ALLOWED_ORIGINS."
326
- });
327
- return;
328
- }
329
- if (req.method === "OPTIONS") {
330
- if (corsDecision.origin && !corsDecision.allowed) {
331
- this.sendJson(res, 403, {
332
- ok: false,
333
- error: "Forbidden by ADMIN_ALLOWED_ORIGINS."
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 = import_node_path2.default.resolve(String(body.codexWorkdir ?? "").trim());
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 = import_node_path2.default.resolve(this.cwd, ".env");
678
- const examplePath = import_node_path2.default.resolve(this.cwd, ".env.example");
679
- const template = import_node_fs2.default.existsSync(envPath) ? import_node_fs2.default.readFileSync(envPath, "utf8") : import_node_fs2.default.existsSync(examplePath) ? import_node_fs2.default.readFileSync(examplePath, "utf8") : "";
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
- import_node_fs2.default.writeFileSync(envPath, next, "utf8");
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 (!import_node_fs2.default.existsSync(targetPath) || !import_node_fs2.default.statSync(targetPath).isDirectory()) {
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 import_node_os = __toESM(require("os"));
1840
- var import_node_path3 = __toESM(require("path"));
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 = import_node_path3.default.join(import_node_os.default.tmpdir(), "codeharbor-media");
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 = import_node_path3.default.join(directory, `${safeEventId}-${index}${extension}`);
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 = import_node_path3.default.extname(fileName).trim();
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 import_node_fs3 = __toESM(require("fs"));
2369
- var import_node_path4 = __toESM(require("path"));
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 = import_node_path4.default.resolve(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 = import_node_path4.default.resolve(input.workdir);
2447
- if (!import_node_fs3.default.existsSync(workdir) || !import_node_fs3.default.statSync(workdir).isDirectory()) {
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 import_node_child_process2 = require("child_process");
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, import_node_child_process2.spawn)(this.options.bin, args, {
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 import_node_fs4 = __toESM(require("fs"));
2717
- var import_node_path5 = __toESM(require("path"));
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 = import_node_path5.default.resolve(filePath);
2723
- import_node_fs4.default.mkdirSync(import_node_path5.default.dirname(this.filePath), { recursive: true });
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 import_node_fs4.default.promises.appendFile(this.filePath, payload, "utf8");
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 import_node_fs5 = __toESM(require("fs"));
4118
- var import_node_path6 = __toESM(require("path"));
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
- import_node_fs5.default.mkdirSync(import_node_path6.default.dirname(this.dbPath), { recursive: true });
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 || !import_node_fs5.default.existsSync(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 = import_node_fs5.default.readFileSync(filePath, "utf8");
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)(import_node_child_process3.execFile);
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 import_node_fs6 = __toESM(require("fs"));
4665
- var import_node_path7 = __toESM(require("path"));
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: import_node_path7.default.resolve(v.CODEX_WORKDIR),
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: import_node_path7.default.resolve(v.STATE_DB_PATH),
4738
- legacyStateJsonPath: v.STATE_PATH.trim() ? import_node_path7.default.resolve(v.STATE_PATH) : null,
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() ? import_node_path7.default.resolve(v.CLI_COMPAT_RECORD_PATH) : null
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 = import_node_path7.default.resolve(process.cwd(), ".env"), env = process.env) {
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
- import_node_fs6.default.mkdirSync(import_node_path7.default.dirname(parsed.data.stateDbPath), { recursive: true });
5152
+ import_node_fs8.default.mkdirSync(import_node_path9.default.dirname(parsed.data.stateDbPath), { recursive: true });
4793
5153
  if (parsed.data.legacyStateJsonPath) {
4794
- import_node_fs6.default.mkdirSync(import_node_path7.default.dirname(parsed.data.legacyStateJsonPath), { recursive: true });
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 import_node_fs7 = __toESM(require("fs"));
4868
- var import_node_path8 = __toESM(require("path"));
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 = import_node_path8.default.resolve(cwd, options.outputPath);
5056
- import_node_fs7.default.mkdirSync(import_node_path8.default.dirname(targetPath), { recursive: true });
5057
- import_node_fs7.default.writeFileSync(targetPath, serialized, "utf8");
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 = import_node_path8.default.resolve(cwd, options.filePath);
5072
- if (!import_node_fs7.default.existsSync(sourcePath)) {
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 ${import_node_path8.default.basename(sourcePath)}`,
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 ${import_node_path8.default.resolve(cwd, ".env")}`,
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 = import_node_fs7.default.readFileSync(filePath, "utf8");
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: import_node_path8.default.resolve(cwd, env.CODEX_WORKDIR),
5191
- STATE_DB_PATH: import_node_path8.default.resolve(cwd, env.STATE_DB_PATH),
5192
- STATE_PATH: env.STATE_PATH.trim() ? import_node_path8.default.resolve(cwd, env.STATE_PATH) : "",
5193
- CLI_COMPAT_RECORD_PATH: env.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path8.default.resolve(cwd, env.CLI_COMPAT_RECORD_PATH) : ""
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 = import_node_path8.default.resolve(cwd, room.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 = import_node_path8.default.resolve(cwd, ".env");
5236
- const examplePath = import_node_path8.default.resolve(cwd, ".env.example");
5237
- const template = import_node_fs7.default.existsSync(envPath) ? import_node_fs7.default.readFileSync(envPath, "utf8") : import_node_fs7.default.existsSync(examplePath) ? import_node_fs7.default.readFileSync(examplePath, "utf8") : "";
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
- import_node_fs7.default.writeFileSync(envPath, next, "utf8");
5603
+ import_node_fs9.default.writeFileSync(envPath, next, "utf8");
5244
5604
  }
5245
5605
  function ensureDirectory2(dirPath, label) {
5246
- if (!import_node_fs7.default.existsSync(dirPath) || !import_node_fs7.default.statSync(dirPath).isDirectory()) {
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 import_node_child_process4 = require("child_process");
5300
- var import_node_fs8 = __toESM(require("fs"));
5301
- var import_node_path9 = __toESM(require("path"));
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)(import_node_child_process4.execFile);
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 ?? import_node_fs8.default.existsSync;
5669
+ const fileExists = options.fileExists ?? import_node_fs10.default.existsSync;
5310
5670
  const isDirectory = options.isDirectory ?? defaultIsDirectory;
5311
5671
  const issues = [];
5312
- const envPath = import_node_path9.default.resolve(cwd, ".env");
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 = import_node_path9.default.resolve(cwd, configuredWorkdir || cwd);
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 import_node_fs8.default.statSync(targetPath).isDirectory();
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeharbor",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Instant-messaging bridge for Codex CLI sessions",
5
5
  "license": "MIT",
6
6
  "main": "dist/cli.js",