codeharbor 0.1.11 → 0.1.13

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 -1
  2. package/dist/cli.js +74 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -91,6 +91,8 @@ codeharbor service uninstall --with-admin
91
91
  Notes:
92
92
 
93
93
  - Service commands auto-elevate with `sudo` when root privileges are required.
94
+ - `codeharbor service install --with-admin` and `install-linux-easy.sh --enable-admin-service` now install
95
+ `/etc/sudoers.d/codeharbor-restart` for non-root service users, so Admin UI restart actions work out-of-box.
94
96
  - If your environment blocks interactive `sudo`, use explicit fallback:
95
97
  - `sudo <node-bin> <codeharbor-cli-script> service ...`
96
98
 
@@ -353,7 +355,8 @@ Note: `PUT /api/admin/config/global` writes to `.env` and marks changes as resta
353
355
  1. Start server: `codeharbor admin serve`.
354
356
  2. Open `/settings/global`, set `Admin Token` (if enabled), then click `Save Auth`.
355
357
  3. Adjust global fields and click `Save Global Config` (UI shows restart-required warning).
356
- 4. Use `Restart Main Service` or `Restart Main + Admin` buttons for one-click restart from Admin UI (requires root-capable service context).
358
+ 4. Use `Restart Main Service` or `Restart Main + Admin` buttons for one-click restart from Admin UI.
359
+ If services were installed with `--with-admin`, restart permissions are auto-configured by installer.
357
360
  5. Open `/settings/rooms`, fill `Room ID + Workdir`, then `Save Room`.
358
361
  6. Open `/health` to run connectivity checks (`codex` + Matrix).
359
362
  7. Open `/audit` to verify config revisions (actor/summary/payload).
package/dist/cli.js CHANGED
@@ -254,6 +254,8 @@ function resolveUserRuntimeHome(env = process.env) {
254
254
  var SYSTEMD_DIR = "/etc/systemd/system";
255
255
  var MAIN_SERVICE_NAME = "codeharbor.service";
256
256
  var ADMIN_SERVICE_NAME = "codeharbor-admin.service";
257
+ var SUDOERS_DIR = "/etc/sudoers.d";
258
+ var RESTART_SUDOERS_FILE = "codeharbor-restart";
257
259
  function resolveDefaultRunUser(env = process.env) {
258
260
  const sudoUser = env.SUDO_USER?.trim();
259
261
  if (sudoUser) {
@@ -336,6 +338,21 @@ function buildAdminServiceUnit(options) {
336
338
  ""
337
339
  ].join("\n");
338
340
  }
341
+ function buildRestartSudoersPolicy(options) {
342
+ const runUser = options.runUser.trim();
343
+ const systemctlPath = options.systemctlPath.trim();
344
+ validateSimpleValue(runUser, "runUser");
345
+ validateSimpleValue(systemctlPath, "systemctlPath");
346
+ if (!import_node_path3.default.isAbsolute(systemctlPath)) {
347
+ throw new Error("systemctlPath must be an absolute path.");
348
+ }
349
+ return [
350
+ "# Managed by CodeHarbor service install; do not edit manually.",
351
+ `Defaults:${runUser} !requiretty`,
352
+ `${runUser} ALL=(root) NOPASSWD: ${systemctlPath} restart ${MAIN_SERVICE_NAME}, ${systemctlPath} restart ${ADMIN_SERVICE_NAME}`,
353
+ ""
354
+ ].join("\n");
355
+ }
339
356
  function installSystemdServices(options) {
340
357
  assertLinuxWithSystemd();
341
358
  assertRootPrivileges();
@@ -352,6 +369,7 @@ function installSystemdServices(options) {
352
369
  runCommand("chown", ["-R", `${runUser}:${runGroup}`, runtimeHome2]);
353
370
  const mainPath = import_node_path3.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
354
371
  const adminPath = import_node_path3.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
372
+ const restartSudoersPath = import_node_path3.default.join(SUDOERS_DIR, RESTART_SUDOERS_FILE);
355
373
  const unitOptions = {
356
374
  runUser,
357
375
  runtimeHome: runtimeHome2,
@@ -361,6 +379,15 @@ function installSystemdServices(options) {
361
379
  import_node_fs3.default.writeFileSync(mainPath, buildMainServiceUnit(unitOptions), "utf8");
362
380
  if (options.installAdmin) {
363
381
  import_node_fs3.default.writeFileSync(adminPath, buildAdminServiceUnit(unitOptions), "utf8");
382
+ if (runUser !== "root") {
383
+ const policy = buildRestartSudoersPolicy({
384
+ runUser,
385
+ systemctlPath: resolveSystemctlPath()
386
+ });
387
+ import_node_fs3.default.mkdirSync(SUDOERS_DIR, { recursive: true });
388
+ import_node_fs3.default.writeFileSync(restartSudoersPath, policy, "utf8");
389
+ import_node_fs3.default.chmodSync(restartSudoersPath, 288);
390
+ }
364
391
  }
365
392
  runSystemctl(["daemon-reload"]);
366
393
  if (options.startNow) {
@@ -379,6 +406,10 @@ function installSystemdServices(options) {
379
406
  if (options.installAdmin) {
380
407
  output.write(`Installed systemd unit: ${adminPath}
381
408
  `);
409
+ if (runUser !== "root") {
410
+ output.write(`Installed sudoers policy: ${restartSudoersPath}
411
+ `);
412
+ }
382
413
  }
383
414
  output.write("Done. Check status with: systemctl status codeharbor --no-pager\n");
384
415
  }
@@ -388,6 +419,7 @@ function uninstallSystemdServices(options) {
388
419
  const output = options.output ?? process.stdout;
389
420
  const mainPath = import_node_path3.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
390
421
  const adminPath = import_node_path3.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
422
+ const restartSudoersPath = import_node_path3.default.join(SUDOERS_DIR, RESTART_SUDOERS_FILE);
391
423
  stopAndDisableIfPresent(MAIN_SERVICE_NAME);
392
424
  if (import_node_fs3.default.existsSync(mainPath)) {
393
425
  import_node_fs3.default.unlinkSync(mainPath);
@@ -397,6 +429,9 @@ function uninstallSystemdServices(options) {
397
429
  if (import_node_fs3.default.existsSync(adminPath)) {
398
430
  import_node_fs3.default.unlinkSync(adminPath);
399
431
  }
432
+ if (import_node_fs3.default.existsSync(restartSudoersPath)) {
433
+ import_node_fs3.default.unlinkSync(restartSudoersPath);
434
+ }
400
435
  }
401
436
  runSystemctl(["daemon-reload"]);
402
437
  runSystemctlIgnoreFailure(["reset-failed"]);
@@ -404,19 +439,22 @@ function uninstallSystemdServices(options) {
404
439
  `);
405
440
  if (options.removeAdmin) {
406
441
  output.write(`Removed systemd unit: ${adminPath}
442
+ `);
443
+ output.write(`Removed sudoers policy: ${restartSudoersPath}
407
444
  `);
408
445
  }
409
446
  output.write("Done.\n");
410
447
  }
411
448
  function restartSystemdServices(options) {
412
449
  assertLinuxWithSystemd();
413
- assertRootPrivileges();
414
450
  const output = options.output ?? process.stdout;
415
- runSystemctl(["restart", MAIN_SERVICE_NAME]);
451
+ const runWithSudoFallback = options.allowSudoFallback ?? true;
452
+ const systemctlRunner = hasRootPrivileges() || !runWithSudoFallback ? runSystemctl : runSystemctlWithNonInteractiveSudo;
453
+ systemctlRunner(["restart", MAIN_SERVICE_NAME]);
416
454
  output.write(`Restarted service: ${MAIN_SERVICE_NAME}
417
455
  `);
418
456
  if (options.restartAdmin) {
419
- runSystemctl(["restart", ADMIN_SERVICE_NAME]);
457
+ systemctlRunner(["restart", ADMIN_SERVICE_NAME]);
420
458
  output.write(`Restarted service: ${ADMIN_SERVICE_NAME}
421
459
  `);
422
460
  }
@@ -460,13 +498,16 @@ function assertLinuxWithSystemd() {
460
498
  }
461
499
  }
462
500
  function assertRootPrivileges() {
463
- if (typeof process.getuid !== "function") {
464
- return;
465
- }
466
- if (process.getuid() !== 0) {
501
+ if (!hasRootPrivileges()) {
467
502
  throw new Error("Root privileges are required. Run with sudo.");
468
503
  }
469
504
  }
505
+ function hasRootPrivileges() {
506
+ if (typeof process.getuid !== "function") {
507
+ return true;
508
+ }
509
+ return process.getuid() === 0;
510
+ }
470
511
  function ensureUserExists(runUser) {
471
512
  runCommand("id", ["-u", runUser]);
472
513
  }
@@ -476,6 +517,17 @@ function resolveUserGroup(runUser) {
476
517
  function runSystemctl(args) {
477
518
  runCommand("systemctl", args);
478
519
  }
520
+ function runSystemctlWithNonInteractiveSudo(args) {
521
+ const systemctlPath = resolveSystemctlPath();
522
+ try {
523
+ runCommand("sudo", ["-n", systemctlPath, ...args]);
524
+ } catch (error) {
525
+ throw new Error(
526
+ "Root privileges are required. Configure passwordless sudo for the CodeHarbor service user or run the CLI command manually with sudo.",
527
+ { cause: error }
528
+ );
529
+ }
530
+ }
479
531
  function stopAndDisableIfPresent(unitName) {
480
532
  runSystemctlIgnoreFailure(["disable", "--now", unitName]);
481
533
  }
@@ -485,6 +537,20 @@ function runSystemctlIgnoreFailure(args) {
485
537
  } catch {
486
538
  }
487
539
  }
540
+ function resolveSystemctlPath() {
541
+ const candidates = [];
542
+ const pathEntries = (process.env.PATH ?? "").split(import_node_path3.default.delimiter).filter(Boolean);
543
+ for (const entry of pathEntries) {
544
+ candidates.push(import_node_path3.default.join(entry, "systemctl"));
545
+ }
546
+ candidates.push("/usr/bin/systemctl", "/bin/systemctl", "/usr/local/bin/systemctl");
547
+ for (const candidate of candidates) {
548
+ if (import_node_path3.default.isAbsolute(candidate) && import_node_fs3.default.existsSync(candidate)) {
549
+ return candidate;
550
+ }
551
+ }
552
+ throw new Error("Unable to resolve absolute systemctl path.");
553
+ }
488
554
  function runCommand(file, args) {
489
555
  try {
490
556
  return (0, import_node_child_process.execFileSync)(file, args, {
@@ -739,7 +805,7 @@ var AdminServer = class {
739
805
  } catch (error) {
740
806
  throw new HttpError(
741
807
  500,
742
- `Service restart failed: ${formatError(error)}. Ensure service has root privileges or run CLI command manually.`
808
+ `Service restart failed: ${formatError(error)}. Install services via "codeharbor service install --with-admin" to auto-configure restart permissions, or run CLI command manually with sudo.`
743
809
  );
744
810
  }
745
811
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeharbor",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Instant-messaging bridge for Codex CLI sessions",
5
5
  "license": "MIT",
6
6
  "main": "dist/cli.js",