codeharbor 0.1.13 → 0.1.15
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 +19 -3
- package/dist/cli.js +856 -134
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -24,30 +24,78 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/cli.ts
|
|
27
|
-
var
|
|
27
|
+
var import_node_child_process7 = require("child_process");
|
|
28
28
|
var import_node_fs11 = __toESM(require("fs"));
|
|
29
|
-
var
|
|
29
|
+
var import_node_path14 = __toESM(require("path"));
|
|
30
30
|
var import_commander = require("commander");
|
|
31
31
|
|
|
32
32
|
// src/app.ts
|
|
33
|
-
var
|
|
34
|
-
var
|
|
33
|
+
var import_node_child_process5 = require("child_process");
|
|
34
|
+
var import_node_util3 = require("util");
|
|
35
35
|
|
|
36
36
|
// src/admin-server.ts
|
|
37
|
-
var
|
|
37
|
+
var import_node_child_process3 = require("child_process");
|
|
38
38
|
var import_node_fs4 = __toESM(require("fs"));
|
|
39
39
|
var import_node_http = __toESM(require("http"));
|
|
40
|
-
var
|
|
41
|
-
var
|
|
40
|
+
var import_node_path5 = __toESM(require("path"));
|
|
41
|
+
var import_node_util2 = require("util");
|
|
42
42
|
|
|
43
43
|
// src/init.ts
|
|
44
44
|
var import_node_fs = __toESM(require("fs"));
|
|
45
|
-
var
|
|
45
|
+
var import_node_path2 = __toESM(require("path"));
|
|
46
46
|
var import_promises = require("readline/promises");
|
|
47
47
|
var import_dotenv = __toESM(require("dotenv"));
|
|
48
|
+
|
|
49
|
+
// src/codex-bin.ts
|
|
50
|
+
var import_node_child_process = require("child_process");
|
|
51
|
+
var import_node_os = __toESM(require("os"));
|
|
52
|
+
var import_node_path = __toESM(require("path"));
|
|
53
|
+
var import_node_util = require("util");
|
|
54
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
55
|
+
function buildCodexBinCandidates(configuredBin, env = process.env) {
|
|
56
|
+
const normalized = configuredBin.trim() || "codex";
|
|
57
|
+
const home = env.HOME?.trim() || import_node_os.default.homedir();
|
|
58
|
+
const npmGlobalBin = home ? import_node_path.default.resolve(home, ".npm-global/bin/codex") : "";
|
|
59
|
+
const candidates = [
|
|
60
|
+
normalized,
|
|
61
|
+
"codex",
|
|
62
|
+
npmGlobalBin,
|
|
63
|
+
"/usr/bin/codex",
|
|
64
|
+
"/usr/local/bin/codex",
|
|
65
|
+
"/opt/homebrew/bin/codex"
|
|
66
|
+
];
|
|
67
|
+
const seen = /* @__PURE__ */ new Set();
|
|
68
|
+
const output = [];
|
|
69
|
+
for (const candidate of candidates) {
|
|
70
|
+
const trimmed = candidate.trim();
|
|
71
|
+
if (!trimmed || seen.has(trimmed)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
seen.add(trimmed);
|
|
75
|
+
output.push(trimmed);
|
|
76
|
+
}
|
|
77
|
+
return output;
|
|
78
|
+
}
|
|
79
|
+
async function findWorkingCodexBin(configuredBin, options = {}) {
|
|
80
|
+
const checkBinary = options.checkBinary ?? defaultCheckBinary;
|
|
81
|
+
const candidates = buildCodexBinCandidates(configuredBin, options.env);
|
|
82
|
+
for (const candidate of candidates) {
|
|
83
|
+
try {
|
|
84
|
+
await checkBinary(candidate);
|
|
85
|
+
return candidate;
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
async function defaultCheckBinary(bin) {
|
|
92
|
+
await execFileAsync(bin, ["--version"]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/init.ts
|
|
48
96
|
async function runInitCommand(options = {}) {
|
|
49
97
|
const cwd = options.cwd ?? process.cwd();
|
|
50
|
-
const envPath =
|
|
98
|
+
const envPath = import_node_path2.default.resolve(cwd, ".env");
|
|
51
99
|
const templatePath = resolveInitTemplatePath(cwd);
|
|
52
100
|
const input = options.input ?? process.stdin;
|
|
53
101
|
const output = options.output ?? process.stdout;
|
|
@@ -70,6 +118,7 @@ async function runInitCommand(options = {}) {
|
|
|
70
118
|
output.write("CodeHarbor setup wizard\n");
|
|
71
119
|
output.write(`Target file: ${envPath}
|
|
72
120
|
`);
|
|
121
|
+
const detectedCodexBin = await findWorkingCodexBin(existingValues.CODEX_BIN ?? "codex");
|
|
73
122
|
const questions = [
|
|
74
123
|
{
|
|
75
124
|
key: "MATRIX_HOMESERVER",
|
|
@@ -109,14 +158,14 @@ async function runInitCommand(options = {}) {
|
|
|
109
158
|
{
|
|
110
159
|
key: "CODEX_BIN",
|
|
111
160
|
label: "Codex binary",
|
|
112
|
-
fallbackValue: "codex"
|
|
161
|
+
fallbackValue: detectedCodexBin ?? "codex"
|
|
113
162
|
},
|
|
114
163
|
{
|
|
115
164
|
key: "CODEX_WORKDIR",
|
|
116
165
|
label: "Codex working directory",
|
|
117
166
|
fallbackValue: cwd,
|
|
118
167
|
validate: (value) => {
|
|
119
|
-
const resolved =
|
|
168
|
+
const resolved = import_node_path2.default.resolve(cwd, value);
|
|
120
169
|
if (!import_node_fs.default.existsSync(resolved) || !import_node_fs.default.statSync(resolved).isDirectory()) {
|
|
121
170
|
return `Directory does not exist: ${resolved}`;
|
|
122
171
|
}
|
|
@@ -144,8 +193,8 @@ async function runInitCommand(options = {}) {
|
|
|
144
193
|
}
|
|
145
194
|
function resolveInitTemplatePath(cwd) {
|
|
146
195
|
const candidates = [
|
|
147
|
-
|
|
148
|
-
|
|
196
|
+
import_node_path2.default.resolve(cwd, ".env.example"),
|
|
197
|
+
import_node_path2.default.resolve(__dirname, "..", ".env.example")
|
|
149
198
|
];
|
|
150
199
|
for (const candidate of candidates) {
|
|
151
200
|
if (import_node_fs.default.existsSync(candidate)) {
|
|
@@ -221,33 +270,33 @@ async function askYesNo(rl, question, defaultValue) {
|
|
|
221
270
|
}
|
|
222
271
|
|
|
223
272
|
// src/service-manager.ts
|
|
224
|
-
var
|
|
273
|
+
var import_node_child_process2 = require("child_process");
|
|
225
274
|
var import_node_fs3 = __toESM(require("fs"));
|
|
226
|
-
var
|
|
227
|
-
var
|
|
275
|
+
var import_node_os3 = __toESM(require("os"));
|
|
276
|
+
var import_node_path4 = __toESM(require("path"));
|
|
228
277
|
|
|
229
278
|
// src/runtime-home.ts
|
|
230
279
|
var import_node_fs2 = __toESM(require("fs"));
|
|
231
|
-
var
|
|
232
|
-
var
|
|
280
|
+
var import_node_os2 = __toESM(require("os"));
|
|
281
|
+
var import_node_path3 = __toESM(require("path"));
|
|
233
282
|
var LEGACY_RUNTIME_HOME = "/opt/codeharbor";
|
|
234
283
|
var USER_RUNTIME_HOME_DIR = ".codeharbor";
|
|
235
|
-
var DEFAULT_RUNTIME_HOME =
|
|
284
|
+
var DEFAULT_RUNTIME_HOME = import_node_path3.default.resolve(import_node_os2.default.homedir(), USER_RUNTIME_HOME_DIR);
|
|
236
285
|
var RUNTIME_HOME_ENV_KEY = "CODEHARBOR_HOME";
|
|
237
286
|
function resolveRuntimeHome(env = process.env) {
|
|
238
287
|
const configured = env[RUNTIME_HOME_ENV_KEY]?.trim();
|
|
239
288
|
if (configured) {
|
|
240
|
-
return
|
|
289
|
+
return import_node_path3.default.resolve(configured);
|
|
241
290
|
}
|
|
242
|
-
const legacyEnvPath =
|
|
291
|
+
const legacyEnvPath = import_node_path3.default.resolve(LEGACY_RUNTIME_HOME, ".env");
|
|
243
292
|
if (import_node_fs2.default.existsSync(legacyEnvPath)) {
|
|
244
293
|
return LEGACY_RUNTIME_HOME;
|
|
245
294
|
}
|
|
246
295
|
return resolveUserRuntimeHome(env);
|
|
247
296
|
}
|
|
248
297
|
function resolveUserRuntimeHome(env = process.env) {
|
|
249
|
-
const home = env.HOME?.trim() ||
|
|
250
|
-
return
|
|
298
|
+
const home = env.HOME?.trim() || import_node_os2.default.homedir();
|
|
299
|
+
return import_node_path3.default.resolve(home, USER_RUNTIME_HOME_DIR);
|
|
251
300
|
}
|
|
252
301
|
|
|
253
302
|
// src/service-manager.ts
|
|
@@ -266,7 +315,7 @@ function resolveDefaultRunUser(env = process.env) {
|
|
|
266
315
|
return user;
|
|
267
316
|
}
|
|
268
317
|
try {
|
|
269
|
-
return
|
|
318
|
+
return import_node_os3.default.userInfo().username;
|
|
270
319
|
} catch {
|
|
271
320
|
return "root";
|
|
272
321
|
}
|
|
@@ -274,17 +323,17 @@ function resolveDefaultRunUser(env = process.env) {
|
|
|
274
323
|
function resolveRuntimeHomeForUser(runUser, env = process.env, explicitRuntimeHome) {
|
|
275
324
|
const configuredRuntimeHome = explicitRuntimeHome?.trim() || env[RUNTIME_HOME_ENV_KEY]?.trim();
|
|
276
325
|
if (configuredRuntimeHome) {
|
|
277
|
-
return
|
|
326
|
+
return import_node_path4.default.resolve(configuredRuntimeHome);
|
|
278
327
|
}
|
|
279
328
|
const userHome = resolveUserHome(runUser);
|
|
280
329
|
if (userHome) {
|
|
281
|
-
return
|
|
330
|
+
return import_node_path4.default.resolve(userHome, USER_RUNTIME_HOME_DIR);
|
|
282
331
|
}
|
|
283
|
-
return
|
|
332
|
+
return import_node_path4.default.resolve(import_node_os3.default.homedir(), USER_RUNTIME_HOME_DIR);
|
|
284
333
|
}
|
|
285
334
|
function buildMainServiceUnit(options) {
|
|
286
335
|
validateUnitOptions(options);
|
|
287
|
-
const runtimeHome2 =
|
|
336
|
+
const runtimeHome2 = import_node_path4.default.resolve(options.runtimeHome);
|
|
288
337
|
return [
|
|
289
338
|
"[Unit]",
|
|
290
339
|
"Description=CodeHarbor main service",
|
|
@@ -296,7 +345,7 @@ function buildMainServiceUnit(options) {
|
|
|
296
345
|
`User=${options.runUser}`,
|
|
297
346
|
`WorkingDirectory=${runtimeHome2}`,
|
|
298
347
|
`Environment=CODEHARBOR_HOME=${runtimeHome2}`,
|
|
299
|
-
`ExecStart=${
|
|
348
|
+
`ExecStart=${import_node_path4.default.resolve(options.nodeBinPath)} ${import_node_path4.default.resolve(options.cliScriptPath)} start`,
|
|
300
349
|
"Restart=always",
|
|
301
350
|
"RestartSec=3",
|
|
302
351
|
"NoNewPrivileges=true",
|
|
@@ -312,7 +361,7 @@ function buildMainServiceUnit(options) {
|
|
|
312
361
|
}
|
|
313
362
|
function buildAdminServiceUnit(options) {
|
|
314
363
|
validateUnitOptions(options);
|
|
315
|
-
const runtimeHome2 =
|
|
364
|
+
const runtimeHome2 = import_node_path4.default.resolve(options.runtimeHome);
|
|
316
365
|
return [
|
|
317
366
|
"[Unit]",
|
|
318
367
|
"Description=CodeHarbor admin service",
|
|
@@ -324,7 +373,7 @@ function buildAdminServiceUnit(options) {
|
|
|
324
373
|
`User=${options.runUser}`,
|
|
325
374
|
`WorkingDirectory=${runtimeHome2}`,
|
|
326
375
|
`Environment=CODEHARBOR_HOME=${runtimeHome2}`,
|
|
327
|
-
`ExecStart=${
|
|
376
|
+
`ExecStart=${import_node_path4.default.resolve(options.nodeBinPath)} ${import_node_path4.default.resolve(options.cliScriptPath)} admin serve`,
|
|
328
377
|
"Restart=always",
|
|
329
378
|
"RestartSec=3",
|
|
330
379
|
"NoNewPrivileges=true",
|
|
@@ -343,7 +392,7 @@ function buildRestartSudoersPolicy(options) {
|
|
|
343
392
|
const systemctlPath = options.systemctlPath.trim();
|
|
344
393
|
validateSimpleValue(runUser, "runUser");
|
|
345
394
|
validateSimpleValue(systemctlPath, "systemctlPath");
|
|
346
|
-
if (!
|
|
395
|
+
if (!import_node_path4.default.isAbsolute(systemctlPath)) {
|
|
347
396
|
throw new Error("systemctlPath must be an absolute path.");
|
|
348
397
|
}
|
|
349
398
|
return [
|
|
@@ -358,7 +407,7 @@ function installSystemdServices(options) {
|
|
|
358
407
|
assertRootPrivileges();
|
|
359
408
|
const output = options.output ?? process.stdout;
|
|
360
409
|
const runUser = options.runUser.trim();
|
|
361
|
-
const runtimeHome2 =
|
|
410
|
+
const runtimeHome2 = import_node_path4.default.resolve(options.runtimeHome);
|
|
362
411
|
validateSimpleValue(runUser, "runUser");
|
|
363
412
|
validateSimpleValue(runtimeHome2, "runtimeHome");
|
|
364
413
|
validateSimpleValue(options.nodeBinPath, "nodeBinPath");
|
|
@@ -367,9 +416,9 @@ function installSystemdServices(options) {
|
|
|
367
416
|
const runGroup = resolveUserGroup(runUser);
|
|
368
417
|
import_node_fs3.default.mkdirSync(runtimeHome2, { recursive: true });
|
|
369
418
|
runCommand("chown", ["-R", `${runUser}:${runGroup}`, runtimeHome2]);
|
|
370
|
-
const mainPath =
|
|
371
|
-
const adminPath =
|
|
372
|
-
const restartSudoersPath =
|
|
419
|
+
const mainPath = import_node_path4.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
|
|
420
|
+
const adminPath = import_node_path4.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
|
|
421
|
+
const restartSudoersPath = import_node_path4.default.join(SUDOERS_DIR, RESTART_SUDOERS_FILE);
|
|
373
422
|
const unitOptions = {
|
|
374
423
|
runUser,
|
|
375
424
|
runtimeHome: runtimeHome2,
|
|
@@ -417,9 +466,9 @@ function uninstallSystemdServices(options) {
|
|
|
417
466
|
assertLinuxWithSystemd();
|
|
418
467
|
assertRootPrivileges();
|
|
419
468
|
const output = options.output ?? process.stdout;
|
|
420
|
-
const mainPath =
|
|
421
|
-
const adminPath =
|
|
422
|
-
const restartSudoersPath =
|
|
469
|
+
const mainPath = import_node_path4.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
|
|
470
|
+
const adminPath = import_node_path4.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
|
|
471
|
+
const restartSudoersPath = import_node_path4.default.join(SUDOERS_DIR, RESTART_SUDOERS_FILE);
|
|
423
472
|
stopAndDisableIfPresent(MAIN_SERVICE_NAME);
|
|
424
473
|
if (import_node_fs3.default.existsSync(mainPath)) {
|
|
425
474
|
import_node_fs3.default.unlinkSync(mainPath);
|
|
@@ -492,7 +541,7 @@ function assertLinuxWithSystemd() {
|
|
|
492
541
|
throw new Error("Systemd service install only supports Linux.");
|
|
493
542
|
}
|
|
494
543
|
try {
|
|
495
|
-
(0,
|
|
544
|
+
(0, import_node_child_process2.execFileSync)("systemctl", ["--version"], { stdio: "ignore" });
|
|
496
545
|
} catch {
|
|
497
546
|
throw new Error("systemctl is required but not found.");
|
|
498
547
|
}
|
|
@@ -539,13 +588,13 @@ function runSystemctlIgnoreFailure(args) {
|
|
|
539
588
|
}
|
|
540
589
|
function resolveSystemctlPath() {
|
|
541
590
|
const candidates = [];
|
|
542
|
-
const pathEntries = (process.env.PATH ?? "").split(
|
|
591
|
+
const pathEntries = (process.env.PATH ?? "").split(import_node_path4.default.delimiter).filter(Boolean);
|
|
543
592
|
for (const entry of pathEntries) {
|
|
544
|
-
candidates.push(
|
|
593
|
+
candidates.push(import_node_path4.default.join(entry, "systemctl"));
|
|
545
594
|
}
|
|
546
595
|
candidates.push("/usr/bin/systemctl", "/bin/systemctl", "/usr/local/bin/systemctl");
|
|
547
596
|
for (const candidate of candidates) {
|
|
548
|
-
if (
|
|
597
|
+
if (import_node_path4.default.isAbsolute(candidate) && import_node_fs3.default.existsSync(candidate)) {
|
|
549
598
|
return candidate;
|
|
550
599
|
}
|
|
551
600
|
}
|
|
@@ -553,7 +602,7 @@ function resolveSystemctlPath() {
|
|
|
553
602
|
}
|
|
554
603
|
function runCommand(file, args) {
|
|
555
604
|
try {
|
|
556
|
-
return (0,
|
|
605
|
+
return (0, import_node_child_process2.execFileSync)(file, args, {
|
|
557
606
|
encoding: "utf8",
|
|
558
607
|
stdio: ["ignore", "pipe", "pipe"]
|
|
559
608
|
});
|
|
@@ -581,7 +630,7 @@ function bufferToTrimmedString(value) {
|
|
|
581
630
|
}
|
|
582
631
|
|
|
583
632
|
// src/admin-server.ts
|
|
584
|
-
var
|
|
633
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
|
|
585
634
|
var HttpError = class extends Error {
|
|
586
635
|
statusCode;
|
|
587
636
|
constructor(statusCode, message) {
|
|
@@ -597,6 +646,7 @@ var AdminServer = class {
|
|
|
597
646
|
host;
|
|
598
647
|
port;
|
|
599
648
|
adminToken;
|
|
649
|
+
adminTokens;
|
|
600
650
|
adminIpAllowlist;
|
|
601
651
|
adminAllowedOrigins;
|
|
602
652
|
cwd;
|
|
@@ -613,6 +663,7 @@ var AdminServer = class {
|
|
|
613
663
|
this.host = options.host;
|
|
614
664
|
this.port = options.port;
|
|
615
665
|
this.adminToken = options.adminToken;
|
|
666
|
+
this.adminTokens = buildAdminTokenMap(options.adminTokens ?? []);
|
|
616
667
|
this.adminIpAllowlist = normalizeAllowlist(options.adminIpAllowlist ?? []);
|
|
617
668
|
this.adminAllowedOrigins = normalizeOriginAllowlist(options.adminAllowedOrigins ?? []);
|
|
618
669
|
this.cwd = options.cwd ?? process.cwd();
|
|
@@ -704,10 +755,19 @@ var AdminServer = class {
|
|
|
704
755
|
this.sendHtml(res, renderAdminConsoleHtml());
|
|
705
756
|
return;
|
|
706
757
|
}
|
|
707
|
-
|
|
758
|
+
const requiredRole = requiredAdminRoleForRequest(req.method, url.pathname);
|
|
759
|
+
const authIdentity = requiredRole ? this.resolveAdminIdentity(req) : null;
|
|
760
|
+
if (requiredRole && !authIdentity) {
|
|
708
761
|
this.sendJson(res, 401, {
|
|
709
762
|
ok: false,
|
|
710
|
-
error: "Unauthorized. Provide Authorization: Bearer <ADMIN_TOKEN
|
|
763
|
+
error: "Unauthorized. Provide Authorization: Bearer <ADMIN_TOKEN> (or token from ADMIN_TOKENS_JSON)."
|
|
764
|
+
});
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (requiredRole && authIdentity && !hasRequiredAdminRole(authIdentity.role, requiredRole)) {
|
|
768
|
+
this.sendJson(res, 403, {
|
|
769
|
+
ok: false,
|
|
770
|
+
error: "Forbidden. This endpoint requires admin write permission."
|
|
711
771
|
});
|
|
712
772
|
return;
|
|
713
773
|
}
|
|
@@ -721,7 +781,7 @@ var AdminServer = class {
|
|
|
721
781
|
}
|
|
722
782
|
if (req.method === "PUT" && url.pathname === "/api/admin/config/global") {
|
|
723
783
|
const body = await readJsonBody(req);
|
|
724
|
-
const actor =
|
|
784
|
+
const actor = resolveAuditActor(req, authIdentity);
|
|
725
785
|
const result = this.updateGlobalConfig(body, actor);
|
|
726
786
|
this.sendJson(res, 200, {
|
|
727
787
|
ok: true,
|
|
@@ -749,13 +809,13 @@ var AdminServer = class {
|
|
|
749
809
|
}
|
|
750
810
|
if (req.method === "PUT") {
|
|
751
811
|
const body = await readJsonBody(req);
|
|
752
|
-
const actor =
|
|
812
|
+
const actor = resolveAuditActor(req, authIdentity);
|
|
753
813
|
const room = this.updateRoomConfig(roomId, body, actor);
|
|
754
814
|
this.sendJson(res, 200, { ok: true, data: room });
|
|
755
815
|
return;
|
|
756
816
|
}
|
|
757
817
|
if (req.method === "DELETE") {
|
|
758
|
-
const actor =
|
|
818
|
+
const actor = resolveAuditActor(req, authIdentity);
|
|
759
819
|
this.configService.deleteRoomSettings(roomId, actor);
|
|
760
820
|
this.sendJson(res, 200, { ok: true, roomId });
|
|
761
821
|
return;
|
|
@@ -785,7 +845,7 @@ var AdminServer = class {
|
|
|
785
845
|
if (req.method === "POST" && url.pathname === "/api/admin/service/restart") {
|
|
786
846
|
const body = asObject(await readJsonBody(req), "service restart payload");
|
|
787
847
|
const restartAdmin = normalizeBoolean(body.withAdmin, false);
|
|
788
|
-
const actor =
|
|
848
|
+
const actor = resolveAuditActor(req, authIdentity);
|
|
789
849
|
try {
|
|
790
850
|
const result = await this.restartServices(restartAdmin);
|
|
791
851
|
this.stateStore.appendConfigRevision(
|
|
@@ -839,7 +899,7 @@ var AdminServer = class {
|
|
|
839
899
|
updatedKeys.push("matrixCommandPrefix");
|
|
840
900
|
}
|
|
841
901
|
if ("codexWorkdir" in body) {
|
|
842
|
-
const workdir =
|
|
902
|
+
const workdir = import_node_path5.default.resolve(String(body.codexWorkdir ?? "").trim());
|
|
843
903
|
ensureDirectory(workdir, "codexWorkdir");
|
|
844
904
|
this.config.codexWorkdir = workdir;
|
|
845
905
|
envUpdates.CODEX_WORKDIR = workdir;
|
|
@@ -1043,14 +1103,34 @@ var AdminServer = class {
|
|
|
1043
1103
|
summary: normalizeOptionalString(body.summary)
|
|
1044
1104
|
});
|
|
1045
1105
|
}
|
|
1046
|
-
|
|
1047
|
-
if (!this.adminToken) {
|
|
1048
|
-
return
|
|
1106
|
+
resolveAdminIdentity(req) {
|
|
1107
|
+
if (!this.adminToken && this.adminTokens.size === 0) {
|
|
1108
|
+
return {
|
|
1109
|
+
role: "admin",
|
|
1110
|
+
actor: null,
|
|
1111
|
+
source: "open"
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
const token = readAdminToken(req);
|
|
1115
|
+
if (!token) {
|
|
1116
|
+
return null;
|
|
1049
1117
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1118
|
+
if (this.adminToken && token === this.adminToken) {
|
|
1119
|
+
return {
|
|
1120
|
+
role: "admin",
|
|
1121
|
+
actor: null,
|
|
1122
|
+
source: "legacy"
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
const mappedIdentity = this.adminTokens.get(token);
|
|
1126
|
+
if (!mappedIdentity) {
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
role: mappedIdentity.role,
|
|
1131
|
+
actor: mappedIdentity.actor,
|
|
1132
|
+
source: "scoped"
|
|
1133
|
+
};
|
|
1054
1134
|
}
|
|
1055
1135
|
isClientAllowed(req) {
|
|
1056
1136
|
if (this.adminIpAllowlist.length === 0) {
|
|
@@ -1063,8 +1143,8 @@ var AdminServer = class {
|
|
|
1063
1143
|
return this.adminIpAllowlist.includes(normalizedRemote);
|
|
1064
1144
|
}
|
|
1065
1145
|
persistEnvUpdates(updates) {
|
|
1066
|
-
const envPath =
|
|
1067
|
-
const examplePath =
|
|
1146
|
+
const envPath = import_node_path5.default.resolve(this.cwd, ".env");
|
|
1147
|
+
const examplePath = import_node_path5.default.resolve(this.cwd, ".env.example");
|
|
1068
1148
|
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") : "";
|
|
1069
1149
|
const next = applyEnvOverrides(template, updates);
|
|
1070
1150
|
import_node_fs4.default.writeFileSync(envPath, next, "utf8");
|
|
@@ -1342,10 +1422,54 @@ function normalizeHeaderValue(value) {
|
|
|
1342
1422
|
}
|
|
1343
1423
|
return value.trim();
|
|
1344
1424
|
}
|
|
1345
|
-
function
|
|
1425
|
+
function readAdminToken(req) {
|
|
1426
|
+
const authorization = normalizeHeaderValue(req.headers.authorization);
|
|
1427
|
+
if (authorization) {
|
|
1428
|
+
const match = /^bearer\s+(.+)$/i.exec(authorization);
|
|
1429
|
+
const token = match?.[1]?.trim() ?? "";
|
|
1430
|
+
if (token) {
|
|
1431
|
+
return token;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
const fromHeader = normalizeHeaderValue(req.headers["x-admin-token"]);
|
|
1435
|
+
return fromHeader || null;
|
|
1436
|
+
}
|
|
1437
|
+
function resolveAuditActor(req, identity) {
|
|
1438
|
+
if (identity?.source === "scoped") {
|
|
1439
|
+
if (identity.actor) {
|
|
1440
|
+
return identity.actor;
|
|
1441
|
+
}
|
|
1442
|
+
return identity.role === "admin" ? "admin-token" : "viewer-token";
|
|
1443
|
+
}
|
|
1346
1444
|
const actor = normalizeHeaderValue(req.headers["x-admin-actor"]);
|
|
1347
1445
|
return actor || null;
|
|
1348
1446
|
}
|
|
1447
|
+
function requiredAdminRoleForRequest(method, pathname) {
|
|
1448
|
+
if (!pathname.startsWith("/api/admin/")) {
|
|
1449
|
+
return null;
|
|
1450
|
+
}
|
|
1451
|
+
const normalizedMethod = (method ?? "GET").toUpperCase();
|
|
1452
|
+
if (normalizedMethod === "GET" || normalizedMethod === "HEAD") {
|
|
1453
|
+
return "viewer";
|
|
1454
|
+
}
|
|
1455
|
+
return "admin";
|
|
1456
|
+
}
|
|
1457
|
+
function hasRequiredAdminRole(role, requiredRole) {
|
|
1458
|
+
if (requiredRole === "viewer") {
|
|
1459
|
+
return role === "viewer" || role === "admin";
|
|
1460
|
+
}
|
|
1461
|
+
return role === "admin";
|
|
1462
|
+
}
|
|
1463
|
+
function buildAdminTokenMap(tokens) {
|
|
1464
|
+
const mapped = /* @__PURE__ */ new Map();
|
|
1465
|
+
for (const token of tokens) {
|
|
1466
|
+
mapped.set(token.token, {
|
|
1467
|
+
role: token.role,
|
|
1468
|
+
actor: token.actor
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
return mapped;
|
|
1472
|
+
}
|
|
1349
1473
|
function formatError(error) {
|
|
1350
1474
|
if (error instanceof Error) {
|
|
1351
1475
|
return error.message;
|
|
@@ -2219,7 +2343,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
2219
2343
|
`;
|
|
2220
2344
|
async function defaultCheckCodex(bin) {
|
|
2221
2345
|
try {
|
|
2222
|
-
const { stdout } = await
|
|
2346
|
+
const { stdout } = await execFileAsync2(bin, ["--version"]);
|
|
2223
2347
|
return {
|
|
2224
2348
|
ok: true,
|
|
2225
2349
|
version: stdout.trim() || null,
|
|
@@ -2262,8 +2386,8 @@ async function defaultCheckMatrix(homeserver, timeoutMs) {
|
|
|
2262
2386
|
|
|
2263
2387
|
// src/channels/matrix-channel.ts
|
|
2264
2388
|
var import_promises2 = __toESM(require("fs/promises"));
|
|
2265
|
-
var
|
|
2266
|
-
var
|
|
2389
|
+
var import_node_os4 = __toESM(require("os"));
|
|
2390
|
+
var import_node_path6 = __toESM(require("path"));
|
|
2267
2391
|
var import_matrix_js_sdk = require("matrix-js-sdk");
|
|
2268
2392
|
|
|
2269
2393
|
// src/utils/message.ts
|
|
@@ -2686,10 +2810,10 @@ var MatrixChannel = class {
|
|
|
2686
2810
|
}
|
|
2687
2811
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
2688
2812
|
const extension = resolveFileExtension(fileName, mimeType);
|
|
2689
|
-
const directory =
|
|
2813
|
+
const directory = import_node_path6.default.join(import_node_os4.default.tmpdir(), "codeharbor-media");
|
|
2690
2814
|
await import_promises2.default.mkdir(directory, { recursive: true });
|
|
2691
2815
|
const safeEventId = sanitizeFilename(eventId);
|
|
2692
|
-
const targetPath =
|
|
2816
|
+
const targetPath = import_node_path6.default.join(directory, `${safeEventId}-${index}${extension}`);
|
|
2693
2817
|
await import_promises2.default.writeFile(targetPath, bytes);
|
|
2694
2818
|
return targetPath;
|
|
2695
2819
|
}
|
|
@@ -2774,7 +2898,7 @@ function sanitizeFilename(value) {
|
|
|
2774
2898
|
return value.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
|
|
2775
2899
|
}
|
|
2776
2900
|
function resolveFileExtension(fileName, mimeType) {
|
|
2777
|
-
const ext =
|
|
2901
|
+
const ext = import_node_path6.default.extname(fileName).trim();
|
|
2778
2902
|
if (ext) {
|
|
2779
2903
|
return ext;
|
|
2780
2904
|
}
|
|
@@ -2792,13 +2916,13 @@ function resolveFileExtension(fileName, mimeType) {
|
|
|
2792
2916
|
|
|
2793
2917
|
// src/config-service.ts
|
|
2794
2918
|
var import_node_fs5 = __toESM(require("fs"));
|
|
2795
|
-
var
|
|
2919
|
+
var import_node_path7 = __toESM(require("path"));
|
|
2796
2920
|
var ConfigService = class {
|
|
2797
2921
|
stateStore;
|
|
2798
2922
|
defaultWorkdir;
|
|
2799
2923
|
constructor(stateStore, defaultWorkdir) {
|
|
2800
2924
|
this.stateStore = stateStore;
|
|
2801
|
-
this.defaultWorkdir =
|
|
2925
|
+
this.defaultWorkdir = import_node_path7.default.resolve(defaultWorkdir);
|
|
2802
2926
|
}
|
|
2803
2927
|
resolveRoomConfig(roomId, fallbackPolicy) {
|
|
2804
2928
|
const room = this.stateStore.getRoomSettings(roomId);
|
|
@@ -2869,7 +2993,7 @@ function normalizeRoomSettingsInput(input) {
|
|
|
2869
2993
|
if (!roomId) {
|
|
2870
2994
|
throw new Error("roomId is required.");
|
|
2871
2995
|
}
|
|
2872
|
-
const workdir =
|
|
2996
|
+
const workdir = import_node_path7.default.resolve(input.workdir);
|
|
2873
2997
|
if (!import_node_fs5.default.existsSync(workdir) || !import_node_fs5.default.statSync(workdir).isDirectory()) {
|
|
2874
2998
|
throw new Error(`workdir does not exist or is not a directory: ${workdir}`);
|
|
2875
2999
|
}
|
|
@@ -2885,7 +3009,7 @@ function normalizeRoomSettingsInput(input) {
|
|
|
2885
3009
|
}
|
|
2886
3010
|
|
|
2887
3011
|
// src/executor/codex-executor.ts
|
|
2888
|
-
var
|
|
3012
|
+
var import_node_child_process4 = require("child_process");
|
|
2889
3013
|
var import_node_readline = __toESM(require("readline"));
|
|
2890
3014
|
var CodexExecutionCancelledError = class extends Error {
|
|
2891
3015
|
constructor(message = "codex execution cancelled") {
|
|
@@ -2903,7 +3027,7 @@ var CodexExecutor = class {
|
|
|
2903
3027
|
}
|
|
2904
3028
|
startExecution(prompt, sessionId, onProgress, startOptions) {
|
|
2905
3029
|
const args = buildCodexArgs(prompt, sessionId, this.options, startOptions);
|
|
2906
|
-
const child = (0,
|
|
3030
|
+
const child = (0, import_node_child_process4.spawn)(this.options.bin, args, {
|
|
2907
3031
|
cwd: startOptions?.workdir ?? this.options.workdir,
|
|
2908
3032
|
env: {
|
|
2909
3033
|
...process.env,
|
|
@@ -3136,17 +3260,17 @@ function stringify(value) {
|
|
|
3136
3260
|
|
|
3137
3261
|
// src/orchestrator.ts
|
|
3138
3262
|
var import_async_mutex = require("async-mutex");
|
|
3139
|
-
var
|
|
3263
|
+
var import_promises4 = __toESM(require("fs/promises"));
|
|
3140
3264
|
|
|
3141
3265
|
// src/compat/cli-compat-recorder.ts
|
|
3142
3266
|
var import_node_fs6 = __toESM(require("fs"));
|
|
3143
|
-
var
|
|
3267
|
+
var import_node_path8 = __toESM(require("path"));
|
|
3144
3268
|
var CliCompatRecorder = class {
|
|
3145
3269
|
filePath;
|
|
3146
3270
|
chain = Promise.resolve();
|
|
3147
3271
|
constructor(filePath) {
|
|
3148
|
-
this.filePath =
|
|
3149
|
-
import_node_fs6.default.mkdirSync(
|
|
3272
|
+
this.filePath = import_node_path8.default.resolve(filePath);
|
|
3273
|
+
import_node_fs6.default.mkdirSync(import_node_path8.default.dirname(this.filePath), { recursive: true });
|
|
3150
3274
|
}
|
|
3151
3275
|
append(entry) {
|
|
3152
3276
|
const payload = `${JSON.stringify(entry)}
|
|
@@ -3623,6 +3747,296 @@ function createIdleWorkflowSnapshot() {
|
|
|
3623
3747
|
};
|
|
3624
3748
|
}
|
|
3625
3749
|
|
|
3750
|
+
// src/workflow/autodev.ts
|
|
3751
|
+
var import_promises3 = __toESM(require("fs/promises"));
|
|
3752
|
+
var import_node_path9 = __toESM(require("path"));
|
|
3753
|
+
function parseAutoDevCommand(text) {
|
|
3754
|
+
const normalized = text.trim();
|
|
3755
|
+
if (!/^\/autodev(?:\s|$)/i.test(normalized)) {
|
|
3756
|
+
return null;
|
|
3757
|
+
}
|
|
3758
|
+
const parts = normalized.split(/\s+/);
|
|
3759
|
+
if (parts.length === 1 || parts[1]?.toLowerCase() === "status") {
|
|
3760
|
+
return { kind: "status" };
|
|
3761
|
+
}
|
|
3762
|
+
if (parts[1]?.toLowerCase() !== "run") {
|
|
3763
|
+
return null;
|
|
3764
|
+
}
|
|
3765
|
+
const taskId = normalized.replace(/^\/autodev\s+run\s*/i, "").trim();
|
|
3766
|
+
return {
|
|
3767
|
+
kind: "run",
|
|
3768
|
+
taskId: taskId || null
|
|
3769
|
+
};
|
|
3770
|
+
}
|
|
3771
|
+
async function loadAutoDevContext(workdir) {
|
|
3772
|
+
const requirementsPath = import_node_path9.default.join(workdir, "REQUIREMENTS.md");
|
|
3773
|
+
const taskListPath = import_node_path9.default.join(workdir, "TASK_LIST.md");
|
|
3774
|
+
const requirementsContent = await readOptionalFile(requirementsPath);
|
|
3775
|
+
const taskListContent = await readOptionalFile(taskListPath);
|
|
3776
|
+
return {
|
|
3777
|
+
workdir,
|
|
3778
|
+
requirementsPath,
|
|
3779
|
+
taskListPath,
|
|
3780
|
+
requirementsContent,
|
|
3781
|
+
taskListContent,
|
|
3782
|
+
tasks: taskListContent ? parseTasks(taskListContent) : []
|
|
3783
|
+
};
|
|
3784
|
+
}
|
|
3785
|
+
function summarizeAutoDevTasks(tasks) {
|
|
3786
|
+
const summary = {
|
|
3787
|
+
total: tasks.length,
|
|
3788
|
+
pending: 0,
|
|
3789
|
+
inProgress: 0,
|
|
3790
|
+
completed: 0,
|
|
3791
|
+
cancelled: 0,
|
|
3792
|
+
blocked: 0
|
|
3793
|
+
};
|
|
3794
|
+
for (const task of tasks) {
|
|
3795
|
+
if (task.status === "pending") {
|
|
3796
|
+
summary.pending += 1;
|
|
3797
|
+
continue;
|
|
3798
|
+
}
|
|
3799
|
+
if (task.status === "in_progress") {
|
|
3800
|
+
summary.inProgress += 1;
|
|
3801
|
+
continue;
|
|
3802
|
+
}
|
|
3803
|
+
if (task.status === "completed") {
|
|
3804
|
+
summary.completed += 1;
|
|
3805
|
+
continue;
|
|
3806
|
+
}
|
|
3807
|
+
if (task.status === "cancelled") {
|
|
3808
|
+
summary.cancelled += 1;
|
|
3809
|
+
continue;
|
|
3810
|
+
}
|
|
3811
|
+
summary.blocked += 1;
|
|
3812
|
+
}
|
|
3813
|
+
return summary;
|
|
3814
|
+
}
|
|
3815
|
+
function selectAutoDevTask(tasks, taskId) {
|
|
3816
|
+
if (taskId) {
|
|
3817
|
+
const normalizedTarget = taskId.trim().toLowerCase();
|
|
3818
|
+
return tasks.find((task) => task.id.toLowerCase() === normalizedTarget) ?? null;
|
|
3819
|
+
}
|
|
3820
|
+
const inProgressTask = tasks.find((task) => task.status === "in_progress");
|
|
3821
|
+
if (inProgressTask) {
|
|
3822
|
+
return inProgressTask;
|
|
3823
|
+
}
|
|
3824
|
+
return tasks.find((task) => task.status === "pending") ?? null;
|
|
3825
|
+
}
|
|
3826
|
+
function buildAutoDevObjective(task) {
|
|
3827
|
+
return [
|
|
3828
|
+
"\u4F60\u6B63\u5728\u6267\u884C CodeHarbor AutoDev \u4EFB\u52A1\uFF0C\u8BF7\u5728\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u5B8C\u6210\u6307\u5B9A\u5F00\u53D1\u76EE\u6807\u3002",
|
|
3829
|
+
"",
|
|
3830
|
+
`\u4EFB\u52A1ID: ${task.id}`,
|
|
3831
|
+
`\u4EFB\u52A1\u63CF\u8FF0: ${task.description}`,
|
|
3832
|
+
"",
|
|
3833
|
+
"\u4E0A\u4E0B\u6587\u6587\u4EF6\uFF1A",
|
|
3834
|
+
"- REQUIREMENTS.md\uFF08\u9700\u6C42\u57FA\u7EBF\uFF09",
|
|
3835
|
+
"- TASK_LIST.md\uFF08\u4EFB\u52A1\u72B6\u6001\uFF09",
|
|
3836
|
+
"",
|
|
3837
|
+
"\u6267\u884C\u8981\u6C42\uFF1A",
|
|
3838
|
+
"1. \u5148\u8BFB\u53D6 REQUIREMENTS.md \u548C TASK_LIST.md\uFF0C\u786E\u8BA4\u8FB9\u754C\u4E0E\u7EA6\u675F\u3002",
|
|
3839
|
+
"2. \u5728\u5F53\u524D\u4ED3\u5E93\u76F4\u63A5\u5B8C\u6210\u4EE3\u7801\u4E0E\u6D4B\u8BD5\u6539\u52A8\u3002",
|
|
3840
|
+
"3. \u8FD0\u884C\u53D7\u5F71\u54CD\u9A8C\u8BC1\u547D\u4EE4\u5E76\u6C47\u603B\u7ED3\u679C\u3002",
|
|
3841
|
+
"4. \u8F93\u51FA\u6539\u52A8\u6587\u4EF6\u548C\u98CE\u9669\u8BF4\u660E\u3002"
|
|
3842
|
+
].join("\n");
|
|
3843
|
+
}
|
|
3844
|
+
function formatTaskForDisplay(task) {
|
|
3845
|
+
return `${task.id} ${task.description} (${statusToSymbol(task.status)})`;
|
|
3846
|
+
}
|
|
3847
|
+
function statusToSymbol(status) {
|
|
3848
|
+
if (status === "pending") {
|
|
3849
|
+
return "\u2B1C";
|
|
3850
|
+
}
|
|
3851
|
+
if (status === "in_progress") {
|
|
3852
|
+
return "\u{1F504}";
|
|
3853
|
+
}
|
|
3854
|
+
if (status === "completed") {
|
|
3855
|
+
return "\u2705";
|
|
3856
|
+
}
|
|
3857
|
+
if (status === "cancelled") {
|
|
3858
|
+
return "\u274C";
|
|
3859
|
+
}
|
|
3860
|
+
return "\u{1F6AB}";
|
|
3861
|
+
}
|
|
3862
|
+
async function updateAutoDevTaskStatus(taskListPath, task, nextStatus) {
|
|
3863
|
+
const content = await import_promises3.default.readFile(taskListPath, "utf8");
|
|
3864
|
+
const lines = splitLines(content);
|
|
3865
|
+
if (task.lineIndex < 0 || task.lineIndex >= lines.length) {
|
|
3866
|
+
throw new Error(`task ${task.id} line index out of range`);
|
|
3867
|
+
}
|
|
3868
|
+
const updatedLine = replaceLineStatus(lines[task.lineIndex] ?? "", task.id, nextStatus);
|
|
3869
|
+
if (!updatedLine) {
|
|
3870
|
+
throw new Error(`failed to update task status for ${task.id}`);
|
|
3871
|
+
}
|
|
3872
|
+
lines[task.lineIndex] = updatedLine;
|
|
3873
|
+
await import_promises3.default.writeFile(taskListPath, lines.join("\n"), "utf8");
|
|
3874
|
+
return {
|
|
3875
|
+
...task,
|
|
3876
|
+
status: nextStatus
|
|
3877
|
+
};
|
|
3878
|
+
}
|
|
3879
|
+
async function readOptionalFile(filePath) {
|
|
3880
|
+
try {
|
|
3881
|
+
return await import_promises3.default.readFile(filePath, "utf8");
|
|
3882
|
+
} catch (error) {
|
|
3883
|
+
if (error.code === "ENOENT") {
|
|
3884
|
+
return null;
|
|
3885
|
+
}
|
|
3886
|
+
throw error;
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
function parseTasks(content) {
|
|
3890
|
+
const lines = splitLines(content);
|
|
3891
|
+
const tasks = [];
|
|
3892
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
3893
|
+
const line = lines[index] ?? "";
|
|
3894
|
+
const tableTask = parseTableTaskLine(line, index);
|
|
3895
|
+
if (tableTask) {
|
|
3896
|
+
tasks.push(tableTask);
|
|
3897
|
+
continue;
|
|
3898
|
+
}
|
|
3899
|
+
const listTask = parseListTaskLine(line, index);
|
|
3900
|
+
if (listTask) {
|
|
3901
|
+
tasks.push(listTask);
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
return tasks;
|
|
3905
|
+
}
|
|
3906
|
+
function parseTableTaskLine(line, lineIndex) {
|
|
3907
|
+
const trimmed = line.trim();
|
|
3908
|
+
if (!trimmed.startsWith("|")) {
|
|
3909
|
+
return null;
|
|
3910
|
+
}
|
|
3911
|
+
const cells = trimmed.split("|").slice(1, -1).map((cell) => cell.trim());
|
|
3912
|
+
if (cells.length < 3) {
|
|
3913
|
+
return null;
|
|
3914
|
+
}
|
|
3915
|
+
const taskId = cells[0] ?? "";
|
|
3916
|
+
if (!isLikelyTaskId(taskId)) {
|
|
3917
|
+
return null;
|
|
3918
|
+
}
|
|
3919
|
+
const statusCell = cells[cells.length - 1] ?? "";
|
|
3920
|
+
const status = parseStatusToken(statusCell);
|
|
3921
|
+
if (!status) {
|
|
3922
|
+
return null;
|
|
3923
|
+
}
|
|
3924
|
+
return {
|
|
3925
|
+
id: taskId,
|
|
3926
|
+
description: cells[1] ?? taskId,
|
|
3927
|
+
status,
|
|
3928
|
+
lineIndex
|
|
3929
|
+
};
|
|
3930
|
+
}
|
|
3931
|
+
function parseListTaskLine(line, lineIndex) {
|
|
3932
|
+
const checkboxMatch = line.match(/^\s*[-*]\s+\[( |x|X)\]\s+(.+)$/);
|
|
3933
|
+
if (checkboxMatch) {
|
|
3934
|
+
const rawText2 = checkboxMatch[2]?.trim() ?? "";
|
|
3935
|
+
const taskId2 = extractTaskId(rawText2);
|
|
3936
|
+
if (!taskId2) {
|
|
3937
|
+
return null;
|
|
3938
|
+
}
|
|
3939
|
+
return {
|
|
3940
|
+
id: taskId2,
|
|
3941
|
+
description: stripTaskIdPrefix(rawText2, taskId2),
|
|
3942
|
+
status: checkboxMatch[1]?.toLowerCase() === "x" ? "completed" : "pending",
|
|
3943
|
+
lineIndex
|
|
3944
|
+
};
|
|
3945
|
+
}
|
|
3946
|
+
const symbolMatch = line.match(/^\s*[-*]\s*(⬜|🔄|✅|❌|🚫)\s+(.+)$/);
|
|
3947
|
+
if (!symbolMatch) {
|
|
3948
|
+
return null;
|
|
3949
|
+
}
|
|
3950
|
+
const rawText = symbolMatch[2]?.trim() ?? "";
|
|
3951
|
+
const taskId = extractTaskId(rawText);
|
|
3952
|
+
if (!taskId) {
|
|
3953
|
+
return null;
|
|
3954
|
+
}
|
|
3955
|
+
const status = parseStatusToken(symbolMatch[1] ?? "");
|
|
3956
|
+
if (!status) {
|
|
3957
|
+
return null;
|
|
3958
|
+
}
|
|
3959
|
+
return {
|
|
3960
|
+
id: taskId,
|
|
3961
|
+
description: stripTaskIdPrefix(rawText, taskId),
|
|
3962
|
+
status,
|
|
3963
|
+
lineIndex
|
|
3964
|
+
};
|
|
3965
|
+
}
|
|
3966
|
+
function stripTaskIdPrefix(text, taskId) {
|
|
3967
|
+
const normalized = text.trim();
|
|
3968
|
+
const escapedId = escapeRegex(taskId);
|
|
3969
|
+
return normalized.replace(new RegExp(`^${escapedId}[\\s:\uFF1A\\-]+`, "i"), "").trim() || normalized;
|
|
3970
|
+
}
|
|
3971
|
+
function extractTaskId(text) {
|
|
3972
|
+
const normalized = text.trim();
|
|
3973
|
+
const bracketMatch = normalized.match(/\(([A-Za-z][A-Za-z0-9._-]*)\)/);
|
|
3974
|
+
if (bracketMatch?.[1] && isLikelyTaskId(bracketMatch[1])) {
|
|
3975
|
+
return bracketMatch[1];
|
|
3976
|
+
}
|
|
3977
|
+
const token = normalized.split(/\s+/)[0]?.replace(/[,::|]+$/, "") ?? "";
|
|
3978
|
+
if (!isLikelyTaskId(token)) {
|
|
3979
|
+
return null;
|
|
3980
|
+
}
|
|
3981
|
+
return token;
|
|
3982
|
+
}
|
|
3983
|
+
function isLikelyTaskId(taskId) {
|
|
3984
|
+
if (!/^[A-Za-z][A-Za-z0-9._-]*$/.test(taskId)) {
|
|
3985
|
+
return false;
|
|
3986
|
+
}
|
|
3987
|
+
return /\d/.test(taskId);
|
|
3988
|
+
}
|
|
3989
|
+
function parseStatusToken(text) {
|
|
3990
|
+
const normalized = text.trim().toLowerCase();
|
|
3991
|
+
if (!normalized) {
|
|
3992
|
+
return null;
|
|
3993
|
+
}
|
|
3994
|
+
if (normalized.includes("\u2705") || normalized.includes("[x]") || normalized.includes("done")) {
|
|
3995
|
+
return "completed";
|
|
3996
|
+
}
|
|
3997
|
+
if (normalized.includes("\u2B1C") || normalized.includes("\u2610") || normalized.includes("[ ]") || normalized === "todo") {
|
|
3998
|
+
return "pending";
|
|
3999
|
+
}
|
|
4000
|
+
if (normalized.includes("\u{1F504}") || normalized.includes("\u8FDB\u884C\u4E2D") || normalized.includes("in progress")) {
|
|
4001
|
+
return "in_progress";
|
|
4002
|
+
}
|
|
4003
|
+
if (normalized.includes("\u274C") || normalized.includes("\u53D6\u6D88") || normalized.includes("cancel")) {
|
|
4004
|
+
return "cancelled";
|
|
4005
|
+
}
|
|
4006
|
+
if (normalized.includes("\u{1F6AB}") || normalized.includes("\u963B\u585E") || normalized.includes("block")) {
|
|
4007
|
+
return "blocked";
|
|
4008
|
+
}
|
|
4009
|
+
return null;
|
|
4010
|
+
}
|
|
4011
|
+
function replaceLineStatus(line, taskId, status) {
|
|
4012
|
+
const trimmed = line.trim();
|
|
4013
|
+
const symbol = statusToSymbol(status);
|
|
4014
|
+
if (trimmed.startsWith("|")) {
|
|
4015
|
+
const cells = trimmed.split("|").slice(1, -1).map((cell) => cell.trim());
|
|
4016
|
+
if (cells.length >= 3 && cells[0]?.toLowerCase() === taskId.toLowerCase()) {
|
|
4017
|
+
const rawParts = line.split("|");
|
|
4018
|
+
if (rawParts.length >= 3) {
|
|
4019
|
+
rawParts[rawParts.length - 2] = ` ${symbol} `;
|
|
4020
|
+
return rawParts.join("|");
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
if (/\[( |x|X)\]/.test(line)) {
|
|
4025
|
+
const checkbox = status === "completed" ? "[x]" : "[ ]";
|
|
4026
|
+
return line.replace(/\[( |x|X)\]/, checkbox);
|
|
4027
|
+
}
|
|
4028
|
+
if (/(⬜|🔄|✅|❌|🚫)/.test(line)) {
|
|
4029
|
+
return line.replace(/(⬜|🔄|✅|❌|🚫)/, symbol);
|
|
4030
|
+
}
|
|
4031
|
+
return null;
|
|
4032
|
+
}
|
|
4033
|
+
function splitLines(content) {
|
|
4034
|
+
return content.replace(/\r\n/g, "\n").split("\n");
|
|
4035
|
+
}
|
|
4036
|
+
function escapeRegex(value) {
|
|
4037
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4038
|
+
}
|
|
4039
|
+
|
|
3626
4040
|
// src/orchestrator.ts
|
|
3627
4041
|
var RequestMetrics = class {
|
|
3628
4042
|
total = 0;
|
|
@@ -3710,6 +4124,7 @@ var Orchestrator = class {
|
|
|
3710
4124
|
cliCompatRecorder;
|
|
3711
4125
|
workflowRunner;
|
|
3712
4126
|
workflowSnapshots = /* @__PURE__ */ new Map();
|
|
4127
|
+
autoDevSnapshots = /* @__PURE__ */ new Map();
|
|
3713
4128
|
metrics = new RequestMetrics();
|
|
3714
4129
|
lastLockPruneAt = 0;
|
|
3715
4130
|
constructor(channel, executor, stateStore, logger, options) {
|
|
@@ -3804,11 +4219,17 @@ var Orchestrator = class {
|
|
|
3804
4219
|
return;
|
|
3805
4220
|
}
|
|
3806
4221
|
const workflowCommand = this.workflowRunner.isEnabled() ? parseWorkflowCommand(route.prompt) : null;
|
|
4222
|
+
const autoDevCommand = this.workflowRunner.isEnabled() ? parseAutoDevCommand(route.prompt) : null;
|
|
3807
4223
|
if (workflowCommand?.kind === "status") {
|
|
3808
4224
|
await this.handleWorkflowStatusCommand(sessionKey, message);
|
|
3809
4225
|
this.stateStore.markEventProcessed(sessionKey, message.eventId);
|
|
3810
4226
|
return;
|
|
3811
4227
|
}
|
|
4228
|
+
if (autoDevCommand?.kind === "status") {
|
|
4229
|
+
await this.handleAutoDevStatusCommand(sessionKey, message, roomConfig.workdir);
|
|
4230
|
+
this.stateStore.markEventProcessed(sessionKey, message.eventId);
|
|
4231
|
+
return;
|
|
4232
|
+
}
|
|
3812
4233
|
const rateDecision = this.rateLimiter.tryAcquire({
|
|
3813
4234
|
userId: message.senderId,
|
|
3814
4235
|
roomId: message.conversationId
|
|
@@ -3857,6 +4278,37 @@ var Orchestrator = class {
|
|
|
3857
4278
|
}
|
|
3858
4279
|
return;
|
|
3859
4280
|
}
|
|
4281
|
+
if (autoDevCommand?.kind === "run") {
|
|
4282
|
+
const executionStartedAt = Date.now();
|
|
4283
|
+
let sendDurationMs2 = 0;
|
|
4284
|
+
this.stateStore.activateSession(sessionKey, this.sessionActiveWindowMs);
|
|
4285
|
+
try {
|
|
4286
|
+
const sendStartedAt = Date.now();
|
|
4287
|
+
await this.handleAutoDevRunCommand(
|
|
4288
|
+
autoDevCommand.taskId,
|
|
4289
|
+
sessionKey,
|
|
4290
|
+
message,
|
|
4291
|
+
requestId,
|
|
4292
|
+
roomConfig.workdir
|
|
4293
|
+
);
|
|
4294
|
+
sendDurationMs2 += Date.now() - sendStartedAt;
|
|
4295
|
+
this.stateStore.markEventProcessed(sessionKey, message.eventId);
|
|
4296
|
+
this.metrics.record("success", queueWaitMs, Date.now() - executionStartedAt, sendDurationMs2);
|
|
4297
|
+
} catch (error) {
|
|
4298
|
+
sendDurationMs2 += await this.sendAutoDevFailure(message.conversationId, error);
|
|
4299
|
+
this.stateStore.commitExecutionHandled(sessionKey, message.eventId);
|
|
4300
|
+
const status = classifyExecutionOutcome(error);
|
|
4301
|
+
this.metrics.record(status, queueWaitMs, Date.now() - executionStartedAt, sendDurationMs2);
|
|
4302
|
+
this.logger.error("AutoDev request failed", {
|
|
4303
|
+
requestId,
|
|
4304
|
+
sessionKey,
|
|
4305
|
+
error: formatError2(error)
|
|
4306
|
+
});
|
|
4307
|
+
} finally {
|
|
4308
|
+
rateDecision.release?.();
|
|
4309
|
+
}
|
|
4310
|
+
return;
|
|
4311
|
+
}
|
|
3860
4312
|
this.stateStore.activateSession(sessionKey, this.sessionActiveWindowMs);
|
|
3861
4313
|
const previousCodexSessionId = this.stateStore.getCodexSessionId(sessionKey);
|
|
3862
4314
|
const executionPrompt = this.buildExecutionPrompt(route.prompt, message);
|
|
@@ -4078,6 +4530,7 @@ var Orchestrator = class {
|
|
|
4078
4530
|
const limiter = this.rateLimiter.snapshot();
|
|
4079
4531
|
const runtime = this.sessionRuntime.getRuntimeStats();
|
|
4080
4532
|
const workflow = this.workflowSnapshots.get(sessionKey) ?? createIdleWorkflowSnapshot();
|
|
4533
|
+
const autoDev = this.autoDevSnapshots.get(sessionKey) ?? createIdleAutoDevSnapshot();
|
|
4081
4534
|
await this.channel.sendNotice(
|
|
4082
4535
|
message.conversationId,
|
|
4083
4536
|
`[CodeHarbor] \u5F53\u524D\u72B6\u6001
|
|
@@ -4091,7 +4544,8 @@ var Orchestrator = class {
|
|
|
4091
4544
|
- \u5E73\u5747\u8017\u65F6: queue=${metrics.avgQueueMs}ms, exec=${metrics.avgExecMs}ms, send=${metrics.avgSendMs}ms
|
|
4092
4545
|
- \u9650\u6D41\u5E76\u53D1: global=${limiter.activeGlobal}, users=${limiter.activeUsers}, rooms=${limiter.activeRooms}
|
|
4093
4546
|
- CLI runtime: workers=${runtime.workerCount}, running=${runtime.runningCount}, compat_mode=${this.cliCompat.enabled ? "on" : "off"}
|
|
4094
|
-
- Multi-Agent workflow: enabled=${this.workflowRunner.isEnabled() ? "on" : "off"}, state=${workflow.state}
|
|
4547
|
+
- Multi-Agent workflow: enabled=${this.workflowRunner.isEnabled() ? "on" : "off"}, state=${workflow.state}
|
|
4548
|
+
- AutoDev: enabled=${this.workflowRunner.isEnabled() ? "on" : "off"}, state=${autoDev.state}, task=${autoDev.taskId ?? "N/A"}`
|
|
4095
4549
|
);
|
|
4096
4550
|
}
|
|
4097
4551
|
async handleWorkflowStatusCommand(sessionKey, message) {
|
|
@@ -4108,11 +4562,158 @@ var Orchestrator = class {
|
|
|
4108
4562
|
- error: ${snapshot.error ?? "N/A"}`
|
|
4109
4563
|
);
|
|
4110
4564
|
}
|
|
4565
|
+
async handleAutoDevStatusCommand(sessionKey, message, workdir) {
|
|
4566
|
+
const snapshot = this.autoDevSnapshots.get(sessionKey) ?? createIdleAutoDevSnapshot();
|
|
4567
|
+
try {
|
|
4568
|
+
const context = await loadAutoDevContext(workdir);
|
|
4569
|
+
const summary = summarizeAutoDevTasks(context.tasks);
|
|
4570
|
+
const nextTask = selectAutoDevTask(context.tasks);
|
|
4571
|
+
await this.channel.sendNotice(
|
|
4572
|
+
message.conversationId,
|
|
4573
|
+
`[CodeHarbor] AutoDev \u72B6\u6001
|
|
4574
|
+
- workdir: ${workdir}
|
|
4575
|
+
- REQUIREMENTS.md: ${context.requirementsContent ? "found" : "missing"}
|
|
4576
|
+
- TASK_LIST.md: ${context.taskListContent ? "found" : "missing"}
|
|
4577
|
+
- tasks: total=${summary.total}, pending=${summary.pending}, in_progress=${summary.inProgress}, completed=${summary.completed}, blocked=${summary.blocked}, cancelled=${summary.cancelled}
|
|
4578
|
+
- nextTask: ${nextTask ? formatTaskForDisplay(nextTask) : "N/A"}
|
|
4579
|
+
- runState: ${snapshot.state}
|
|
4580
|
+
- runTask: ${snapshot.taskId ? `${snapshot.taskId} ${snapshot.taskDescription ?? ""}`.trim() : "N/A"}
|
|
4581
|
+
- runApproved: ${snapshot.approved === null ? "N/A" : snapshot.approved ? "yes" : "no"}
|
|
4582
|
+
- runError: ${snapshot.error ?? "N/A"}`
|
|
4583
|
+
);
|
|
4584
|
+
} catch (error) {
|
|
4585
|
+
await this.channel.sendNotice(message.conversationId, `[CodeHarbor] AutoDev \u72B6\u6001\u8BFB\u53D6\u5931\u8D25: ${formatError2(error)}`);
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
async handleAutoDevRunCommand(taskId, sessionKey, message, requestId, workdir) {
|
|
4589
|
+
const requestedTaskId = taskId?.trim() || null;
|
|
4590
|
+
const context = await loadAutoDevContext(workdir);
|
|
4591
|
+
if (!context.requirementsContent) {
|
|
4592
|
+
await this.channel.sendNotice(
|
|
4593
|
+
message.conversationId,
|
|
4594
|
+
`[CodeHarbor] AutoDev \u9700\u8981 ${context.requirementsPath}\uFF0C\u8BF7\u5148\u51C6\u5907\u9700\u6C42\u6587\u6863\u3002`
|
|
4595
|
+
);
|
|
4596
|
+
return;
|
|
4597
|
+
}
|
|
4598
|
+
if (!context.taskListContent) {
|
|
4599
|
+
await this.channel.sendNotice(
|
|
4600
|
+
message.conversationId,
|
|
4601
|
+
`[CodeHarbor] AutoDev \u9700\u8981 ${context.taskListPath}\uFF0C\u8BF7\u5148\u51C6\u5907\u4EFB\u52A1\u6E05\u5355\u3002`
|
|
4602
|
+
);
|
|
4603
|
+
return;
|
|
4604
|
+
}
|
|
4605
|
+
if (context.tasks.length === 0) {
|
|
4606
|
+
await this.channel.sendNotice(
|
|
4607
|
+
message.conversationId,
|
|
4608
|
+
"[CodeHarbor] \u672A\u5728 TASK_LIST.md \u8BC6\u522B\u5230\u4EFB\u52A1\uFF08\u9700\u5305\u542B\u4EFB\u52A1 ID \u4E0E\u72B6\u6001\u5217\uFF09\u3002"
|
|
4609
|
+
);
|
|
4610
|
+
return;
|
|
4611
|
+
}
|
|
4612
|
+
const selectedTask = selectAutoDevTask(context.tasks, requestedTaskId);
|
|
4613
|
+
if (!selectedTask) {
|
|
4614
|
+
if (requestedTaskId) {
|
|
4615
|
+
await this.channel.sendNotice(message.conversationId, `[CodeHarbor] \u672A\u627E\u5230\u4EFB\u52A1 ${requestedTaskId}\u3002`);
|
|
4616
|
+
return;
|
|
4617
|
+
}
|
|
4618
|
+
await this.channel.sendNotice(message.conversationId, "[CodeHarbor] \u5F53\u524D\u6CA1\u6709\u53EF\u6267\u884C\u4EFB\u52A1\uFF08pending/in_progress\uFF09\u3002");
|
|
4619
|
+
return;
|
|
4620
|
+
}
|
|
4621
|
+
if (selectedTask.status === "completed") {
|
|
4622
|
+
await this.channel.sendNotice(message.conversationId, `[CodeHarbor] \u4EFB\u52A1 ${selectedTask.id} \u5DF2\u5B8C\u6210\uFF08\u2705\uFF09\u3002`);
|
|
4623
|
+
return;
|
|
4624
|
+
}
|
|
4625
|
+
if (selectedTask.status === "cancelled") {
|
|
4626
|
+
await this.channel.sendNotice(message.conversationId, `[CodeHarbor] \u4EFB\u52A1 ${selectedTask.id} \u5DF2\u53D6\u6D88\uFF08\u274C\uFF09\u3002`);
|
|
4627
|
+
return;
|
|
4628
|
+
}
|
|
4629
|
+
let activeTask = selectedTask;
|
|
4630
|
+
let promotedToInProgress = false;
|
|
4631
|
+
if (selectedTask.status === "pending") {
|
|
4632
|
+
activeTask = await updateAutoDevTaskStatus(context.taskListPath, selectedTask, "in_progress");
|
|
4633
|
+
promotedToInProgress = true;
|
|
4634
|
+
}
|
|
4635
|
+
const startedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
4636
|
+
this.autoDevSnapshots.set(sessionKey, {
|
|
4637
|
+
state: "running",
|
|
4638
|
+
startedAt: startedAtIso,
|
|
4639
|
+
endedAt: null,
|
|
4640
|
+
taskId: activeTask.id,
|
|
4641
|
+
taskDescription: activeTask.description,
|
|
4642
|
+
approved: null,
|
|
4643
|
+
repairRounds: 0,
|
|
4644
|
+
error: null
|
|
4645
|
+
});
|
|
4646
|
+
await this.channel.sendNotice(
|
|
4647
|
+
message.conversationId,
|
|
4648
|
+
`[CodeHarbor] AutoDev \u542F\u52A8\u4EFB\u52A1 ${activeTask.id}: ${activeTask.description}`
|
|
4649
|
+
);
|
|
4650
|
+
try {
|
|
4651
|
+
const result = await this.handleWorkflowRunCommand(
|
|
4652
|
+
buildAutoDevObjective(activeTask),
|
|
4653
|
+
sessionKey,
|
|
4654
|
+
message,
|
|
4655
|
+
requestId,
|
|
4656
|
+
workdir
|
|
4657
|
+
);
|
|
4658
|
+
if (!result) {
|
|
4659
|
+
return;
|
|
4660
|
+
}
|
|
4661
|
+
let finalTask = activeTask;
|
|
4662
|
+
if (result.approved) {
|
|
4663
|
+
finalTask = await updateAutoDevTaskStatus(context.taskListPath, activeTask, "completed");
|
|
4664
|
+
}
|
|
4665
|
+
const endedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
4666
|
+
this.autoDevSnapshots.set(sessionKey, {
|
|
4667
|
+
state: "succeeded",
|
|
4668
|
+
startedAt: startedAtIso,
|
|
4669
|
+
endedAt: endedAtIso,
|
|
4670
|
+
taskId: finalTask.id,
|
|
4671
|
+
taskDescription: finalTask.description,
|
|
4672
|
+
approved: result.approved,
|
|
4673
|
+
repairRounds: result.repairRounds,
|
|
4674
|
+
error: null
|
|
4675
|
+
});
|
|
4676
|
+
const refreshed = await loadAutoDevContext(workdir);
|
|
4677
|
+
const nextTask = selectAutoDevTask(refreshed.tasks);
|
|
4678
|
+
await this.channel.sendNotice(
|
|
4679
|
+
message.conversationId,
|
|
4680
|
+
`[CodeHarbor] AutoDev \u4EFB\u52A1\u7ED3\u679C
|
|
4681
|
+
- task: ${finalTask.id}
|
|
4682
|
+
- reviewer approved: ${result.approved ? "yes" : "no"}
|
|
4683
|
+
- task status: ${statusToSymbol(finalTask.status)}
|
|
4684
|
+
- nextTask: ${nextTask ? formatTaskForDisplay(nextTask) : "N/A"}`
|
|
4685
|
+
);
|
|
4686
|
+
} catch (error) {
|
|
4687
|
+
if (promotedToInProgress) {
|
|
4688
|
+
try {
|
|
4689
|
+
await updateAutoDevTaskStatus(context.taskListPath, activeTask, "pending");
|
|
4690
|
+
} catch (restoreError) {
|
|
4691
|
+
this.logger.warn("Failed to restore AutoDev task status after failure", {
|
|
4692
|
+
taskId: activeTask.id,
|
|
4693
|
+
error: formatError2(restoreError)
|
|
4694
|
+
});
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4697
|
+
const status = classifyExecutionOutcome(error);
|
|
4698
|
+
const endedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
4699
|
+
this.autoDevSnapshots.set(sessionKey, {
|
|
4700
|
+
state: status === "cancelled" ? "idle" : "failed",
|
|
4701
|
+
startedAt: startedAtIso,
|
|
4702
|
+
endedAt: endedAtIso,
|
|
4703
|
+
taskId: activeTask.id,
|
|
4704
|
+
taskDescription: activeTask.description,
|
|
4705
|
+
approved: null,
|
|
4706
|
+
repairRounds: 0,
|
|
4707
|
+
error: formatError2(error)
|
|
4708
|
+
});
|
|
4709
|
+
throw error;
|
|
4710
|
+
}
|
|
4711
|
+
}
|
|
4111
4712
|
async handleWorkflowRunCommand(objective, sessionKey, message, requestId, workdir) {
|
|
4112
4713
|
const normalizedObjective = objective.trim();
|
|
4113
4714
|
if (!normalizedObjective) {
|
|
4114
4715
|
await this.channel.sendNotice(message.conversationId, "[CodeHarbor] /agents run \u9700\u8981\u63D0\u4F9B\u4EFB\u52A1\u76EE\u6807\u3002");
|
|
4115
|
-
return;
|
|
4716
|
+
return null;
|
|
4116
4717
|
}
|
|
4117
4718
|
const requestStartedAt = Date.now();
|
|
4118
4719
|
let progressNoticeEventId = null;
|
|
@@ -4174,6 +4775,7 @@ var Orchestrator = class {
|
|
|
4174
4775
|
});
|
|
4175
4776
|
await this.channel.sendMessage(message.conversationId, buildWorkflowResultReply(result));
|
|
4176
4777
|
await this.finishProgress(progressCtx, `\u591A\u667A\u80FD\u4F53\u6D41\u7A0B\u5B8C\u6210\uFF08\u8017\u65F6 ${formatDurationMs(Date.now() - requestStartedAt)}\uFF09`);
|
|
4778
|
+
return result;
|
|
4177
4779
|
} catch (error) {
|
|
4178
4780
|
const status = classifyExecutionOutcome(error);
|
|
4179
4781
|
const endedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -4206,6 +4808,16 @@ var Orchestrator = class {
|
|
|
4206
4808
|
await this.channel.sendMessage(conversationId, `[CodeHarbor] Multi-Agent workflow \u5931\u8D25: ${formatError2(error)}`);
|
|
4207
4809
|
return Date.now() - startedAt;
|
|
4208
4810
|
}
|
|
4811
|
+
async sendAutoDevFailure(conversationId, error) {
|
|
4812
|
+
const startedAt = Date.now();
|
|
4813
|
+
const status = classifyExecutionOutcome(error);
|
|
4814
|
+
if (status === "cancelled") {
|
|
4815
|
+
await this.channel.sendNotice(conversationId, "[CodeHarbor] AutoDev \u5DF2\u53D6\u6D88\u3002");
|
|
4816
|
+
return Date.now() - startedAt;
|
|
4817
|
+
}
|
|
4818
|
+
await this.channel.sendMessage(conversationId, `[CodeHarbor] AutoDev \u5931\u8D25: ${formatError2(error)}`);
|
|
4819
|
+
return Date.now() - startedAt;
|
|
4820
|
+
}
|
|
4209
4821
|
async handleStopCommand(sessionKey, message, requestId) {
|
|
4210
4822
|
this.stateStore.deactivateSession(sessionKey);
|
|
4211
4823
|
this.stateStore.clearCodexSessionId(sessionKey);
|
|
@@ -4401,6 +5013,18 @@ ${attachmentSummary}
|
|
|
4401
5013
|
}
|
|
4402
5014
|
}
|
|
4403
5015
|
};
|
|
5016
|
+
function createIdleAutoDevSnapshot() {
|
|
5017
|
+
return {
|
|
5018
|
+
state: "idle",
|
|
5019
|
+
startedAt: null,
|
|
5020
|
+
endedAt: null,
|
|
5021
|
+
taskId: null,
|
|
5022
|
+
taskDescription: null,
|
|
5023
|
+
approved: null,
|
|
5024
|
+
repairRounds: 0,
|
|
5025
|
+
error: null
|
|
5026
|
+
};
|
|
5027
|
+
}
|
|
4404
5028
|
function buildSessionKey(message) {
|
|
4405
5029
|
return `${message.channel}:${message.conversationId}:${message.senderId}`;
|
|
4406
5030
|
}
|
|
@@ -4424,7 +5048,7 @@ async function cleanupAttachmentFiles(imagePaths) {
|
|
|
4424
5048
|
await Promise.all(
|
|
4425
5049
|
imagePaths.map(async (imagePath) => {
|
|
4426
5050
|
try {
|
|
4427
|
-
await
|
|
5051
|
+
await import_promises4.default.unlink(imagePath);
|
|
4428
5052
|
} catch {
|
|
4429
5053
|
}
|
|
4430
5054
|
})
|
|
@@ -4474,11 +5098,11 @@ function stripLeadingBotMention(text, matrixUserId) {
|
|
|
4474
5098
|
if (!matrixUserId) {
|
|
4475
5099
|
return text;
|
|
4476
5100
|
}
|
|
4477
|
-
const escapedUserId =
|
|
5101
|
+
const escapedUserId = escapeRegex2(matrixUserId);
|
|
4478
5102
|
const mentionPattern = new RegExp(`^\\s*(?:<)?${escapedUserId}(?:>)?[\\s,:\uFF0C\uFF1A-]*`, "i");
|
|
4479
5103
|
return text.replace(mentionPattern, "").trim();
|
|
4480
5104
|
}
|
|
4481
|
-
function
|
|
5105
|
+
function escapeRegex2(value) {
|
|
4482
5106
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4483
5107
|
}
|
|
4484
5108
|
function formatDurationMs(durationMs) {
|
|
@@ -4541,7 +5165,7 @@ ${result.review}
|
|
|
4541
5165
|
|
|
4542
5166
|
// src/store/state-store.ts
|
|
4543
5167
|
var import_node_fs7 = __toESM(require("fs"));
|
|
4544
|
-
var
|
|
5168
|
+
var import_node_path10 = __toESM(require("path"));
|
|
4545
5169
|
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4546
5170
|
var PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
4547
5171
|
var SQLITE_MODULE_ID = `node:${"sqlite"}`;
|
|
@@ -4567,7 +5191,7 @@ var StateStore = class {
|
|
|
4567
5191
|
this.maxProcessedEventsPerSession = maxProcessedEventsPerSession;
|
|
4568
5192
|
this.maxSessionAgeMs = maxSessionAgeDays * ONE_DAY_MS;
|
|
4569
5193
|
this.maxSessions = maxSessions;
|
|
4570
|
-
import_node_fs7.default.mkdirSync(
|
|
5194
|
+
import_node_fs7.default.mkdirSync(import_node_path10.default.dirname(this.dbPath), { recursive: true });
|
|
4571
5195
|
this.db = new DatabaseSync(this.dbPath);
|
|
4572
5196
|
this.initializeSchema();
|
|
4573
5197
|
this.importLegacyStateIfNeeded();
|
|
@@ -4938,7 +5562,7 @@ function boolToInt(value) {
|
|
|
4938
5562
|
}
|
|
4939
5563
|
|
|
4940
5564
|
// src/app.ts
|
|
4941
|
-
var
|
|
5565
|
+
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process5.execFile);
|
|
4942
5566
|
var CodeHarborApp = class {
|
|
4943
5567
|
config;
|
|
4944
5568
|
logger;
|
|
@@ -5024,6 +5648,7 @@ var CodeHarborAdminApp = class {
|
|
|
5024
5648
|
host: options?.host ?? config.adminBindHost,
|
|
5025
5649
|
port: options?.port ?? config.adminPort,
|
|
5026
5650
|
adminToken: config.adminToken,
|
|
5651
|
+
adminTokens: config.adminTokens,
|
|
5027
5652
|
adminIpAllowlist: config.adminIpAllowlist,
|
|
5028
5653
|
adminAllowedOrigins: config.adminAllowedOrigins
|
|
5029
5654
|
});
|
|
@@ -5034,7 +5659,7 @@ var CodeHarborAdminApp = class {
|
|
|
5034
5659
|
this.logger.info("CodeHarbor admin server started", {
|
|
5035
5660
|
host: address?.host ?? this.config.adminBindHost,
|
|
5036
5661
|
port: address?.port ?? this.config.adminPort,
|
|
5037
|
-
tokenProtected: Boolean(this.config.adminToken)
|
|
5662
|
+
tokenProtected: Boolean(this.config.adminToken) || this.config.adminTokens.length > 0
|
|
5038
5663
|
});
|
|
5039
5664
|
}
|
|
5040
5665
|
async stop() {
|
|
@@ -5050,7 +5675,7 @@ async function runDoctor(config) {
|
|
|
5050
5675
|
const logger = new Logger(config.logLevel);
|
|
5051
5676
|
logger.info("Doctor check started");
|
|
5052
5677
|
try {
|
|
5053
|
-
const { stdout } = await
|
|
5678
|
+
const { stdout } = await execFileAsync3(config.codexBin, ["--version"]);
|
|
5054
5679
|
logger.info("codex available", { version: stdout.trim() });
|
|
5055
5680
|
} catch (error) {
|
|
5056
5681
|
logger.error("codex unavailable", error);
|
|
@@ -5088,7 +5713,7 @@ function isNonLoopbackHost(host) {
|
|
|
5088
5713
|
|
|
5089
5714
|
// src/config.ts
|
|
5090
5715
|
var import_node_fs8 = __toESM(require("fs"));
|
|
5091
|
-
var
|
|
5716
|
+
var import_node_path11 = __toESM(require("path"));
|
|
5092
5717
|
var import_dotenv2 = __toESM(require("dotenv"));
|
|
5093
5718
|
var import_zod = require("zod");
|
|
5094
5719
|
var configSchema = import_zod.z.object({
|
|
@@ -5139,6 +5764,7 @@ var configSchema = import_zod.z.object({
|
|
|
5139
5764
|
ADMIN_BIND_HOST: import_zod.z.string().default("127.0.0.1"),
|
|
5140
5765
|
ADMIN_PORT: import_zod.z.string().default("8787").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().min(1).max(65535)),
|
|
5141
5766
|
ADMIN_TOKEN: import_zod.z.string().default(""),
|
|
5767
|
+
ADMIN_TOKENS_JSON: import_zod.z.string().default(""),
|
|
5142
5768
|
ADMIN_IP_ALLOWLIST: import_zod.z.string().default(""),
|
|
5143
5769
|
ADMIN_ALLOWED_ORIGINS: import_zod.z.string().default(""),
|
|
5144
5770
|
LOG_LEVEL: import_zod.z.enum(["debug", "info", "warn", "error"]).default("info")
|
|
@@ -5149,7 +5775,7 @@ var configSchema = import_zod.z.object({
|
|
|
5149
5775
|
matrixCommandPrefix: v.MATRIX_COMMAND_PREFIX,
|
|
5150
5776
|
codexBin: v.CODEX_BIN,
|
|
5151
5777
|
codexModel: v.CODEX_MODEL?.trim() || null,
|
|
5152
|
-
codexWorkdir:
|
|
5778
|
+
codexWorkdir: import_node_path11.default.resolve(v.CODEX_WORKDIR),
|
|
5153
5779
|
codexDangerousBypass: v.CODEX_DANGEROUS_BYPASS,
|
|
5154
5780
|
codexExecTimeoutMs: v.CODEX_EXEC_TIMEOUT_MS,
|
|
5155
5781
|
codexSandboxMode: v.CODEX_SANDBOX_MODE?.trim() || null,
|
|
@@ -5160,8 +5786,8 @@ var configSchema = import_zod.z.object({
|
|
|
5160
5786
|
enabled: v.AGENT_WORKFLOW_ENABLED,
|
|
5161
5787
|
autoRepairMaxRounds: v.AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS
|
|
5162
5788
|
},
|
|
5163
|
-
stateDbPath:
|
|
5164
|
-
legacyStateJsonPath: v.STATE_PATH.trim() ?
|
|
5789
|
+
stateDbPath: import_node_path11.default.resolve(v.STATE_DB_PATH),
|
|
5790
|
+
legacyStateJsonPath: v.STATE_PATH.trim() ? import_node_path11.default.resolve(v.STATE_PATH) : null,
|
|
5165
5791
|
maxProcessedEventsPerSession: v.MAX_PROCESSED_EVENTS_PER_SESSION,
|
|
5166
5792
|
maxSessionAgeDays: v.MAX_SESSION_AGE_DAYS,
|
|
5167
5793
|
maxSessions: v.MAX_SESSIONS,
|
|
@@ -5192,17 +5818,18 @@ var configSchema = import_zod.z.object({
|
|
|
5192
5818
|
disableReplyChunkSplit: v.CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT,
|
|
5193
5819
|
progressThrottleMs: v.CLI_COMPAT_PROGRESS_THROTTLE_MS,
|
|
5194
5820
|
fetchMedia: v.CLI_COMPAT_FETCH_MEDIA,
|
|
5195
|
-
recordPath: v.CLI_COMPAT_RECORD_PATH.trim() ?
|
|
5821
|
+
recordPath: v.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path11.default.resolve(v.CLI_COMPAT_RECORD_PATH) : null
|
|
5196
5822
|
},
|
|
5197
5823
|
doctorHttpTimeoutMs: v.DOCTOR_HTTP_TIMEOUT_MS,
|
|
5198
5824
|
adminBindHost: v.ADMIN_BIND_HOST.trim() || "127.0.0.1",
|
|
5199
5825
|
adminPort: v.ADMIN_PORT,
|
|
5200
5826
|
adminToken: v.ADMIN_TOKEN.trim() || null,
|
|
5827
|
+
adminTokens: parseAdminTokens(v.ADMIN_TOKENS_JSON),
|
|
5201
5828
|
adminIpAllowlist: parseCsvList(v.ADMIN_IP_ALLOWLIST),
|
|
5202
5829
|
adminAllowedOrigins: parseCsvList(v.ADMIN_ALLOWED_ORIGINS),
|
|
5203
5830
|
logLevel: v.LOG_LEVEL
|
|
5204
5831
|
}));
|
|
5205
|
-
function loadEnvFromFile(filePath =
|
|
5832
|
+
function loadEnvFromFile(filePath = import_node_path11.default.resolve(process.cwd(), ".env"), env = process.env) {
|
|
5206
5833
|
import_dotenv2.default.config({
|
|
5207
5834
|
path: filePath,
|
|
5208
5835
|
processEnv: env,
|
|
@@ -5215,9 +5842,9 @@ function loadConfig(env = process.env) {
|
|
|
5215
5842
|
const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "config"}: ${issue.message}`).join("; ");
|
|
5216
5843
|
throw new Error(`Invalid configuration: ${message}`);
|
|
5217
5844
|
}
|
|
5218
|
-
import_node_fs8.default.mkdirSync(
|
|
5845
|
+
import_node_fs8.default.mkdirSync(import_node_path11.default.dirname(parsed.data.stateDbPath), { recursive: true });
|
|
5219
5846
|
if (parsed.data.legacyStateJsonPath) {
|
|
5220
|
-
import_node_fs8.default.mkdirSync(
|
|
5847
|
+
import_node_fs8.default.mkdirSync(import_node_path11.default.dirname(parsed.data.legacyStateJsonPath), { recursive: true });
|
|
5221
5848
|
}
|
|
5222
5849
|
return parsed.data;
|
|
5223
5850
|
}
|
|
@@ -5288,10 +5915,57 @@ function parseExtraEnv(raw) {
|
|
|
5288
5915
|
function parseCsvList(raw) {
|
|
5289
5916
|
return raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
5290
5917
|
}
|
|
5918
|
+
function parseAdminTokens(raw) {
|
|
5919
|
+
const trimmed = raw.trim();
|
|
5920
|
+
if (!trimmed) {
|
|
5921
|
+
return [];
|
|
5922
|
+
}
|
|
5923
|
+
let parsed;
|
|
5924
|
+
try {
|
|
5925
|
+
parsed = JSON.parse(trimmed);
|
|
5926
|
+
} catch {
|
|
5927
|
+
throw new Error("ADMIN_TOKENS_JSON must be valid JSON.");
|
|
5928
|
+
}
|
|
5929
|
+
if (!Array.isArray(parsed)) {
|
|
5930
|
+
throw new Error("ADMIN_TOKENS_JSON must be a JSON array.");
|
|
5931
|
+
}
|
|
5932
|
+
const seenTokens = /* @__PURE__ */ new Set();
|
|
5933
|
+
return parsed.map((entry, index) => {
|
|
5934
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
5935
|
+
throw new Error(`ADMIN_TOKENS_JSON[${index}] must be an object.`);
|
|
5936
|
+
}
|
|
5937
|
+
const payload = entry;
|
|
5938
|
+
const tokenValue = payload.token;
|
|
5939
|
+
if (typeof tokenValue !== "string" || !tokenValue.trim()) {
|
|
5940
|
+
throw new Error(`ADMIN_TOKENS_JSON[${index}].token must be a non-empty string.`);
|
|
5941
|
+
}
|
|
5942
|
+
const token = tokenValue.trim();
|
|
5943
|
+
if (seenTokens.has(token)) {
|
|
5944
|
+
throw new Error(`ADMIN_TOKENS_JSON contains duplicated token at index ${index}.`);
|
|
5945
|
+
}
|
|
5946
|
+
seenTokens.add(token);
|
|
5947
|
+
let role = "admin";
|
|
5948
|
+
if (payload.role !== void 0) {
|
|
5949
|
+
if (payload.role !== "admin" && payload.role !== "viewer") {
|
|
5950
|
+
throw new Error(`ADMIN_TOKENS_JSON[${index}].role must be "admin" or "viewer".`);
|
|
5951
|
+
}
|
|
5952
|
+
role = payload.role;
|
|
5953
|
+
}
|
|
5954
|
+
if (payload.actor !== void 0 && payload.actor !== null && typeof payload.actor !== "string") {
|
|
5955
|
+
throw new Error(`ADMIN_TOKENS_JSON[${index}].actor must be a string when provided.`);
|
|
5956
|
+
}
|
|
5957
|
+
const actor = typeof payload.actor === "string" ? payload.actor.trim() || null : null;
|
|
5958
|
+
return {
|
|
5959
|
+
token,
|
|
5960
|
+
role,
|
|
5961
|
+
actor
|
|
5962
|
+
};
|
|
5963
|
+
});
|
|
5964
|
+
}
|
|
5291
5965
|
|
|
5292
5966
|
// src/config-snapshot.ts
|
|
5293
5967
|
var import_node_fs9 = __toESM(require("fs"));
|
|
5294
|
-
var
|
|
5968
|
+
var import_node_path12 = __toESM(require("path"));
|
|
5295
5969
|
var import_zod2 = require("zod");
|
|
5296
5970
|
var CONFIG_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
5297
5971
|
var CONFIG_SNAPSHOT_ENV_KEYS = [
|
|
@@ -5342,6 +6016,7 @@ var CONFIG_SNAPSHOT_ENV_KEYS = [
|
|
|
5342
6016
|
"ADMIN_BIND_HOST",
|
|
5343
6017
|
"ADMIN_PORT",
|
|
5344
6018
|
"ADMIN_TOKEN",
|
|
6019
|
+
"ADMIN_TOKENS_JSON",
|
|
5345
6020
|
"ADMIN_IP_ALLOWLIST",
|
|
5346
6021
|
"ADMIN_ALLOWED_ORIGINS",
|
|
5347
6022
|
"LOG_LEVEL"
|
|
@@ -5406,6 +6081,7 @@ var envSnapshotSchema = import_zod2.z.object({
|
|
|
5406
6081
|
ADMIN_BIND_HOST: import_zod2.z.string(),
|
|
5407
6082
|
ADMIN_PORT: integerStringSchema("ADMIN_PORT", 1, 65535),
|
|
5408
6083
|
ADMIN_TOKEN: import_zod2.z.string(),
|
|
6084
|
+
ADMIN_TOKENS_JSON: jsonArrayStringSchema("ADMIN_TOKENS_JSON", true).default(""),
|
|
5409
6085
|
ADMIN_IP_ALLOWLIST: import_zod2.z.string(),
|
|
5410
6086
|
ADMIN_ALLOWED_ORIGINS: import_zod2.z.string().default(""),
|
|
5411
6087
|
LOG_LEVEL: import_zod2.z.enum(LOG_LEVELS)
|
|
@@ -5478,8 +6154,8 @@ async function runConfigExportCommand(options = {}) {
|
|
|
5478
6154
|
const snapshot = buildConfigSnapshot(config, stateStore.listRoomSettings(), options.now ?? /* @__PURE__ */ new Date());
|
|
5479
6155
|
const serialized = serializeConfigSnapshot(snapshot);
|
|
5480
6156
|
if (options.outputPath) {
|
|
5481
|
-
const targetPath =
|
|
5482
|
-
import_node_fs9.default.mkdirSync(
|
|
6157
|
+
const targetPath = import_node_path12.default.resolve(cwd, options.outputPath);
|
|
6158
|
+
import_node_fs9.default.mkdirSync(import_node_path12.default.dirname(targetPath), { recursive: true });
|
|
5483
6159
|
import_node_fs9.default.writeFileSync(targetPath, serialized, "utf8");
|
|
5484
6160
|
output.write(`Exported config snapshot to ${targetPath}
|
|
5485
6161
|
`);
|
|
@@ -5494,7 +6170,7 @@ async function runConfigImportCommand(options) {
|
|
|
5494
6170
|
const cwd = options.cwd ?? process.cwd();
|
|
5495
6171
|
const output = options.output ?? process.stdout;
|
|
5496
6172
|
const actor = options.actor?.trim() || "cli:config-import";
|
|
5497
|
-
const sourcePath =
|
|
6173
|
+
const sourcePath = import_node_path12.default.resolve(cwd, options.filePath);
|
|
5498
6174
|
if (!import_node_fs9.default.existsSync(sourcePath)) {
|
|
5499
6175
|
throw new Error(`Config snapshot file not found: ${sourcePath}`);
|
|
5500
6176
|
}
|
|
@@ -5525,7 +6201,7 @@ async function runConfigImportCommand(options) {
|
|
|
5525
6201
|
synchronizeRoomSettings(stateStore, normalizedRooms);
|
|
5526
6202
|
stateStore.appendConfigRevision(
|
|
5527
6203
|
actor,
|
|
5528
|
-
`import config snapshot from ${
|
|
6204
|
+
`import config snapshot from ${import_node_path12.default.basename(sourcePath)}`,
|
|
5529
6205
|
JSON.stringify({
|
|
5530
6206
|
type: "config_snapshot_import",
|
|
5531
6207
|
sourcePath,
|
|
@@ -5539,7 +6215,7 @@ async function runConfigImportCommand(options) {
|
|
|
5539
6215
|
output.write(
|
|
5540
6216
|
[
|
|
5541
6217
|
`Imported config snapshot from ${sourcePath}`,
|
|
5542
|
-
`- updated .env in ${
|
|
6218
|
+
`- updated .env in ${import_node_path12.default.resolve(cwd, ".env")}`,
|
|
5543
6219
|
`- synchronized room settings: ${normalizedRooms.length}`,
|
|
5544
6220
|
"- restart required: yes (global env settings are restart-scoped)"
|
|
5545
6221
|
].join("\n") + "\n"
|
|
@@ -5594,6 +6270,7 @@ function buildSnapshotEnv(config) {
|
|
|
5594
6270
|
ADMIN_BIND_HOST: config.adminBindHost,
|
|
5595
6271
|
ADMIN_PORT: String(config.adminPort),
|
|
5596
6272
|
ADMIN_TOKEN: config.adminToken ?? "",
|
|
6273
|
+
ADMIN_TOKENS_JSON: serializeAdminTokens(config.adminTokens),
|
|
5597
6274
|
ADMIN_IP_ALLOWLIST: config.adminIpAllowlist.join(","),
|
|
5598
6275
|
ADMIN_ALLOWED_ORIGINS: config.adminAllowedOrigins.join(","),
|
|
5599
6276
|
LOG_LEVEL: config.logLevel
|
|
@@ -5613,10 +6290,10 @@ function parseJsonFile(filePath) {
|
|
|
5613
6290
|
function normalizeSnapshotEnv(env, cwd) {
|
|
5614
6291
|
return {
|
|
5615
6292
|
...env,
|
|
5616
|
-
CODEX_WORKDIR:
|
|
5617
|
-
STATE_DB_PATH:
|
|
5618
|
-
STATE_PATH: env.STATE_PATH.trim() ?
|
|
5619
|
-
CLI_COMPAT_RECORD_PATH: env.CLI_COMPAT_RECORD_PATH.trim() ?
|
|
6293
|
+
CODEX_WORKDIR: import_node_path12.default.resolve(cwd, env.CODEX_WORKDIR),
|
|
6294
|
+
STATE_DB_PATH: import_node_path12.default.resolve(cwd, env.STATE_DB_PATH),
|
|
6295
|
+
STATE_PATH: env.STATE_PATH.trim() ? import_node_path12.default.resolve(cwd, env.STATE_PATH) : "",
|
|
6296
|
+
CLI_COMPAT_RECORD_PATH: env.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path12.default.resolve(cwd, env.CLI_COMPAT_RECORD_PATH) : ""
|
|
5620
6297
|
};
|
|
5621
6298
|
}
|
|
5622
6299
|
function normalizeSnapshotRooms(rooms, cwd) {
|
|
@@ -5631,7 +6308,7 @@ function normalizeSnapshotRooms(rooms, cwd) {
|
|
|
5631
6308
|
throw new Error(`Duplicate roomId in snapshot: ${roomId}`);
|
|
5632
6309
|
}
|
|
5633
6310
|
seen.add(roomId);
|
|
5634
|
-
const workdir =
|
|
6311
|
+
const workdir = import_node_path12.default.resolve(cwd, room.workdir);
|
|
5635
6312
|
ensureDirectory2(workdir, `room workdir (${roomId})`);
|
|
5636
6313
|
normalized.push({
|
|
5637
6314
|
roomId,
|
|
@@ -5658,8 +6335,8 @@ function synchronizeRoomSettings(stateStore, rooms) {
|
|
|
5658
6335
|
}
|
|
5659
6336
|
}
|
|
5660
6337
|
function persistEnvSnapshot(cwd, env) {
|
|
5661
|
-
const envPath =
|
|
5662
|
-
const examplePath =
|
|
6338
|
+
const envPath = import_node_path12.default.resolve(cwd, ".env");
|
|
6339
|
+
const examplePath = import_node_path12.default.resolve(cwd, ".env.example");
|
|
5663
6340
|
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") : "";
|
|
5664
6341
|
const overrides = {};
|
|
5665
6342
|
for (const key of CONFIG_SNAPSHOT_ENV_KEYS) {
|
|
@@ -5683,6 +6360,9 @@ function parseIntStrict(raw) {
|
|
|
5683
6360
|
function serializeJsonObject(value) {
|
|
5684
6361
|
return Object.keys(value).length > 0 ? JSON.stringify(value) : "";
|
|
5685
6362
|
}
|
|
6363
|
+
function serializeAdminTokens(tokens) {
|
|
6364
|
+
return tokens.length > 0 ? JSON.stringify(tokens) : "";
|
|
6365
|
+
}
|
|
5686
6366
|
function booleanStringSchema(key) {
|
|
5687
6367
|
return import_zod2.z.string().refine((value) => BOOLEAN_STRING.test(value), {
|
|
5688
6368
|
message: `${key} must be a boolean string (true/false).`
|
|
@@ -5720,13 +6400,30 @@ function jsonObjectStringSchema(key, allowEmpty) {
|
|
|
5720
6400
|
message: `${key} must be an empty string or a JSON object string.`
|
|
5721
6401
|
});
|
|
5722
6402
|
}
|
|
6403
|
+
function jsonArrayStringSchema(key, allowEmpty) {
|
|
6404
|
+
return import_zod2.z.string().refine((value) => {
|
|
6405
|
+
const trimmed = value.trim();
|
|
6406
|
+
if (!trimmed) {
|
|
6407
|
+
return allowEmpty;
|
|
6408
|
+
}
|
|
6409
|
+
let parsed;
|
|
6410
|
+
try {
|
|
6411
|
+
parsed = JSON.parse(trimmed);
|
|
6412
|
+
} catch {
|
|
6413
|
+
return false;
|
|
6414
|
+
}
|
|
6415
|
+
return Array.isArray(parsed);
|
|
6416
|
+
}, {
|
|
6417
|
+
message: `${key} must be an empty string or a JSON array string.`
|
|
6418
|
+
});
|
|
6419
|
+
}
|
|
5723
6420
|
|
|
5724
6421
|
// src/preflight.ts
|
|
5725
|
-
var
|
|
6422
|
+
var import_node_child_process6 = require("child_process");
|
|
5726
6423
|
var import_node_fs10 = __toESM(require("fs"));
|
|
5727
|
-
var
|
|
5728
|
-
var
|
|
5729
|
-
var
|
|
6424
|
+
var import_node_path13 = __toESM(require("path"));
|
|
6425
|
+
var import_node_util4 = require("util");
|
|
6426
|
+
var execFileAsync4 = (0, import_node_util4.promisify)(import_node_child_process6.execFile);
|
|
5730
6427
|
var REQUIRED_ENV_KEYS = ["MATRIX_HOMESERVER", "MATRIX_USER_ID", "MATRIX_ACCESS_TOKEN"];
|
|
5731
6428
|
async function runStartupPreflight(options = {}) {
|
|
5732
6429
|
const env = options.env ?? process.env;
|
|
@@ -5735,7 +6432,9 @@ async function runStartupPreflight(options = {}) {
|
|
|
5735
6432
|
const fileExists = options.fileExists ?? import_node_fs10.default.existsSync;
|
|
5736
6433
|
const isDirectory = options.isDirectory ?? defaultIsDirectory;
|
|
5737
6434
|
const issues = [];
|
|
5738
|
-
const envPath =
|
|
6435
|
+
const envPath = import_node_path13.default.resolve(cwd, ".env");
|
|
6436
|
+
let resolvedCodexBin = null;
|
|
6437
|
+
let usedCodexFallback = false;
|
|
5739
6438
|
if (!fileExists(envPath)) {
|
|
5740
6439
|
issues.push({
|
|
5741
6440
|
level: "warn",
|
|
@@ -5783,18 +6482,34 @@ async function runStartupPreflight(options = {}) {
|
|
|
5783
6482
|
const codexBin = readEnv(env, "CODEX_BIN") || "codex";
|
|
5784
6483
|
try {
|
|
5785
6484
|
await checkCodexBinary(codexBin);
|
|
6485
|
+
resolvedCodexBin = codexBin;
|
|
5786
6486
|
} catch (error) {
|
|
5787
|
-
const
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
6487
|
+
const fallbackBin = await findWorkingCodexBin(codexBin, { env, checkBinary: checkCodexBinary });
|
|
6488
|
+
if (fallbackBin && fallbackBin !== codexBin) {
|
|
6489
|
+
resolvedCodexBin = fallbackBin;
|
|
6490
|
+
usedCodexFallback = true;
|
|
6491
|
+
issues.push({
|
|
6492
|
+
level: "warn",
|
|
6493
|
+
code: "codex_bin_fallback",
|
|
6494
|
+
check: "CODEX_BIN",
|
|
6495
|
+
message: `Configured CODEX_BIN "${codexBin}" is unavailable; fallback to "${fallbackBin}".`,
|
|
6496
|
+
fix: `Update CODEX_BIN=${fallbackBin} in .env to avoid fallback probing on startup.`
|
|
6497
|
+
});
|
|
6498
|
+
} else if (fallbackBin) {
|
|
6499
|
+
resolvedCodexBin = fallbackBin;
|
|
6500
|
+
} else {
|
|
6501
|
+
const reason = error instanceof Error && error.message ? ` (${error.message})` : "";
|
|
6502
|
+
issues.push({
|
|
6503
|
+
level: "error",
|
|
6504
|
+
code: "missing_codex_bin",
|
|
6505
|
+
check: "CODEX_BIN",
|
|
6506
|
+
message: `Unable to execute "${codexBin}"${reason}.`,
|
|
6507
|
+
fix: `Install Codex CLI and ensure "${codexBin}" is in PATH, or set CODEX_BIN=/absolute/path/to/codex.`
|
|
6508
|
+
});
|
|
6509
|
+
}
|
|
5795
6510
|
}
|
|
5796
6511
|
const configuredWorkdir = readEnv(env, "CODEX_WORKDIR");
|
|
5797
|
-
const workdir =
|
|
6512
|
+
const workdir = import_node_path13.default.resolve(cwd, configuredWorkdir || cwd);
|
|
5798
6513
|
if (!fileExists(workdir) || !isDirectory(workdir)) {
|
|
5799
6514
|
issues.push({
|
|
5800
6515
|
level: "error",
|
|
@@ -5806,7 +6521,9 @@ async function runStartupPreflight(options = {}) {
|
|
|
5806
6521
|
}
|
|
5807
6522
|
return {
|
|
5808
6523
|
ok: issues.every((issue) => issue.level !== "error"),
|
|
5809
|
-
issues
|
|
6524
|
+
issues,
|
|
6525
|
+
resolvedCodexBin,
|
|
6526
|
+
usedCodexFallback
|
|
5810
6527
|
};
|
|
5811
6528
|
}
|
|
5812
6529
|
function formatPreflightReport(result, commandName) {
|
|
@@ -5829,7 +6546,7 @@ function formatPreflightReport(result, commandName) {
|
|
|
5829
6546
|
`;
|
|
5830
6547
|
}
|
|
5831
6548
|
async function defaultCheckCodexBinary(bin) {
|
|
5832
|
-
await
|
|
6549
|
+
await execFileAsync4(bin, ["--version"]);
|
|
5833
6550
|
}
|
|
5834
6551
|
function defaultIsDirectory(targetPath) {
|
|
5835
6552
|
try {
|
|
@@ -5892,11 +6609,12 @@ admin.command("serve").description("Start admin config API server").option("--ho
|
|
|
5892
6609
|
const host = options.host?.trim() || config.adminBindHost;
|
|
5893
6610
|
const port = options.port ? parsePortOption(options.port, config.adminPort) : config.adminPort;
|
|
5894
6611
|
const allowInsecureNoToken = options.allowInsecureNoToken ?? false;
|
|
5895
|
-
|
|
6612
|
+
const hasAdminAuth = Boolean(config.adminToken) || config.adminTokens.length > 0;
|
|
6613
|
+
if (!hasAdminAuth && !allowInsecureNoToken && isNonLoopbackHost(host)) {
|
|
5896
6614
|
process.stderr.write(
|
|
5897
6615
|
[
|
|
5898
|
-
"Refusing to start admin server on non-loopback host without
|
|
5899
|
-
"Fix: set ADMIN_TOKEN in .env, or explicitly pass --allow-insecure-no-token.",
|
|
6616
|
+
"Refusing to start admin server on non-loopback host without admin auth token.",
|
|
6617
|
+
"Fix: set ADMIN_TOKEN or ADMIN_TOKENS_JSON in .env, or explicitly pass --allow-insecure-no-token.",
|
|
5900
6618
|
""
|
|
5901
6619
|
].join("\n")
|
|
5902
6620
|
);
|
|
@@ -6015,7 +6733,11 @@ if (process.argv.length <= 2) {
|
|
|
6015
6733
|
}
|
|
6016
6734
|
void program.parseAsync(process.argv);
|
|
6017
6735
|
async function loadConfigWithPreflight(commandName, runtimeHomePath) {
|
|
6018
|
-
const
|
|
6736
|
+
const env = { ...process.env };
|
|
6737
|
+
const preflight = await runStartupPreflight({ cwd: runtimeHomePath, env });
|
|
6738
|
+
if (preflight.resolvedCodexBin) {
|
|
6739
|
+
env.CODEX_BIN = preflight.resolvedCodexBin;
|
|
6740
|
+
}
|
|
6019
6741
|
if (preflight.issues.length > 0) {
|
|
6020
6742
|
const report = formatPreflightReport(preflight, commandName);
|
|
6021
6743
|
if (preflight.ok) {
|
|
@@ -6026,7 +6748,7 @@ async function loadConfigWithPreflight(commandName, runtimeHomePath) {
|
|
|
6026
6748
|
}
|
|
6027
6749
|
}
|
|
6028
6750
|
try {
|
|
6029
|
-
return loadConfig();
|
|
6751
|
+
return loadConfig(env);
|
|
6030
6752
|
} catch (error) {
|
|
6031
6753
|
const message = error instanceof Error ? error.message : String(error);
|
|
6032
6754
|
process.stderr.write(`Configuration error: ${message}
|
|
@@ -6056,7 +6778,7 @@ function ensureRuntimeHomeOrExit() {
|
|
|
6056
6778
|
`);
|
|
6057
6779
|
process.exit(1);
|
|
6058
6780
|
}
|
|
6059
|
-
loadEnvFromFile(
|
|
6781
|
+
loadEnvFromFile(import_node_path14.default.resolve(home, ".env"));
|
|
6060
6782
|
runtimeHome = home;
|
|
6061
6783
|
return runtimeHome;
|
|
6062
6784
|
}
|
|
@@ -6071,7 +6793,7 @@ function parsePortOption(raw, fallback) {
|
|
|
6071
6793
|
}
|
|
6072
6794
|
function resolveCliVersion() {
|
|
6073
6795
|
try {
|
|
6074
|
-
const packagePath =
|
|
6796
|
+
const packagePath = import_node_path14.default.resolve(__dirname, "..", "package.json");
|
|
6075
6797
|
const content = import_node_fs11.default.readFileSync(packagePath, "utf8");
|
|
6076
6798
|
const parsed = JSON.parse(content);
|
|
6077
6799
|
return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version : "0.0.0";
|
|
@@ -6082,9 +6804,9 @@ function resolveCliVersion() {
|
|
|
6082
6804
|
function resolveCliScriptPath() {
|
|
6083
6805
|
const argvPath = process.argv[1];
|
|
6084
6806
|
if (argvPath && argvPath.trim()) {
|
|
6085
|
-
return
|
|
6807
|
+
return import_node_path14.default.resolve(argvPath);
|
|
6086
6808
|
}
|
|
6087
|
-
return
|
|
6809
|
+
return import_node_path14.default.resolve(__dirname, "cli.js");
|
|
6088
6810
|
}
|
|
6089
6811
|
function maybeReexecServiceCommandWithSudo() {
|
|
6090
6812
|
if (typeof process.getuid !== "function" || process.getuid() === 0) {
|
|
@@ -6095,7 +6817,7 @@ function maybeReexecServiceCommandWithSudo() {
|
|
|
6095
6817
|
return;
|
|
6096
6818
|
}
|
|
6097
6819
|
const cliScriptPath = resolveCliScriptPath();
|
|
6098
|
-
const child = (0,
|
|
6820
|
+
const child = (0, import_node_child_process7.spawnSync)("sudo", [process.execPath, cliScriptPath, ...serviceArgs], {
|
|
6099
6821
|
stdio: "inherit"
|
|
6100
6822
|
});
|
|
6101
6823
|
if (child.error) {
|