codeharbor 0.1.9 → 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/.env.example +5 -0
- package/README.md +25 -7
- package/dist/cli.js +1271 -664
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -24,19 +24,20 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/cli.ts
|
|
27
|
+
var import_node_child_process6 = require("child_process");
|
|
27
28
|
var import_node_fs11 = __toESM(require("fs"));
|
|
28
29
|
var import_node_path12 = __toESM(require("path"));
|
|
29
30
|
var import_commander = require("commander");
|
|
30
31
|
|
|
31
32
|
// src/app.ts
|
|
32
|
-
var
|
|
33
|
+
var import_node_child_process4 = require("child_process");
|
|
33
34
|
var import_node_util2 = require("util");
|
|
34
35
|
|
|
35
36
|
// src/admin-server.ts
|
|
36
|
-
var
|
|
37
|
-
var
|
|
37
|
+
var import_node_child_process2 = require("child_process");
|
|
38
|
+
var import_node_fs4 = __toESM(require("fs"));
|
|
38
39
|
var import_node_http = __toESM(require("http"));
|
|
39
|
-
var
|
|
40
|
+
var import_node_path4 = __toESM(require("path"));
|
|
40
41
|
var import_node_util = require("util");
|
|
41
42
|
|
|
42
43
|
// src/init.ts
|
|
@@ -219,117 +220,413 @@ async function askYesNo(rl, question, defaultValue) {
|
|
|
219
220
|
return answer === "y" || answer === "yes";
|
|
220
221
|
}
|
|
221
222
|
|
|
222
|
-
// src/
|
|
223
|
-
var
|
|
224
|
-
var
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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);
|
|
229
241
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
logger;
|
|
234
|
-
stateStore;
|
|
235
|
-
configService;
|
|
236
|
-
host;
|
|
237
|
-
port;
|
|
238
|
-
adminToken;
|
|
239
|
-
adminIpAllowlist;
|
|
240
|
-
adminAllowedOrigins;
|
|
241
|
-
cwd;
|
|
242
|
-
checkCodex;
|
|
243
|
-
checkMatrix;
|
|
244
|
-
server = null;
|
|
245
|
-
address = null;
|
|
246
|
-
constructor(config, logger, stateStore, configService, options) {
|
|
247
|
-
this.config = config;
|
|
248
|
-
this.logger = logger;
|
|
249
|
-
this.stateStore = stateStore;
|
|
250
|
-
this.configService = configService;
|
|
251
|
-
this.host = options.host;
|
|
252
|
-
this.port = options.port;
|
|
253
|
-
this.adminToken = options.adminToken;
|
|
254
|
-
this.adminIpAllowlist = normalizeAllowlist(options.adminIpAllowlist ?? []);
|
|
255
|
-
this.adminAllowedOrigins = normalizeOriginAllowlist(options.adminAllowedOrigins ?? []);
|
|
256
|
-
this.cwd = options.cwd ?? process.cwd();
|
|
257
|
-
this.checkCodex = options.checkCodex ?? defaultCheckCodex;
|
|
258
|
-
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;
|
|
259
245
|
}
|
|
260
|
-
|
|
261
|
-
|
|
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;
|
|
262
261
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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]);
|
|
266
375
|
}
|
|
267
|
-
this.server = import_node_http.default.createServer((req, res) => {
|
|
268
|
-
void this.handleRequest(req, res);
|
|
269
|
-
});
|
|
270
|
-
await new Promise((resolve, reject) => {
|
|
271
|
-
if (!this.server) {
|
|
272
|
-
reject(new Error("admin server is not initialized"));
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
this.server.once("error", reject);
|
|
276
|
-
this.server.listen(this.port, this.host, () => {
|
|
277
|
-
this.server?.removeListener("error", reject);
|
|
278
|
-
const address = this.server?.address();
|
|
279
|
-
if (!address || typeof address === "string") {
|
|
280
|
-
reject(new Error("failed to resolve admin server address"));
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
this.address = {
|
|
284
|
-
host: address.address,
|
|
285
|
-
port: address.port
|
|
286
|
-
};
|
|
287
|
-
resolve();
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
376
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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);
|
|
294
399
|
}
|
|
295
|
-
const server = this.server;
|
|
296
|
-
this.server = null;
|
|
297
|
-
this.address = null;
|
|
298
|
-
await new Promise((resolve, reject) => {
|
|
299
|
-
server.close((error) => {
|
|
300
|
-
if (error) {
|
|
301
|
-
reject(error);
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
resolve();
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
400
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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."
|
|
333
630
|
});
|
|
334
631
|
return;
|
|
335
632
|
}
|
|
@@ -419,6 +716,33 @@ var AdminServer = class {
|
|
|
419
716
|
});
|
|
420
717
|
return;
|
|
421
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
|
+
}
|
|
422
746
|
this.sendJson(res, 404, {
|
|
423
747
|
ok: false,
|
|
424
748
|
error: `Not found: ${req.method ?? "GET"} ${url.pathname}`
|
|
@@ -449,7 +773,7 @@ var AdminServer = class {
|
|
|
449
773
|
updatedKeys.push("matrixCommandPrefix");
|
|
450
774
|
}
|
|
451
775
|
if ("codexWorkdir" in body) {
|
|
452
|
-
const workdir =
|
|
776
|
+
const workdir = import_node_path4.default.resolve(String(body.codexWorkdir ?? "").trim());
|
|
453
777
|
ensureDirectory(workdir, "codexWorkdir");
|
|
454
778
|
this.config.codexWorkdir = workdir;
|
|
455
779
|
envUpdates.CODEX_WORKDIR = workdir;
|
|
@@ -599,6 +923,27 @@ var AdminServer = class {
|
|
|
599
923
|
updatedKeys.push("cliCompat.fetchMedia");
|
|
600
924
|
}
|
|
601
925
|
}
|
|
926
|
+
if ("agentWorkflow" in body) {
|
|
927
|
+
const workflow = asObject(body.agentWorkflow, "agentWorkflow");
|
|
928
|
+
const currentAgentWorkflow = ensureAgentWorkflowConfig(this.config);
|
|
929
|
+
if ("enabled" in workflow) {
|
|
930
|
+
const value = normalizeBoolean(workflow.enabled, currentAgentWorkflow.enabled);
|
|
931
|
+
currentAgentWorkflow.enabled = value;
|
|
932
|
+
envUpdates.AGENT_WORKFLOW_ENABLED = String(value);
|
|
933
|
+
updatedKeys.push("agentWorkflow.enabled");
|
|
934
|
+
}
|
|
935
|
+
if ("autoRepairMaxRounds" in workflow) {
|
|
936
|
+
const value = normalizePositiveInt(
|
|
937
|
+
workflow.autoRepairMaxRounds,
|
|
938
|
+
currentAgentWorkflow.autoRepairMaxRounds,
|
|
939
|
+
0,
|
|
940
|
+
10
|
|
941
|
+
);
|
|
942
|
+
currentAgentWorkflow.autoRepairMaxRounds = value;
|
|
943
|
+
envUpdates.AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS = String(value);
|
|
944
|
+
updatedKeys.push("agentWorkflow.autoRepairMaxRounds");
|
|
945
|
+
}
|
|
946
|
+
}
|
|
602
947
|
if (updatedKeys.length === 0) {
|
|
603
948
|
throw new HttpError(400, "No supported global config fields provided.");
|
|
604
949
|
}
|
|
@@ -652,11 +997,11 @@ var AdminServer = class {
|
|
|
652
997
|
return this.adminIpAllowlist.includes(normalizedRemote);
|
|
653
998
|
}
|
|
654
999
|
persistEnvUpdates(updates) {
|
|
655
|
-
const envPath =
|
|
656
|
-
const examplePath =
|
|
657
|
-
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") : "";
|
|
658
1003
|
const next = applyEnvOverrides(template, updates);
|
|
659
|
-
|
|
1004
|
+
import_node_fs4.default.writeFileSync(envPath, next, "utf8");
|
|
660
1005
|
}
|
|
661
1006
|
resolveCors(req) {
|
|
662
1007
|
const origin = normalizeOriginHeader(req.headers.origin);
|
|
@@ -683,7 +1028,7 @@ var AdminServer = class {
|
|
|
683
1028
|
}
|
|
684
1029
|
res.setHeader("Access-Control-Allow-Origin", corsDecision.origin);
|
|
685
1030
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Admin-Token, X-Admin-Actor");
|
|
686
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, PUT, DELETE, OPTIONS");
|
|
1031
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS");
|
|
687
1032
|
appendVaryHeader(res, "Origin");
|
|
688
1033
|
}
|
|
689
1034
|
setSecurityHeaders(res) {
|
|
@@ -719,9 +1064,23 @@ function buildGlobalConfigSnapshot(config) {
|
|
|
719
1064
|
matrixProgressMinIntervalMs: config.matrixProgressMinIntervalMs,
|
|
720
1065
|
matrixTypingTimeoutMs: config.matrixTypingTimeoutMs,
|
|
721
1066
|
sessionActiveWindowMinutes: config.sessionActiveWindowMinutes,
|
|
722
|
-
cliCompat: { ...config.cliCompat }
|
|
1067
|
+
cliCompat: { ...config.cliCompat },
|
|
1068
|
+
agentWorkflow: { ...ensureAgentWorkflowConfig(config) }
|
|
723
1069
|
};
|
|
724
1070
|
}
|
|
1071
|
+
function ensureAgentWorkflowConfig(config) {
|
|
1072
|
+
const mutable = config;
|
|
1073
|
+
const existing = mutable.agentWorkflow;
|
|
1074
|
+
if (existing && typeof existing.enabled === "boolean" && Number.isFinite(existing.autoRepairMaxRounds)) {
|
|
1075
|
+
return existing;
|
|
1076
|
+
}
|
|
1077
|
+
const fallback = {
|
|
1078
|
+
enabled: false,
|
|
1079
|
+
autoRepairMaxRounds: 1
|
|
1080
|
+
};
|
|
1081
|
+
mutable.agentWorkflow = fallback;
|
|
1082
|
+
return fallback;
|
|
1083
|
+
}
|
|
725
1084
|
function formatAuditEntry(entry) {
|
|
726
1085
|
return {
|
|
727
1086
|
id: entry.id,
|
|
@@ -740,6 +1099,22 @@ function parseJsonLoose(raw) {
|
|
|
740
1099
|
return raw;
|
|
741
1100
|
}
|
|
742
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
|
+
}
|
|
743
1118
|
function isUiPath(pathname) {
|
|
744
1119
|
return pathname === "/" || pathname === "/index.html" || pathname === "/settings/global" || pathname === "/settings/rooms" || pathname === "/health" || pathname === "/audit";
|
|
745
1120
|
}
|
|
@@ -888,7 +1263,7 @@ function normalizeNonNegativeInt(value, fallback) {
|
|
|
888
1263
|
return normalizePositiveInt(value, fallback, 0, Number.MAX_SAFE_INTEGER);
|
|
889
1264
|
}
|
|
890
1265
|
function ensureDirectory(targetPath, fieldName) {
|
|
891
|
-
if (!
|
|
1266
|
+
if (!import_node_fs4.default.existsSync(targetPath) || !import_node_fs4.default.statSync(targetPath).isDirectory()) {
|
|
892
1267
|
throw new HttpError(400, `${fieldName} must be an existing directory: ${targetPath}`);
|
|
893
1268
|
}
|
|
894
1269
|
}
|
|
@@ -1221,10 +1596,17 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1221
1596
|
<input id="global-cli-throttle" type="number" min="0" />
|
|
1222
1597
|
</label>
|
|
1223
1598
|
<label class="checkbox"><input id="global-cli-fetch-media" type="checkbox" /><span>Fetch media attachments</span></label>
|
|
1599
|
+
<label class="checkbox"><input id="global-agent-enabled" type="checkbox" /><span>Enable multi-agent workflow</span></label>
|
|
1600
|
+
<label class="field">
|
|
1601
|
+
<span class="field-label">Workflow auto-repair rounds</span>
|
|
1602
|
+
<input id="global-agent-repair-rounds" type="number" min="0" max="10" />
|
|
1603
|
+
</label>
|
|
1224
1604
|
</div>
|
|
1225
1605
|
<div class="actions">
|
|
1226
1606
|
<button id="global-save-btn" type="button">Save Global Config</button>
|
|
1227
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>
|
|
1228
1610
|
</div>
|
|
1229
1611
|
<p class="muted">Saving global config updates .env and requires restart to fully take effect.</p>
|
|
1230
1612
|
</section>
|
|
@@ -1367,6 +1749,12 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1367
1749
|
|
|
1368
1750
|
document.getElementById("global-save-btn").addEventListener("click", saveGlobal);
|
|
1369
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
|
+
});
|
|
1370
1758
|
document.getElementById("room-load-btn").addEventListener("click", loadRoom);
|
|
1371
1759
|
document.getElementById("room-save-btn").addEventListener("click", saveRoom);
|
|
1372
1760
|
document.getElementById("room-delete-btn").addEventListener("click", deleteRoom);
|
|
@@ -1488,6 +1876,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1488
1876
|
var rateLimiter = data.rateLimiter || {};
|
|
1489
1877
|
var trigger = data.defaultGroupTriggerPolicy || {};
|
|
1490
1878
|
var cliCompat = data.cliCompat || {};
|
|
1879
|
+
var agentWorkflow = data.agentWorkflow || {};
|
|
1491
1880
|
|
|
1492
1881
|
document.getElementById("global-matrix-prefix").value = data.matrixCommandPrefix || "";
|
|
1493
1882
|
document.getElementById("global-workdir").value = data.codexWorkdir || "";
|
|
@@ -1513,6 +1902,10 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1513
1902
|
document.getElementById("global-cli-disable-split").checked = Boolean(cliCompat.disableReplyChunkSplit);
|
|
1514
1903
|
document.getElementById("global-cli-throttle").value = String(cliCompat.progressThrottleMs || 0);
|
|
1515
1904
|
document.getElementById("global-cli-fetch-media").checked = Boolean(cliCompat.fetchMedia);
|
|
1905
|
+
document.getElementById("global-agent-enabled").checked = Boolean(agentWorkflow.enabled);
|
|
1906
|
+
document.getElementById("global-agent-repair-rounds").value = String(
|
|
1907
|
+
typeof agentWorkflow.autoRepairMaxRounds === "number" ? agentWorkflow.autoRepairMaxRounds : 1
|
|
1908
|
+
);
|
|
1516
1909
|
|
|
1517
1910
|
showNotice("ok", "Global config loaded.");
|
|
1518
1911
|
} catch (error) {
|
|
@@ -1550,6 +1943,10 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1550
1943
|
disableReplyChunkSplit: asBool("global-cli-disable-split"),
|
|
1551
1944
|
progressThrottleMs: asNumber("global-cli-throttle", 300),
|
|
1552
1945
|
fetchMedia: asBool("global-cli-fetch-media")
|
|
1946
|
+
},
|
|
1947
|
+
agentWorkflow: {
|
|
1948
|
+
enabled: asBool("global-agent-enabled"),
|
|
1949
|
+
autoRepairMaxRounds: asNumber("global-agent-repair-rounds", 1)
|
|
1553
1950
|
}
|
|
1554
1951
|
};
|
|
1555
1952
|
var response = await apiRequest("/api/admin/config/global", "PUT", body);
|
|
@@ -1561,11 +1958,24 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1561
1958
|
}
|
|
1562
1959
|
}
|
|
1563
1960
|
|
|
1564
|
-
async function
|
|
1961
|
+
async function restartManagedServices(withAdmin) {
|
|
1565
1962
|
try {
|
|
1566
|
-
var response = await apiRequest("/api/admin/
|
|
1567
|
-
|
|
1568
|
-
|
|
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
|
+
|
|
1974
|
+
async function refreshRoomList() {
|
|
1975
|
+
try {
|
|
1976
|
+
var response = await apiRequest("/api/admin/config/rooms", "GET");
|
|
1977
|
+
var items = Array.isArray(response.data) ? response.data : [];
|
|
1978
|
+
roomListBody.innerHTML = "";
|
|
1569
1979
|
if (items.length === 0) {
|
|
1570
1980
|
renderEmptyRow(roomListBody, 4, "No room settings.");
|
|
1571
1981
|
return;
|
|
@@ -1786,8 +2196,8 @@ async function defaultCheckMatrix(homeserver, timeoutMs) {
|
|
|
1786
2196
|
|
|
1787
2197
|
// src/channels/matrix-channel.ts
|
|
1788
2198
|
var import_promises2 = __toESM(require("fs/promises"));
|
|
1789
|
-
var
|
|
1790
|
-
var
|
|
2199
|
+
var import_node_os3 = __toESM(require("os"));
|
|
2200
|
+
var import_node_path5 = __toESM(require("path"));
|
|
1791
2201
|
var import_matrix_js_sdk = require("matrix-js-sdk");
|
|
1792
2202
|
|
|
1793
2203
|
// src/utils/message.ts
|
|
@@ -2210,10 +2620,10 @@ var MatrixChannel = class {
|
|
|
2210
2620
|
}
|
|
2211
2621
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
2212
2622
|
const extension = resolveFileExtension(fileName, mimeType);
|
|
2213
|
-
const directory =
|
|
2623
|
+
const directory = import_node_path5.default.join(import_node_os3.default.tmpdir(), "codeharbor-media");
|
|
2214
2624
|
await import_promises2.default.mkdir(directory, { recursive: true });
|
|
2215
2625
|
const safeEventId = sanitizeFilename(eventId);
|
|
2216
|
-
const targetPath =
|
|
2626
|
+
const targetPath = import_node_path5.default.join(directory, `${safeEventId}-${index}${extension}`);
|
|
2217
2627
|
await import_promises2.default.writeFile(targetPath, bytes);
|
|
2218
2628
|
return targetPath;
|
|
2219
2629
|
}
|
|
@@ -2298,7 +2708,7 @@ function sanitizeFilename(value) {
|
|
|
2298
2708
|
return value.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
|
|
2299
2709
|
}
|
|
2300
2710
|
function resolveFileExtension(fileName, mimeType) {
|
|
2301
|
-
const ext =
|
|
2711
|
+
const ext = import_node_path5.default.extname(fileName).trim();
|
|
2302
2712
|
if (ext) {
|
|
2303
2713
|
return ext;
|
|
2304
2714
|
}
|
|
@@ -2315,14 +2725,14 @@ function resolveFileExtension(fileName, mimeType) {
|
|
|
2315
2725
|
}
|
|
2316
2726
|
|
|
2317
2727
|
// src/config-service.ts
|
|
2318
|
-
var
|
|
2319
|
-
var
|
|
2728
|
+
var import_node_fs5 = __toESM(require("fs"));
|
|
2729
|
+
var import_node_path6 = __toESM(require("path"));
|
|
2320
2730
|
var ConfigService = class {
|
|
2321
2731
|
stateStore;
|
|
2322
2732
|
defaultWorkdir;
|
|
2323
2733
|
constructor(stateStore, defaultWorkdir) {
|
|
2324
2734
|
this.stateStore = stateStore;
|
|
2325
|
-
this.defaultWorkdir =
|
|
2735
|
+
this.defaultWorkdir = import_node_path6.default.resolve(defaultWorkdir);
|
|
2326
2736
|
}
|
|
2327
2737
|
resolveRoomConfig(roomId, fallbackPolicy) {
|
|
2328
2738
|
const room = this.stateStore.getRoomSettings(roomId);
|
|
@@ -2393,8 +2803,8 @@ function normalizeRoomSettingsInput(input) {
|
|
|
2393
2803
|
if (!roomId) {
|
|
2394
2804
|
throw new Error("roomId is required.");
|
|
2395
2805
|
}
|
|
2396
|
-
const workdir =
|
|
2397
|
-
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()) {
|
|
2398
2808
|
throw new Error(`workdir does not exist or is not a directory: ${workdir}`);
|
|
2399
2809
|
}
|
|
2400
2810
|
return {
|
|
@@ -2409,7 +2819,7 @@ function normalizeRoomSettingsInput(input) {
|
|
|
2409
2819
|
}
|
|
2410
2820
|
|
|
2411
2821
|
// src/executor/codex-executor.ts
|
|
2412
|
-
var
|
|
2822
|
+
var import_node_child_process3 = require("child_process");
|
|
2413
2823
|
var import_node_readline = __toESM(require("readline"));
|
|
2414
2824
|
var CodexExecutionCancelledError = class extends Error {
|
|
2415
2825
|
constructor(message = "codex execution cancelled") {
|
|
@@ -2427,7 +2837,7 @@ var CodexExecutor = class {
|
|
|
2427
2837
|
}
|
|
2428
2838
|
startExecution(prompt, sessionId, onProgress, startOptions) {
|
|
2429
2839
|
const args = buildCodexArgs(prompt, sessionId, this.options, startOptions);
|
|
2430
|
-
const child = (0,
|
|
2840
|
+
const child = (0, import_node_child_process3.spawn)(this.options.bin, args, {
|
|
2431
2841
|
cwd: startOptions?.workdir ?? this.options.workdir,
|
|
2432
2842
|
env: {
|
|
2433
2843
|
...process.env,
|
|
@@ -2663,20 +3073,20 @@ var import_async_mutex = require("async-mutex");
|
|
|
2663
3073
|
var import_promises3 = __toESM(require("fs/promises"));
|
|
2664
3074
|
|
|
2665
3075
|
// src/compat/cli-compat-recorder.ts
|
|
2666
|
-
var
|
|
2667
|
-
var
|
|
3076
|
+
var import_node_fs6 = __toESM(require("fs"));
|
|
3077
|
+
var import_node_path7 = __toESM(require("path"));
|
|
2668
3078
|
var CliCompatRecorder = class {
|
|
2669
3079
|
filePath;
|
|
2670
3080
|
chain = Promise.resolve();
|
|
2671
3081
|
constructor(filePath) {
|
|
2672
|
-
this.filePath =
|
|
2673
|
-
|
|
3082
|
+
this.filePath = import_node_path7.default.resolve(filePath);
|
|
3083
|
+
import_node_fs6.default.mkdirSync(import_node_path7.default.dirname(this.filePath), { recursive: true });
|
|
2674
3084
|
}
|
|
2675
3085
|
append(entry) {
|
|
2676
3086
|
const payload = `${JSON.stringify(entry)}
|
|
2677
3087
|
`;
|
|
2678
3088
|
this.chain = this.chain.then(async () => {
|
|
2679
|
-
await
|
|
3089
|
+
await import_node_fs6.default.promises.appendFile(this.filePath, payload, "utf8");
|
|
2680
3090
|
});
|
|
2681
3091
|
return this.chain;
|
|
2682
3092
|
}
|
|
@@ -2880,6 +3290,273 @@ function computeRetryAfter(timestamps, windowMs, now) {
|
|
|
2880
3290
|
return Math.max(0, oldest + windowMs - now);
|
|
2881
3291
|
}
|
|
2882
3292
|
|
|
3293
|
+
// src/workflow/multi-agent-workflow.ts
|
|
3294
|
+
var MultiAgentWorkflowRunner = class {
|
|
3295
|
+
executor;
|
|
3296
|
+
logger;
|
|
3297
|
+
config;
|
|
3298
|
+
constructor(executor, logger, config) {
|
|
3299
|
+
this.executor = executor;
|
|
3300
|
+
this.logger = logger;
|
|
3301
|
+
this.config = config;
|
|
3302
|
+
}
|
|
3303
|
+
isEnabled() {
|
|
3304
|
+
return this.config.enabled;
|
|
3305
|
+
}
|
|
3306
|
+
async run(input) {
|
|
3307
|
+
const startedAt = Date.now();
|
|
3308
|
+
const objective = input.objective.trim();
|
|
3309
|
+
if (!objective) {
|
|
3310
|
+
throw new Error("workflow objective cannot be empty.");
|
|
3311
|
+
}
|
|
3312
|
+
const maxRepairRounds = Math.max(0, this.config.autoRepairMaxRounds);
|
|
3313
|
+
let plannerSessionId = null;
|
|
3314
|
+
let executorSessionId = null;
|
|
3315
|
+
let reviewerSessionId = null;
|
|
3316
|
+
let activeHandle = null;
|
|
3317
|
+
let cancelled = false;
|
|
3318
|
+
input.onRegisterCancel?.(() => {
|
|
3319
|
+
cancelled = true;
|
|
3320
|
+
activeHandle?.cancel();
|
|
3321
|
+
});
|
|
3322
|
+
await emitProgress(input, {
|
|
3323
|
+
stage: "planner",
|
|
3324
|
+
round: 0,
|
|
3325
|
+
message: "Planner \u6B63\u5728\u751F\u6210\u6267\u884C\u8BA1\u5212"
|
|
3326
|
+
});
|
|
3327
|
+
const planResult = await this.executeRole(
|
|
3328
|
+
"planner",
|
|
3329
|
+
buildPlannerPrompt(objective),
|
|
3330
|
+
plannerSessionId,
|
|
3331
|
+
input.workdir,
|
|
3332
|
+
() => cancelled,
|
|
3333
|
+
(handle) => {
|
|
3334
|
+
activeHandle = handle;
|
|
3335
|
+
}
|
|
3336
|
+
);
|
|
3337
|
+
plannerSessionId = planResult.sessionId;
|
|
3338
|
+
const plan = planResult.reply;
|
|
3339
|
+
await emitProgress(input, {
|
|
3340
|
+
stage: "executor",
|
|
3341
|
+
round: 0,
|
|
3342
|
+
message: "Executor \u6B63\u5728\u6839\u636E\u8BA1\u5212\u6267\u884C\u4EFB\u52A1"
|
|
3343
|
+
});
|
|
3344
|
+
let outputResult = await this.executeRole(
|
|
3345
|
+
"executor",
|
|
3346
|
+
buildExecutorPrompt(objective, plan),
|
|
3347
|
+
executorSessionId,
|
|
3348
|
+
input.workdir,
|
|
3349
|
+
() => cancelled,
|
|
3350
|
+
(handle) => {
|
|
3351
|
+
activeHandle = handle;
|
|
3352
|
+
}
|
|
3353
|
+
);
|
|
3354
|
+
executorSessionId = outputResult.sessionId;
|
|
3355
|
+
let finalReviewReply = "";
|
|
3356
|
+
let approved = false;
|
|
3357
|
+
let repairRounds = 0;
|
|
3358
|
+
for (let attempt = 0; attempt <= maxRepairRounds; attempt += 1) {
|
|
3359
|
+
await emitProgress(input, {
|
|
3360
|
+
stage: "reviewer",
|
|
3361
|
+
round: attempt,
|
|
3362
|
+
message: `Reviewer \u6B63\u5728\u8FDB\u884C\u8D28\u91CF\u5BA1\u67E5\uFF08round ${attempt + 1}\uFF09`
|
|
3363
|
+
});
|
|
3364
|
+
const reviewResult = await this.executeRole(
|
|
3365
|
+
"reviewer",
|
|
3366
|
+
buildReviewerPrompt(objective, plan, outputResult.reply),
|
|
3367
|
+
reviewerSessionId,
|
|
3368
|
+
input.workdir,
|
|
3369
|
+
() => cancelled,
|
|
3370
|
+
(handle) => {
|
|
3371
|
+
activeHandle = handle;
|
|
3372
|
+
}
|
|
3373
|
+
);
|
|
3374
|
+
reviewerSessionId = reviewResult.sessionId;
|
|
3375
|
+
finalReviewReply = reviewResult.reply;
|
|
3376
|
+
const verdict = parseReviewerVerdict(finalReviewReply);
|
|
3377
|
+
if (verdict.approved) {
|
|
3378
|
+
approved = true;
|
|
3379
|
+
break;
|
|
3380
|
+
}
|
|
3381
|
+
if (attempt >= maxRepairRounds) {
|
|
3382
|
+
break;
|
|
3383
|
+
}
|
|
3384
|
+
repairRounds = attempt + 1;
|
|
3385
|
+
await emitProgress(input, {
|
|
3386
|
+
stage: "repair",
|
|
3387
|
+
round: repairRounds,
|
|
3388
|
+
message: `Executor \u6B63\u5728\u6309 Reviewer \u53CD\u9988\u8FDB\u884C\u4FEE\u590D\uFF08round ${repairRounds}\uFF09`
|
|
3389
|
+
});
|
|
3390
|
+
outputResult = await this.executeRole(
|
|
3391
|
+
"executor",
|
|
3392
|
+
buildRepairPrompt(objective, plan, outputResult.reply, verdict.feedback, repairRounds),
|
|
3393
|
+
executorSessionId,
|
|
3394
|
+
input.workdir,
|
|
3395
|
+
() => cancelled,
|
|
3396
|
+
(handle) => {
|
|
3397
|
+
activeHandle = handle;
|
|
3398
|
+
}
|
|
3399
|
+
);
|
|
3400
|
+
executorSessionId = outputResult.sessionId;
|
|
3401
|
+
}
|
|
3402
|
+
const durationMs = Date.now() - startedAt;
|
|
3403
|
+
this.logger.info("Multi-agent workflow finished", {
|
|
3404
|
+
objective,
|
|
3405
|
+
approved,
|
|
3406
|
+
repairRounds,
|
|
3407
|
+
durationMs
|
|
3408
|
+
});
|
|
3409
|
+
return {
|
|
3410
|
+
objective,
|
|
3411
|
+
plan,
|
|
3412
|
+
output: outputResult.reply,
|
|
3413
|
+
review: finalReviewReply,
|
|
3414
|
+
approved,
|
|
3415
|
+
repairRounds,
|
|
3416
|
+
durationMs
|
|
3417
|
+
};
|
|
3418
|
+
}
|
|
3419
|
+
async executeRole(role, prompt, sessionId, workdir, getCancelled, setActiveHandle) {
|
|
3420
|
+
if (getCancelled()) {
|
|
3421
|
+
throw new CodexExecutionCancelledError("workflow cancelled");
|
|
3422
|
+
}
|
|
3423
|
+
const handle = this.executor.startExecution(prompt, sessionId, void 0, { workdir });
|
|
3424
|
+
setActiveHandle(handle);
|
|
3425
|
+
try {
|
|
3426
|
+
const result = await handle.result;
|
|
3427
|
+
return {
|
|
3428
|
+
sessionId: result.sessionId,
|
|
3429
|
+
reply: result.reply
|
|
3430
|
+
};
|
|
3431
|
+
} finally {
|
|
3432
|
+
setActiveHandle(null);
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
};
|
|
3436
|
+
async function emitProgress(input, event) {
|
|
3437
|
+
if (!input.onProgress) {
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3440
|
+
await input.onProgress(event);
|
|
3441
|
+
}
|
|
3442
|
+
function buildPlannerPrompt(objective) {
|
|
3443
|
+
return [
|
|
3444
|
+
"[role:planner]",
|
|
3445
|
+
"\u4F60\u662F\u8F6F\u4EF6\u4EA4\u4ED8\u89C4\u5212\u4EE3\u7406\u3002\u8BF7\u57FA\u4E8E\u76EE\u6807\u7ED9\u51FA\u53EF\u6267\u884C\u8BA1\u5212\u3002",
|
|
3446
|
+
"\u8F93\u51FA\u8981\u6C42\uFF1A",
|
|
3447
|
+
"1. \u4EFB\u52A1\u62C6\u89E3\uFF083-7 \u6B65\uFF09",
|
|
3448
|
+
"2. \u6BCF\u6B65\u8F93\u5165/\u8F93\u51FA",
|
|
3449
|
+
"3. \u98CE\u9669\u4E0E\u56DE\u9000\u65B9\u6848",
|
|
3450
|
+
"",
|
|
3451
|
+
`\u76EE\u6807\uFF1A${objective}`
|
|
3452
|
+
].join("\n");
|
|
3453
|
+
}
|
|
3454
|
+
function buildExecutorPrompt(objective, plan) {
|
|
3455
|
+
return [
|
|
3456
|
+
"[role:executor]",
|
|
3457
|
+
"\u4F60\u662F\u8F6F\u4EF6\u6267\u884C\u4EE3\u7406\u3002\u8BF7\u6839\u636E\u8BA1\u5212\u5B8C\u6210\u4EA4\u4ED8\u5185\u5BB9\u3002",
|
|
3458
|
+
"\u8F93\u51FA\u8981\u6C42\uFF1A",
|
|
3459
|
+
"1. \u76F4\u63A5\u7ED9\u51FA\u6700\u7EC8\u53EF\u6267\u884C\u7ED3\u679C",
|
|
3460
|
+
"2. \u8BF4\u660E\u4F60\u5B9E\u9645\u5B8C\u6210\u4E86\u54EA\u4E9B\u6B65\u9AA4",
|
|
3461
|
+
"3. \u5982\u679C\u9700\u8981\u6587\u4EF6\u843D\u76D8\uFF0C\u8BF7\u7ED9\u51FA\u7EDD\u5BF9\u8DEF\u5F84",
|
|
3462
|
+
"",
|
|
3463
|
+
`\u76EE\u6807\uFF1A${objective}`,
|
|
3464
|
+
"",
|
|
3465
|
+
"[planner_plan]",
|
|
3466
|
+
plan,
|
|
3467
|
+
"[/planner_plan]"
|
|
3468
|
+
].join("\n");
|
|
3469
|
+
}
|
|
3470
|
+
function buildReviewerPrompt(objective, plan, output) {
|
|
3471
|
+
return [
|
|
3472
|
+
"[role:reviewer]",
|
|
3473
|
+
"\u4F60\u662F\u8D28\u91CF\u5BA1\u67E5\u4EE3\u7406\u3002\u8BF7\u4E25\u683C\u5BA1\u67E5\u6267\u884C\u7ED3\u679C\u662F\u5426\u8FBE\u6210\u76EE\u6807\u3002",
|
|
3474
|
+
"\u8F93\u51FA\u683C\u5F0F\u5FC5\u987B\u5305\u542B\u4EE5\u4E0B\u5B57\u6BB5\uFF1A",
|
|
3475
|
+
"VERDICT: APPROVED \u6216 REJECTED",
|
|
3476
|
+
"SUMMARY: \u4E00\u53E5\u8BDD\u603B\u7ED3",
|
|
3477
|
+
"ISSUES:",
|
|
3478
|
+
"- issue 1",
|
|
3479
|
+
"- issue 2",
|
|
3480
|
+
"SUGGESTIONS:",
|
|
3481
|
+
"- suggestion 1",
|
|
3482
|
+
"- suggestion 2",
|
|
3483
|
+
"",
|
|
3484
|
+
`\u76EE\u6807\uFF1A${objective}`,
|
|
3485
|
+
"",
|
|
3486
|
+
"[planner_plan]",
|
|
3487
|
+
plan,
|
|
3488
|
+
"[/planner_plan]",
|
|
3489
|
+
"",
|
|
3490
|
+
"[executor_output]",
|
|
3491
|
+
output,
|
|
3492
|
+
"[/executor_output]"
|
|
3493
|
+
].join("\n");
|
|
3494
|
+
}
|
|
3495
|
+
function buildRepairPrompt(objective, plan, previousOutput, reviewerFeedback, round) {
|
|
3496
|
+
return [
|
|
3497
|
+
"[role:executor]",
|
|
3498
|
+
`\u4F60\u662F\u8F6F\u4EF6\u6267\u884C\u4EE3\u7406\u3002\u8BF7\u6839\u636E\u5BA1\u67E5\u53CD\u9988\u8FDB\u884C\u7B2C ${round} \u8F6E\u4FEE\u590D\u5E76\u8F93\u51FA\u6700\u7EC8\u7248\u672C\u3002`,
|
|
3499
|
+
"\u8981\u6C42\uFF1A\u4FDD\u6301\u6B63\u786E\u5185\u5BB9\uFF0C\u4FEE\u590D\u95EE\u9898\uFF0C\u4E0D\u8981\u4E22\u5931\u5DF2\u5B8C\u6210\u90E8\u5206\u3002",
|
|
3500
|
+
"",
|
|
3501
|
+
`\u76EE\u6807\uFF1A${objective}`,
|
|
3502
|
+
"",
|
|
3503
|
+
"[planner_plan]",
|
|
3504
|
+
plan,
|
|
3505
|
+
"[/planner_plan]",
|
|
3506
|
+
"",
|
|
3507
|
+
"[previous_output]",
|
|
3508
|
+
previousOutput,
|
|
3509
|
+
"[/previous_output]",
|
|
3510
|
+
"",
|
|
3511
|
+
"[reviewer_feedback]",
|
|
3512
|
+
reviewerFeedback,
|
|
3513
|
+
"[/reviewer_feedback]"
|
|
3514
|
+
].join("\n");
|
|
3515
|
+
}
|
|
3516
|
+
function parseReviewerVerdict(review) {
|
|
3517
|
+
const approved = /\bVERDICT\s*:\s*APPROVED\b/i.test(review);
|
|
3518
|
+
const rejected = /\bVERDICT\s*:\s*REJECTED\b/i.test(review);
|
|
3519
|
+
if (approved) {
|
|
3520
|
+
return { approved: true, feedback: review };
|
|
3521
|
+
}
|
|
3522
|
+
if (rejected) {
|
|
3523
|
+
return { approved: false, feedback: review };
|
|
3524
|
+
}
|
|
3525
|
+
return {
|
|
3526
|
+
approved: false,
|
|
3527
|
+
feedback: review.trim() || "Reviewer \u672A\u8FD4\u56DE\u89C4\u8303 verdict\uFF0C\u9ED8\u8BA4\u6309 REJECTED \u5904\u7406\u3002"
|
|
3528
|
+
};
|
|
3529
|
+
}
|
|
3530
|
+
function parseWorkflowCommand(text) {
|
|
3531
|
+
const normalized = text.trim();
|
|
3532
|
+
if (!normalized.startsWith("/agents")) {
|
|
3533
|
+
return null;
|
|
3534
|
+
}
|
|
3535
|
+
const parts = normalized.split(/\s+/);
|
|
3536
|
+
if (parts.length === 1 || parts[1]?.toLowerCase() === "status") {
|
|
3537
|
+
return { kind: "status" };
|
|
3538
|
+
}
|
|
3539
|
+
if (parts[1]?.toLowerCase() !== "run") {
|
|
3540
|
+
return null;
|
|
3541
|
+
}
|
|
3542
|
+
const objective = normalized.replace(/^\/agents\s+run\s*/i, "").trim();
|
|
3543
|
+
return {
|
|
3544
|
+
kind: "run",
|
|
3545
|
+
objective
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
function createIdleWorkflowSnapshot() {
|
|
3549
|
+
return {
|
|
3550
|
+
state: "idle",
|
|
3551
|
+
startedAt: null,
|
|
3552
|
+
endedAt: null,
|
|
3553
|
+
objective: null,
|
|
3554
|
+
approved: null,
|
|
3555
|
+
repairRounds: 0,
|
|
3556
|
+
error: null
|
|
3557
|
+
};
|
|
3558
|
+
}
|
|
3559
|
+
|
|
2883
3560
|
// src/orchestrator.ts
|
|
2884
3561
|
var RequestMetrics = class {
|
|
2885
3562
|
total = 0;
|
|
@@ -2965,6 +3642,8 @@ var Orchestrator = class {
|
|
|
2965
3642
|
rateLimiter;
|
|
2966
3643
|
cliCompat;
|
|
2967
3644
|
cliCompatRecorder;
|
|
3645
|
+
workflowRunner;
|
|
3646
|
+
workflowSnapshots = /* @__PURE__ */ new Map();
|
|
2968
3647
|
metrics = new RequestMetrics();
|
|
2969
3648
|
lastLockPruneAt = 0;
|
|
2970
3649
|
constructor(channel, executor, stateStore, logger, options) {
|
|
@@ -3011,6 +3690,10 @@ var Orchestrator = class {
|
|
|
3011
3690
|
maxConcurrentPerRoom: 4
|
|
3012
3691
|
}
|
|
3013
3692
|
);
|
|
3693
|
+
this.workflowRunner = new MultiAgentWorkflowRunner(this.executor, this.logger, {
|
|
3694
|
+
enabled: options?.multiAgentWorkflow?.enabled ?? false,
|
|
3695
|
+
autoRepairMaxRounds: options?.multiAgentWorkflow?.autoRepairMaxRounds ?? 1
|
|
3696
|
+
});
|
|
3014
3697
|
this.sessionRuntime = new CodexSessionRuntime(this.executor);
|
|
3015
3698
|
}
|
|
3016
3699
|
async handleMessage(message) {
|
|
@@ -3054,6 +3737,12 @@ var Orchestrator = class {
|
|
|
3054
3737
|
this.stateStore.markEventProcessed(sessionKey, message.eventId);
|
|
3055
3738
|
return;
|
|
3056
3739
|
}
|
|
3740
|
+
const workflowCommand = this.workflowRunner.isEnabled() ? parseWorkflowCommand(route.prompt) : null;
|
|
3741
|
+
if (workflowCommand?.kind === "status") {
|
|
3742
|
+
await this.handleWorkflowStatusCommand(sessionKey, message);
|
|
3743
|
+
this.stateStore.markEventProcessed(sessionKey, message.eventId);
|
|
3744
|
+
return;
|
|
3745
|
+
}
|
|
3057
3746
|
const rateDecision = this.rateLimiter.tryAcquire({
|
|
3058
3747
|
userId: message.senderId,
|
|
3059
3748
|
roomId: message.conversationId
|
|
@@ -3071,6 +3760,37 @@ var Orchestrator = class {
|
|
|
3071
3760
|
});
|
|
3072
3761
|
return;
|
|
3073
3762
|
}
|
|
3763
|
+
if (workflowCommand?.kind === "run") {
|
|
3764
|
+
const executionStartedAt = Date.now();
|
|
3765
|
+
let sendDurationMs2 = 0;
|
|
3766
|
+
this.stateStore.activateSession(sessionKey, this.sessionActiveWindowMs);
|
|
3767
|
+
try {
|
|
3768
|
+
const sendStartedAt = Date.now();
|
|
3769
|
+
await this.handleWorkflowRunCommand(
|
|
3770
|
+
workflowCommand.objective,
|
|
3771
|
+
sessionKey,
|
|
3772
|
+
message,
|
|
3773
|
+
requestId,
|
|
3774
|
+
roomConfig.workdir
|
|
3775
|
+
);
|
|
3776
|
+
sendDurationMs2 += Date.now() - sendStartedAt;
|
|
3777
|
+
this.stateStore.markEventProcessed(sessionKey, message.eventId);
|
|
3778
|
+
this.metrics.record("success", queueWaitMs, Date.now() - executionStartedAt, sendDurationMs2);
|
|
3779
|
+
} catch (error) {
|
|
3780
|
+
sendDurationMs2 += await this.sendWorkflowFailure(message.conversationId, error);
|
|
3781
|
+
this.stateStore.commitExecutionHandled(sessionKey, message.eventId);
|
|
3782
|
+
const status = classifyExecutionOutcome(error);
|
|
3783
|
+
this.metrics.record(status, queueWaitMs, Date.now() - executionStartedAt, sendDurationMs2);
|
|
3784
|
+
this.logger.error("Workflow request failed", {
|
|
3785
|
+
requestId,
|
|
3786
|
+
sessionKey,
|
|
3787
|
+
error: formatError2(error)
|
|
3788
|
+
});
|
|
3789
|
+
} finally {
|
|
3790
|
+
rateDecision.release?.();
|
|
3791
|
+
}
|
|
3792
|
+
return;
|
|
3793
|
+
}
|
|
3074
3794
|
this.stateStore.activateSession(sessionKey, this.sessionActiveWindowMs);
|
|
3075
3795
|
const previousCodexSessionId = this.stateStore.getCodexSessionId(sessionKey);
|
|
3076
3796
|
const executionPrompt = this.buildExecutionPrompt(route.prompt, message);
|
|
@@ -3291,6 +4011,7 @@ var Orchestrator = class {
|
|
|
3291
4011
|
const metrics = this.metrics.snapshot(this.runningExecutions.size);
|
|
3292
4012
|
const limiter = this.rateLimiter.snapshot();
|
|
3293
4013
|
const runtime = this.sessionRuntime.getRuntimeStats();
|
|
4014
|
+
const workflow = this.workflowSnapshots.get(sessionKey) ?? createIdleWorkflowSnapshot();
|
|
3294
4015
|
await this.channel.sendNotice(
|
|
3295
4016
|
message.conversationId,
|
|
3296
4017
|
`[CodeHarbor] \u5F53\u524D\u72B6\u6001
|
|
@@ -3303,9 +4024,122 @@ var Orchestrator = class {
|
|
|
3303
4024
|
- \u6307\u6807: total=${metrics.total}, success=${metrics.success}, failed=${metrics.failed}, timeout=${metrics.timeout}, cancelled=${metrics.cancelled}, rate_limited=${metrics.rateLimited}
|
|
3304
4025
|
- \u5E73\u5747\u8017\u65F6: queue=${metrics.avgQueueMs}ms, exec=${metrics.avgExecMs}ms, send=${metrics.avgSendMs}ms
|
|
3305
4026
|
- \u9650\u6D41\u5E76\u53D1: global=${limiter.activeGlobal}, users=${limiter.activeUsers}, rooms=${limiter.activeRooms}
|
|
3306
|
-
- CLI runtime: workers=${runtime.workerCount}, running=${runtime.runningCount}, compat_mode=${this.cliCompat.enabled ? "on" : "off"}
|
|
4027
|
+
- CLI runtime: workers=${runtime.workerCount}, running=${runtime.runningCount}, compat_mode=${this.cliCompat.enabled ? "on" : "off"}
|
|
4028
|
+
- Multi-Agent workflow: enabled=${this.workflowRunner.isEnabled() ? "on" : "off"}, state=${workflow.state}`
|
|
4029
|
+
);
|
|
4030
|
+
}
|
|
4031
|
+
async handleWorkflowStatusCommand(sessionKey, message) {
|
|
4032
|
+
const snapshot = this.workflowSnapshots.get(sessionKey) ?? createIdleWorkflowSnapshot();
|
|
4033
|
+
await this.channel.sendNotice(
|
|
4034
|
+
message.conversationId,
|
|
4035
|
+
`[CodeHarbor] Multi-Agent \u5DE5\u4F5C\u6D41\u72B6\u6001
|
|
4036
|
+
- state: ${snapshot.state}
|
|
4037
|
+
- startedAt: ${snapshot.startedAt ?? "N/A"}
|
|
4038
|
+
- endedAt: ${snapshot.endedAt ?? "N/A"}
|
|
4039
|
+
- objective: ${snapshot.objective ?? "N/A"}
|
|
4040
|
+
- approved: ${snapshot.approved === null ? "N/A" : snapshot.approved ? "yes" : "no"}
|
|
4041
|
+
- repairRounds: ${snapshot.repairRounds}
|
|
4042
|
+
- error: ${snapshot.error ?? "N/A"}`
|
|
3307
4043
|
);
|
|
3308
4044
|
}
|
|
4045
|
+
async handleWorkflowRunCommand(objective, sessionKey, message, requestId, workdir) {
|
|
4046
|
+
const normalizedObjective = objective.trim();
|
|
4047
|
+
if (!normalizedObjective) {
|
|
4048
|
+
await this.channel.sendNotice(message.conversationId, "[CodeHarbor] /agents run \u9700\u8981\u63D0\u4F9B\u4EFB\u52A1\u76EE\u6807\u3002");
|
|
4049
|
+
return;
|
|
4050
|
+
}
|
|
4051
|
+
const requestStartedAt = Date.now();
|
|
4052
|
+
let progressNoticeEventId = null;
|
|
4053
|
+
const progressCtx = {
|
|
4054
|
+
conversationId: message.conversationId,
|
|
4055
|
+
isDirectMessage: message.isDirectMessage,
|
|
4056
|
+
getProgressNoticeEventId: () => progressNoticeEventId,
|
|
4057
|
+
setProgressNoticeEventId: (next) => {
|
|
4058
|
+
progressNoticeEventId = next;
|
|
4059
|
+
}
|
|
4060
|
+
};
|
|
4061
|
+
const startedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
4062
|
+
this.workflowSnapshots.set(sessionKey, {
|
|
4063
|
+
state: "running",
|
|
4064
|
+
startedAt: startedAtIso,
|
|
4065
|
+
endedAt: null,
|
|
4066
|
+
objective: normalizedObjective,
|
|
4067
|
+
approved: null,
|
|
4068
|
+
repairRounds: 0,
|
|
4069
|
+
error: null
|
|
4070
|
+
});
|
|
4071
|
+
const stopTyping = this.startTypingHeartbeat(message.conversationId);
|
|
4072
|
+
let cancelWorkflow = () => {
|
|
4073
|
+
};
|
|
4074
|
+
let cancelRequested = false;
|
|
4075
|
+
this.runningExecutions.set(sessionKey, {
|
|
4076
|
+
requestId,
|
|
4077
|
+
startedAt: requestStartedAt,
|
|
4078
|
+
cancel: () => {
|
|
4079
|
+
cancelRequested = true;
|
|
4080
|
+
cancelWorkflow();
|
|
4081
|
+
}
|
|
4082
|
+
});
|
|
4083
|
+
await this.sendProgressUpdate(progressCtx, "[CodeHarbor] Multi-Agent workflow \u542F\u52A8\uFF1APlanner -> Executor -> Reviewer");
|
|
4084
|
+
try {
|
|
4085
|
+
const result = await this.workflowRunner.run({
|
|
4086
|
+
objective: normalizedObjective,
|
|
4087
|
+
workdir,
|
|
4088
|
+
onRegisterCancel: (cancel) => {
|
|
4089
|
+
cancelWorkflow = cancel;
|
|
4090
|
+
if (cancelRequested) {
|
|
4091
|
+
cancelWorkflow();
|
|
4092
|
+
}
|
|
4093
|
+
},
|
|
4094
|
+
onProgress: async (event) => {
|
|
4095
|
+
const stepLabel = event.stage.toUpperCase();
|
|
4096
|
+
await this.sendProgressUpdate(progressCtx, `[CodeHarbor] [${stepLabel}] ${event.message}`);
|
|
4097
|
+
}
|
|
4098
|
+
});
|
|
4099
|
+
const endedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
4100
|
+
this.workflowSnapshots.set(sessionKey, {
|
|
4101
|
+
state: "succeeded",
|
|
4102
|
+
startedAt: startedAtIso,
|
|
4103
|
+
endedAt: endedAtIso,
|
|
4104
|
+
objective: normalizedObjective,
|
|
4105
|
+
approved: result.approved,
|
|
4106
|
+
repairRounds: result.repairRounds,
|
|
4107
|
+
error: null
|
|
4108
|
+
});
|
|
4109
|
+
await this.channel.sendMessage(message.conversationId, buildWorkflowResultReply(result));
|
|
4110
|
+
await this.finishProgress(progressCtx, `\u591A\u667A\u80FD\u4F53\u6D41\u7A0B\u5B8C\u6210\uFF08\u8017\u65F6 ${formatDurationMs(Date.now() - requestStartedAt)}\uFF09`);
|
|
4111
|
+
} catch (error) {
|
|
4112
|
+
const status = classifyExecutionOutcome(error);
|
|
4113
|
+
const endedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
4114
|
+
this.workflowSnapshots.set(sessionKey, {
|
|
4115
|
+
state: status === "cancelled" ? "idle" : "failed",
|
|
4116
|
+
startedAt: startedAtIso,
|
|
4117
|
+
endedAt: endedAtIso,
|
|
4118
|
+
objective: normalizedObjective,
|
|
4119
|
+
approved: null,
|
|
4120
|
+
repairRounds: 0,
|
|
4121
|
+
error: formatError2(error)
|
|
4122
|
+
});
|
|
4123
|
+
await this.finishProgress(progressCtx, buildFailureProgressSummary(status, requestStartedAt, error));
|
|
4124
|
+
throw error;
|
|
4125
|
+
} finally {
|
|
4126
|
+
const running = this.runningExecutions.get(sessionKey);
|
|
4127
|
+
if (running?.requestId === requestId) {
|
|
4128
|
+
this.runningExecutions.delete(sessionKey);
|
|
4129
|
+
}
|
|
4130
|
+
await stopTyping();
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
async sendWorkflowFailure(conversationId, error) {
|
|
4134
|
+
const startedAt = Date.now();
|
|
4135
|
+
const status = classifyExecutionOutcome(error);
|
|
4136
|
+
if (status === "cancelled") {
|
|
4137
|
+
await this.channel.sendNotice(conversationId, "[CodeHarbor] Multi-Agent workflow \u5DF2\u53D6\u6D88\u3002");
|
|
4138
|
+
return Date.now() - startedAt;
|
|
4139
|
+
}
|
|
4140
|
+
await this.channel.sendMessage(conversationId, `[CodeHarbor] Multi-Agent workflow \u5931\u8D25: ${formatError2(error)}`);
|
|
4141
|
+
return Date.now() - startedAt;
|
|
4142
|
+
}
|
|
3309
4143
|
async handleStopCommand(sessionKey, message, requestId) {
|
|
3310
4144
|
this.stateStore.deactivateSession(sessionKey);
|
|
3311
4145
|
this.stateStore.clearCodexSessionId(sessionKey);
|
|
@@ -3619,10 +4453,29 @@ function buildFailureProgressSummary(status, startedAt, error) {
|
|
|
3619
4453
|
}
|
|
3620
4454
|
return `\u5904\u7406\u5931\u8D25\uFF08\u8017\u65F6 ${elapsed}\uFF09: ${formatError2(error)}`;
|
|
3621
4455
|
}
|
|
4456
|
+
function buildWorkflowResultReply(result) {
|
|
4457
|
+
return `[CodeHarbor] Multi-Agent workflow \u5B8C\u6210
|
|
4458
|
+
- objective: ${result.objective}
|
|
4459
|
+
- approved: ${result.approved ? "yes" : "no"}
|
|
4460
|
+
- repairRounds: ${result.repairRounds}
|
|
4461
|
+
- duration: ${formatDurationMs(result.durationMs)}
|
|
4462
|
+
|
|
4463
|
+
[planner]
|
|
4464
|
+
${result.plan}
|
|
4465
|
+
[/planner]
|
|
4466
|
+
|
|
4467
|
+
[executor]
|
|
4468
|
+
${result.output}
|
|
4469
|
+
[/executor]
|
|
4470
|
+
|
|
4471
|
+
[reviewer]
|
|
4472
|
+
${result.review}
|
|
4473
|
+
[/reviewer]`;
|
|
4474
|
+
}
|
|
3622
4475
|
|
|
3623
4476
|
// src/store/state-store.ts
|
|
3624
|
-
var
|
|
3625
|
-
var
|
|
4477
|
+
var import_node_fs7 = __toESM(require("fs"));
|
|
4478
|
+
var import_node_path8 = __toESM(require("path"));
|
|
3626
4479
|
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
3627
4480
|
var PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
3628
4481
|
var SQLITE_MODULE_ID = `node:${"sqlite"}`;
|
|
@@ -3648,7 +4501,7 @@ var StateStore = class {
|
|
|
3648
4501
|
this.maxProcessedEventsPerSession = maxProcessedEventsPerSession;
|
|
3649
4502
|
this.maxSessionAgeMs = maxSessionAgeDays * ONE_DAY_MS;
|
|
3650
4503
|
this.maxSessions = maxSessions;
|
|
3651
|
-
|
|
4504
|
+
import_node_fs7.default.mkdirSync(import_node_path8.default.dirname(this.dbPath), { recursive: true });
|
|
3652
4505
|
this.db = new DatabaseSync(this.dbPath);
|
|
3653
4506
|
this.initializeSchema();
|
|
3654
4507
|
this.importLegacyStateIfNeeded();
|
|
@@ -3893,7 +4746,7 @@ var StateStore = class {
|
|
|
3893
4746
|
`);
|
|
3894
4747
|
}
|
|
3895
4748
|
importLegacyStateIfNeeded() {
|
|
3896
|
-
if (!this.legacyJsonPath || !
|
|
4749
|
+
if (!this.legacyJsonPath || !import_node_fs7.default.existsSync(this.legacyJsonPath)) {
|
|
3897
4750
|
return;
|
|
3898
4751
|
}
|
|
3899
4752
|
const countRow = this.db.prepare("SELECT COUNT(*) AS count FROM sessions").get();
|
|
@@ -3976,7 +4829,7 @@ var StateStore = class {
|
|
|
3976
4829
|
};
|
|
3977
4830
|
function loadLegacyState(filePath) {
|
|
3978
4831
|
try {
|
|
3979
|
-
const raw =
|
|
4832
|
+
const raw = import_node_fs7.default.readFileSync(filePath, "utf8");
|
|
3980
4833
|
const parsed = JSON.parse(raw);
|
|
3981
4834
|
if (!parsed.sessions || typeof parsed.sessions !== "object") {
|
|
3982
4835
|
return null;
|
|
@@ -4019,7 +4872,7 @@ function boolToInt(value) {
|
|
|
4019
4872
|
}
|
|
4020
4873
|
|
|
4021
4874
|
// src/app.ts
|
|
4022
|
-
var execFileAsync2 = (0, import_node_util2.promisify)(
|
|
4875
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process4.execFile);
|
|
4023
4876
|
var CodeHarborApp = class {
|
|
4024
4877
|
config;
|
|
4025
4878
|
logger;
|
|
@@ -4061,6 +4914,7 @@ var CodeHarborApp = class {
|
|
|
4061
4914
|
roomTriggerPolicies: config.roomTriggerPolicies,
|
|
4062
4915
|
rateLimiterOptions: config.rateLimiter,
|
|
4063
4916
|
cliCompat: config.cliCompat,
|
|
4917
|
+
multiAgentWorkflow: config.agentWorkflow,
|
|
4064
4918
|
configService: this.configService,
|
|
4065
4919
|
defaultCodexWorkdir: config.codexWorkdir
|
|
4066
4920
|
});
|
|
@@ -4167,8 +5021,8 @@ function isNonLoopbackHost(host) {
|
|
|
4167
5021
|
}
|
|
4168
5022
|
|
|
4169
5023
|
// src/config.ts
|
|
4170
|
-
var
|
|
4171
|
-
var
|
|
5024
|
+
var import_node_fs8 = __toESM(require("fs"));
|
|
5025
|
+
var import_node_path9 = __toESM(require("path"));
|
|
4172
5026
|
var import_dotenv2 = __toESM(require("dotenv"));
|
|
4173
5027
|
var import_zod = require("zod");
|
|
4174
5028
|
var configSchema = import_zod.z.object({
|
|
@@ -4185,6 +5039,8 @@ var configSchema = import_zod.z.object({
|
|
|
4185
5039
|
CODEX_APPROVAL_POLICY: import_zod.z.string().optional(),
|
|
4186
5040
|
CODEX_EXTRA_ARGS: import_zod.z.string().default(""),
|
|
4187
5041
|
CODEX_EXTRA_ENV_JSON: import_zod.z.string().default(""),
|
|
5042
|
+
AGENT_WORKFLOW_ENABLED: import_zod.z.string().default("false").transform((v) => v.toLowerCase() === "true"),
|
|
5043
|
+
AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS: import_zod.z.string().default("1").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().min(0).max(10)),
|
|
4188
5044
|
STATE_DB_PATH: import_zod.z.string().default("data/state.db"),
|
|
4189
5045
|
STATE_PATH: import_zod.z.string().default("data/state.json"),
|
|
4190
5046
|
MAX_PROCESSED_EVENTS_PER_SESSION: import_zod.z.string().default("200").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
@@ -4227,15 +5083,19 @@ var configSchema = import_zod.z.object({
|
|
|
4227
5083
|
matrixCommandPrefix: v.MATRIX_COMMAND_PREFIX,
|
|
4228
5084
|
codexBin: v.CODEX_BIN,
|
|
4229
5085
|
codexModel: v.CODEX_MODEL?.trim() || null,
|
|
4230
|
-
codexWorkdir:
|
|
5086
|
+
codexWorkdir: import_node_path9.default.resolve(v.CODEX_WORKDIR),
|
|
4231
5087
|
codexDangerousBypass: v.CODEX_DANGEROUS_BYPASS,
|
|
4232
5088
|
codexExecTimeoutMs: v.CODEX_EXEC_TIMEOUT_MS,
|
|
4233
5089
|
codexSandboxMode: v.CODEX_SANDBOX_MODE?.trim() || null,
|
|
4234
5090
|
codexApprovalPolicy: v.CODEX_APPROVAL_POLICY?.trim() || null,
|
|
4235
5091
|
codexExtraArgs: parseExtraArgs(v.CODEX_EXTRA_ARGS),
|
|
4236
5092
|
codexExtraEnv: parseExtraEnv(v.CODEX_EXTRA_ENV_JSON),
|
|
4237
|
-
|
|
4238
|
-
|
|
5093
|
+
agentWorkflow: {
|
|
5094
|
+
enabled: v.AGENT_WORKFLOW_ENABLED,
|
|
5095
|
+
autoRepairMaxRounds: v.AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS
|
|
5096
|
+
},
|
|
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,
|
|
4239
5099
|
maxProcessedEventsPerSession: v.MAX_PROCESSED_EVENTS_PER_SESSION,
|
|
4240
5100
|
maxSessionAgeDays: v.MAX_SESSION_AGE_DAYS,
|
|
4241
5101
|
maxSessions: v.MAX_SESSIONS,
|
|
@@ -4266,7 +5126,7 @@ var configSchema = import_zod.z.object({
|
|
|
4266
5126
|
disableReplyChunkSplit: v.CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT,
|
|
4267
5127
|
progressThrottleMs: v.CLI_COMPAT_PROGRESS_THROTTLE_MS,
|
|
4268
5128
|
fetchMedia: v.CLI_COMPAT_FETCH_MEDIA,
|
|
4269
|
-
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
|
|
4270
5130
|
},
|
|
4271
5131
|
doctorHttpTimeoutMs: v.DOCTOR_HTTP_TIMEOUT_MS,
|
|
4272
5132
|
adminBindHost: v.ADMIN_BIND_HOST.trim() || "127.0.0.1",
|
|
@@ -4276,7 +5136,7 @@ var configSchema = import_zod.z.object({
|
|
|
4276
5136
|
adminAllowedOrigins: parseCsvList(v.ADMIN_ALLOWED_ORIGINS),
|
|
4277
5137
|
logLevel: v.LOG_LEVEL
|
|
4278
5138
|
}));
|
|
4279
|
-
function loadEnvFromFile(filePath =
|
|
5139
|
+
function loadEnvFromFile(filePath = import_node_path9.default.resolve(process.cwd(), ".env"), env = process.env) {
|
|
4280
5140
|
import_dotenv2.default.config({
|
|
4281
5141
|
path: filePath,
|
|
4282
5142
|
processEnv: env,
|
|
@@ -4289,9 +5149,9 @@ function loadConfig(env = process.env) {
|
|
|
4289
5149
|
const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "config"}: ${issue.message}`).join("; ");
|
|
4290
5150
|
throw new Error(`Invalid configuration: ${message}`);
|
|
4291
5151
|
}
|
|
4292
|
-
|
|
5152
|
+
import_node_fs8.default.mkdirSync(import_node_path9.default.dirname(parsed.data.stateDbPath), { recursive: true });
|
|
4293
5153
|
if (parsed.data.legacyStateJsonPath) {
|
|
4294
|
-
|
|
5154
|
+
import_node_fs8.default.mkdirSync(import_node_path9.default.dirname(parsed.data.legacyStateJsonPath), { recursive: true });
|
|
4295
5155
|
}
|
|
4296
5156
|
return parsed.data;
|
|
4297
5157
|
}
|
|
@@ -4364,8 +5224,8 @@ function parseCsvList(raw) {
|
|
|
4364
5224
|
}
|
|
4365
5225
|
|
|
4366
5226
|
// src/config-snapshot.ts
|
|
4367
|
-
var
|
|
4368
|
-
var
|
|
5227
|
+
var import_node_fs9 = __toESM(require("fs"));
|
|
5228
|
+
var import_node_path10 = __toESM(require("path"));
|
|
4369
5229
|
var import_zod2 = require("zod");
|
|
4370
5230
|
var CONFIG_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
4371
5231
|
var CONFIG_SNAPSHOT_ENV_KEYS = [
|
|
@@ -4382,6 +5242,8 @@ var CONFIG_SNAPSHOT_ENV_KEYS = [
|
|
|
4382
5242
|
"CODEX_APPROVAL_POLICY",
|
|
4383
5243
|
"CODEX_EXTRA_ARGS",
|
|
4384
5244
|
"CODEX_EXTRA_ENV_JSON",
|
|
5245
|
+
"AGENT_WORKFLOW_ENABLED",
|
|
5246
|
+
"AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS",
|
|
4385
5247
|
"STATE_DB_PATH",
|
|
4386
5248
|
"STATE_PATH",
|
|
4387
5249
|
"MAX_PROCESSED_EVENTS_PER_SESSION",
|
|
@@ -4444,6 +5306,8 @@ var envSnapshotSchema = import_zod2.z.object({
|
|
|
4444
5306
|
CODEX_APPROVAL_POLICY: import_zod2.z.string(),
|
|
4445
5307
|
CODEX_EXTRA_ARGS: import_zod2.z.string(),
|
|
4446
5308
|
CODEX_EXTRA_ENV_JSON: jsonObjectStringSchema("CODEX_EXTRA_ENV_JSON", true),
|
|
5309
|
+
AGENT_WORKFLOW_ENABLED: booleanStringSchema("AGENT_WORKFLOW_ENABLED"),
|
|
5310
|
+
AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS: integerStringSchema("AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS", 0, 10),
|
|
4447
5311
|
STATE_DB_PATH: import_zod2.z.string().min(1),
|
|
4448
5312
|
STATE_PATH: import_zod2.z.string(),
|
|
4449
5313
|
MAX_PROCESSED_EVENTS_PER_SESSION: integerStringSchema("MAX_PROCESSED_EVENTS_PER_SESSION", 1),
|
|
@@ -4548,9 +5412,9 @@ async function runConfigExportCommand(options = {}) {
|
|
|
4548
5412
|
const snapshot = buildConfigSnapshot(config, stateStore.listRoomSettings(), options.now ?? /* @__PURE__ */ new Date());
|
|
4549
5413
|
const serialized = serializeConfigSnapshot(snapshot);
|
|
4550
5414
|
if (options.outputPath) {
|
|
4551
|
-
const targetPath =
|
|
4552
|
-
|
|
4553
|
-
|
|
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");
|
|
4554
5418
|
output.write(`Exported config snapshot to ${targetPath}
|
|
4555
5419
|
`);
|
|
4556
5420
|
return;
|
|
@@ -4564,8 +5428,8 @@ async function runConfigImportCommand(options) {
|
|
|
4564
5428
|
const cwd = options.cwd ?? process.cwd();
|
|
4565
5429
|
const output = options.output ?? process.stdout;
|
|
4566
5430
|
const actor = options.actor?.trim() || "cli:config-import";
|
|
4567
|
-
const sourcePath =
|
|
4568
|
-
if (!
|
|
5431
|
+
const sourcePath = import_node_path10.default.resolve(cwd, options.filePath);
|
|
5432
|
+
if (!import_node_fs9.default.existsSync(sourcePath)) {
|
|
4569
5433
|
throw new Error(`Config snapshot file not found: ${sourcePath}`);
|
|
4570
5434
|
}
|
|
4571
5435
|
const snapshot = parseConfigSnapshot(parseJsonFile(sourcePath));
|
|
@@ -4595,7 +5459,7 @@ async function runConfigImportCommand(options) {
|
|
|
4595
5459
|
synchronizeRoomSettings(stateStore, normalizedRooms);
|
|
4596
5460
|
stateStore.appendConfigRevision(
|
|
4597
5461
|
actor,
|
|
4598
|
-
`import config snapshot from ${
|
|
5462
|
+
`import config snapshot from ${import_node_path10.default.basename(sourcePath)}`,
|
|
4599
5463
|
JSON.stringify({
|
|
4600
5464
|
type: "config_snapshot_import",
|
|
4601
5465
|
sourcePath,
|
|
@@ -4609,7 +5473,7 @@ async function runConfigImportCommand(options) {
|
|
|
4609
5473
|
output.write(
|
|
4610
5474
|
[
|
|
4611
5475
|
`Imported config snapshot from ${sourcePath}`,
|
|
4612
|
-
`- updated .env in ${
|
|
5476
|
+
`- updated .env in ${import_node_path10.default.resolve(cwd, ".env")}`,
|
|
4613
5477
|
`- synchronized room settings: ${normalizedRooms.length}`,
|
|
4614
5478
|
"- restart required: yes (global env settings are restart-scoped)"
|
|
4615
5479
|
].join("\n") + "\n"
|
|
@@ -4630,6 +5494,8 @@ function buildSnapshotEnv(config) {
|
|
|
4630
5494
|
CODEX_APPROVAL_POLICY: config.codexApprovalPolicy ?? "",
|
|
4631
5495
|
CODEX_EXTRA_ARGS: config.codexExtraArgs.join(" "),
|
|
4632
5496
|
CODEX_EXTRA_ENV_JSON: serializeJsonObject(config.codexExtraEnv),
|
|
5497
|
+
AGENT_WORKFLOW_ENABLED: String(config.agentWorkflow.enabled),
|
|
5498
|
+
AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS: String(config.agentWorkflow.autoRepairMaxRounds),
|
|
4633
5499
|
STATE_DB_PATH: config.stateDbPath,
|
|
4634
5500
|
STATE_PATH: config.legacyStateJsonPath ?? "",
|
|
4635
5501
|
MAX_PROCESSED_EVENTS_PER_SESSION: String(config.maxProcessedEventsPerSession),
|
|
@@ -4669,7 +5535,7 @@ function buildSnapshotEnv(config) {
|
|
|
4669
5535
|
}
|
|
4670
5536
|
function parseJsonFile(filePath) {
|
|
4671
5537
|
try {
|
|
4672
|
-
const raw =
|
|
5538
|
+
const raw = import_node_fs9.default.readFileSync(filePath, "utf8");
|
|
4673
5539
|
return JSON.parse(raw);
|
|
4674
5540
|
} catch (error) {
|
|
4675
5541
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -4681,525 +5547,233 @@ function parseJsonFile(filePath) {
|
|
|
4681
5547
|
function normalizeSnapshotEnv(env, cwd) {
|
|
4682
5548
|
return {
|
|
4683
5549
|
...env,
|
|
4684
|
-
CODEX_WORKDIR:
|
|
4685
|
-
STATE_DB_PATH:
|
|
4686
|
-
STATE_PATH: env.STATE_PATH.trim() ?
|
|
4687
|
-
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) : ""
|
|
4688
5554
|
};
|
|
4689
5555
|
}
|
|
4690
5556
|
function normalizeSnapshotRooms(rooms, cwd) {
|
|
4691
|
-
const normalized = [];
|
|
4692
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4693
|
-
for (const room of rooms) {
|
|
4694
|
-
const roomId = room.roomId.trim();
|
|
4695
|
-
if (!roomId) {
|
|
4696
|
-
throw new Error("roomId is required for every room in snapshot.");
|
|
4697
|
-
}
|
|
4698
|
-
if (seen.has(roomId)) {
|
|
4699
|
-
throw new Error(`Duplicate roomId in snapshot: ${roomId}`);
|
|
4700
|
-
}
|
|
4701
|
-
seen.add(roomId);
|
|
4702
|
-
const workdir = import_node_path8.default.resolve(cwd, room.workdir);
|
|
4703
|
-
ensureDirectory2(workdir, `room workdir (${roomId})`);
|
|
4704
|
-
normalized.push({
|
|
4705
|
-
roomId,
|
|
4706
|
-
enabled: room.enabled,
|
|
4707
|
-
allowMention: room.allowMention,
|
|
4708
|
-
allowReply: room.allowReply,
|
|
4709
|
-
allowActiveWindow: room.allowActiveWindow,
|
|
4710
|
-
allowPrefix: room.allowPrefix,
|
|
4711
|
-
workdir
|
|
4712
|
-
});
|
|
4713
|
-
}
|
|
4714
|
-
return normalized;
|
|
4715
|
-
}
|
|
4716
|
-
function synchronizeRoomSettings(stateStore, rooms) {
|
|
4717
|
-
const incoming = new Map(rooms.map((room) => [room.roomId, room]));
|
|
4718
|
-
const existing = stateStore.listRoomSettings();
|
|
4719
|
-
for (const room of existing) {
|
|
4720
|
-
if (!incoming.has(room.roomId)) {
|
|
4721
|
-
stateStore.deleteRoomSettings(room.roomId);
|
|
4722
|
-
}
|
|
4723
|
-
}
|
|
4724
|
-
for (const room of rooms) {
|
|
4725
|
-
stateStore.upsertRoomSettings(room);
|
|
4726
|
-
}
|
|
4727
|
-
}
|
|
4728
|
-
function persistEnvSnapshot(cwd, env) {
|
|
4729
|
-
const envPath = import_node_path8.default.resolve(cwd, ".env");
|
|
4730
|
-
const examplePath = import_node_path8.default.resolve(cwd, ".env.example");
|
|
4731
|
-
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") : "";
|
|
4732
|
-
const overrides = {};
|
|
4733
|
-
for (const key of CONFIG_SNAPSHOT_ENV_KEYS) {
|
|
4734
|
-
overrides[key] = env[key];
|
|
4735
|
-
}
|
|
4736
|
-
const next = applyEnvOverrides(template, overrides);
|
|
4737
|
-
import_node_fs7.default.writeFileSync(envPath, next, "utf8");
|
|
4738
|
-
}
|
|
4739
|
-
function ensureDirectory2(dirPath, label) {
|
|
4740
|
-
if (!import_node_fs7.default.existsSync(dirPath) || !import_node_fs7.default.statSync(dirPath).isDirectory()) {
|
|
4741
|
-
throw new Error(`${label} does not exist or is not a directory: ${dirPath}`);
|
|
4742
|
-
}
|
|
4743
|
-
}
|
|
4744
|
-
function parseIntStrict(raw) {
|
|
4745
|
-
const value = Number.parseInt(raw, 10);
|
|
4746
|
-
if (!Number.isFinite(value)) {
|
|
4747
|
-
throw new Error(`Invalid integer value: ${raw}`);
|
|
4748
|
-
}
|
|
4749
|
-
return value;
|
|
4750
|
-
}
|
|
4751
|
-
function serializeJsonObject(value) {
|
|
4752
|
-
return Object.keys(value).length > 0 ? JSON.stringify(value) : "";
|
|
4753
|
-
}
|
|
4754
|
-
function booleanStringSchema(key) {
|
|
4755
|
-
return import_zod2.z.string().refine((value) => BOOLEAN_STRING.test(value), {
|
|
4756
|
-
message: `${key} must be a boolean string (true/false).`
|
|
4757
|
-
});
|
|
4758
|
-
}
|
|
4759
|
-
function integerStringSchema(key, min, max = Number.MAX_SAFE_INTEGER) {
|
|
4760
|
-
return import_zod2.z.string().refine((value) => {
|
|
4761
|
-
const trimmed = value.trim();
|
|
4762
|
-
if (!INTEGER_STRING.test(trimmed)) {
|
|
4763
|
-
return false;
|
|
4764
|
-
}
|
|
4765
|
-
const parsed = Number.parseInt(trimmed, 10);
|
|
4766
|
-
if (!Number.isFinite(parsed)) {
|
|
4767
|
-
return false;
|
|
4768
|
-
}
|
|
4769
|
-
return parsed >= min && parsed <= max;
|
|
4770
|
-
}, {
|
|
4771
|
-
message: `${key} must be an integer string in range [${min}, ${max}].`
|
|
4772
|
-
});
|
|
4773
|
-
}
|
|
4774
|
-
function jsonObjectStringSchema(key, allowEmpty) {
|
|
4775
|
-
return import_zod2.z.string().refine((value) => {
|
|
4776
|
-
const trimmed = value.trim();
|
|
4777
|
-
if (!trimmed) {
|
|
4778
|
-
return allowEmpty;
|
|
4779
|
-
}
|
|
4780
|
-
let parsed;
|
|
4781
|
-
try {
|
|
4782
|
-
parsed = JSON.parse(trimmed);
|
|
4783
|
-
} catch {
|
|
4784
|
-
return false;
|
|
4785
|
-
}
|
|
4786
|
-
return Boolean(parsed) && typeof parsed === "object" && !Array.isArray(parsed);
|
|
4787
|
-
}, {
|
|
4788
|
-
message: `${key} must be an empty string or a JSON object string.`
|
|
4789
|
-
});
|
|
4790
|
-
}
|
|
4791
|
-
|
|
4792
|
-
// src/preflight.ts
|
|
4793
|
-
var import_node_child_process4 = require("child_process");
|
|
4794
|
-
var import_node_fs8 = __toESM(require("fs"));
|
|
4795
|
-
var import_node_path9 = __toESM(require("path"));
|
|
4796
|
-
var import_node_util3 = require("util");
|
|
4797
|
-
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
|
|
4798
|
-
var REQUIRED_ENV_KEYS = ["MATRIX_HOMESERVER", "MATRIX_USER_ID", "MATRIX_ACCESS_TOKEN"];
|
|
4799
|
-
async function runStartupPreflight(options = {}) {
|
|
4800
|
-
const env = options.env ?? process.env;
|
|
4801
|
-
const cwd = options.cwd ?? process.cwd();
|
|
4802
|
-
const checkCodexBinary = options.checkCodexBinary ?? defaultCheckCodexBinary;
|
|
4803
|
-
const fileExists = options.fileExists ?? import_node_fs8.default.existsSync;
|
|
4804
|
-
const isDirectory = options.isDirectory ?? defaultIsDirectory;
|
|
4805
|
-
const issues = [];
|
|
4806
|
-
const envPath = import_node_path9.default.resolve(cwd, ".env");
|
|
4807
|
-
if (!fileExists(envPath)) {
|
|
4808
|
-
issues.push({
|
|
4809
|
-
level: "warn",
|
|
4810
|
-
code: "missing_dotenv",
|
|
4811
|
-
check: ".env",
|
|
4812
|
-
message: `No .env file found at ${envPath}.`,
|
|
4813
|
-
fix: 'Run "codeharbor init" to create baseline config.'
|
|
4814
|
-
});
|
|
4815
|
-
}
|
|
4816
|
-
for (const key of REQUIRED_ENV_KEYS) {
|
|
4817
|
-
if (!readEnv(env, key)) {
|
|
4818
|
-
issues.push({
|
|
4819
|
-
level: "error",
|
|
4820
|
-
code: "missing_env",
|
|
4821
|
-
check: key,
|
|
4822
|
-
message: `${key} is required.`,
|
|
4823
|
-
fix: `Run "codeharbor init" or set ${key} in .env.`
|
|
4824
|
-
});
|
|
4825
|
-
}
|
|
4826
|
-
}
|
|
4827
|
-
const matrixHomeserver = readEnv(env, "MATRIX_HOMESERVER");
|
|
4828
|
-
if (matrixHomeserver) {
|
|
4829
|
-
try {
|
|
4830
|
-
new URL(matrixHomeserver);
|
|
4831
|
-
} catch {
|
|
4832
|
-
issues.push({
|
|
4833
|
-
level: "error",
|
|
4834
|
-
code: "invalid_matrix_homeserver",
|
|
4835
|
-
check: "MATRIX_HOMESERVER",
|
|
4836
|
-
message: `Invalid URL: "${matrixHomeserver}".`,
|
|
4837
|
-
fix: "Set MATRIX_HOMESERVER to a full URL, for example https://matrix.example.com."
|
|
4838
|
-
});
|
|
4839
|
-
}
|
|
4840
|
-
}
|
|
4841
|
-
const matrixUserId = readEnv(env, "MATRIX_USER_ID");
|
|
4842
|
-
if (matrixUserId && !/^@[^:\s]+:.+/.test(matrixUserId)) {
|
|
4843
|
-
issues.push({
|
|
4844
|
-
level: "error",
|
|
4845
|
-
code: "invalid_matrix_user_id",
|
|
4846
|
-
check: "MATRIX_USER_ID",
|
|
4847
|
-
message: `Unexpected Matrix user id format: "${matrixUserId}".`,
|
|
4848
|
-
fix: "Set MATRIX_USER_ID like @bot:example.com."
|
|
4849
|
-
});
|
|
4850
|
-
}
|
|
4851
|
-
const codexBin = readEnv(env, "CODEX_BIN") || "codex";
|
|
4852
|
-
try {
|
|
4853
|
-
await checkCodexBinary(codexBin);
|
|
4854
|
-
} catch (error) {
|
|
4855
|
-
const reason = error instanceof Error && error.message ? ` (${error.message})` : "";
|
|
4856
|
-
issues.push({
|
|
4857
|
-
level: "error",
|
|
4858
|
-
code: "missing_codex_bin",
|
|
4859
|
-
check: "CODEX_BIN",
|
|
4860
|
-
message: `Unable to execute "${codexBin}"${reason}.`,
|
|
4861
|
-
fix: `Install Codex CLI and ensure "${codexBin}" is in PATH, or set CODEX_BIN=/absolute/path/to/codex.`
|
|
4862
|
-
});
|
|
4863
|
-
}
|
|
4864
|
-
const configuredWorkdir = readEnv(env, "CODEX_WORKDIR");
|
|
4865
|
-
const workdir = import_node_path9.default.resolve(cwd, configuredWorkdir || cwd);
|
|
4866
|
-
if (!fileExists(workdir) || !isDirectory(workdir)) {
|
|
4867
|
-
issues.push({
|
|
4868
|
-
level: "error",
|
|
4869
|
-
code: "invalid_codex_workdir",
|
|
4870
|
-
check: "CODEX_WORKDIR",
|
|
4871
|
-
message: `Working directory does not exist or is not a directory: ${workdir}.`,
|
|
4872
|
-
fix: `Set CODEX_WORKDIR to an existing directory, for example CODEX_WORKDIR=${cwd}.`
|
|
4873
|
-
});
|
|
4874
|
-
}
|
|
4875
|
-
return {
|
|
4876
|
-
ok: issues.every((issue) => issue.level !== "error"),
|
|
4877
|
-
issues
|
|
4878
|
-
};
|
|
4879
|
-
}
|
|
4880
|
-
function formatPreflightReport(result, commandName) {
|
|
4881
|
-
const lines = [];
|
|
4882
|
-
const errors = result.issues.filter((issue) => issue.level === "error").length;
|
|
4883
|
-
const warnings = result.issues.filter((issue) => issue.level === "warn").length;
|
|
4884
|
-
if (result.ok) {
|
|
4885
|
-
lines.push(`Preflight check passed for "codeharbor ${commandName}" with ${warnings} warning(s).`);
|
|
4886
|
-
} else {
|
|
4887
|
-
lines.push(
|
|
4888
|
-
`Preflight check failed for "codeharbor ${commandName}" with ${errors} error(s) and ${warnings} warning(s).`
|
|
4889
|
-
);
|
|
4890
|
-
}
|
|
4891
|
-
for (const issue of result.issues) {
|
|
4892
|
-
const level = issue.level.toUpperCase();
|
|
4893
|
-
lines.push(`- [${level}] ${issue.check}: ${issue.message}`);
|
|
4894
|
-
lines.push(` fix: ${issue.fix}`);
|
|
4895
|
-
}
|
|
4896
|
-
return `${lines.join("\n")}
|
|
4897
|
-
`;
|
|
4898
|
-
}
|
|
4899
|
-
async function defaultCheckCodexBinary(bin) {
|
|
4900
|
-
await execFileAsync3(bin, ["--version"]);
|
|
4901
|
-
}
|
|
4902
|
-
function defaultIsDirectory(targetPath) {
|
|
4903
|
-
try {
|
|
4904
|
-
return import_node_fs8.default.statSync(targetPath).isDirectory();
|
|
4905
|
-
} catch {
|
|
4906
|
-
return false;
|
|
4907
|
-
}
|
|
4908
|
-
}
|
|
4909
|
-
function readEnv(env, key) {
|
|
4910
|
-
return env[key]?.trim() ?? "";
|
|
4911
|
-
}
|
|
4912
|
-
|
|
4913
|
-
// src/runtime-home.ts
|
|
4914
|
-
var import_node_fs9 = __toESM(require("fs"));
|
|
4915
|
-
var import_node_os2 = __toESM(require("os"));
|
|
4916
|
-
var import_node_path10 = __toESM(require("path"));
|
|
4917
|
-
var LEGACY_RUNTIME_HOME = "/opt/codeharbor";
|
|
4918
|
-
var USER_RUNTIME_HOME_DIR = ".codeharbor";
|
|
4919
|
-
var DEFAULT_RUNTIME_HOME = import_node_path10.default.resolve(import_node_os2.default.homedir(), USER_RUNTIME_HOME_DIR);
|
|
4920
|
-
var RUNTIME_HOME_ENV_KEY = "CODEHARBOR_HOME";
|
|
4921
|
-
function resolveRuntimeHome(env = process.env) {
|
|
4922
|
-
const configured = env[RUNTIME_HOME_ENV_KEY]?.trim();
|
|
4923
|
-
if (configured) {
|
|
4924
|
-
return import_node_path10.default.resolve(configured);
|
|
4925
|
-
}
|
|
4926
|
-
const legacyEnvPath = import_node_path10.default.resolve(LEGACY_RUNTIME_HOME, ".env");
|
|
4927
|
-
if (import_node_fs9.default.existsSync(legacyEnvPath)) {
|
|
4928
|
-
return LEGACY_RUNTIME_HOME;
|
|
4929
|
-
}
|
|
4930
|
-
return resolveUserRuntimeHome(env);
|
|
4931
|
-
}
|
|
4932
|
-
function resolveUserRuntimeHome(env = process.env) {
|
|
4933
|
-
const home = env.HOME?.trim() || import_node_os2.default.homedir();
|
|
4934
|
-
return import_node_path10.default.resolve(home, USER_RUNTIME_HOME_DIR);
|
|
4935
|
-
}
|
|
4936
|
-
|
|
4937
|
-
// src/service-manager.ts
|
|
4938
|
-
var import_node_child_process5 = require("child_process");
|
|
4939
|
-
var import_node_fs10 = __toESM(require("fs"));
|
|
4940
|
-
var import_node_os3 = __toESM(require("os"));
|
|
4941
|
-
var import_node_path11 = __toESM(require("path"));
|
|
4942
|
-
var SYSTEMD_DIR = "/etc/systemd/system";
|
|
4943
|
-
var MAIN_SERVICE_NAME = "codeharbor.service";
|
|
4944
|
-
var ADMIN_SERVICE_NAME = "codeharbor-admin.service";
|
|
4945
|
-
function resolveDefaultRunUser(env = process.env) {
|
|
4946
|
-
const sudoUser = env.SUDO_USER?.trim();
|
|
4947
|
-
if (sudoUser) {
|
|
4948
|
-
return sudoUser;
|
|
4949
|
-
}
|
|
4950
|
-
const user = env.USER?.trim();
|
|
4951
|
-
if (user) {
|
|
4952
|
-
return user;
|
|
4953
|
-
}
|
|
4954
|
-
try {
|
|
4955
|
-
return import_node_os3.default.userInfo().username;
|
|
4956
|
-
} catch {
|
|
4957
|
-
return "root";
|
|
4958
|
-
}
|
|
4959
|
-
}
|
|
4960
|
-
function resolveRuntimeHomeForUser(runUser, env = process.env, explicitRuntimeHome) {
|
|
4961
|
-
const configuredRuntimeHome = explicitRuntimeHome?.trim() || env[RUNTIME_HOME_ENV_KEY]?.trim();
|
|
4962
|
-
if (configuredRuntimeHome) {
|
|
4963
|
-
return import_node_path11.default.resolve(configuredRuntimeHome);
|
|
4964
|
-
}
|
|
4965
|
-
const userHome = resolveUserHome(runUser);
|
|
4966
|
-
if (userHome) {
|
|
4967
|
-
return import_node_path11.default.resolve(userHome, USER_RUNTIME_HOME_DIR);
|
|
4968
|
-
}
|
|
4969
|
-
return import_node_path11.default.resolve(import_node_os3.default.homedir(), USER_RUNTIME_HOME_DIR);
|
|
4970
|
-
}
|
|
4971
|
-
function buildMainServiceUnit(options) {
|
|
4972
|
-
validateUnitOptions(options);
|
|
4973
|
-
const runtimeHome2 = import_node_path11.default.resolve(options.runtimeHome);
|
|
4974
|
-
return [
|
|
4975
|
-
"[Unit]",
|
|
4976
|
-
"Description=CodeHarbor main service",
|
|
4977
|
-
"After=network-online.target",
|
|
4978
|
-
"Wants=network-online.target",
|
|
4979
|
-
"",
|
|
4980
|
-
"[Service]",
|
|
4981
|
-
"Type=simple",
|
|
4982
|
-
`User=${options.runUser}`,
|
|
4983
|
-
`WorkingDirectory=${runtimeHome2}`,
|
|
4984
|
-
`Environment=CODEHARBOR_HOME=${runtimeHome2}`,
|
|
4985
|
-
`ExecStart=${import_node_path11.default.resolve(options.nodeBinPath)} ${import_node_path11.default.resolve(options.cliScriptPath)} start`,
|
|
4986
|
-
"Restart=always",
|
|
4987
|
-
"RestartSec=3",
|
|
4988
|
-
"NoNewPrivileges=true",
|
|
4989
|
-
"PrivateTmp=true",
|
|
4990
|
-
"ProtectSystem=full",
|
|
4991
|
-
"ProtectHome=false",
|
|
4992
|
-
`ReadWritePaths=${runtimeHome2}`,
|
|
4993
|
-
"",
|
|
4994
|
-
"[Install]",
|
|
4995
|
-
"WantedBy=multi-user.target",
|
|
4996
|
-
""
|
|
4997
|
-
].join("\n");
|
|
4998
|
-
}
|
|
4999
|
-
function buildAdminServiceUnit(options) {
|
|
5000
|
-
validateUnitOptions(options);
|
|
5001
|
-
const runtimeHome2 = import_node_path11.default.resolve(options.runtimeHome);
|
|
5002
|
-
return [
|
|
5003
|
-
"[Unit]",
|
|
5004
|
-
"Description=CodeHarbor admin service",
|
|
5005
|
-
"After=network-online.target",
|
|
5006
|
-
"Wants=network-online.target",
|
|
5007
|
-
"",
|
|
5008
|
-
"[Service]",
|
|
5009
|
-
"Type=simple",
|
|
5010
|
-
`User=${options.runUser}`,
|
|
5011
|
-
`WorkingDirectory=${runtimeHome2}`,
|
|
5012
|
-
`Environment=CODEHARBOR_HOME=${runtimeHome2}`,
|
|
5013
|
-
`ExecStart=${import_node_path11.default.resolve(options.nodeBinPath)} ${import_node_path11.default.resolve(options.cliScriptPath)} admin serve`,
|
|
5014
|
-
"Restart=always",
|
|
5015
|
-
"RestartSec=3",
|
|
5016
|
-
"NoNewPrivileges=true",
|
|
5017
|
-
"PrivateTmp=true",
|
|
5018
|
-
"ProtectSystem=full",
|
|
5019
|
-
"ProtectHome=false",
|
|
5020
|
-
`ReadWritePaths=${runtimeHome2}`,
|
|
5021
|
-
"",
|
|
5022
|
-
"[Install]",
|
|
5023
|
-
"WantedBy=multi-user.target",
|
|
5024
|
-
""
|
|
5025
|
-
].join("\n");
|
|
5026
|
-
}
|
|
5027
|
-
function installSystemdServices(options) {
|
|
5028
|
-
assertLinuxWithSystemd();
|
|
5029
|
-
assertRootPrivileges();
|
|
5030
|
-
const output = options.output ?? process.stdout;
|
|
5031
|
-
const runUser = options.runUser.trim();
|
|
5032
|
-
const runtimeHome2 = import_node_path11.default.resolve(options.runtimeHome);
|
|
5033
|
-
validateSimpleValue(runUser, "runUser");
|
|
5034
|
-
validateSimpleValue(runtimeHome2, "runtimeHome");
|
|
5035
|
-
validateSimpleValue(options.nodeBinPath, "nodeBinPath");
|
|
5036
|
-
validateSimpleValue(options.cliScriptPath, "cliScriptPath");
|
|
5037
|
-
ensureUserExists(runUser);
|
|
5038
|
-
const runGroup = resolveUserGroup(runUser);
|
|
5039
|
-
import_node_fs10.default.mkdirSync(runtimeHome2, { recursive: true });
|
|
5040
|
-
runCommand("chown", ["-R", `${runUser}:${runGroup}`, runtimeHome2]);
|
|
5041
|
-
const mainPath = import_node_path11.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
|
|
5042
|
-
const adminPath = import_node_path11.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
|
|
5043
|
-
const unitOptions = {
|
|
5044
|
-
runUser,
|
|
5045
|
-
runtimeHome: runtimeHome2,
|
|
5046
|
-
nodeBinPath: options.nodeBinPath,
|
|
5047
|
-
cliScriptPath: options.cliScriptPath
|
|
5048
|
-
};
|
|
5049
|
-
import_node_fs10.default.writeFileSync(mainPath, buildMainServiceUnit(unitOptions), "utf8");
|
|
5050
|
-
if (options.installAdmin) {
|
|
5051
|
-
import_node_fs10.default.writeFileSync(adminPath, buildAdminServiceUnit(unitOptions), "utf8");
|
|
5052
|
-
}
|
|
5053
|
-
runSystemctl(["daemon-reload"]);
|
|
5054
|
-
if (options.startNow) {
|
|
5055
|
-
runSystemctl(["enable", "--now", MAIN_SERVICE_NAME]);
|
|
5056
|
-
if (options.installAdmin) {
|
|
5057
|
-
runSystemctl(["enable", "--now", ADMIN_SERVICE_NAME]);
|
|
5058
|
-
}
|
|
5059
|
-
} else {
|
|
5060
|
-
runSystemctl(["enable", MAIN_SERVICE_NAME]);
|
|
5061
|
-
if (options.installAdmin) {
|
|
5062
|
-
runSystemctl(["enable", ADMIN_SERVICE_NAME]);
|
|
5063
|
-
}
|
|
5064
|
-
}
|
|
5065
|
-
output.write(`Installed systemd unit: ${mainPath}
|
|
5066
|
-
`);
|
|
5067
|
-
if (options.installAdmin) {
|
|
5068
|
-
output.write(`Installed systemd unit: ${adminPath}
|
|
5069
|
-
`);
|
|
5070
|
-
}
|
|
5071
|
-
output.write("Done. Check status with: systemctl status codeharbor --no-pager\n");
|
|
5072
|
-
}
|
|
5073
|
-
function uninstallSystemdServices(options) {
|
|
5074
|
-
assertLinuxWithSystemd();
|
|
5075
|
-
assertRootPrivileges();
|
|
5076
|
-
const output = options.output ?? process.stdout;
|
|
5077
|
-
const mainPath = import_node_path11.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
|
|
5078
|
-
const adminPath = import_node_path11.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
|
|
5079
|
-
stopAndDisableIfPresent(MAIN_SERVICE_NAME);
|
|
5080
|
-
if (import_node_fs10.default.existsSync(mainPath)) {
|
|
5081
|
-
import_node_fs10.default.unlinkSync(mainPath);
|
|
5082
|
-
}
|
|
5083
|
-
if (options.removeAdmin) {
|
|
5084
|
-
stopAndDisableIfPresent(ADMIN_SERVICE_NAME);
|
|
5085
|
-
if (import_node_fs10.default.existsSync(adminPath)) {
|
|
5086
|
-
import_node_fs10.default.unlinkSync(adminPath);
|
|
5087
|
-
}
|
|
5088
|
-
}
|
|
5089
|
-
runSystemctl(["daemon-reload"]);
|
|
5090
|
-
runSystemctlIgnoreFailure(["reset-failed"]);
|
|
5091
|
-
output.write(`Removed systemd unit: ${mainPath}
|
|
5092
|
-
`);
|
|
5093
|
-
if (options.removeAdmin) {
|
|
5094
|
-
output.write(`Removed systemd unit: ${adminPath}
|
|
5095
|
-
`);
|
|
5096
|
-
}
|
|
5097
|
-
output.write("Done.\n");
|
|
5098
|
-
}
|
|
5099
|
-
function restartSystemdServices(options) {
|
|
5100
|
-
assertLinuxWithSystemd();
|
|
5101
|
-
assertRootPrivileges();
|
|
5102
|
-
const output = options.output ?? process.stdout;
|
|
5103
|
-
runSystemctl(["restart", MAIN_SERVICE_NAME]);
|
|
5104
|
-
output.write(`Restarted service: ${MAIN_SERVICE_NAME}
|
|
5105
|
-
`);
|
|
5106
|
-
if (options.restartAdmin) {
|
|
5107
|
-
runSystemctl(["restart", ADMIN_SERVICE_NAME]);
|
|
5108
|
-
output.write(`Restarted service: ${ADMIN_SERVICE_NAME}
|
|
5109
|
-
`);
|
|
5110
|
-
}
|
|
5111
|
-
output.write("Done.\n");
|
|
5112
|
-
}
|
|
5113
|
-
function resolveUserHome(runUser) {
|
|
5114
|
-
try {
|
|
5115
|
-
const passwdRaw = import_node_fs10.default.readFileSync("/etc/passwd", "utf8");
|
|
5116
|
-
const line = passwdRaw.split(/\r?\n/).find((item) => item.startsWith(`${runUser}:`));
|
|
5117
|
-
if (!line) {
|
|
5118
|
-
return null;
|
|
5557
|
+
const normalized = [];
|
|
5558
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5559
|
+
for (const room of rooms) {
|
|
5560
|
+
const roomId = room.roomId.trim();
|
|
5561
|
+
if (!roomId) {
|
|
5562
|
+
throw new Error("roomId is required for every room in snapshot.");
|
|
5119
5563
|
}
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5564
|
+
if (seen.has(roomId)) {
|
|
5565
|
+
throw new Error(`Duplicate roomId in snapshot: ${roomId}`);
|
|
5566
|
+
}
|
|
5567
|
+
seen.add(roomId);
|
|
5568
|
+
const workdir = import_node_path10.default.resolve(cwd, room.workdir);
|
|
5569
|
+
ensureDirectory2(workdir, `room workdir (${roomId})`);
|
|
5570
|
+
normalized.push({
|
|
5571
|
+
roomId,
|
|
5572
|
+
enabled: room.enabled,
|
|
5573
|
+
allowMention: room.allowMention,
|
|
5574
|
+
allowReply: room.allowReply,
|
|
5575
|
+
allowActiveWindow: room.allowActiveWindow,
|
|
5576
|
+
allowPrefix: room.allowPrefix,
|
|
5577
|
+
workdir
|
|
5578
|
+
});
|
|
5124
5579
|
}
|
|
5580
|
+
return normalized;
|
|
5125
5581
|
}
|
|
5126
|
-
function
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
if (!value.trim()) {
|
|
5134
|
-
throw new Error(`${key} cannot be empty.`);
|
|
5582
|
+
function synchronizeRoomSettings(stateStore, rooms) {
|
|
5583
|
+
const incoming = new Map(rooms.map((room) => [room.roomId, room]));
|
|
5584
|
+
const existing = stateStore.listRoomSettings();
|
|
5585
|
+
for (const room of existing) {
|
|
5586
|
+
if (!incoming.has(room.roomId)) {
|
|
5587
|
+
stateStore.deleteRoomSettings(room.roomId);
|
|
5588
|
+
}
|
|
5135
5589
|
}
|
|
5136
|
-
|
|
5137
|
-
|
|
5590
|
+
for (const room of rooms) {
|
|
5591
|
+
stateStore.upsertRoomSettings(room);
|
|
5138
5592
|
}
|
|
5139
5593
|
}
|
|
5140
|
-
function
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
throw new Error("systemctl is required but not found.");
|
|
5594
|
+
function persistEnvSnapshot(cwd, env) {
|
|
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") : "";
|
|
5598
|
+
const overrides = {};
|
|
5599
|
+
for (const key of CONFIG_SNAPSHOT_ENV_KEYS) {
|
|
5600
|
+
overrides[key] = env[key];
|
|
5148
5601
|
}
|
|
5602
|
+
const next = applyEnvOverrides(template, overrides);
|
|
5603
|
+
import_node_fs9.default.writeFileSync(envPath, next, "utf8");
|
|
5149
5604
|
}
|
|
5150
|
-
function
|
|
5151
|
-
if (
|
|
5152
|
-
|
|
5605
|
+
function ensureDirectory2(dirPath, label) {
|
|
5606
|
+
if (!import_node_fs9.default.existsSync(dirPath) || !import_node_fs9.default.statSync(dirPath).isDirectory()) {
|
|
5607
|
+
throw new Error(`${label} does not exist or is not a directory: ${dirPath}`);
|
|
5153
5608
|
}
|
|
5154
|
-
|
|
5155
|
-
|
|
5609
|
+
}
|
|
5610
|
+
function parseIntStrict(raw) {
|
|
5611
|
+
const value = Number.parseInt(raw, 10);
|
|
5612
|
+
if (!Number.isFinite(value)) {
|
|
5613
|
+
throw new Error(`Invalid integer value: ${raw}`);
|
|
5156
5614
|
}
|
|
5615
|
+
return value;
|
|
5157
5616
|
}
|
|
5158
|
-
function
|
|
5159
|
-
|
|
5617
|
+
function serializeJsonObject(value) {
|
|
5618
|
+
return Object.keys(value).length > 0 ? JSON.stringify(value) : "";
|
|
5160
5619
|
}
|
|
5161
|
-
function
|
|
5162
|
-
return
|
|
5620
|
+
function booleanStringSchema(key) {
|
|
5621
|
+
return import_zod2.z.string().refine((value) => BOOLEAN_STRING.test(value), {
|
|
5622
|
+
message: `${key} must be a boolean string (true/false).`
|
|
5623
|
+
});
|
|
5163
5624
|
}
|
|
5164
|
-
function
|
|
5165
|
-
|
|
5625
|
+
function integerStringSchema(key, min, max = Number.MAX_SAFE_INTEGER) {
|
|
5626
|
+
return import_zod2.z.string().refine((value) => {
|
|
5627
|
+
const trimmed = value.trim();
|
|
5628
|
+
if (!INTEGER_STRING.test(trimmed)) {
|
|
5629
|
+
return false;
|
|
5630
|
+
}
|
|
5631
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
5632
|
+
if (!Number.isFinite(parsed)) {
|
|
5633
|
+
return false;
|
|
5634
|
+
}
|
|
5635
|
+
return parsed >= min && parsed <= max;
|
|
5636
|
+
}, {
|
|
5637
|
+
message: `${key} must be an integer string in range [${min}, ${max}].`
|
|
5638
|
+
});
|
|
5166
5639
|
}
|
|
5167
|
-
function
|
|
5168
|
-
|
|
5640
|
+
function jsonObjectStringSchema(key, allowEmpty) {
|
|
5641
|
+
return import_zod2.z.string().refine((value) => {
|
|
5642
|
+
const trimmed = value.trim();
|
|
5643
|
+
if (!trimmed) {
|
|
5644
|
+
return allowEmpty;
|
|
5645
|
+
}
|
|
5646
|
+
let parsed;
|
|
5647
|
+
try {
|
|
5648
|
+
parsed = JSON.parse(trimmed);
|
|
5649
|
+
} catch {
|
|
5650
|
+
return false;
|
|
5651
|
+
}
|
|
5652
|
+
return Boolean(parsed) && typeof parsed === "object" && !Array.isArray(parsed);
|
|
5653
|
+
}, {
|
|
5654
|
+
message: `${key} must be an empty string or a JSON object string.`
|
|
5655
|
+
});
|
|
5169
5656
|
}
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5657
|
+
|
|
5658
|
+
// src/preflight.ts
|
|
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"));
|
|
5662
|
+
var import_node_util3 = require("util");
|
|
5663
|
+
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process5.execFile);
|
|
5664
|
+
var REQUIRED_ENV_KEYS = ["MATRIX_HOMESERVER", "MATRIX_USER_ID", "MATRIX_ACCESS_TOKEN"];
|
|
5665
|
+
async function runStartupPreflight(options = {}) {
|
|
5666
|
+
const env = options.env ?? process.env;
|
|
5667
|
+
const cwd = options.cwd ?? process.cwd();
|
|
5668
|
+
const checkCodexBinary = options.checkCodexBinary ?? defaultCheckCodexBinary;
|
|
5669
|
+
const fileExists = options.fileExists ?? import_node_fs10.default.existsSync;
|
|
5670
|
+
const isDirectory = options.isDirectory ?? defaultIsDirectory;
|
|
5671
|
+
const issues = [];
|
|
5672
|
+
const envPath = import_node_path11.default.resolve(cwd, ".env");
|
|
5673
|
+
if (!fileExists(envPath)) {
|
|
5674
|
+
issues.push({
|
|
5675
|
+
level: "warn",
|
|
5676
|
+
code: "missing_dotenv",
|
|
5677
|
+
check: ".env",
|
|
5678
|
+
message: `No .env file found at ${envPath}.`,
|
|
5679
|
+
fix: 'Run "codeharbor init" to create baseline config.'
|
|
5680
|
+
});
|
|
5174
5681
|
}
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5682
|
+
for (const key of REQUIRED_ENV_KEYS) {
|
|
5683
|
+
if (!readEnv(env, key)) {
|
|
5684
|
+
issues.push({
|
|
5685
|
+
level: "error",
|
|
5686
|
+
code: "missing_env",
|
|
5687
|
+
check: key,
|
|
5688
|
+
message: `${key} is required.`,
|
|
5689
|
+
fix: `Run "codeharbor init" or set ${key} in .env.`
|
|
5690
|
+
});
|
|
5691
|
+
}
|
|
5692
|
+
}
|
|
5693
|
+
const matrixHomeserver = readEnv(env, "MATRIX_HOMESERVER");
|
|
5694
|
+
if (matrixHomeserver) {
|
|
5695
|
+
try {
|
|
5696
|
+
new URL(matrixHomeserver);
|
|
5697
|
+
} catch {
|
|
5698
|
+
issues.push({
|
|
5699
|
+
level: "error",
|
|
5700
|
+
code: "invalid_matrix_homeserver",
|
|
5701
|
+
check: "MATRIX_HOMESERVER",
|
|
5702
|
+
message: `Invalid URL: "${matrixHomeserver}".`,
|
|
5703
|
+
fix: "Set MATRIX_HOMESERVER to a full URL, for example https://matrix.example.com."
|
|
5704
|
+
});
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5707
|
+
const matrixUserId = readEnv(env, "MATRIX_USER_ID");
|
|
5708
|
+
if (matrixUserId && !/^@[^:\s]+:.+/.test(matrixUserId)) {
|
|
5709
|
+
issues.push({
|
|
5710
|
+
level: "error",
|
|
5711
|
+
code: "invalid_matrix_user_id",
|
|
5712
|
+
check: "MATRIX_USER_ID",
|
|
5713
|
+
message: `Unexpected Matrix user id format: "${matrixUserId}".`,
|
|
5714
|
+
fix: "Set MATRIX_USER_ID like @bot:example.com."
|
|
5181
5715
|
});
|
|
5716
|
+
}
|
|
5717
|
+
const codexBin = readEnv(env, "CODEX_BIN") || "codex";
|
|
5718
|
+
try {
|
|
5719
|
+
await checkCodexBinary(codexBin);
|
|
5182
5720
|
} catch (error) {
|
|
5183
|
-
|
|
5721
|
+
const reason = error instanceof Error && error.message ? ` (${error.message})` : "";
|
|
5722
|
+
issues.push({
|
|
5723
|
+
level: "error",
|
|
5724
|
+
code: "missing_codex_bin",
|
|
5725
|
+
check: "CODEX_BIN",
|
|
5726
|
+
message: `Unable to execute "${codexBin}"${reason}.`,
|
|
5727
|
+
fix: `Install Codex CLI and ensure "${codexBin}" is in PATH, or set CODEX_BIN=/absolute/path/to/codex.`
|
|
5728
|
+
});
|
|
5729
|
+
}
|
|
5730
|
+
const configuredWorkdir = readEnv(env, "CODEX_WORKDIR");
|
|
5731
|
+
const workdir = import_node_path11.default.resolve(cwd, configuredWorkdir || cwd);
|
|
5732
|
+
if (!fileExists(workdir) || !isDirectory(workdir)) {
|
|
5733
|
+
issues.push({
|
|
5734
|
+
level: "error",
|
|
5735
|
+
code: "invalid_codex_workdir",
|
|
5736
|
+
check: "CODEX_WORKDIR",
|
|
5737
|
+
message: `Working directory does not exist or is not a directory: ${workdir}.`,
|
|
5738
|
+
fix: `Set CODEX_WORKDIR to an existing directory, for example CODEX_WORKDIR=${cwd}.`
|
|
5739
|
+
});
|
|
5184
5740
|
}
|
|
5741
|
+
return {
|
|
5742
|
+
ok: issues.every((issue) => issue.level !== "error"),
|
|
5743
|
+
issues
|
|
5744
|
+
};
|
|
5185
5745
|
}
|
|
5186
|
-
function
|
|
5187
|
-
const
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5746
|
+
function formatPreflightReport(result, commandName) {
|
|
5747
|
+
const lines = [];
|
|
5748
|
+
const errors = result.issues.filter((issue) => issue.level === "error").length;
|
|
5749
|
+
const warnings = result.issues.filter((issue) => issue.level === "warn").length;
|
|
5750
|
+
if (result.ok) {
|
|
5751
|
+
lines.push(`Preflight check passed for "codeharbor ${commandName}" with ${warnings} warning(s).`);
|
|
5752
|
+
} else {
|
|
5753
|
+
lines.push(
|
|
5754
|
+
`Preflight check failed for "codeharbor ${commandName}" with ${errors} error(s) and ${warnings} warning(s).`
|
|
5755
|
+
);
|
|
5194
5756
|
}
|
|
5195
|
-
|
|
5757
|
+
for (const issue of result.issues) {
|
|
5758
|
+
const level = issue.level.toUpperCase();
|
|
5759
|
+
lines.push(`- [${level}] ${issue.check}: ${issue.message}`);
|
|
5760
|
+
lines.push(` fix: ${issue.fix}`);
|
|
5761
|
+
}
|
|
5762
|
+
return `${lines.join("\n")}
|
|
5763
|
+
`;
|
|
5196
5764
|
}
|
|
5197
|
-
function
|
|
5198
|
-
|
|
5199
|
-
|
|
5765
|
+
async function defaultCheckCodexBinary(bin) {
|
|
5766
|
+
await execFileAsync3(bin, ["--version"]);
|
|
5767
|
+
}
|
|
5768
|
+
function defaultIsDirectory(targetPath) {
|
|
5769
|
+
try {
|
|
5770
|
+
return import_node_fs10.default.statSync(targetPath).isDirectory();
|
|
5771
|
+
} catch {
|
|
5772
|
+
return false;
|
|
5200
5773
|
}
|
|
5201
|
-
|
|
5202
|
-
|
|
5774
|
+
}
|
|
5775
|
+
function readEnv(env, key) {
|
|
5776
|
+
return env[key]?.trim() ?? "";
|
|
5203
5777
|
}
|
|
5204
5778
|
|
|
5205
5779
|
// src/cli.ts
|
|
@@ -5302,6 +5876,7 @@ configCommand.command("import").description("Import config snapshot from JSON").
|
|
|
5302
5876
|
});
|
|
5303
5877
|
serviceCommand.command("install").description("Install and enable codeharbor systemd service (requires root)").option("--run-user <user>", "service user (default: sudo user or current user)").option("--runtime-home <path>", "runtime home used as CODEHARBOR_HOME").option("--with-admin", "also install codeharbor-admin.service").option("--no-start", "enable service without starting immediately").action((options) => {
|
|
5304
5878
|
try {
|
|
5879
|
+
maybeReexecServiceCommandWithSudo();
|
|
5305
5880
|
const runUser = options.runUser?.trim() || resolveDefaultRunUser();
|
|
5306
5881
|
const runtimeHomePath = resolveRuntimeHomeForUser(runUser, process.env, options.runtimeHome);
|
|
5307
5882
|
installSystemdServices({
|
|
@@ -5317,9 +5892,10 @@ serviceCommand.command("install").description("Install and enable codeharbor sys
|
|
|
5317
5892
|
`);
|
|
5318
5893
|
process.stderr.write(
|
|
5319
5894
|
[
|
|
5320
|
-
"Hint:
|
|
5321
|
-
|
|
5322
|
-
"
|
|
5895
|
+
"Hint:",
|
|
5896
|
+
" - Run directly: codeharbor service install --with-admin",
|
|
5897
|
+
" - The command auto-elevates with sudo when needed.",
|
|
5898
|
+
` - Fallback explicit form: ${buildExplicitSudoCommand("service install --with-admin")}`,
|
|
5323
5899
|
""
|
|
5324
5900
|
].join("\n")
|
|
5325
5901
|
);
|
|
@@ -5328,6 +5904,7 @@ serviceCommand.command("install").description("Install and enable codeharbor sys
|
|
|
5328
5904
|
});
|
|
5329
5905
|
serviceCommand.command("uninstall").description("Remove codeharbor systemd service (requires root)").option("--with-admin", "also remove codeharbor-admin.service").action((options) => {
|
|
5330
5906
|
try {
|
|
5907
|
+
maybeReexecServiceCommandWithSudo();
|
|
5331
5908
|
uninstallSystemdServices({
|
|
5332
5909
|
removeAdmin: options.withAdmin ?? false
|
|
5333
5910
|
});
|
|
@@ -5336,8 +5913,10 @@ serviceCommand.command("uninstall").description("Remove codeharbor systemd servi
|
|
|
5336
5913
|
`);
|
|
5337
5914
|
process.stderr.write(
|
|
5338
5915
|
[
|
|
5339
|
-
"Hint:
|
|
5340
|
-
|
|
5916
|
+
"Hint:",
|
|
5917
|
+
" - Run directly: codeharbor service uninstall --with-admin",
|
|
5918
|
+
" - The command auto-elevates with sudo when needed.",
|
|
5919
|
+
` - Fallback explicit form: ${buildExplicitSudoCommand("service uninstall --with-admin")}`,
|
|
5341
5920
|
""
|
|
5342
5921
|
].join("\n")
|
|
5343
5922
|
);
|
|
@@ -5346,6 +5925,7 @@ serviceCommand.command("uninstall").description("Remove codeharbor systemd servi
|
|
|
5346
5925
|
});
|
|
5347
5926
|
serviceCommand.command("restart").description("Restart installed codeharbor systemd service (requires root)").option("--with-admin", "also restart codeharbor-admin.service").action((options) => {
|
|
5348
5927
|
try {
|
|
5928
|
+
maybeReexecServiceCommandWithSudo();
|
|
5349
5929
|
restartSystemdServices({
|
|
5350
5930
|
restartAdmin: options.withAdmin ?? false
|
|
5351
5931
|
});
|
|
@@ -5354,9 +5934,10 @@ serviceCommand.command("restart").description("Restart installed codeharbor syst
|
|
|
5354
5934
|
`);
|
|
5355
5935
|
process.stderr.write(
|
|
5356
5936
|
[
|
|
5357
|
-
"Hint:
|
|
5358
|
-
|
|
5359
|
-
"
|
|
5937
|
+
"Hint:",
|
|
5938
|
+
" - Run directly: codeharbor service restart --with-admin",
|
|
5939
|
+
" - The command auto-elevates with sudo when needed.",
|
|
5940
|
+
` - Fallback explicit form: ${buildExplicitSudoCommand("service restart --with-admin")}`,
|
|
5360
5941
|
""
|
|
5361
5942
|
].join("\n")
|
|
5362
5943
|
);
|
|
@@ -5439,6 +6020,32 @@ function resolveCliScriptPath() {
|
|
|
5439
6020
|
}
|
|
5440
6021
|
return import_node_path12.default.resolve(__dirname, "cli.js");
|
|
5441
6022
|
}
|
|
6023
|
+
function maybeReexecServiceCommandWithSudo() {
|
|
6024
|
+
if (typeof process.getuid !== "function" || process.getuid() === 0) {
|
|
6025
|
+
return;
|
|
6026
|
+
}
|
|
6027
|
+
const serviceArgs = process.argv.slice(2);
|
|
6028
|
+
if (serviceArgs.length === 0 || serviceArgs[0] !== "service") {
|
|
6029
|
+
return;
|
|
6030
|
+
}
|
|
6031
|
+
const cliScriptPath = resolveCliScriptPath();
|
|
6032
|
+
const child = (0, import_node_child_process6.spawnSync)("sudo", [process.execPath, cliScriptPath, ...serviceArgs], {
|
|
6033
|
+
stdio: "inherit"
|
|
6034
|
+
});
|
|
6035
|
+
if (child.error) {
|
|
6036
|
+
throw new Error(`failed to auto-elevate with sudo: ${child.error.message}`);
|
|
6037
|
+
}
|
|
6038
|
+
process.exit(child.status ?? 1);
|
|
6039
|
+
}
|
|
6040
|
+
function shellQuote(value) {
|
|
6041
|
+
if (!value) {
|
|
6042
|
+
return "''";
|
|
6043
|
+
}
|
|
6044
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
6045
|
+
}
|
|
6046
|
+
function buildExplicitSudoCommand(subcommand) {
|
|
6047
|
+
return `sudo ${shellQuote(process.execPath)} ${shellQuote(resolveCliScriptPath())} ${subcommand}`;
|
|
6048
|
+
}
|
|
5442
6049
|
function formatError3(error) {
|
|
5443
6050
|
if (error instanceof Error) {
|
|
5444
6051
|
return error.message;
|