copilot-hub 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/apps/agent-engine/dist/config.js +58 -0
- package/apps/agent-engine/dist/index.js +90 -16
- package/apps/control-plane/dist/channels/codex-quota-cache.js +16 -0
- package/apps/control-plane/dist/channels/hub-model-utils.js +244 -24
- package/apps/control-plane/dist/channels/hub-ops-commands.js +631 -279
- package/apps/control-plane/dist/channels/telegram-channel.js +5 -7
- package/apps/control-plane/dist/config.js +58 -0
- package/apps/control-plane/dist/index.js +16 -0
- package/apps/control-plane/dist/test/hub-model-utils.test.js +110 -13
- package/package.json +3 -2
- package/packages/core/dist/agent-supervisor.d.ts +5 -0
- package/packages/core/dist/agent-supervisor.js +11 -0
- package/packages/core/dist/agent-supervisor.js.map +1 -1
- package/packages/core/dist/bot-manager.js +17 -1
- package/packages/core/dist/bot-manager.js.map +1 -1
- package/packages/core/dist/bot-runtime.d.ts +4 -0
- package/packages/core/dist/bot-runtime.js +5 -1
- package/packages/core/dist/bot-runtime.js.map +1 -1
- package/packages/core/dist/codex-app-client.d.ts +13 -2
- package/packages/core/dist/codex-app-client.js +51 -13
- package/packages/core/dist/codex-app-client.js.map +1 -1
- package/packages/core/dist/codex-app-utils.d.ts +6 -0
- package/packages/core/dist/codex-app-utils.js +49 -0
- package/packages/core/dist/codex-app-utils.js.map +1 -1
- package/packages/core/dist/codex-provider.d.ts +3 -1
- package/packages/core/dist/codex-provider.js +3 -1
- package/packages/core/dist/codex-provider.js.map +1 -1
- package/packages/core/dist/kernel-control-plane.d.ts +1 -0
- package/packages/core/dist/kernel-control-plane.js +132 -13
- package/packages/core/dist/kernel-control-plane.js.map +1 -1
- package/packages/core/dist/provider-factory.d.ts +2 -0
- package/packages/core/dist/provider-factory.js +3 -0
- package/packages/core/dist/provider-factory.js.map +1 -1
- package/packages/core/dist/provider-options.js +24 -17
- package/packages/core/dist/provider-options.js.map +1 -1
- package/packages/core/dist/state-store.d.ts +1 -0
- package/packages/core/dist/state-store.js +28 -2
- package/packages/core/dist/state-store.js.map +1 -1
- package/packages/core/dist/telegram-channel.d.ts +1 -0
- package/packages/core/dist/telegram-channel.js +3 -0
- package/packages/core/dist/telegram-channel.js.map +1 -1
- package/scripts/dist/cli.mjs +132 -203
- package/scripts/dist/codex-runtime.mjs +352 -0
- package/scripts/dist/codex-version.mjs +91 -0
- package/scripts/dist/daemon.mjs +58 -0
- package/scripts/src/cli.mts +166 -233
- package/scripts/src/codex-runtime.mts +499 -0
- package/scripts/src/codex-version.mts +114 -0
- package/scripts/src/daemon.mts +69 -0
- package/scripts/test/codex-version.test.mjs +21 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import {
|
|
6
|
+
compareSemver,
|
|
7
|
+
codexVersionRequirementLabel,
|
|
8
|
+
extractSemver,
|
|
9
|
+
isCodexVersionCompatible,
|
|
10
|
+
} from "./codex-version.mjs";
|
|
11
|
+
|
|
12
|
+
type ResolvedCodexBin = {
|
|
13
|
+
bin: string;
|
|
14
|
+
source: string;
|
|
15
|
+
userConfigured: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type ProbeCodexVersionResult = {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
stdout: string;
|
|
21
|
+
stderr: string;
|
|
22
|
+
errorMessage: string;
|
|
23
|
+
errorCode: string;
|
|
24
|
+
version: string;
|
|
25
|
+
rawVersion: string;
|
|
26
|
+
compatible: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function resolveCodexBinForStart({
|
|
30
|
+
repoRoot,
|
|
31
|
+
agentEngineEnvPath,
|
|
32
|
+
controlPlaneEnvPath,
|
|
33
|
+
env = process.env,
|
|
34
|
+
}: {
|
|
35
|
+
repoRoot: string;
|
|
36
|
+
agentEngineEnvPath: string;
|
|
37
|
+
controlPlaneEnvPath: string;
|
|
38
|
+
env?: NodeJS.ProcessEnv;
|
|
39
|
+
}): ResolvedCodexBin {
|
|
40
|
+
const fromEnv = nonEmpty(env.CODEX_BIN);
|
|
41
|
+
if (fromEnv) {
|
|
42
|
+
return buildResolvedCodexBin({
|
|
43
|
+
value: fromEnv,
|
|
44
|
+
source: "process_env",
|
|
45
|
+
env,
|
|
46
|
+
repoRoot,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const [source, envPath] of [
|
|
51
|
+
["agent_env", agentEngineEnvPath],
|
|
52
|
+
["control_plane_env", controlPlaneEnvPath],
|
|
53
|
+
] as const) {
|
|
54
|
+
const value = readEnvValue(envPath, "CODEX_BIN");
|
|
55
|
+
if (value) {
|
|
56
|
+
return buildResolvedCodexBin({
|
|
57
|
+
value,
|
|
58
|
+
source,
|
|
59
|
+
env,
|
|
60
|
+
repoRoot,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const detected = findDetectedCodexBin(env, repoRoot);
|
|
66
|
+
if (detected) {
|
|
67
|
+
return {
|
|
68
|
+
bin: detected,
|
|
69
|
+
source: "detected",
|
|
70
|
+
userConfigured: false,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
bin: "codex",
|
|
76
|
+
source: "default",
|
|
77
|
+
userConfigured: false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function resolveCompatibleInstalledCodexBin({
|
|
82
|
+
repoRoot,
|
|
83
|
+
env = process.env,
|
|
84
|
+
}: {
|
|
85
|
+
repoRoot: string;
|
|
86
|
+
env?: NodeJS.ProcessEnv;
|
|
87
|
+
}): string {
|
|
88
|
+
const matches: Array<{ candidate: string; version: string; priority: number }> = [];
|
|
89
|
+
|
|
90
|
+
for (const candidate of listCodexBinCandidates(env, repoRoot)) {
|
|
91
|
+
const probe = probeCodexVersion({
|
|
92
|
+
codexBin: candidate,
|
|
93
|
+
repoRoot,
|
|
94
|
+
});
|
|
95
|
+
if (!probe.ok || !probe.compatible) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
matches.push({
|
|
100
|
+
candidate,
|
|
101
|
+
version: probe.version,
|
|
102
|
+
priority: getCodexCandidatePriority(candidate, env, repoRoot),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (matches.length === 0) {
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
matches.sort((left, right) => {
|
|
111
|
+
const versionOrder = compareSemver(right.version, left.version);
|
|
112
|
+
if (versionOrder !== 0) {
|
|
113
|
+
return versionOrder;
|
|
114
|
+
}
|
|
115
|
+
return left.priority - right.priority;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return matches[0]?.candidate ?? "";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function probeCodexVersion({
|
|
122
|
+
codexBin,
|
|
123
|
+
repoRoot,
|
|
124
|
+
}: {
|
|
125
|
+
codexBin: string;
|
|
126
|
+
repoRoot: string;
|
|
127
|
+
}): ProbeCodexVersionResult {
|
|
128
|
+
const status = runCodex({
|
|
129
|
+
codexBin,
|
|
130
|
+
args: ["--version"],
|
|
131
|
+
repoRoot,
|
|
132
|
+
});
|
|
133
|
+
if (!status.ok) {
|
|
134
|
+
return {
|
|
135
|
+
...status,
|
|
136
|
+
version: "",
|
|
137
|
+
rawVersion: "",
|
|
138
|
+
compatible: false,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const rawVersion = firstLine(status.stdout) || firstLine(status.stderr);
|
|
143
|
+
const version = extractSemver(rawVersion);
|
|
144
|
+
if (!version) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
stdout: status.stdout,
|
|
148
|
+
stderr: status.stderr,
|
|
149
|
+
errorMessage: `Could not parse Codex version from '${rawVersion || "empty output"}'.`,
|
|
150
|
+
errorCode: "INVALID_VERSION",
|
|
151
|
+
version: "",
|
|
152
|
+
rawVersion,
|
|
153
|
+
compatible: false,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
...status,
|
|
159
|
+
version,
|
|
160
|
+
rawVersion,
|
|
161
|
+
compatible: isCodexVersionCompatible(version),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function buildCodexCompatibilitySummary({
|
|
166
|
+
resolved,
|
|
167
|
+
probe,
|
|
168
|
+
}: {
|
|
169
|
+
resolved: ResolvedCodexBin;
|
|
170
|
+
probe: ProbeCodexVersionResult;
|
|
171
|
+
}): string {
|
|
172
|
+
if (probe.ok) {
|
|
173
|
+
return `Codex binary '${resolved.bin}' is version ${probe.version}.`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (probe.errorCode === "ENOENT") {
|
|
177
|
+
return `Codex binary '${resolved.bin}' was not found.`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return probe.errorMessage || `Codex binary '${resolved.bin}' is not usable.`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function buildCodexCompatibilityNotice({
|
|
184
|
+
resolved,
|
|
185
|
+
probe,
|
|
186
|
+
}: {
|
|
187
|
+
resolved: ResolvedCodexBin;
|
|
188
|
+
probe: ProbeCodexVersionResult;
|
|
189
|
+
}): string {
|
|
190
|
+
return [
|
|
191
|
+
buildCodexCompatibilitySummary({ resolved, probe }),
|
|
192
|
+
`copilot-hub requires Codex CLI ${codexVersionRequirementLabel}.`,
|
|
193
|
+
].join("\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function buildCodexCompatibilityError({
|
|
197
|
+
resolved,
|
|
198
|
+
probe,
|
|
199
|
+
includeInstallHint,
|
|
200
|
+
installCommand,
|
|
201
|
+
}: {
|
|
202
|
+
resolved: ResolvedCodexBin;
|
|
203
|
+
probe: ProbeCodexVersionResult;
|
|
204
|
+
includeInstallHint: boolean;
|
|
205
|
+
installCommand: string;
|
|
206
|
+
}): string {
|
|
207
|
+
const lines = [
|
|
208
|
+
buildCodexCompatibilitySummary({ resolved, probe }),
|
|
209
|
+
`copilot-hub requires Codex CLI ${codexVersionRequirementLabel}.`,
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
if (includeInstallHint) {
|
|
213
|
+
lines.push(`Install a compatible version with '${installCommand}', then retry.`);
|
|
214
|
+
} else {
|
|
215
|
+
lines.push("Update that binary or point CODEX_BIN to a compatible executable, then retry.");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return lines.join("\n");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function buildResolvedCodexBin({
|
|
222
|
+
value,
|
|
223
|
+
source,
|
|
224
|
+
env,
|
|
225
|
+
repoRoot,
|
|
226
|
+
}: {
|
|
227
|
+
value: string;
|
|
228
|
+
source: string;
|
|
229
|
+
env: NodeJS.ProcessEnv;
|
|
230
|
+
repoRoot: string;
|
|
231
|
+
}): ResolvedCodexBin {
|
|
232
|
+
const normalized = String(value ?? "")
|
|
233
|
+
.trim()
|
|
234
|
+
.toLowerCase();
|
|
235
|
+
if (normalized && normalized !== "codex") {
|
|
236
|
+
return {
|
|
237
|
+
bin: value,
|
|
238
|
+
source,
|
|
239
|
+
userConfigured: true,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const detected = findDetectedCodexBin(env, repoRoot);
|
|
244
|
+
return {
|
|
245
|
+
bin: detected || "codex",
|
|
246
|
+
source,
|
|
247
|
+
userConfigured: false,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function findDetectedCodexBin(env: NodeJS.ProcessEnv, repoRoot: string): string {
|
|
252
|
+
if (process.platform !== "win32") {
|
|
253
|
+
return "";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return findWindowsNpmGlobalCodexBin(env, repoRoot) || findVscodeCodexExe(env) || "";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function listCodexBinCandidates(env: NodeJS.ProcessEnv, repoRoot: string): string[] {
|
|
260
|
+
return dedupe(["codex", findWindowsNpmGlobalCodexBin(env, repoRoot), findVscodeCodexExe(env)]);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getCodexCandidatePriority(
|
|
264
|
+
candidate: string,
|
|
265
|
+
env: NodeJS.ProcessEnv,
|
|
266
|
+
repoRoot: string,
|
|
267
|
+
): number {
|
|
268
|
+
if (candidate === "codex") {
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const npmGlobal = findWindowsNpmGlobalCodexBin(env, repoRoot);
|
|
273
|
+
if (npmGlobal && candidate === npmGlobal) {
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const vscode = findVscodeCodexExe(env);
|
|
278
|
+
if (vscode && candidate === vscode) {
|
|
279
|
+
return 2;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return 3;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function findVscodeCodexExe(env: NodeJS.ProcessEnv): string {
|
|
286
|
+
const userProfile = nonEmpty(env.USERPROFILE);
|
|
287
|
+
if (!userProfile) {
|
|
288
|
+
return "";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const extensionsDir = path.join(userProfile, ".vscode", "extensions");
|
|
292
|
+
if (!fs.existsSync(extensionsDir)) {
|
|
293
|
+
return "";
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const candidates = fs
|
|
297
|
+
.readdirSync(extensionsDir, { withFileTypes: true })
|
|
298
|
+
.filter((entry) => entry.isDirectory())
|
|
299
|
+
.map((entry) => entry.name)
|
|
300
|
+
.filter((name) => name.startsWith("openai.chatgpt-"))
|
|
301
|
+
.sort()
|
|
302
|
+
.reverse();
|
|
303
|
+
|
|
304
|
+
for (const folder of candidates) {
|
|
305
|
+
const exePath = path.join(extensionsDir, folder, "bin", "windows-x86_64", "codex.exe");
|
|
306
|
+
if (fs.existsSync(exePath)) {
|
|
307
|
+
return exePath;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return "";
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function findWindowsNpmGlobalCodexBin(env: NodeJS.ProcessEnv, repoRoot: string): string {
|
|
315
|
+
if (process.platform !== "win32") {
|
|
316
|
+
return "";
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const candidates: string[] = [];
|
|
320
|
+
const appData = nonEmpty(env.APPDATA);
|
|
321
|
+
if (appData) {
|
|
322
|
+
candidates.push(path.join(appData, "npm", "codex.cmd"));
|
|
323
|
+
candidates.push(path.join(appData, "npm", "codex.exe"));
|
|
324
|
+
candidates.push(path.join(appData, "npm", "codex"));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const npmPrefix = readNpmPrefix(repoRoot);
|
|
328
|
+
if (npmPrefix) {
|
|
329
|
+
candidates.push(path.join(npmPrefix, "codex.cmd"));
|
|
330
|
+
candidates.push(path.join(npmPrefix, "codex.exe"));
|
|
331
|
+
candidates.push(path.join(npmPrefix, "codex"));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
for (const candidate of dedupe(candidates)) {
|
|
335
|
+
if (fs.existsSync(candidate)) {
|
|
336
|
+
return candidate;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return "";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function readNpmPrefix(repoRoot: string): string {
|
|
344
|
+
const result = spawnNpm(["config", "get", "prefix"], repoRoot);
|
|
345
|
+
if (result.error || result.status !== 0) {
|
|
346
|
+
return "";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const value = String(result.stdout ?? "").trim();
|
|
350
|
+
if (!value || value.toLowerCase() === "undefined") {
|
|
351
|
+
return "";
|
|
352
|
+
}
|
|
353
|
+
return value;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function runCodex({
|
|
357
|
+
codexBin,
|
|
358
|
+
args,
|
|
359
|
+
repoRoot,
|
|
360
|
+
}: {
|
|
361
|
+
codexBin: string;
|
|
362
|
+
args: string[];
|
|
363
|
+
repoRoot: string;
|
|
364
|
+
}) {
|
|
365
|
+
const result = spawnCodex(codexBin, args, repoRoot);
|
|
366
|
+
|
|
367
|
+
if (result.error) {
|
|
368
|
+
return {
|
|
369
|
+
ok: false,
|
|
370
|
+
stdout: "",
|
|
371
|
+
stderr: "",
|
|
372
|
+
errorMessage: formatCodexSpawnError(codexBin, result.error),
|
|
373
|
+
errorCode: normalizeErrorCode(result.error),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const code = Number.isInteger(result.status) ? result.status : 1;
|
|
378
|
+
return {
|
|
379
|
+
ok: code === 0,
|
|
380
|
+
stdout: String(result.stdout ?? "").trim(),
|
|
381
|
+
stderr: String(result.stderr ?? "").trim(),
|
|
382
|
+
errorMessage: "",
|
|
383
|
+
errorCode: "",
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function spawnCodex(codexBin: string, args: string[], repoRoot: string) {
|
|
388
|
+
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(codexBin)) {
|
|
389
|
+
const commandLine = [
|
|
390
|
+
quoteWindowsShellValue(codexBin),
|
|
391
|
+
...args.map(quoteWindowsShellValue),
|
|
392
|
+
].join(" ");
|
|
393
|
+
return spawnSync(commandLine, {
|
|
394
|
+
cwd: repoRoot,
|
|
395
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
396
|
+
shell: true,
|
|
397
|
+
encoding: "utf8",
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return spawnSync(codexBin, args, {
|
|
402
|
+
cwd: repoRoot,
|
|
403
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
404
|
+
shell: false,
|
|
405
|
+
encoding: "utf8",
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function readEnvValue(filePath: string, key: string): string {
|
|
410
|
+
if (!fs.existsSync(filePath)) {
|
|
411
|
+
return "";
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/);
|
|
415
|
+
const pattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=\\s*(.*)\\s*$`);
|
|
416
|
+
for (const line of lines) {
|
|
417
|
+
const match = line.match(pattern);
|
|
418
|
+
if (!match) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
return unquote(match[1]);
|
|
422
|
+
}
|
|
423
|
+
return "";
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function unquote(value: string): string {
|
|
427
|
+
const raw = String(value ?? "").trim();
|
|
428
|
+
if (!raw) {
|
|
429
|
+
return "";
|
|
430
|
+
}
|
|
431
|
+
if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
|
|
432
|
+
return raw.slice(1, -1).trim();
|
|
433
|
+
}
|
|
434
|
+
return raw;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function escapeRegex(value: string): string {
|
|
438
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function nonEmpty(value: unknown): string {
|
|
442
|
+
const normalized = String(value ?? "").trim();
|
|
443
|
+
return normalized || "";
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function firstLine(value: unknown): string {
|
|
447
|
+
return (
|
|
448
|
+
String(value ?? "")
|
|
449
|
+
.split(/\r?\n/)
|
|
450
|
+
.map((line) => line.trim())
|
|
451
|
+
.find(Boolean) ?? ""
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function formatCodexSpawnError(command: string, error: NodeJS.ErrnoException): string {
|
|
456
|
+
const code = normalizeErrorCode(error);
|
|
457
|
+
if (code === "ENOENT") {
|
|
458
|
+
return `Codex binary '${command}' was not found. Install Codex CLI or set CODEX_BIN.`;
|
|
459
|
+
}
|
|
460
|
+
if (code === "EPERM") {
|
|
461
|
+
return `Codex binary '${command}' cannot be executed (EPERM). Check permissions or CODEX_BIN.`;
|
|
462
|
+
}
|
|
463
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
464
|
+
return `Failed to execute '${command}': ${firstLine(message)}`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function normalizeErrorCode(error: unknown): string {
|
|
468
|
+
return String((error as { code?: unknown } | null | undefined)?.code ?? "")
|
|
469
|
+
.trim()
|
|
470
|
+
.toUpperCase();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function dedupe(values: Array<string | null | undefined>): string[] {
|
|
474
|
+
return [...new Set(values.map((value) => String(value ?? "").trim()).filter(Boolean))];
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function quoteWindowsShellValue(value: string): string {
|
|
478
|
+
return `"${String(value ?? "").replace(/"/g, '\\"')}"`;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function spawnNpm(args: string[], repoRoot: string) {
|
|
482
|
+
if (process.platform === "win32") {
|
|
483
|
+
const comspec = process.env.ComSpec || "cmd.exe";
|
|
484
|
+
const commandLine = ["npm", ...args].join(" ");
|
|
485
|
+
return spawnSync(comspec, ["/d", "/s", "/c", commandLine], {
|
|
486
|
+
cwd: repoRoot,
|
|
487
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
488
|
+
shell: false,
|
|
489
|
+
encoding: "utf8",
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return spawnSync("npm", args, {
|
|
494
|
+
cwd: repoRoot,
|
|
495
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
496
|
+
shell: false,
|
|
497
|
+
encoding: "utf8",
|
|
498
|
+
});
|
|
499
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export const codexNpmPackage = "@openai/codex";
|
|
2
|
+
export const minimumCodexVersion = "0.113.0";
|
|
3
|
+
export const maximumCodexVersionExclusive = "0.114.0";
|
|
4
|
+
export const codexInstallPackageSpec = `${codexNpmPackage}@${minimumCodexVersion}`;
|
|
5
|
+
export const codexVersionRequirementLabel = `>= ${minimumCodexVersion} < ${maximumCodexVersionExclusive}`;
|
|
6
|
+
|
|
7
|
+
type Semver = {
|
|
8
|
+
major: number;
|
|
9
|
+
minor: number;
|
|
10
|
+
patch: number;
|
|
11
|
+
prerelease: string[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function extractSemver(value: string): string {
|
|
15
|
+
const match = String(value ?? "").match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/);
|
|
16
|
+
return String(match?.[0] ?? "").trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function compareSemver(left: string, right: string): number {
|
|
20
|
+
const a = parseSemver(left);
|
|
21
|
+
const b = parseSemver(right);
|
|
22
|
+
if (!a || !b) {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (a.major !== b.major) {
|
|
27
|
+
return a.major > b.major ? 1 : -1;
|
|
28
|
+
}
|
|
29
|
+
if (a.minor !== b.minor) {
|
|
30
|
+
return a.minor > b.minor ? 1 : -1;
|
|
31
|
+
}
|
|
32
|
+
if (a.patch !== b.patch) {
|
|
33
|
+
return a.patch > b.patch ? 1 : -1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (a.prerelease.length === 0 && b.prerelease.length === 0) {
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
if (a.prerelease.length === 0) {
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
if (b.prerelease.length === 0) {
|
|
43
|
+
return -1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const maxLength = Math.max(a.prerelease.length, b.prerelease.length);
|
|
47
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
48
|
+
const leftPart = a.prerelease[index];
|
|
49
|
+
const rightPart = b.prerelease[index];
|
|
50
|
+
if (leftPart === undefined) {
|
|
51
|
+
return -1;
|
|
52
|
+
}
|
|
53
|
+
if (rightPart === undefined) {
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
if (leftPart === rightPart) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const leftNumeric = /^\d+$/.test(leftPart);
|
|
61
|
+
const rightNumeric = /^\d+$/.test(rightPart);
|
|
62
|
+
if (leftNumeric && rightNumeric) {
|
|
63
|
+
const leftValue = Number.parseInt(leftPart, 10);
|
|
64
|
+
const rightValue = Number.parseInt(rightPart, 10);
|
|
65
|
+
if (leftValue !== rightValue) {
|
|
66
|
+
return leftValue > rightValue ? 1 : -1;
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (leftNumeric) {
|
|
71
|
+
return -1;
|
|
72
|
+
}
|
|
73
|
+
if (rightNumeric) {
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
return leftPart > rightPart ? 1 : -1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isCodexVersionCompatible(version: string): boolean {
|
|
83
|
+
const parsed = parseSemver(version);
|
|
84
|
+
if (!parsed || parsed.prerelease.length > 0) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
compareSemver(version, minimumCodexVersion) >= 0 &&
|
|
90
|
+
compareSemver(version, maximumCodexVersionExclusive) < 0
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseSemver(value: string): Semver | null {
|
|
95
|
+
const normalized = extractSemver(value);
|
|
96
|
+
const match = normalized.match(
|
|
97
|
+
/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/,
|
|
98
|
+
);
|
|
99
|
+
if (!match) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const prerelease = String(match[4] ?? "")
|
|
104
|
+
.split(".")
|
|
105
|
+
.map((part) => part.trim())
|
|
106
|
+
.filter(Boolean);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
major: Number.parseInt(match[1] ?? "", 10),
|
|
110
|
+
minor: Number.parseInt(match[2] ?? "", 10),
|
|
111
|
+
patch: Number.parseInt(match[3] ?? "", 10),
|
|
112
|
+
prerelease,
|
|
113
|
+
};
|
|
114
|
+
}
|
package/scripts/src/daemon.mts
CHANGED
|
@@ -5,6 +5,13 @@ import process from "node:process";
|
|
|
5
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { codexInstallPackageSpec } from "./codex-version.mjs";
|
|
9
|
+
import {
|
|
10
|
+
buildCodexCompatibilityError,
|
|
11
|
+
probeCodexVersion,
|
|
12
|
+
resolveCodexBinForStart,
|
|
13
|
+
resolveCompatibleInstalledCodexBin,
|
|
14
|
+
} from "./codex-runtime.mjs";
|
|
8
15
|
|
|
9
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
17
|
const __dirname = path.dirname(__filename);
|
|
@@ -22,6 +29,9 @@ const agentEngineLogPath = path.join(logsDir, "agent-engine.log");
|
|
|
22
29
|
const daemonScriptPath = path.join(repoRoot, "scripts", "dist", "daemon.mjs");
|
|
23
30
|
const supervisorScriptPath = path.join(repoRoot, "scripts", "dist", "supervisor.mjs");
|
|
24
31
|
const nodeBin = process.execPath;
|
|
32
|
+
const agentEngineEnvPath = path.join(repoRoot, "apps", "agent-engine", ".env");
|
|
33
|
+
const controlPlaneEnvPath = path.join(repoRoot, "apps", "control-plane", ".env");
|
|
34
|
+
const codexInstallCommand = `npm install -g ${codexInstallPackageSpec}`;
|
|
25
35
|
|
|
26
36
|
const BASE_CHECK_MS = 5000;
|
|
27
37
|
const MAX_BACKOFF_MS = 60000;
|
|
@@ -120,6 +130,26 @@ async function runDaemonLoop() {
|
|
|
120
130
|
const state = { stopping: false, shuttingDown: false };
|
|
121
131
|
setupSignalHandlers(state);
|
|
122
132
|
|
|
133
|
+
try {
|
|
134
|
+
ensureCompatibleCodexForDaemon();
|
|
135
|
+
clearLastStartupError();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
writeLastStartupError({
|
|
138
|
+
detectedAt: new Date().toISOString(),
|
|
139
|
+
reason: getErrorMessage(error),
|
|
140
|
+
action: buildCodexCompatibilityAction(error),
|
|
141
|
+
});
|
|
142
|
+
console.error(`[daemon] fatal startup error: ${getErrorMessage(error)}`);
|
|
143
|
+
console.error(`[daemon] action required: ${buildCodexCompatibilityAction(error)}`);
|
|
144
|
+
state.stopping = true;
|
|
145
|
+
await shutdownDaemon(state, {
|
|
146
|
+
reason: "fatal-codex-compatibility",
|
|
147
|
+
exitCode: 1,
|
|
148
|
+
pauseBeforeExit: true,
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
123
153
|
console.log(`[daemon] running (pid ${process.pid})`);
|
|
124
154
|
|
|
125
155
|
let failureCount = 0;
|
|
@@ -614,6 +644,45 @@ function printLastStartupError() {
|
|
|
614
644
|
}
|
|
615
645
|
}
|
|
616
646
|
|
|
647
|
+
function ensureCompatibleCodexForDaemon(): void {
|
|
648
|
+
const resolved = resolveCodexBinForStart({
|
|
649
|
+
repoRoot,
|
|
650
|
+
agentEngineEnvPath,
|
|
651
|
+
controlPlaneEnvPath,
|
|
652
|
+
});
|
|
653
|
+
const currentProbe = probeCodexVersion({
|
|
654
|
+
codexBin: resolved.bin,
|
|
655
|
+
repoRoot,
|
|
656
|
+
});
|
|
657
|
+
if (currentProbe.ok && currentProbe.compatible) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (!resolved.userConfigured) {
|
|
662
|
+
const compatibleInstalled = resolveCompatibleInstalledCodexBin({ repoRoot });
|
|
663
|
+
if (compatibleInstalled) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
throw new Error(
|
|
669
|
+
buildCodexCompatibilityError({
|
|
670
|
+
resolved,
|
|
671
|
+
probe: currentProbe,
|
|
672
|
+
includeInstallHint: !resolved.userConfigured,
|
|
673
|
+
installCommand: codexInstallCommand,
|
|
674
|
+
}),
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function buildCodexCompatibilityAction(error: unknown): string {
|
|
679
|
+
const message = getErrorMessage(error);
|
|
680
|
+
if (message.includes("Install a compatible version with")) {
|
|
681
|
+
return `Install a compatible version with '${codexInstallCommand}', then restart the service.`;
|
|
682
|
+
}
|
|
683
|
+
return "Update that binary or point CODEX_BIN to a compatible executable, then restart the service.";
|
|
684
|
+
}
|
|
685
|
+
|
|
617
686
|
function printUsage() {
|
|
618
687
|
console.log("Usage: node scripts/dist/daemon.mjs <start|run|stop|status|help>");
|
|
619
688
|
}
|