codeharbor 0.1.11 → 0.1.12
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 -1
- package/dist/cli.js +74 -8
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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)}.
|
|
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
|
}
|