@mostok/codexes 0.1.0
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/LICENSE +15 -0
- package/README.md +88 -0
- package/dist/accounts/account-registry.d.ts +30 -0
- package/dist/accounts/account-registry.js +263 -0
- package/dist/accounts/account-registry.js.map +1 -0
- package/dist/accounts/account-resolution.d.ts +16 -0
- package/dist/accounts/account-resolution.js +71 -0
- package/dist/accounts/account-resolution.js.map +1 -0
- package/dist/accounts/resolve-active-account.d.ts +6 -0
- package/dist/accounts/resolve-active-account.js +32 -0
- package/dist/accounts/resolve-active-account.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3426 -0
- package/dist/cli.js.map +7 -0
- package/dist/commands/account-add/run-account-add-command.d.ts +2 -0
- package/dist/commands/account-add/run-account-add-command.js +296 -0
- package/dist/commands/account-add/run-account-add-command.js.map +1 -0
- package/dist/commands/account-list/run-account-list-command.d.ts +2 -0
- package/dist/commands/account-list/run-account-list-command.js +48 -0
- package/dist/commands/account-list/run-account-list-command.js.map +1 -0
- package/dist/commands/account-remove/run-account-remove-command.d.ts +2 -0
- package/dist/commands/account-remove/run-account-remove-command.js +52 -0
- package/dist/commands/account-remove/run-account-remove-command.js.map +1 -0
- package/dist/commands/account-use/run-account-use-command.d.ts +2 -0
- package/dist/commands/account-use/run-account-use-command.js +78 -0
- package/dist/commands/account-use/run-account-use-command.js.map +1 -0
- package/dist/commands/root/run-root-command.d.ts +2 -0
- package/dist/commands/root/run-root-command.js +175 -0
- package/dist/commands/root/run-root-command.js.map +1 -0
- package/dist/config/wrapper-config.d.ts +24 -0
- package/dist/config/wrapper-config.js +145 -0
- package/dist/config/wrapper-config.js.map +1 -0
- package/dist/core/bootstrap.d.ts +8 -0
- package/dist/core/bootstrap.js +32 -0
- package/dist/core/bootstrap.js.map +1 -0
- package/dist/core/context.d.ts +33 -0
- package/dist/core/context.js +72 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/paths.d.ts +12 -0
- package/dist/core/paths.js +30 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/logging/logger.d.ts +18 -0
- package/dist/logging/logger.js +56 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/process/codex-launch-spec.d.ts +5 -0
- package/dist/process/codex-launch-spec.js +80 -0
- package/dist/process/codex-launch-spec.js.map +1 -0
- package/dist/process/find-codex-binary.d.ts +14 -0
- package/dist/process/find-codex-binary.js +73 -0
- package/dist/process/find-codex-binary.js.map +1 -0
- package/dist/process/run-codex-login.d.ts +14 -0
- package/dist/process/run-codex-login.js +97 -0
- package/dist/process/run-codex-login.js.map +1 -0
- package/dist/process/spawn-codex-command.d.ts +7 -0
- package/dist/process/spawn-codex-command.js +69 -0
- package/dist/process/spawn-codex-command.js.map +1 -0
- package/dist/runtime/activate-account/activate-account.d.ts +27 -0
- package/dist/runtime/activate-account/activate-account.js +298 -0
- package/dist/runtime/activate-account/activate-account.js.map +1 -0
- package/dist/runtime/auth-state-probe.d.ts +57 -0
- package/dist/runtime/auth-state-probe.js +394 -0
- package/dist/runtime/auth-state-probe.js.map +1 -0
- package/dist/runtime/init/initialize-runtime.d.ts +19 -0
- package/dist/runtime/init/initialize-runtime.js +275 -0
- package/dist/runtime/init/initialize-runtime.js.map +1 -0
- package/dist/runtime/lock/runtime-lock.d.ts +11 -0
- package/dist/runtime/lock/runtime-lock.js +99 -0
- package/dist/runtime/lock/runtime-lock.js.map +1 -0
- package/dist/runtime/login-workspace.d.ts +18 -0
- package/dist/runtime/login-workspace.js +171 -0
- package/dist/runtime/login-workspace.js.map +1 -0
- package/dist/runtime/runtime-contract.d.ts +44 -0
- package/dist/runtime/runtime-contract.js +79 -0
- package/dist/runtime/runtime-contract.js.map +1 -0
- package/dist/selection/account-auth-state.d.ts +23 -0
- package/dist/selection/account-auth-state.js +132 -0
- package/dist/selection/account-auth-state.js.map +1 -0
- package/dist/selection/select-account.d.ts +11 -0
- package/dist/selection/select-account.js +168 -0
- package/dist/selection/select-account.js.map +1 -0
- package/dist/selection/usage-cache.d.ts +24 -0
- package/dist/selection/usage-cache.js +106 -0
- package/dist/selection/usage-cache.js.map +1 -0
- package/dist/selection/usage-client.d.ts +23 -0
- package/dist/selection/usage-client.js +143 -0
- package/dist/selection/usage-client.js.map +1 -0
- package/dist/selection/usage-normalize.d.ts +7 -0
- package/dist/selection/usage-normalize.js +209 -0
- package/dist/selection/usage-normalize.js.map +1 -0
- package/dist/selection/usage-probe-coordinator.d.ts +18 -0
- package/dist/selection/usage-probe-coordinator.js +69 -0
- package/dist/selection/usage-probe-coordinator.js.map +1 -0
- package/dist/selection/usage-types.d.ts +59 -0
- package/dist/selection/usage-types.js +2 -0
- package/dist/selection/usage-types.js.map +1 -0
- package/package.json +59 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3426 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/logging/logger.ts
|
|
4
|
+
var LOG_LEVEL_ORDER = {
|
|
5
|
+
DEBUG: 10,
|
|
6
|
+
INFO: 20,
|
|
7
|
+
WARN: 30,
|
|
8
|
+
ERROR: 40
|
|
9
|
+
};
|
|
10
|
+
function resolveLogLevel(value) {
|
|
11
|
+
switch (value?.toUpperCase()) {
|
|
12
|
+
case "ERROR":
|
|
13
|
+
return "ERROR";
|
|
14
|
+
case "WARN":
|
|
15
|
+
return "WARN";
|
|
16
|
+
case "INFO":
|
|
17
|
+
return "INFO";
|
|
18
|
+
case "DEBUG":
|
|
19
|
+
return "DEBUG";
|
|
20
|
+
default:
|
|
21
|
+
return "ERROR";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function createLogSink(stream) {
|
|
25
|
+
return {
|
|
26
|
+
write(level, event, details = {}) {
|
|
27
|
+
stream.write(
|
|
28
|
+
`${JSON.stringify({
|
|
29
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
30
|
+
level,
|
|
31
|
+
event,
|
|
32
|
+
details
|
|
33
|
+
})}
|
|
34
|
+
`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function createLogger(input) {
|
|
40
|
+
const configuredLevel = resolveLogLevel(input.level);
|
|
41
|
+
return {
|
|
42
|
+
debug(event, details) {
|
|
43
|
+
logWithLevel("DEBUG", input.name, configuredLevel, input.sink, event, details);
|
|
44
|
+
},
|
|
45
|
+
info(event, details) {
|
|
46
|
+
logWithLevel("INFO", input.name, configuredLevel, input.sink, event, details);
|
|
47
|
+
},
|
|
48
|
+
warn(event, details) {
|
|
49
|
+
logWithLevel("WARN", input.name, configuredLevel, input.sink, event, details);
|
|
50
|
+
},
|
|
51
|
+
error(event, details) {
|
|
52
|
+
logWithLevel("ERROR", input.name, configuredLevel, input.sink, event, details);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function logWithLevel(level, name, configuredLevel, sink, event, details) {
|
|
57
|
+
if (LOG_LEVEL_ORDER[level] < LOG_LEVEL_ORDER[configuredLevel]) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
sink.write(level, `${name}.${event}`, details);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/config/wrapper-config.ts
|
|
64
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
65
|
+
var DEFAULT_EXPERIMENTAL_PROBE_TIMEOUT_MS = 3500;
|
|
66
|
+
var DEFAULT_EXPERIMENTAL_CACHE_TTL_MS = 6e4;
|
|
67
|
+
async function resolveWrapperConfig(input) {
|
|
68
|
+
await mkdir(input.paths.dataRoot, { recursive: true });
|
|
69
|
+
const credentialStoreMode = await detectCredentialStoreMode(
|
|
70
|
+
input.paths.codexConfigFile,
|
|
71
|
+
input.logger
|
|
72
|
+
);
|
|
73
|
+
const resolved = {
|
|
74
|
+
configFilePath: input.paths.wrapperConfigFile,
|
|
75
|
+
codexConfigFilePath: input.paths.codexConfigFile,
|
|
76
|
+
selectionCacheFilePath: input.paths.selectionCacheFile,
|
|
77
|
+
credentialStoreMode,
|
|
78
|
+
credentialStorePolicyReason: credentialStoreMode === "file" ? "file mode detected in Codex config" : "codexes currently supports only file-backed auth storage",
|
|
79
|
+
accountSelectionStrategy: resolveAccountSelectionStrategy(input.env),
|
|
80
|
+
experimentalSelection: resolveExperimentalSelectionConfig(input.env, input.logger)
|
|
81
|
+
};
|
|
82
|
+
input.logger.info("wrapper_config.resolved", {
|
|
83
|
+
configFilePath: resolved.configFilePath,
|
|
84
|
+
codexConfigFilePath: resolved.codexConfigFilePath,
|
|
85
|
+
selectionCacheFilePath: resolved.selectionCacheFilePath,
|
|
86
|
+
credentialStoreMode: resolved.credentialStoreMode,
|
|
87
|
+
accountSelectionStrategy: resolved.accountSelectionStrategy,
|
|
88
|
+
experimentalSelection: resolved.experimentalSelection
|
|
89
|
+
});
|
|
90
|
+
return resolved;
|
|
91
|
+
}
|
|
92
|
+
function resolveExperimentalSelectionConfig(env, logger) {
|
|
93
|
+
const probeTimeoutMs = resolvePositiveIntegerEnv({
|
|
94
|
+
defaultValue: DEFAULT_EXPERIMENTAL_PROBE_TIMEOUT_MS,
|
|
95
|
+
env,
|
|
96
|
+
envKey: "CODEXES_EXPERIMENTAL_SELECTION_TIMEOUT_MS",
|
|
97
|
+
logger
|
|
98
|
+
});
|
|
99
|
+
const cacheTtlMs = resolvePositiveIntegerEnv({
|
|
100
|
+
defaultValue: DEFAULT_EXPERIMENTAL_CACHE_TTL_MS,
|
|
101
|
+
env,
|
|
102
|
+
envKey: "CODEXES_EXPERIMENTAL_SELECTION_CACHE_TTL_MS",
|
|
103
|
+
logger
|
|
104
|
+
});
|
|
105
|
+
const useAccountIdHeader = resolveBooleanEnv(
|
|
106
|
+
env.CODEXES_EXPERIMENTAL_SELECTION_USE_ACCOUNT_ID_HEADER
|
|
107
|
+
);
|
|
108
|
+
const enabled = resolveAccountSelectionStrategy(env) === "remaining-limit-experimental";
|
|
109
|
+
logger.debug("wrapper_config.experimental_selection_resolved", {
|
|
110
|
+
enabled,
|
|
111
|
+
probeTimeoutMs,
|
|
112
|
+
cacheTtlMs,
|
|
113
|
+
useAccountIdHeader
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
enabled,
|
|
117
|
+
probeTimeoutMs,
|
|
118
|
+
cacheTtlMs,
|
|
119
|
+
useAccountIdHeader
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function resolveAccountSelectionStrategy(env) {
|
|
123
|
+
switch (env.CODEXES_ACCOUNT_SELECTION_STRATEGY?.trim().toLowerCase()) {
|
|
124
|
+
case "single-account":
|
|
125
|
+
return "single-account";
|
|
126
|
+
case "remaining-limit-experimental":
|
|
127
|
+
return "remaining-limit-experimental";
|
|
128
|
+
case "manual-default":
|
|
129
|
+
case void 0:
|
|
130
|
+
case "":
|
|
131
|
+
return "manual-default";
|
|
132
|
+
default:
|
|
133
|
+
return "manual-default";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function detectCredentialStoreMode(configFile, logger) {
|
|
137
|
+
try {
|
|
138
|
+
const rawConfig = await readFile(configFile, "utf8");
|
|
139
|
+
const mode = parseCredentialStoreMode(rawConfig);
|
|
140
|
+
logger.debug("credential_store.detected", {
|
|
141
|
+
configFile,
|
|
142
|
+
credentialStoreMode: mode
|
|
143
|
+
});
|
|
144
|
+
return mode;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
147
|
+
logger.warn("credential_store.config_missing", {
|
|
148
|
+
configFile,
|
|
149
|
+
fallbackMode: "missing"
|
|
150
|
+
});
|
|
151
|
+
return "missing";
|
|
152
|
+
}
|
|
153
|
+
logger.error("credential_store.read_failed", {
|
|
154
|
+
configFile,
|
|
155
|
+
message: error instanceof Error ? error.message : String(error)
|
|
156
|
+
});
|
|
157
|
+
return "unknown";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function parseCredentialStoreMode(rawConfig) {
|
|
161
|
+
const match = rawConfig.match(/^\s*cli_auth_credentials_store\s*=\s*"([^"]+)"/m);
|
|
162
|
+
if (!match) {
|
|
163
|
+
return "missing";
|
|
164
|
+
}
|
|
165
|
+
const configuredValue = match[1];
|
|
166
|
+
if (!configuredValue) {
|
|
167
|
+
return "unknown";
|
|
168
|
+
}
|
|
169
|
+
switch (configuredValue.trim().toLowerCase()) {
|
|
170
|
+
case "file":
|
|
171
|
+
return "file";
|
|
172
|
+
case "keyring":
|
|
173
|
+
return "keyring";
|
|
174
|
+
case "auto":
|
|
175
|
+
return "auto";
|
|
176
|
+
default:
|
|
177
|
+
return "unknown";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function resolvePositiveIntegerEnv(input) {
|
|
181
|
+
const raw = input.env[input.envKey]?.trim();
|
|
182
|
+
if (!raw) {
|
|
183
|
+
return input.defaultValue;
|
|
184
|
+
}
|
|
185
|
+
const parsed = Number.parseInt(raw, 10);
|
|
186
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
187
|
+
input.logger.warn("wrapper_config.invalid_env_override", {
|
|
188
|
+
envKey: input.envKey,
|
|
189
|
+
rawValue: raw,
|
|
190
|
+
fallbackValue: input.defaultValue
|
|
191
|
+
});
|
|
192
|
+
return input.defaultValue;
|
|
193
|
+
}
|
|
194
|
+
return parsed;
|
|
195
|
+
}
|
|
196
|
+
function resolveBooleanEnv(value) {
|
|
197
|
+
switch (value?.trim().toLowerCase()) {
|
|
198
|
+
case "1":
|
|
199
|
+
case "true":
|
|
200
|
+
case "yes":
|
|
201
|
+
case "on":
|
|
202
|
+
return true;
|
|
203
|
+
default:
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/core/paths.ts
|
|
209
|
+
import path from "node:path";
|
|
210
|
+
import os from "node:os";
|
|
211
|
+
function resolvePaths(cwd, env) {
|
|
212
|
+
const baseDataDir = resolveBaseDataDir(env);
|
|
213
|
+
return {
|
|
214
|
+
projectRoot: cwd,
|
|
215
|
+
dataRoot: baseDataDir,
|
|
216
|
+
sharedCodexHome: env.CODEX_HOME ?? path.join(baseDataDir, "shared-home"),
|
|
217
|
+
accountRoot: path.join(baseDataDir, "accounts"),
|
|
218
|
+
runtimeRoot: path.join(baseDataDir, "runtime"),
|
|
219
|
+
registryFile: path.join(baseDataDir, "registry.json"),
|
|
220
|
+
wrapperConfigFile: path.join(baseDataDir, "codexes.json"),
|
|
221
|
+
codexConfigFile: path.join(env.CODEX_HOME ?? path.join(baseDataDir, "shared-home"), "config.toml"),
|
|
222
|
+
selectionCacheFile: path.join(baseDataDir, "selection-cache.json")
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function resolveBaseDataDir(env) {
|
|
226
|
+
if (process.platform === "win32") {
|
|
227
|
+
return path.join(
|
|
228
|
+
env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local"),
|
|
229
|
+
"codexes"
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (process.platform === "darwin") {
|
|
233
|
+
return path.join(
|
|
234
|
+
env.HOME ?? os.homedir(),
|
|
235
|
+
"Library",
|
|
236
|
+
"Application Support",
|
|
237
|
+
"codexes"
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
const xdgStateHome = env.XDG_STATE_HOME;
|
|
241
|
+
if (xdgStateHome) {
|
|
242
|
+
return path.join(xdgStateHome, "codexes");
|
|
243
|
+
}
|
|
244
|
+
return path.join(env.HOME ?? os.homedir(), ".local", "state", "codexes");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/process/find-codex-binary.ts
|
|
248
|
+
import { access, stat } from "node:fs/promises";
|
|
249
|
+
import path2 from "node:path";
|
|
250
|
+
async function findCodexBinary(input) {
|
|
251
|
+
const candidates = buildCandidates(input.env);
|
|
252
|
+
const rejectedCandidates = [];
|
|
253
|
+
input.logger.debug("binary_resolution.start", {
|
|
254
|
+
wrapperExecutablePath: input.wrapperExecutablePath,
|
|
255
|
+
candidateCount: candidates.length
|
|
256
|
+
});
|
|
257
|
+
for (const candidate of candidates) {
|
|
258
|
+
const reason = await getRejectionReason(candidate, input.wrapperExecutablePath);
|
|
259
|
+
if (reason) {
|
|
260
|
+
rejectedCandidates.push({ candidate, reason });
|
|
261
|
+
input.logger.debug("binary_resolution.rejected", { candidate, reason });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
input.logger.info("binary_resolution.selected", { candidate });
|
|
265
|
+
return {
|
|
266
|
+
path: candidate,
|
|
267
|
+
candidates,
|
|
268
|
+
rejectedCandidates
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
input.logger.warn("binary_resolution.missing", {
|
|
272
|
+
wrapperExecutablePath: input.wrapperExecutablePath,
|
|
273
|
+
rejectedCandidates
|
|
274
|
+
});
|
|
275
|
+
return {
|
|
276
|
+
path: null,
|
|
277
|
+
candidates,
|
|
278
|
+
rejectedCandidates
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function buildCandidates(env) {
|
|
282
|
+
const pathValue = env.PATH ?? "";
|
|
283
|
+
const pathEntries = pathValue.split(path2.delimiter).map((entry) => entry.trim()).filter(Boolean);
|
|
284
|
+
const executableNames = process.platform === "win32" ? ["codex.cmd", "codex.exe", "codex.bat"] : ["codex"];
|
|
285
|
+
return Array.from(
|
|
286
|
+
new Set(
|
|
287
|
+
pathEntries.flatMap(
|
|
288
|
+
(entry) => executableNames.map((executableName) => path2.join(entry, executableName))
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
async function getRejectionReason(candidate, wrapperExecutablePath) {
|
|
294
|
+
try {
|
|
295
|
+
await access(candidate);
|
|
296
|
+
} catch {
|
|
297
|
+
return "not_accessible";
|
|
298
|
+
}
|
|
299
|
+
const [candidateStat, wrapperStat] = await Promise.all([
|
|
300
|
+
stat(candidate).catch(() => null),
|
|
301
|
+
stat(wrapperExecutablePath).catch(() => null)
|
|
302
|
+
]);
|
|
303
|
+
if (!candidateStat || !candidateStat.isFile()) {
|
|
304
|
+
return "not_a_file";
|
|
305
|
+
}
|
|
306
|
+
if (wrapperStat && isSameFile(candidate, wrapperExecutablePath, candidateStat, wrapperStat)) {
|
|
307
|
+
return "self_recursive_wrapper_path";
|
|
308
|
+
}
|
|
309
|
+
if (path2.basename(candidate).toLowerCase().startsWith("codexes")) {
|
|
310
|
+
return "wrapper_named_binary";
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
function isSameFile(candidate, wrapperExecutablePath, candidateStat, wrapperStat) {
|
|
315
|
+
if (path2.resolve(candidate) === path2.resolve(wrapperExecutablePath)) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
return candidateStat.ino === wrapperStat.ino && candidateStat.dev === wrapperStat.dev;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/runtime/init/initialize-runtime.ts
|
|
322
|
+
import os2 from "node:os";
|
|
323
|
+
import path4 from "node:path";
|
|
324
|
+
import {
|
|
325
|
+
copyFile,
|
|
326
|
+
cp,
|
|
327
|
+
mkdir as mkdir3,
|
|
328
|
+
readFile as readFile3,
|
|
329
|
+
stat as stat3,
|
|
330
|
+
writeFile as writeFile2
|
|
331
|
+
} from "node:fs/promises";
|
|
332
|
+
|
|
333
|
+
// src/accounts/account-registry.ts
|
|
334
|
+
import { randomUUID } from "node:crypto";
|
|
335
|
+
import { mkdir as mkdir2, readFile as readFile2, rename, rm, stat as stat2, writeFile } from "node:fs/promises";
|
|
336
|
+
import path3 from "node:path";
|
|
337
|
+
var REGISTRY_SCHEMA_VERSION = 1;
|
|
338
|
+
function createAccountRegistry(input) {
|
|
339
|
+
return {
|
|
340
|
+
addAccount(details) {
|
|
341
|
+
return withRegistryMutation(input, "registry.add", async (document, now) => {
|
|
342
|
+
const normalizedLabel = normalizeLabel(details.label);
|
|
343
|
+
const duplicate = document.accounts.find(
|
|
344
|
+
(account) => account.label.toLowerCase() === normalizedLabel.toLowerCase()
|
|
345
|
+
);
|
|
346
|
+
if (duplicate) {
|
|
347
|
+
input.logger.warn("registry.duplicate_label", {
|
|
348
|
+
label: normalizedLabel,
|
|
349
|
+
existingAccountId: duplicate.id
|
|
350
|
+
});
|
|
351
|
+
throw new Error(`An account named "${normalizedLabel}" already exists.`);
|
|
352
|
+
}
|
|
353
|
+
const accountId = randomUUID();
|
|
354
|
+
const record = {
|
|
355
|
+
id: accountId,
|
|
356
|
+
label: normalizedLabel,
|
|
357
|
+
authDirectory: details.authDirectory ?? path3.join(input.accountRoot, accountId),
|
|
358
|
+
createdAt: now,
|
|
359
|
+
updatedAt: now,
|
|
360
|
+
lastUsedAt: null
|
|
361
|
+
};
|
|
362
|
+
document.accounts.push(record);
|
|
363
|
+
if (!document.defaultAccountId) {
|
|
364
|
+
document.defaultAccountId = record.id;
|
|
365
|
+
}
|
|
366
|
+
input.logger.info("registry.account_added", {
|
|
367
|
+
accountId: record.id,
|
|
368
|
+
label: record.label,
|
|
369
|
+
authDirectory: record.authDirectory,
|
|
370
|
+
defaultAccountId: document.defaultAccountId
|
|
371
|
+
});
|
|
372
|
+
return record;
|
|
373
|
+
});
|
|
374
|
+
},
|
|
375
|
+
async getDefaultAccount() {
|
|
376
|
+
const document = await readRegistryDocument(input);
|
|
377
|
+
const account = document.defaultAccountId ? document.accounts.find((entry) => entry.id === document.defaultAccountId) ?? null : null;
|
|
378
|
+
input.logger.debug("registry.default_loaded", {
|
|
379
|
+
defaultAccountId: document.defaultAccountId,
|
|
380
|
+
resolvedAccountId: account?.id ?? null
|
|
381
|
+
});
|
|
382
|
+
return account;
|
|
383
|
+
},
|
|
384
|
+
async listAccounts() {
|
|
385
|
+
const document = await readRegistryDocument(input);
|
|
386
|
+
input.logger.debug("registry.list_loaded", {
|
|
387
|
+
accountCount: document.accounts.length,
|
|
388
|
+
defaultAccountId: document.defaultAccountId
|
|
389
|
+
});
|
|
390
|
+
return [...document.accounts];
|
|
391
|
+
},
|
|
392
|
+
removeAccount(accountId) {
|
|
393
|
+
return withRegistryMutation(input, "registry.remove", async (document, now) => {
|
|
394
|
+
const record = document.accounts.find((account) => account.id === accountId);
|
|
395
|
+
if (!record) {
|
|
396
|
+
input.logger.warn("registry.remove_missing", { accountId });
|
|
397
|
+
throw new Error(`Account "${accountId}" was not found.`);
|
|
398
|
+
}
|
|
399
|
+
document.accounts = document.accounts.filter((account) => account.id !== accountId);
|
|
400
|
+
if (document.defaultAccountId === accountId) {
|
|
401
|
+
document.defaultAccountId = document.accounts[0]?.id ?? null;
|
|
402
|
+
}
|
|
403
|
+
record.updatedAt = now;
|
|
404
|
+
input.logger.info("registry.account_removed", {
|
|
405
|
+
accountId,
|
|
406
|
+
nextDefaultAccountId: document.defaultAccountId
|
|
407
|
+
});
|
|
408
|
+
return record;
|
|
409
|
+
});
|
|
410
|
+
},
|
|
411
|
+
selectAccount(accountId) {
|
|
412
|
+
return withRegistryMutation(input, "registry.select", async (document, now) => {
|
|
413
|
+
const record = document.accounts.find((account) => account.id === accountId);
|
|
414
|
+
if (!record) {
|
|
415
|
+
input.logger.warn("registry.select_missing", { accountId });
|
|
416
|
+
throw new Error(`Account "${accountId}" was not found.`);
|
|
417
|
+
}
|
|
418
|
+
document.defaultAccountId = record.id;
|
|
419
|
+
record.updatedAt = now;
|
|
420
|
+
record.lastUsedAt = now;
|
|
421
|
+
input.logger.info("registry.account_selected", {
|
|
422
|
+
accountId,
|
|
423
|
+
label: record.label
|
|
424
|
+
});
|
|
425
|
+
return record;
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
async function withRegistryMutation(input, operation, mutate) {
|
|
431
|
+
await mkdir2(input.accountRoot, { recursive: true });
|
|
432
|
+
await mkdir2(path3.dirname(input.registryFile), { recursive: true });
|
|
433
|
+
const document = await readRegistryDocument(input);
|
|
434
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
435
|
+
input.logger.debug(`${operation}.start`, {
|
|
436
|
+
registryFile: input.registryFile,
|
|
437
|
+
accountRoot: input.accountRoot,
|
|
438
|
+
accountCount: document.accounts.length
|
|
439
|
+
});
|
|
440
|
+
const result = await mutate(document, now);
|
|
441
|
+
await persistRegistryDocument(input, document);
|
|
442
|
+
input.logger.debug(`${operation}.complete`, {
|
|
443
|
+
registryFile: input.registryFile,
|
|
444
|
+
accountCount: document.accounts.length,
|
|
445
|
+
defaultAccountId: document.defaultAccountId
|
|
446
|
+
});
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
async function readRegistryDocument(input) {
|
|
450
|
+
await mkdir2(input.accountRoot, { recursive: true });
|
|
451
|
+
await mkdir2(path3.dirname(input.registryFile), { recursive: true });
|
|
452
|
+
try {
|
|
453
|
+
const raw = await readFile2(input.registryFile, "utf8");
|
|
454
|
+
const parsed = JSON.parse(raw);
|
|
455
|
+
const migrated = migrateRegistryDocument(parsed, input.logger, input.registryFile);
|
|
456
|
+
input.logger.debug("registry.read_success", {
|
|
457
|
+
registryFile: input.registryFile,
|
|
458
|
+
schemaVersion: migrated.schemaVersion,
|
|
459
|
+
accountCount: migrated.accounts.length
|
|
460
|
+
});
|
|
461
|
+
return migrated;
|
|
462
|
+
} catch (error) {
|
|
463
|
+
if (isFileMissing(error)) {
|
|
464
|
+
const emptyDocument2 = createEmptyRegistryDocument();
|
|
465
|
+
input.logger.info("registry.read_missing", {
|
|
466
|
+
registryFile: input.registryFile,
|
|
467
|
+
action: "create_empty_registry"
|
|
468
|
+
});
|
|
469
|
+
await persistRegistryDocument(input, emptyDocument2);
|
|
470
|
+
return emptyDocument2;
|
|
471
|
+
}
|
|
472
|
+
const normalized = normalizeUnknownError(error);
|
|
473
|
+
const corruptionBackupPath = `${input.registryFile}.corrupt-${Date.now()}`;
|
|
474
|
+
input.logger.warn("registry.read_corrupt", {
|
|
475
|
+
registryFile: input.registryFile,
|
|
476
|
+
corruptionBackupPath,
|
|
477
|
+
message: normalized.message
|
|
478
|
+
});
|
|
479
|
+
await rename(input.registryFile, corruptionBackupPath).catch(() => void 0);
|
|
480
|
+
const emptyDocument = createEmptyRegistryDocument();
|
|
481
|
+
await persistRegistryDocument(input, emptyDocument);
|
|
482
|
+
return emptyDocument;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async function persistRegistryDocument(input, document) {
|
|
486
|
+
const tempFile = `${input.registryFile}.tmp`;
|
|
487
|
+
const serialized = JSON.stringify(document, null, 2);
|
|
488
|
+
await writeFile(tempFile, serialized, "utf8");
|
|
489
|
+
await rename(tempFile, input.registryFile);
|
|
490
|
+
input.logger.debug("registry.write_success", {
|
|
491
|
+
registryFile: input.registryFile,
|
|
492
|
+
bytes: Buffer.byteLength(serialized, "utf8"),
|
|
493
|
+
schemaVersion: document.schemaVersion,
|
|
494
|
+
defaultAccountId: document.defaultAccountId
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
function migrateRegistryDocument(value, logger, registryFile) {
|
|
498
|
+
if (!isObject(value)) {
|
|
499
|
+
throw new Error("Registry document is not a JSON object.");
|
|
500
|
+
}
|
|
501
|
+
const schemaVersion = typeof value.schemaVersion === "number" ? value.schemaVersion : 0;
|
|
502
|
+
logger.debug("registry.migration_check", {
|
|
503
|
+
registryFile,
|
|
504
|
+
schemaVersion,
|
|
505
|
+
targetSchemaVersion: REGISTRY_SCHEMA_VERSION
|
|
506
|
+
});
|
|
507
|
+
if (schemaVersion > REGISTRY_SCHEMA_VERSION) {
|
|
508
|
+
throw new Error(
|
|
509
|
+
`Registry schema ${schemaVersion} is newer than supported schema ${REGISTRY_SCHEMA_VERSION}.`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
if (schemaVersion === REGISTRY_SCHEMA_VERSION) {
|
|
513
|
+
return normalizeRegistryDocument(value);
|
|
514
|
+
}
|
|
515
|
+
if (schemaVersion === 0) {
|
|
516
|
+
const migrated = normalizeRegistryDocument({
|
|
517
|
+
schemaVersion: REGISTRY_SCHEMA_VERSION,
|
|
518
|
+
defaultAccountId: value.defaultAccountId ?? null,
|
|
519
|
+
accounts: value.accounts ?? []
|
|
520
|
+
});
|
|
521
|
+
logger.info("registry.migration_applied", {
|
|
522
|
+
registryFile,
|
|
523
|
+
fromSchemaVersion: 0,
|
|
524
|
+
toSchemaVersion: REGISTRY_SCHEMA_VERSION
|
|
525
|
+
});
|
|
526
|
+
return migrated;
|
|
527
|
+
}
|
|
528
|
+
throw new Error(`Unsupported registry schema version ${schemaVersion}.`);
|
|
529
|
+
}
|
|
530
|
+
function normalizeRegistryDocument(value) {
|
|
531
|
+
const accounts = Array.isArray(value.accounts) ? value.accounts.map(normalizeAccountRecord) : [];
|
|
532
|
+
const defaultAccountId = typeof value.defaultAccountId === "string" ? value.defaultAccountId : null;
|
|
533
|
+
return {
|
|
534
|
+
schemaVersion: REGISTRY_SCHEMA_VERSION,
|
|
535
|
+
defaultAccountId: defaultAccountId && accounts.some((account) => account.id === defaultAccountId) ? defaultAccountId : accounts[0]?.id ?? null,
|
|
536
|
+
accounts
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function normalizeAccountRecord(value) {
|
|
540
|
+
if (!isObject(value)) {
|
|
541
|
+
throw new Error("Account record is not an object.");
|
|
542
|
+
}
|
|
543
|
+
const id = typeof value.id === "string" ? value.id : randomUUID();
|
|
544
|
+
const createdAt = typeof value.createdAt === "string" ? value.createdAt : (/* @__PURE__ */ new Date(0)).toISOString();
|
|
545
|
+
const updatedAt = typeof value.updatedAt === "string" ? value.updatedAt : createdAt;
|
|
546
|
+
return {
|
|
547
|
+
id,
|
|
548
|
+
label: normalizeLabel(typeof value.label === "string" ? value.label : id),
|
|
549
|
+
authDirectory: typeof value.authDirectory === "string" ? value.authDirectory : path3.join("accounts", id),
|
|
550
|
+
createdAt,
|
|
551
|
+
updatedAt,
|
|
552
|
+
lastUsedAt: typeof value.lastUsedAt === "string" ? value.lastUsedAt : null
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function createEmptyRegistryDocument() {
|
|
556
|
+
return {
|
|
557
|
+
schemaVersion: REGISTRY_SCHEMA_VERSION,
|
|
558
|
+
defaultAccountId: null,
|
|
559
|
+
accounts: []
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function normalizeLabel(label) {
|
|
563
|
+
const normalized = label.trim();
|
|
564
|
+
if (!normalized) {
|
|
565
|
+
throw new Error("Account label cannot be empty.");
|
|
566
|
+
}
|
|
567
|
+
return normalized;
|
|
568
|
+
}
|
|
569
|
+
function isObject(value) {
|
|
570
|
+
return typeof value === "object" && value !== null;
|
|
571
|
+
}
|
|
572
|
+
function isFileMissing(error) {
|
|
573
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
574
|
+
}
|
|
575
|
+
function normalizeUnknownError(error) {
|
|
576
|
+
if (error instanceof Error) {
|
|
577
|
+
return { message: error.message };
|
|
578
|
+
}
|
|
579
|
+
return { message: String(error) };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/runtime/init/initialize-runtime.ts
|
|
583
|
+
async function initializeRuntimeEnvironment(input) {
|
|
584
|
+
const legacyCodexHome = path4.join(os2.homedir(), ".codex");
|
|
585
|
+
const firstRun = !await pathExists(input.paths.sharedCodexHome);
|
|
586
|
+
const createdDirectories = [];
|
|
587
|
+
const createdFiles = [];
|
|
588
|
+
const copiedSharedArtifacts = [];
|
|
589
|
+
const skippedArtifacts = [];
|
|
590
|
+
input.logger.info("runtime_init.start", {
|
|
591
|
+
dataRoot: input.paths.dataRoot,
|
|
592
|
+
sharedCodexHome: input.paths.sharedCodexHome,
|
|
593
|
+
legacyCodexHome,
|
|
594
|
+
firstRun
|
|
595
|
+
});
|
|
596
|
+
for (const directory of [
|
|
597
|
+
input.paths.dataRoot,
|
|
598
|
+
input.paths.sharedCodexHome,
|
|
599
|
+
input.paths.accountRoot,
|
|
600
|
+
input.paths.runtimeRoot,
|
|
601
|
+
path4.join(input.paths.runtimeRoot, "backups"),
|
|
602
|
+
path4.join(input.paths.runtimeRoot, "tmp"),
|
|
603
|
+
path4.dirname(input.paths.registryFile)
|
|
604
|
+
]) {
|
|
605
|
+
if (!await pathExists(directory)) {
|
|
606
|
+
createdDirectories.push(directory);
|
|
607
|
+
input.logger.info("runtime_init.directory_create", { directory });
|
|
608
|
+
} else {
|
|
609
|
+
input.logger.debug("runtime_init.directory_exists", { directory });
|
|
610
|
+
}
|
|
611
|
+
await mkdir3(directory, { recursive: true });
|
|
612
|
+
}
|
|
613
|
+
const configResult = await ensureSharedConfigToml({
|
|
614
|
+
legacyCodexHome,
|
|
615
|
+
logger: input.logger,
|
|
616
|
+
sharedCodexHome: input.paths.sharedCodexHome
|
|
617
|
+
});
|
|
618
|
+
createdFiles.push(...configResult.createdFiles);
|
|
619
|
+
copiedSharedArtifacts.push(...configResult.copiedArtifacts);
|
|
620
|
+
skippedArtifacts.push(...configResult.skippedArtifacts);
|
|
621
|
+
const mcpResult = await ensureSharedFileCopy({
|
|
622
|
+
artifactName: "mcp.json",
|
|
623
|
+
logger: input.logger,
|
|
624
|
+
sharedCodexHome: input.paths.sharedCodexHome,
|
|
625
|
+
sourceCodexHome: legacyCodexHome
|
|
626
|
+
});
|
|
627
|
+
createdFiles.push(...mcpResult.createdFiles);
|
|
628
|
+
copiedSharedArtifacts.push(...mcpResult.copiedArtifacts);
|
|
629
|
+
skippedArtifacts.push(...mcpResult.skippedArtifacts);
|
|
630
|
+
const trustResult = await ensureSharedDirectoryCopy({
|
|
631
|
+
artifactName: "trust",
|
|
632
|
+
logger: input.logger,
|
|
633
|
+
sharedCodexHome: input.paths.sharedCodexHome,
|
|
634
|
+
sourceCodexHome: legacyCodexHome
|
|
635
|
+
});
|
|
636
|
+
copiedSharedArtifacts.push(...trustResult.copiedArtifacts);
|
|
637
|
+
skippedArtifacts.push(...trustResult.skippedArtifacts);
|
|
638
|
+
const registry = createAccountRegistry({
|
|
639
|
+
accountRoot: input.paths.accountRoot,
|
|
640
|
+
logger: input.logger,
|
|
641
|
+
registryFile: input.paths.registryFile
|
|
642
|
+
});
|
|
643
|
+
if (!await pathExists(input.paths.registryFile)) {
|
|
644
|
+
createdFiles.push(input.paths.registryFile);
|
|
645
|
+
input.logger.info("runtime_init.registry_bootstrap", {
|
|
646
|
+
registryFile: input.paths.registryFile
|
|
647
|
+
});
|
|
648
|
+
} else {
|
|
649
|
+
input.logger.debug("runtime_init.registry_exists", {
|
|
650
|
+
registryFile: input.paths.registryFile
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
await registry.listAccounts();
|
|
654
|
+
const result = {
|
|
655
|
+
firstRun,
|
|
656
|
+
legacyCodexHome,
|
|
657
|
+
sharedCodexHome: input.paths.sharedCodexHome,
|
|
658
|
+
createdDirectories,
|
|
659
|
+
createdFiles,
|
|
660
|
+
copiedSharedArtifacts,
|
|
661
|
+
skippedArtifacts
|
|
662
|
+
};
|
|
663
|
+
input.logger.info("runtime_init.complete", {
|
|
664
|
+
firstRun: result.firstRun,
|
|
665
|
+
createdDirectories: result.createdDirectories,
|
|
666
|
+
createdFiles: result.createdFiles,
|
|
667
|
+
copiedSharedArtifacts: result.copiedSharedArtifacts,
|
|
668
|
+
skippedArtifacts: result.skippedArtifacts
|
|
669
|
+
});
|
|
670
|
+
return result;
|
|
671
|
+
}
|
|
672
|
+
async function ensureSharedConfigToml(input) {
|
|
673
|
+
const targetPath = path4.join(input.sharedCodexHome, "config.toml");
|
|
674
|
+
const sourcePath = path4.join(input.legacyCodexHome, "config.toml");
|
|
675
|
+
if (await pathExists(targetPath)) {
|
|
676
|
+
const existingConfig = await readFile3(targetPath, "utf8");
|
|
677
|
+
const existingMode = detectCredentialStoreMode2(existingConfig);
|
|
678
|
+
input.logger.debug("runtime_init.config_exists", {
|
|
679
|
+
targetPath,
|
|
680
|
+
credentialStoreMode: existingMode
|
|
681
|
+
});
|
|
682
|
+
if (existingMode === "missing") {
|
|
683
|
+
await writeFile2(targetPath, pinFileCredentialStore(existingConfig), "utf8");
|
|
684
|
+
input.logger.info("runtime_init.config_file_mode_pinned", {
|
|
685
|
+
targetPath
|
|
686
|
+
});
|
|
687
|
+
} else if (existingMode !== "file") {
|
|
688
|
+
input.logger.warn("runtime_init.config_mode_unsupported", {
|
|
689
|
+
targetPath,
|
|
690
|
+
credentialStoreMode: existingMode
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
return emptyArtifactResult();
|
|
694
|
+
}
|
|
695
|
+
if (await pathExists(sourcePath) && !samePath(sourcePath, targetPath)) {
|
|
696
|
+
const sourceConfig = await readFile3(sourcePath, "utf8");
|
|
697
|
+
const pinnedConfig = pinFileCredentialStore(sourceConfig);
|
|
698
|
+
await writeFile2(targetPath, pinnedConfig, "utf8");
|
|
699
|
+
input.logger.info("runtime_init.config_imported", {
|
|
700
|
+
sourcePath,
|
|
701
|
+
targetPath,
|
|
702
|
+
sourceCredentialStoreMode: detectCredentialStoreMode2(sourceConfig),
|
|
703
|
+
targetCredentialStoreMode: detectCredentialStoreMode2(pinnedConfig)
|
|
704
|
+
});
|
|
705
|
+
return {
|
|
706
|
+
copiedArtifacts: ["config.toml"],
|
|
707
|
+
createdFiles: [targetPath],
|
|
708
|
+
skippedArtifacts: []
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
await writeFile2(
|
|
712
|
+
targetPath,
|
|
713
|
+
[
|
|
714
|
+
'cli_auth_credentials_store = "file"',
|
|
715
|
+
"",
|
|
716
|
+
"# Wrapper-owned shared Codex home for codexes.",
|
|
717
|
+
"# Add MCP and trust configuration here as needed.",
|
|
718
|
+
""
|
|
719
|
+
].join("\n"),
|
|
720
|
+
"utf8"
|
|
721
|
+
);
|
|
722
|
+
input.logger.info("runtime_init.config_created", {
|
|
723
|
+
targetPath
|
|
724
|
+
});
|
|
725
|
+
return {
|
|
726
|
+
copiedArtifacts: [],
|
|
727
|
+
createdFiles: [targetPath],
|
|
728
|
+
skippedArtifacts: []
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
async function ensureSharedFileCopy(input) {
|
|
732
|
+
const targetPath = path4.join(input.sharedCodexHome, input.artifactName);
|
|
733
|
+
const sourcePath = path4.join(input.sourceCodexHome, input.artifactName);
|
|
734
|
+
if (await pathExists(targetPath)) {
|
|
735
|
+
input.logger.debug("runtime_init.file_exists", {
|
|
736
|
+
artifactName: input.artifactName,
|
|
737
|
+
targetPath
|
|
738
|
+
});
|
|
739
|
+
return emptyArtifactResult();
|
|
740
|
+
}
|
|
741
|
+
if (!await pathExists(sourcePath) || samePath(sourcePath, targetPath)) {
|
|
742
|
+
input.logger.debug("runtime_init.file_skip", {
|
|
743
|
+
artifactName: input.artifactName,
|
|
744
|
+
sourcePath,
|
|
745
|
+
targetPath,
|
|
746
|
+
reason: "source_missing_or_same_path"
|
|
747
|
+
});
|
|
748
|
+
return {
|
|
749
|
+
copiedArtifacts: [],
|
|
750
|
+
createdFiles: [],
|
|
751
|
+
skippedArtifacts: [
|
|
752
|
+
{
|
|
753
|
+
path: input.artifactName,
|
|
754
|
+
reason: "source_missing_or_same_path"
|
|
755
|
+
}
|
|
756
|
+
]
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
await copyFile(sourcePath, targetPath);
|
|
760
|
+
input.logger.info("runtime_init.file_copied", {
|
|
761
|
+
artifactName: input.artifactName,
|
|
762
|
+
sourcePath,
|
|
763
|
+
targetPath
|
|
764
|
+
});
|
|
765
|
+
return {
|
|
766
|
+
copiedArtifacts: [input.artifactName],
|
|
767
|
+
createdFiles: [targetPath],
|
|
768
|
+
skippedArtifacts: []
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
async function ensureSharedDirectoryCopy(input) {
|
|
772
|
+
const targetPath = path4.join(input.sharedCodexHome, input.artifactName);
|
|
773
|
+
const sourcePath = path4.join(input.sourceCodexHome, input.artifactName);
|
|
774
|
+
if (await pathExists(targetPath)) {
|
|
775
|
+
input.logger.debug("runtime_init.directory_artifact_exists", {
|
|
776
|
+
artifactName: input.artifactName,
|
|
777
|
+
targetPath
|
|
778
|
+
});
|
|
779
|
+
return emptyArtifactResult();
|
|
780
|
+
}
|
|
781
|
+
if (!await pathExists(sourcePath) || samePath(sourcePath, targetPath)) {
|
|
782
|
+
input.logger.debug("runtime_init.directory_artifact_skip", {
|
|
783
|
+
artifactName: input.artifactName,
|
|
784
|
+
sourcePath,
|
|
785
|
+
targetPath,
|
|
786
|
+
reason: "source_missing_or_same_path"
|
|
787
|
+
});
|
|
788
|
+
return {
|
|
789
|
+
copiedArtifacts: [],
|
|
790
|
+
createdFiles: [],
|
|
791
|
+
skippedArtifacts: [
|
|
792
|
+
{
|
|
793
|
+
path: input.artifactName,
|
|
794
|
+
reason: "source_missing_or_same_path"
|
|
795
|
+
}
|
|
796
|
+
]
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
await cp(sourcePath, targetPath, { recursive: true });
|
|
800
|
+
input.logger.info("runtime_init.directory_artifact_copied", {
|
|
801
|
+
artifactName: input.artifactName,
|
|
802
|
+
sourcePath,
|
|
803
|
+
targetPath
|
|
804
|
+
});
|
|
805
|
+
return {
|
|
806
|
+
copiedArtifacts: [input.artifactName],
|
|
807
|
+
createdFiles: [],
|
|
808
|
+
skippedArtifacts: []
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function detectCredentialStoreMode2(rawConfig) {
|
|
812
|
+
const match = rawConfig.match(/^\s*cli_auth_credentials_store\s*=\s*"([^"]+)"/m);
|
|
813
|
+
return match?.[1]?.trim().toLowerCase() ?? "missing";
|
|
814
|
+
}
|
|
815
|
+
function pinFileCredentialStore(rawConfig) {
|
|
816
|
+
if (/^\s*cli_auth_credentials_store\s*=\s*"file"/m.test(rawConfig)) {
|
|
817
|
+
return rawConfig;
|
|
818
|
+
}
|
|
819
|
+
if (/^\s*cli_auth_credentials_store\s*=\s*"[^"]+"/m.test(rawConfig)) {
|
|
820
|
+
return rawConfig.replace(
|
|
821
|
+
/^\s*cli_auth_credentials_store\s*=\s*"[^"]+"/m,
|
|
822
|
+
'cli_auth_credentials_store = "file"'
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
const trimmed = rawConfig.trim();
|
|
826
|
+
if (!trimmed) {
|
|
827
|
+
return 'cli_auth_credentials_store = "file"\n';
|
|
828
|
+
}
|
|
829
|
+
return ['cli_auth_credentials_store = "file"', "", trimmed, ""].join("\n");
|
|
830
|
+
}
|
|
831
|
+
function samePath(left, right) {
|
|
832
|
+
return path4.resolve(left) === path4.resolve(right);
|
|
833
|
+
}
|
|
834
|
+
async function pathExists(targetPath) {
|
|
835
|
+
try {
|
|
836
|
+
await stat3(targetPath);
|
|
837
|
+
return true;
|
|
838
|
+
} catch (error) {
|
|
839
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
throw error;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function emptyArtifactResult() {
|
|
846
|
+
return {
|
|
847
|
+
copiedArtifacts: [],
|
|
848
|
+
createdFiles: [],
|
|
849
|
+
skippedArtifacts: []
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/core/context.ts
|
|
854
|
+
async function buildAppContext(argv, io) {
|
|
855
|
+
const logLevel = resolveLogLevel(io.env.LOG_LEVEL);
|
|
856
|
+
const sink = createLogSink(io.stderr);
|
|
857
|
+
const paths = resolvePaths(io.cwd, io.env);
|
|
858
|
+
const runtimeInitialization = await initializeRuntimeEnvironment({
|
|
859
|
+
env: io.env,
|
|
860
|
+
logger: createLogger({
|
|
861
|
+
level: logLevel,
|
|
862
|
+
name: "runtime",
|
|
863
|
+
sink
|
|
864
|
+
}),
|
|
865
|
+
paths
|
|
866
|
+
});
|
|
867
|
+
const wrapperConfig = await resolveWrapperConfig({
|
|
868
|
+
env: io.env,
|
|
869
|
+
logger: createLogger({
|
|
870
|
+
level: logLevel,
|
|
871
|
+
name: "config",
|
|
872
|
+
sink
|
|
873
|
+
}),
|
|
874
|
+
paths
|
|
875
|
+
});
|
|
876
|
+
const codexBinary = await findCodexBinary({
|
|
877
|
+
env: io.env,
|
|
878
|
+
logger: createLogger({
|
|
879
|
+
level: logLevel,
|
|
880
|
+
name: "process",
|
|
881
|
+
sink
|
|
882
|
+
}),
|
|
883
|
+
wrapperExecutablePath: io.executablePath
|
|
884
|
+
});
|
|
885
|
+
createLogger({
|
|
886
|
+
level: logLevel,
|
|
887
|
+
name: "context",
|
|
888
|
+
sink
|
|
889
|
+
}).debug("initialized", {
|
|
890
|
+
argv,
|
|
891
|
+
cwd: io.cwd,
|
|
892
|
+
logLevel,
|
|
893
|
+
paths,
|
|
894
|
+
runtimeInitialization,
|
|
895
|
+
wrapperConfig,
|
|
896
|
+
codexBinary
|
|
897
|
+
});
|
|
898
|
+
return {
|
|
899
|
+
argv,
|
|
900
|
+
executablePath: io.executablePath,
|
|
901
|
+
environment: {
|
|
902
|
+
cwd: io.cwd,
|
|
903
|
+
platform: process.platform,
|
|
904
|
+
runtime: process.version
|
|
905
|
+
},
|
|
906
|
+
io: {
|
|
907
|
+
stdout: io.stdout,
|
|
908
|
+
stderr: io.stderr
|
|
909
|
+
},
|
|
910
|
+
logging: {
|
|
911
|
+
level: logLevel,
|
|
912
|
+
sink
|
|
913
|
+
},
|
|
914
|
+
paths,
|
|
915
|
+
runtimeInitialization,
|
|
916
|
+
wrapperConfig,
|
|
917
|
+
codexBinary
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/runtime/runtime-contract.ts
|
|
922
|
+
import path5 from "node:path";
|
|
923
|
+
function createRuntimeContract(input) {
|
|
924
|
+
const contract = {
|
|
925
|
+
credentialStoreMode: input.credentialStoreMode,
|
|
926
|
+
sharedCodexHome: input.sharedCodexHome,
|
|
927
|
+
runtimeRoot: input.runtimeRoot,
|
|
928
|
+
perAccountRoot: input.accountRoot,
|
|
929
|
+
supported: input.credentialStoreMode === "file",
|
|
930
|
+
fileRules: [
|
|
931
|
+
createRule("config.toml", "shared", "never", "Shared CLI behavior and MCP config should remain common."),
|
|
932
|
+
createRule("mcp.json", "shared", "never", "MCP topology is shared across accounts."),
|
|
933
|
+
createRule("trust/**", "shared", "never", "Trust metadata should not be overwritten per account."),
|
|
934
|
+
createRule(
|
|
935
|
+
"auth.json",
|
|
936
|
+
"account",
|
|
937
|
+
"if-changed",
|
|
938
|
+
"Windows probe evidence shows auth.json is sufficient for `codex login status`, so it belongs to the active account profile."
|
|
939
|
+
),
|
|
940
|
+
createRule(
|
|
941
|
+
"sessions/**",
|
|
942
|
+
"account",
|
|
943
|
+
"if-changed",
|
|
944
|
+
"Session refresh artifacts are still treated as account-scoped until a real token-refresh probe proves otherwise."
|
|
945
|
+
),
|
|
946
|
+
createRule("cache/**", "ephemeral", "never", "Transient caches should be recreated instead of copied."),
|
|
947
|
+
createRule("logs/**", "ephemeral", "never", "Runtime logs are diagnostic and should not sync back."),
|
|
948
|
+
createRule("history.jsonl", "ephemeral", "never", "Conversation history should stay local to each runtime session."),
|
|
949
|
+
createRule("models_cache.json", "ephemeral", "never", "Model cache data can be rebuilt and should not drive account switching."),
|
|
950
|
+
createRule("tmp/**", "ephemeral", "never", "Temporary files should be discarded after each run."),
|
|
951
|
+
createRule(
|
|
952
|
+
"state_*.sqlite*",
|
|
953
|
+
"protected",
|
|
954
|
+
"never",
|
|
955
|
+
"Observed SQLite runtime state exists in the live Codex home and remains unproven for cross-account merge or sync-back."
|
|
956
|
+
),
|
|
957
|
+
createRule(
|
|
958
|
+
"logs_*.sqlite*",
|
|
959
|
+
"protected",
|
|
960
|
+
"never",
|
|
961
|
+
"SQLite-backed log databases should stay isolated until a dedicated write-heavy probe proves they are safe to discard or share."
|
|
962
|
+
),
|
|
963
|
+
createRule("keyring/**", "protected", "never", "External credential stores are explicitly unsupported for MVP.")
|
|
964
|
+
],
|
|
965
|
+
syncBackStrategy: {
|
|
966
|
+
whenChildProcessSucceeds: "Compare allowed account-classified files and sync only changed files back to the owning account profile.",
|
|
967
|
+
whenChildProcessFails: "Restore pre-launch runtime state and avoid syncing ambiguous mutations unless the failure is known-safe.",
|
|
968
|
+
compareStrategy: "Use file existence, modified time, and content hash checks before any sync-back write."
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
input.logger.info("runtime.contract_created", {
|
|
972
|
+
sharedCodexHome: contract.sharedCodexHome,
|
|
973
|
+
runtimeRoot: contract.runtimeRoot,
|
|
974
|
+
perAccountRoot: contract.perAccountRoot,
|
|
975
|
+
credentialStoreMode: contract.credentialStoreMode,
|
|
976
|
+
supported: contract.supported
|
|
977
|
+
});
|
|
978
|
+
input.logger.debug("runtime.file_rules", {
|
|
979
|
+
fileRules: contract.fileRules,
|
|
980
|
+
syncBackStrategy: contract.syncBackStrategy
|
|
981
|
+
});
|
|
982
|
+
return contract;
|
|
983
|
+
}
|
|
984
|
+
function resolveAccountRuntimePaths(contract, accountId) {
|
|
985
|
+
const accountDirectory = path5.join(contract.perAccountRoot, accountId);
|
|
986
|
+
return {
|
|
987
|
+
accountDirectory,
|
|
988
|
+
accountStateDirectory: path5.join(accountDirectory, "state"),
|
|
989
|
+
accountMetadataFile: path5.join(accountDirectory, "account.json"),
|
|
990
|
+
runtimeBackupDirectory: path5.join(contract.runtimeRoot, "backups", accountId),
|
|
991
|
+
runtimeTempDirectory: path5.join(contract.runtimeRoot, "tmp", accountId)
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function summarizeRuntimeContract(contract) {
|
|
995
|
+
return {
|
|
996
|
+
supported: contract.supported,
|
|
997
|
+
credentialStoreMode: contract.credentialStoreMode,
|
|
998
|
+
sharedCodexHome: contract.sharedCodexHome,
|
|
999
|
+
runtimeRoot: contract.runtimeRoot,
|
|
1000
|
+
perAccountRoot: contract.perAccountRoot,
|
|
1001
|
+
classifications: contract.fileRules.reduce(
|
|
1002
|
+
(accumulator, rule) => {
|
|
1003
|
+
accumulator[rule.classification] += 1;
|
|
1004
|
+
return accumulator;
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
shared: 0,
|
|
1008
|
+
account: 0,
|
|
1009
|
+
ephemeral: 0,
|
|
1010
|
+
protected: 0
|
|
1011
|
+
}
|
|
1012
|
+
)
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function createRule(pathPattern, classification, syncBack, reason) {
|
|
1016
|
+
return {
|
|
1017
|
+
pathPattern,
|
|
1018
|
+
classification,
|
|
1019
|
+
syncBack,
|
|
1020
|
+
reason
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/commands/account-add/run-account-add-command.ts
|
|
1025
|
+
import { mkdir as mkdir5, readFile as readFile5, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
|
|
1026
|
+
import path8 from "node:path";
|
|
1027
|
+
|
|
1028
|
+
// src/runtime/login-workspace.ts
|
|
1029
|
+
import { copyFile as copyFile2, cp as cp2, mkdir as mkdir4, mkdtemp, readFile as readFile4, stat as stat4, writeFile as writeFile3 } from "node:fs/promises";
|
|
1030
|
+
import path6 from "node:path";
|
|
1031
|
+
async function prepareLoginWorkspace(input) {
|
|
1032
|
+
const workspaceParent = path6.join(input.runtimeRoot, "tmp");
|
|
1033
|
+
await mkdir4(workspaceParent, { recursive: true });
|
|
1034
|
+
const workspaceRoot = await mkdtemp(path6.join(workspaceParent, "account-add-"));
|
|
1035
|
+
const codexHome = path6.join(workspaceRoot, "codex-home");
|
|
1036
|
+
await mkdir4(codexHome, { recursive: true });
|
|
1037
|
+
input.logger.info("login_workspace.created", {
|
|
1038
|
+
workspaceRoot,
|
|
1039
|
+
codexHome,
|
|
1040
|
+
sharedCodexHome: input.sharedCodexHome
|
|
1041
|
+
});
|
|
1042
|
+
await ensurePinnedFileConfig({
|
|
1043
|
+
codexHome,
|
|
1044
|
+
logger: input.logger,
|
|
1045
|
+
sharedCodexHome: input.sharedCodexHome
|
|
1046
|
+
});
|
|
1047
|
+
await copySharedArtifactIfPresent({
|
|
1048
|
+
artifactName: "mcp.json",
|
|
1049
|
+
codexHome,
|
|
1050
|
+
logger: input.logger,
|
|
1051
|
+
sharedCodexHome: input.sharedCodexHome
|
|
1052
|
+
});
|
|
1053
|
+
await copySharedDirectoryIfPresent({
|
|
1054
|
+
artifactName: "trust",
|
|
1055
|
+
codexHome,
|
|
1056
|
+
logger: input.logger,
|
|
1057
|
+
sharedCodexHome: input.sharedCodexHome
|
|
1058
|
+
});
|
|
1059
|
+
await mkdir4(path6.join(codexHome, "sessions"), { recursive: true });
|
|
1060
|
+
return { workspaceRoot, codexHome };
|
|
1061
|
+
}
|
|
1062
|
+
async function copyAccountScopedAuthArtifacts(input) {
|
|
1063
|
+
const accountRules = input.runtimeContract.fileRules.filter(
|
|
1064
|
+
(rule) => rule.classification === "account"
|
|
1065
|
+
);
|
|
1066
|
+
input.logger.info("login_workspace.copy_account_artifacts.start", {
|
|
1067
|
+
accountId: input.accountId,
|
|
1068
|
+
destinationRoot: input.destinationRoot,
|
|
1069
|
+
sourceCodexHome: input.sourceCodexHome,
|
|
1070
|
+
allowedPatterns: accountRules.map((rule) => rule.pathPattern)
|
|
1071
|
+
});
|
|
1072
|
+
await mkdir4(input.destinationRoot, { recursive: true });
|
|
1073
|
+
for (const rule of accountRules) {
|
|
1074
|
+
await copyRuleArtifacts({
|
|
1075
|
+
destinationRoot: input.destinationRoot,
|
|
1076
|
+
logger: input.logger,
|
|
1077
|
+
rule,
|
|
1078
|
+
sourceCodexHome: input.sourceCodexHome
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
input.logger.info("login_workspace.copy_account_artifacts.complete", {
|
|
1082
|
+
accountId: input.accountId,
|
|
1083
|
+
destinationRoot: input.destinationRoot
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
async function ensurePinnedFileConfig(input) {
|
|
1087
|
+
const sourceConfigPath = path6.join(input.sharedCodexHome, "config.toml");
|
|
1088
|
+
const targetConfigPath = path6.join(input.codexHome, "config.toml");
|
|
1089
|
+
const configContents = await readFile4(sourceConfigPath, "utf8").catch(() => "");
|
|
1090
|
+
const pinnedConfig = pinFileCredentialStore2(configContents);
|
|
1091
|
+
await writeFile3(targetConfigPath, pinnedConfig, "utf8");
|
|
1092
|
+
input.logger.info("login_workspace.config_prepared", {
|
|
1093
|
+
sourceConfigPath,
|
|
1094
|
+
targetConfigPath,
|
|
1095
|
+
credentialStoreMode: "file"
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
async function copySharedArtifactIfPresent(input) {
|
|
1099
|
+
const sourcePath = path6.join(input.sharedCodexHome, input.artifactName);
|
|
1100
|
+
const targetPath = path6.join(input.codexHome, input.artifactName);
|
|
1101
|
+
if (!await pathExists2(sourcePath)) {
|
|
1102
|
+
input.logger.debug("login_workspace.shared_file_skipped", {
|
|
1103
|
+
artifactName: input.artifactName,
|
|
1104
|
+
sourcePath,
|
|
1105
|
+
reason: "missing"
|
|
1106
|
+
});
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
await copyFile2(sourcePath, targetPath);
|
|
1110
|
+
input.logger.debug("login_workspace.shared_file_copied", {
|
|
1111
|
+
artifactName: input.artifactName,
|
|
1112
|
+
sourcePath,
|
|
1113
|
+
targetPath
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
async function copySharedDirectoryIfPresent(input) {
|
|
1117
|
+
const sourcePath = path6.join(input.sharedCodexHome, input.artifactName);
|
|
1118
|
+
const targetPath = path6.join(input.codexHome, input.artifactName);
|
|
1119
|
+
if (!await pathExists2(sourcePath)) {
|
|
1120
|
+
input.logger.debug("login_workspace.shared_directory_skipped", {
|
|
1121
|
+
artifactName: input.artifactName,
|
|
1122
|
+
sourcePath,
|
|
1123
|
+
reason: "missing"
|
|
1124
|
+
});
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
await cp2(sourcePath, targetPath, { recursive: true });
|
|
1128
|
+
input.logger.debug("login_workspace.shared_directory_copied", {
|
|
1129
|
+
artifactName: input.artifactName,
|
|
1130
|
+
sourcePath,
|
|
1131
|
+
targetPath
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
async function copyRuleArtifacts(input) {
|
|
1135
|
+
if (input.rule.pathPattern.endsWith("/**")) {
|
|
1136
|
+
const relativeDirectory = input.rule.pathPattern.slice(0, -3);
|
|
1137
|
+
const sourceDirectory = path6.join(input.sourceCodexHome, relativeDirectory);
|
|
1138
|
+
const targetDirectory = path6.join(input.destinationRoot, relativeDirectory);
|
|
1139
|
+
if (!await pathExists2(sourceDirectory)) {
|
|
1140
|
+
input.logger.debug("login_workspace.account_directory_skipped", {
|
|
1141
|
+
pathPattern: input.rule.pathPattern,
|
|
1142
|
+
sourceDirectory,
|
|
1143
|
+
reason: "missing"
|
|
1144
|
+
});
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
await cp2(sourceDirectory, targetDirectory, { recursive: true });
|
|
1148
|
+
input.logger.debug("login_workspace.account_directory_copied", {
|
|
1149
|
+
pathPattern: input.rule.pathPattern,
|
|
1150
|
+
sourceDirectory,
|
|
1151
|
+
targetDirectory
|
|
1152
|
+
});
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
const sourceFile = path6.join(input.sourceCodexHome, input.rule.pathPattern);
|
|
1156
|
+
const targetFile = path6.join(input.destinationRoot, input.rule.pathPattern);
|
|
1157
|
+
if (!await pathExists2(sourceFile)) {
|
|
1158
|
+
input.logger.debug("login_workspace.account_file_skipped", {
|
|
1159
|
+
pathPattern: input.rule.pathPattern,
|
|
1160
|
+
sourceFile,
|
|
1161
|
+
reason: "missing"
|
|
1162
|
+
});
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
await mkdir4(path6.dirname(targetFile), { recursive: true });
|
|
1166
|
+
await copyFile2(sourceFile, targetFile);
|
|
1167
|
+
input.logger.debug("login_workspace.account_file_copied", {
|
|
1168
|
+
pathPattern: input.rule.pathPattern,
|
|
1169
|
+
sourceFile,
|
|
1170
|
+
targetFile
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
function pinFileCredentialStore2(rawConfig) {
|
|
1174
|
+
if (/^\s*cli_auth_credentials_store\s*=\s*"file"/m.test(rawConfig)) {
|
|
1175
|
+
return rawConfig;
|
|
1176
|
+
}
|
|
1177
|
+
if (/^\s*cli_auth_credentials_store\s*=\s*"[^"]+"/m.test(rawConfig)) {
|
|
1178
|
+
return rawConfig.replace(
|
|
1179
|
+
/^\s*cli_auth_credentials_store\s*=\s*"[^"]+"/m,
|
|
1180
|
+
'cli_auth_credentials_store = "file"'
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
const trimmed = rawConfig.trim();
|
|
1184
|
+
if (!trimmed) {
|
|
1185
|
+
return 'cli_auth_credentials_store = "file"\n';
|
|
1186
|
+
}
|
|
1187
|
+
return ['cli_auth_credentials_store = "file"', "", trimmed, ""].join("\n");
|
|
1188
|
+
}
|
|
1189
|
+
async function pathExists2(targetPath) {
|
|
1190
|
+
try {
|
|
1191
|
+
await stat4(targetPath);
|
|
1192
|
+
return true;
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
throw error;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// src/process/run-codex-login.ts
|
|
1202
|
+
import { spawn } from "node:child_process";
|
|
1203
|
+
|
|
1204
|
+
// src/process/codex-launch-spec.ts
|
|
1205
|
+
import { access as access2 } from "node:fs/promises";
|
|
1206
|
+
import path7 from "node:path";
|
|
1207
|
+
async function resolveCodexLaunchSpec(codexBinaryPath, args) {
|
|
1208
|
+
if (process.platform !== "win32") {
|
|
1209
|
+
return {
|
|
1210
|
+
command: codexBinaryPath,
|
|
1211
|
+
args
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
const npmShim = await resolveNpmCodexShim(codexBinaryPath);
|
|
1215
|
+
if (npmShim) {
|
|
1216
|
+
return {
|
|
1217
|
+
command: npmShim.nodeBinary,
|
|
1218
|
+
args: [npmShim.codexScript, ...args]
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
if (/\.(cmd|bat)$/i.test(codexBinaryPath)) {
|
|
1222
|
+
return {
|
|
1223
|
+
command: process.env.ComSpec ?? "cmd.exe",
|
|
1224
|
+
args: ["/d", "/s", "/c", buildCmdInvocation(codexBinaryPath, args)]
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
if (/\.ps1$/i.test(codexBinaryPath)) {
|
|
1228
|
+
return {
|
|
1229
|
+
command: process.env.ComSpec ? "powershell.exe" : "pwsh",
|
|
1230
|
+
args: [
|
|
1231
|
+
"-NoProfile",
|
|
1232
|
+
"-ExecutionPolicy",
|
|
1233
|
+
"Bypass",
|
|
1234
|
+
"-File",
|
|
1235
|
+
codexBinaryPath,
|
|
1236
|
+
...args
|
|
1237
|
+
]
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
command: codexBinaryPath,
|
|
1242
|
+
args
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
function buildCmdInvocation(binaryPath, args) {
|
|
1246
|
+
return [quoteForCmd(binaryPath), ...args.map(quoteForCmd)].join(" ");
|
|
1247
|
+
}
|
|
1248
|
+
function quoteForCmd(value) {
|
|
1249
|
+
if (value.length === 0) {
|
|
1250
|
+
return '""';
|
|
1251
|
+
}
|
|
1252
|
+
if (!/[\s"]/u.test(value)) {
|
|
1253
|
+
return value;
|
|
1254
|
+
}
|
|
1255
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
1256
|
+
}
|
|
1257
|
+
async function resolveNpmCodexShim(codexBinaryPath) {
|
|
1258
|
+
const basename = path7.basename(codexBinaryPath).toLowerCase();
|
|
1259
|
+
if (!["codex", "codex.cmd", "codex.bat", "codex.ps1"].includes(basename)) {
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
const directory = path7.dirname(codexBinaryPath);
|
|
1263
|
+
const codexScript = path7.join(directory, "node_modules", "@openai", "codex", "bin", "codex.js");
|
|
1264
|
+
if (!await pathExists3(codexScript)) {
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
const bundledNode = path7.join(directory, "node.exe");
|
|
1268
|
+
return {
|
|
1269
|
+
codexScript,
|
|
1270
|
+
nodeBinary: await pathExists3(bundledNode) ? bundledNode : "node"
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
async function pathExists3(filePath) {
|
|
1274
|
+
try {
|
|
1275
|
+
await access2(filePath);
|
|
1276
|
+
return true;
|
|
1277
|
+
} catch {
|
|
1278
|
+
return false;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// src/process/run-codex-login.ts
|
|
1283
|
+
async function runInteractiveCodexLogin(input) {
|
|
1284
|
+
const launchSpec = await resolveCodexLaunchSpec(input.codexBinaryPath, ["login"]);
|
|
1285
|
+
input.logger.info("login.spawn.start", {
|
|
1286
|
+
codexBinaryPath: input.codexBinaryPath,
|
|
1287
|
+
resolvedCommand: launchSpec.command,
|
|
1288
|
+
codexHome: input.codexHome,
|
|
1289
|
+
argv: launchSpec.args,
|
|
1290
|
+
timeoutMs: input.timeoutMs
|
|
1291
|
+
});
|
|
1292
|
+
return new Promise((resolve, reject) => {
|
|
1293
|
+
const child = spawn(launchSpec.command, launchSpec.args, {
|
|
1294
|
+
env: {
|
|
1295
|
+
...process.env,
|
|
1296
|
+
CODEX_HOME: input.codexHome
|
|
1297
|
+
},
|
|
1298
|
+
shell: false,
|
|
1299
|
+
stdio: "inherit",
|
|
1300
|
+
windowsHide: false
|
|
1301
|
+
});
|
|
1302
|
+
let timedOut = false;
|
|
1303
|
+
let cancelledBySignal = false;
|
|
1304
|
+
let settled = false;
|
|
1305
|
+
const timeoutHandle = setTimeout(() => {
|
|
1306
|
+
timedOut = true;
|
|
1307
|
+
input.logger.warn("login.spawn.timeout", {
|
|
1308
|
+
codexBinaryPath: input.codexBinaryPath,
|
|
1309
|
+
codexHome: input.codexHome,
|
|
1310
|
+
timeoutMs: input.timeoutMs
|
|
1311
|
+
});
|
|
1312
|
+
terminateChild(child);
|
|
1313
|
+
}, input.timeoutMs);
|
|
1314
|
+
const forwardSignal = (signal) => {
|
|
1315
|
+
cancelledBySignal = true;
|
|
1316
|
+
input.logger.warn("login.spawn.parent_signal", {
|
|
1317
|
+
signal,
|
|
1318
|
+
pid: child.pid ?? null
|
|
1319
|
+
});
|
|
1320
|
+
child.kill(signal);
|
|
1321
|
+
};
|
|
1322
|
+
const signalHandlers = {
|
|
1323
|
+
SIGINT: () => forwardSignal("SIGINT"),
|
|
1324
|
+
SIGTERM: () => forwardSignal("SIGTERM")
|
|
1325
|
+
};
|
|
1326
|
+
process.on("SIGINT", signalHandlers.SIGINT);
|
|
1327
|
+
process.on("SIGTERM", signalHandlers.SIGTERM);
|
|
1328
|
+
const cleanup = () => {
|
|
1329
|
+
clearTimeout(timeoutHandle);
|
|
1330
|
+
process.off("SIGINT", signalHandlers.SIGINT);
|
|
1331
|
+
process.off("SIGTERM", signalHandlers.SIGTERM);
|
|
1332
|
+
};
|
|
1333
|
+
child.on("error", (error) => {
|
|
1334
|
+
if (settled) {
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
settled = true;
|
|
1338
|
+
cleanup();
|
|
1339
|
+
input.logger.error("login.spawn.error", {
|
|
1340
|
+
codexBinaryPath: input.codexBinaryPath,
|
|
1341
|
+
codexHome: input.codexHome,
|
|
1342
|
+
message: error.message
|
|
1343
|
+
});
|
|
1344
|
+
reject(error);
|
|
1345
|
+
});
|
|
1346
|
+
child.on("exit", (exitCode2, signal) => {
|
|
1347
|
+
if (settled) {
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
settled = true;
|
|
1351
|
+
cleanup();
|
|
1352
|
+
input.logger.info("login.spawn.complete", {
|
|
1353
|
+
codexBinaryPath: input.codexBinaryPath,
|
|
1354
|
+
codexHome: input.codexHome,
|
|
1355
|
+
exitCode: exitCode2,
|
|
1356
|
+
signal,
|
|
1357
|
+
timedOut,
|
|
1358
|
+
cancelledBySignal
|
|
1359
|
+
});
|
|
1360
|
+
resolve({
|
|
1361
|
+
exitCode: exitCode2,
|
|
1362
|
+
signal,
|
|
1363
|
+
timedOut,
|
|
1364
|
+
timeoutMs: input.timeoutMs,
|
|
1365
|
+
cancelledBySignal
|
|
1366
|
+
});
|
|
1367
|
+
});
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
function terminateChild(child) {
|
|
1371
|
+
if (process.platform === "win32") {
|
|
1372
|
+
child.kill();
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
child.kill("SIGTERM");
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// src/commands/account-add/run-account-add-command.ts
|
|
1379
|
+
var DEFAULT_LOGIN_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1380
|
+
var ACCOUNT_METADATA_SCHEMA_VERSION = 1;
|
|
1381
|
+
async function runAccountAddCommand(context, argv) {
|
|
1382
|
+
const logger = createLogger({
|
|
1383
|
+
level: context.logging.level,
|
|
1384
|
+
name: "account_add",
|
|
1385
|
+
sink: context.logging.sink
|
|
1386
|
+
});
|
|
1387
|
+
if (argv.includes("--help")) {
|
|
1388
|
+
context.io.stdout.write(`${buildAccountAddHelpText()}
|
|
1389
|
+
`);
|
|
1390
|
+
logger.info("help.rendered");
|
|
1391
|
+
return 0;
|
|
1392
|
+
}
|
|
1393
|
+
const parsed = parseAccountAddArgs(argv);
|
|
1394
|
+
logger.info("command.start", {
|
|
1395
|
+
requestedLabel: parsed.label,
|
|
1396
|
+
timeoutMs: parsed.timeoutMs,
|
|
1397
|
+
codexBinaryPath: context.codexBinary.path,
|
|
1398
|
+
sharedCodexHome: context.paths.sharedCodexHome,
|
|
1399
|
+
accountRoot: context.paths.accountRoot,
|
|
1400
|
+
runtimeRoot: context.paths.runtimeRoot
|
|
1401
|
+
});
|
|
1402
|
+
if (!context.codexBinary.path) {
|
|
1403
|
+
logger.error("command.binary_missing", {
|
|
1404
|
+
candidates: context.codexBinary.candidates,
|
|
1405
|
+
rejectedCandidates: context.codexBinary.rejectedCandidates
|
|
1406
|
+
});
|
|
1407
|
+
throw new Error("Could not find the real `codex` binary on PATH.");
|
|
1408
|
+
}
|
|
1409
|
+
if (context.wrapperConfig.credentialStoreMode !== "file") {
|
|
1410
|
+
logger.error("command.unsupported_credential_store", {
|
|
1411
|
+
credentialStoreMode: context.wrapperConfig.credentialStoreMode,
|
|
1412
|
+
configFilePath: context.wrapperConfig.codexConfigFilePath,
|
|
1413
|
+
reason: context.wrapperConfig.credentialStorePolicyReason
|
|
1414
|
+
});
|
|
1415
|
+
throw new Error(
|
|
1416
|
+
`codexes account add requires cli_auth_credentials_store = "file"; detected ${context.wrapperConfig.credentialStoreMode}.`
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
const registry = createAccountRegistry({
|
|
1420
|
+
accountRoot: context.paths.accountRoot,
|
|
1421
|
+
logger,
|
|
1422
|
+
registryFile: context.paths.registryFile
|
|
1423
|
+
});
|
|
1424
|
+
const existingAccounts = await registry.listAccounts();
|
|
1425
|
+
const duplicate = existingAccounts.find(
|
|
1426
|
+
(account) => account.label.toLowerCase() === parsed.label.toLowerCase()
|
|
1427
|
+
);
|
|
1428
|
+
if (duplicate) {
|
|
1429
|
+
logger.warn("command.duplicate_label", {
|
|
1430
|
+
requestedLabel: parsed.label,
|
|
1431
|
+
existingAccountId: duplicate.id
|
|
1432
|
+
});
|
|
1433
|
+
throw new Error(`An account named "${parsed.label}" already exists.`);
|
|
1434
|
+
}
|
|
1435
|
+
const runtimeContract = createRuntimeContract({
|
|
1436
|
+
accountRoot: context.paths.accountRoot,
|
|
1437
|
+
credentialStoreMode: context.wrapperConfig.credentialStoreMode,
|
|
1438
|
+
logger,
|
|
1439
|
+
runtimeRoot: context.paths.runtimeRoot,
|
|
1440
|
+
sharedCodexHome: context.paths.sharedCodexHome
|
|
1441
|
+
});
|
|
1442
|
+
const workspace = await prepareLoginWorkspace({
|
|
1443
|
+
logger,
|
|
1444
|
+
runtimeRoot: context.paths.runtimeRoot,
|
|
1445
|
+
sharedCodexHome: context.paths.sharedCodexHome
|
|
1446
|
+
});
|
|
1447
|
+
let loginResult = null;
|
|
1448
|
+
try {
|
|
1449
|
+
loginResult = await runInteractiveCodexLogin({
|
|
1450
|
+
codexBinaryPath: context.codexBinary.path,
|
|
1451
|
+
codexHome: workspace.codexHome,
|
|
1452
|
+
logger,
|
|
1453
|
+
timeoutMs: parsed.timeoutMs
|
|
1454
|
+
});
|
|
1455
|
+
if (loginResult.exitCode !== 0) {
|
|
1456
|
+
logger.warn("command.login_failed", {
|
|
1457
|
+
exitCode: loginResult.exitCode,
|
|
1458
|
+
signal: loginResult.signal,
|
|
1459
|
+
timedOut: loginResult.timedOut,
|
|
1460
|
+
timeoutMs: loginResult.timeoutMs,
|
|
1461
|
+
cancelledBySignal: loginResult.cancelledBySignal
|
|
1462
|
+
});
|
|
1463
|
+
context.io.stderr.write(buildLoginFailureMessage(loginResult));
|
|
1464
|
+
return loginResult.exitCode ?? 1;
|
|
1465
|
+
}
|
|
1466
|
+
const authSummary = await readAuthSummary(workspace.codexHome, logger);
|
|
1467
|
+
if (!authSummary.present) {
|
|
1468
|
+
logger.error("command.auth_missing_after_login", {
|
|
1469
|
+
codexHome: workspace.codexHome
|
|
1470
|
+
});
|
|
1471
|
+
throw new Error(
|
|
1472
|
+
"codex login completed without creating auth.json in the isolated workspace."
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
const account = await registry.addAccount({ label: parsed.label });
|
|
1476
|
+
try {
|
|
1477
|
+
const runtimePaths = resolveAccountRuntimePaths(runtimeContract, account.id);
|
|
1478
|
+
await mkdir5(runtimePaths.accountDirectory, { recursive: true });
|
|
1479
|
+
await copyAccountScopedAuthArtifacts({
|
|
1480
|
+
accountId: account.id,
|
|
1481
|
+
destinationRoot: runtimePaths.accountStateDirectory,
|
|
1482
|
+
logger,
|
|
1483
|
+
runtimeContract,
|
|
1484
|
+
sourceCodexHome: workspace.codexHome
|
|
1485
|
+
});
|
|
1486
|
+
await writeAccountMetadata({
|
|
1487
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1488
|
+
destinationFile: runtimePaths.accountMetadataFile,
|
|
1489
|
+
label: account.label,
|
|
1490
|
+
logger,
|
|
1491
|
+
recordId: account.id,
|
|
1492
|
+
summary: authSummary
|
|
1493
|
+
});
|
|
1494
|
+
logger.info("command.complete", {
|
|
1495
|
+
accountId: account.id,
|
|
1496
|
+
label: account.label,
|
|
1497
|
+
authDirectory: account.authDirectory,
|
|
1498
|
+
authAccountId: authSummary.accountId
|
|
1499
|
+
});
|
|
1500
|
+
context.io.stdout.write(
|
|
1501
|
+
[
|
|
1502
|
+
`Added account "${account.label}"`,
|
|
1503
|
+
` id: ${account.id}`,
|
|
1504
|
+
` auth state: ${runtimePaths.accountStateDirectory}`,
|
|
1505
|
+
authSummary.accountId ? ` auth account id: ${authSummary.accountId}` : null
|
|
1506
|
+
].filter((line) => Boolean(line)).join("\n") + "\n"
|
|
1507
|
+
);
|
|
1508
|
+
return 0;
|
|
1509
|
+
} catch (error) {
|
|
1510
|
+
logger.error("command.persist_failed", {
|
|
1511
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1512
|
+
accountLabel: parsed.label
|
|
1513
|
+
});
|
|
1514
|
+
await cleanupFailedAccountRecord(registry, logger, parsed.label);
|
|
1515
|
+
throw error;
|
|
1516
|
+
}
|
|
1517
|
+
} finally {
|
|
1518
|
+
await cleanupWorkspace(workspace, logger, loginResult);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
function parseAccountAddArgs(argv) {
|
|
1522
|
+
let label = null;
|
|
1523
|
+
let timeoutMs = DEFAULT_LOGIN_TIMEOUT_MS;
|
|
1524
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1525
|
+
const token = argv[index];
|
|
1526
|
+
if (!token) {
|
|
1527
|
+
continue;
|
|
1528
|
+
}
|
|
1529
|
+
if (token === "--timeout-ms") {
|
|
1530
|
+
const next = argv[index + 1];
|
|
1531
|
+
if (!next) {
|
|
1532
|
+
throw new Error("Expected a number after --timeout-ms.");
|
|
1533
|
+
}
|
|
1534
|
+
const parsedTimeout = Number.parseInt(next, 10);
|
|
1535
|
+
if (!Number.isFinite(parsedTimeout) || parsedTimeout <= 0) {
|
|
1536
|
+
throw new Error(`Invalid timeout: ${next}`);
|
|
1537
|
+
}
|
|
1538
|
+
timeoutMs = parsedTimeout;
|
|
1539
|
+
index += 1;
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
if (token.startsWith("--timeout-ms=")) {
|
|
1543
|
+
const raw = token.slice("--timeout-ms=".length);
|
|
1544
|
+
const parsedTimeout = Number.parseInt(raw, 10);
|
|
1545
|
+
if (!Number.isFinite(parsedTimeout) || parsedTimeout <= 0) {
|
|
1546
|
+
throw new Error(`Invalid timeout: ${raw}`);
|
|
1547
|
+
}
|
|
1548
|
+
timeoutMs = parsedTimeout;
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
if (token.startsWith("--")) {
|
|
1552
|
+
throw new Error(`Unknown option for account add: ${token}`);
|
|
1553
|
+
}
|
|
1554
|
+
if (label) {
|
|
1555
|
+
throw new Error(`Unexpected argument for account add: ${token}`);
|
|
1556
|
+
}
|
|
1557
|
+
label = token.trim();
|
|
1558
|
+
}
|
|
1559
|
+
if (!label) {
|
|
1560
|
+
throw new Error(buildAccountAddHelpText());
|
|
1561
|
+
}
|
|
1562
|
+
return { label, timeoutMs };
|
|
1563
|
+
}
|
|
1564
|
+
function buildAccountAddHelpText() {
|
|
1565
|
+
return [
|
|
1566
|
+
"Usage:",
|
|
1567
|
+
" codexes account add <label> [--timeout-ms <milliseconds>]",
|
|
1568
|
+
"",
|
|
1569
|
+
"Examples:",
|
|
1570
|
+
" codexes account add work",
|
|
1571
|
+
" codexes account add personal --timeout-ms 900000"
|
|
1572
|
+
].join("\n");
|
|
1573
|
+
}
|
|
1574
|
+
async function readAuthSummary(codexHome, logger) {
|
|
1575
|
+
const authFile = path8.join(codexHome, "auth.json");
|
|
1576
|
+
try {
|
|
1577
|
+
const raw = await readFile5(authFile, "utf8");
|
|
1578
|
+
const parsed = JSON.parse(raw);
|
|
1579
|
+
const tokens = typeof parsed.tokens === "object" && parsed.tokens !== null ? parsed.tokens : null;
|
|
1580
|
+
const summary = {
|
|
1581
|
+
present: true,
|
|
1582
|
+
authMode: typeof parsed.auth_mode === "string" ? parsed.auth_mode : null,
|
|
1583
|
+
accountId: typeof tokens?.account_id === "string" ? tokens.account_id : null,
|
|
1584
|
+
lastRefresh: typeof parsed.last_refresh === "string" ? parsed.last_refresh : null
|
|
1585
|
+
};
|
|
1586
|
+
logger.debug("auth_summary.loaded", {
|
|
1587
|
+
authFile,
|
|
1588
|
+
authMode: summary.authMode,
|
|
1589
|
+
accountId: summary.accountId,
|
|
1590
|
+
lastRefresh: summary.lastRefresh
|
|
1591
|
+
});
|
|
1592
|
+
return summary;
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
1595
|
+
logger.warn("auth_summary.missing", { authFile });
|
|
1596
|
+
return {
|
|
1597
|
+
present: false,
|
|
1598
|
+
authMode: null,
|
|
1599
|
+
accountId: null,
|
|
1600
|
+
lastRefresh: null
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
logger.error("auth_summary.failed", {
|
|
1604
|
+
authFile,
|
|
1605
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1606
|
+
});
|
|
1607
|
+
throw error;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
async function writeAccountMetadata(input) {
|
|
1611
|
+
const metadata = {
|
|
1612
|
+
schemaVersion: ACCOUNT_METADATA_SCHEMA_VERSION,
|
|
1613
|
+
accountId: input.recordId,
|
|
1614
|
+
label: input.label,
|
|
1615
|
+
capturedAt: input.capturedAt,
|
|
1616
|
+
authMode: input.summary.authMode,
|
|
1617
|
+
authAccountId: input.summary.accountId,
|
|
1618
|
+
lastRefresh: input.summary.lastRefresh,
|
|
1619
|
+
loginStatus: "succeeded"
|
|
1620
|
+
};
|
|
1621
|
+
await writeFile4(input.destinationFile, JSON.stringify(metadata, null, 2), "utf8");
|
|
1622
|
+
input.logger.info("account_metadata.written", {
|
|
1623
|
+
destinationFile: input.destinationFile,
|
|
1624
|
+
accountId: metadata.accountId,
|
|
1625
|
+
authAccountId: metadata.authAccountId
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
async function cleanupFailedAccountRecord(registry, logger, label) {
|
|
1629
|
+
const accounts = await registry.listAccounts();
|
|
1630
|
+
const account = [...accounts].reverse().find((entry) => entry.label.toLowerCase() === label.toLowerCase());
|
|
1631
|
+
if (!account) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
await registry.removeAccount(account.id).catch(() => void 0);
|
|
1635
|
+
await rm2(account.authDirectory, { force: true, recursive: true }).catch(() => void 0);
|
|
1636
|
+
logger.warn("command.rollback_account_record", {
|
|
1637
|
+
accountId: account.id,
|
|
1638
|
+
label: account.label
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
async function cleanupWorkspace(workspace, logger, loginResult) {
|
|
1642
|
+
logger.debug("workspace.cleanup.start", {
|
|
1643
|
+
workspaceRoot: workspace.workspaceRoot,
|
|
1644
|
+
codexHome: workspace.codexHome,
|
|
1645
|
+
loginExitCode: loginResult?.exitCode ?? null
|
|
1646
|
+
});
|
|
1647
|
+
await rm2(workspace.workspaceRoot, {
|
|
1648
|
+
force: true,
|
|
1649
|
+
recursive: true
|
|
1650
|
+
}).catch(() => void 0);
|
|
1651
|
+
logger.debug("workspace.cleanup.complete", {
|
|
1652
|
+
workspaceRoot: workspace.workspaceRoot
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
function buildLoginFailureMessage(loginResult) {
|
|
1656
|
+
if (loginResult.timedOut) {
|
|
1657
|
+
return `codexes: account login timed out after ${loginResult.timeoutMs}ms.
|
|
1658
|
+
`;
|
|
1659
|
+
}
|
|
1660
|
+
if (loginResult.cancelledBySignal) {
|
|
1661
|
+
return "codexes: account login was cancelled.\n";
|
|
1662
|
+
}
|
|
1663
|
+
return `codexes: account login failed with exit code ${loginResult.exitCode ?? "unknown"}.
|
|
1664
|
+
`;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// src/accounts/account-resolution.ts
|
|
1668
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
1669
|
+
import path9 from "node:path";
|
|
1670
|
+
function resolveAccountBySelector(input) {
|
|
1671
|
+
const normalizedSelector = input.selector.trim();
|
|
1672
|
+
const matches = input.accounts.filter(
|
|
1673
|
+
(account) => account.id === normalizedSelector || account.label.toLowerCase() === normalizedSelector.toLowerCase()
|
|
1674
|
+
);
|
|
1675
|
+
input.logger.debug("account_resolution.lookup", {
|
|
1676
|
+
selector: normalizedSelector,
|
|
1677
|
+
accountCount: input.accounts.length,
|
|
1678
|
+
matchCount: matches.length,
|
|
1679
|
+
matchedAccountIds: matches.map((account) => account.id)
|
|
1680
|
+
});
|
|
1681
|
+
if (matches.length === 0) {
|
|
1682
|
+
throw new Error(`No account matches "${normalizedSelector}".`);
|
|
1683
|
+
}
|
|
1684
|
+
if (matches.length > 1) {
|
|
1685
|
+
throw new Error(
|
|
1686
|
+
`Selector "${normalizedSelector}" matched multiple accounts; use the account id instead.`
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
const [match] = matches;
|
|
1690
|
+
if (!match) {
|
|
1691
|
+
throw new Error(`No account matches "${normalizedSelector}".`);
|
|
1692
|
+
}
|
|
1693
|
+
return match;
|
|
1694
|
+
}
|
|
1695
|
+
async function buildAccountPresentations(input) {
|
|
1696
|
+
const presentations = [];
|
|
1697
|
+
for (const account of input.accounts) {
|
|
1698
|
+
presentations.push({
|
|
1699
|
+
account,
|
|
1700
|
+
...await readAccountMetadataSummary(account, input.logger)
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
return presentations;
|
|
1704
|
+
}
|
|
1705
|
+
async function readAccountMetadataSummary(account, logger) {
|
|
1706
|
+
const metadataFile = path9.join(account.authDirectory, "account.json");
|
|
1707
|
+
try {
|
|
1708
|
+
const raw = await readFile6(metadataFile, "utf8");
|
|
1709
|
+
const parsed = JSON.parse(raw);
|
|
1710
|
+
const summary = {
|
|
1711
|
+
authAccountId: typeof parsed.authAccountId === "string" ? parsed.authAccountId : null,
|
|
1712
|
+
authMode: typeof parsed.authMode === "string" ? parsed.authMode : null
|
|
1713
|
+
};
|
|
1714
|
+
logger.debug("account_resolution.metadata_loaded", {
|
|
1715
|
+
accountId: account.id,
|
|
1716
|
+
metadataFile,
|
|
1717
|
+
authAccountId: summary.authAccountId,
|
|
1718
|
+
authMode: summary.authMode
|
|
1719
|
+
});
|
|
1720
|
+
return summary;
|
|
1721
|
+
} catch (error) {
|
|
1722
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
1723
|
+
logger.debug("account_resolution.metadata_missing", {
|
|
1724
|
+
accountId: account.id,
|
|
1725
|
+
metadataFile
|
|
1726
|
+
});
|
|
1727
|
+
return { authAccountId: null, authMode: null };
|
|
1728
|
+
}
|
|
1729
|
+
logger.warn("account_resolution.metadata_failed", {
|
|
1730
|
+
accountId: account.id,
|
|
1731
|
+
metadataFile,
|
|
1732
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1733
|
+
});
|
|
1734
|
+
return { authAccountId: null, authMode: null };
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// src/commands/account-list/run-account-list-command.ts
|
|
1739
|
+
async function runAccountListCommand(context) {
|
|
1740
|
+
const logger = createLogger({
|
|
1741
|
+
level: context.logging.level,
|
|
1742
|
+
name: "account_list",
|
|
1743
|
+
sink: context.logging.sink
|
|
1744
|
+
});
|
|
1745
|
+
const registry = createAccountRegistry({
|
|
1746
|
+
accountRoot: context.paths.accountRoot,
|
|
1747
|
+
logger,
|
|
1748
|
+
registryFile: context.paths.registryFile
|
|
1749
|
+
});
|
|
1750
|
+
const [accounts, defaultAccount] = await Promise.all([
|
|
1751
|
+
registry.listAccounts(),
|
|
1752
|
+
registry.getDefaultAccount()
|
|
1753
|
+
]);
|
|
1754
|
+
logger.info("command.start", {
|
|
1755
|
+
accountCount: accounts.length,
|
|
1756
|
+
defaultAccountId: defaultAccount?.id ?? null
|
|
1757
|
+
});
|
|
1758
|
+
if (accounts.length === 0) {
|
|
1759
|
+
context.io.stdout.write(
|
|
1760
|
+
[
|
|
1761
|
+
"No accounts configured.",
|
|
1762
|
+
"Add one with: codexes account add <label>"
|
|
1763
|
+
].join("\n") + "\n"
|
|
1764
|
+
);
|
|
1765
|
+
logger.info("command.empty");
|
|
1766
|
+
return 0;
|
|
1767
|
+
}
|
|
1768
|
+
const presentations = await buildAccountPresentations({ accounts, logger });
|
|
1769
|
+
const lines = presentations.map(({ account, authAccountId, authMode }) => {
|
|
1770
|
+
const markers = [
|
|
1771
|
+
defaultAccount?.id === account.id ? "default" : null,
|
|
1772
|
+
authMode ? `auth=${authMode}` : null,
|
|
1773
|
+
authAccountId ? `authAccountId=${authAccountId}` : null
|
|
1774
|
+
].filter((value) => Boolean(value)).join(", ");
|
|
1775
|
+
return `${defaultAccount?.id === account.id ? "*" : " "} ${account.label} (${account.id})${markers ? ` [${markers}]` : ""}`;
|
|
1776
|
+
});
|
|
1777
|
+
context.io.stdout.write(`${lines.join("\n")}
|
|
1778
|
+
`);
|
|
1779
|
+
logger.info("command.complete", {
|
|
1780
|
+
accountIds: presentations.map(({ account }) => account.id)
|
|
1781
|
+
});
|
|
1782
|
+
return 0;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// src/commands/account-remove/run-account-remove-command.ts
|
|
1786
|
+
import { rm as rm3 } from "node:fs/promises";
|
|
1787
|
+
async function runAccountRemoveCommand(context, argv) {
|
|
1788
|
+
const logger = createLogger({
|
|
1789
|
+
level: context.logging.level,
|
|
1790
|
+
name: "account_remove",
|
|
1791
|
+
sink: context.logging.sink
|
|
1792
|
+
});
|
|
1793
|
+
if (argv.includes("--help")) {
|
|
1794
|
+
context.io.stdout.write(`${buildAccountRemoveHelpText()}
|
|
1795
|
+
`);
|
|
1796
|
+
logger.info("help.rendered");
|
|
1797
|
+
return 0;
|
|
1798
|
+
}
|
|
1799
|
+
const selector = argv[0]?.trim();
|
|
1800
|
+
if (!selector || argv.length > 1) {
|
|
1801
|
+
throw new Error(buildAccountRemoveHelpText());
|
|
1802
|
+
}
|
|
1803
|
+
const registry = createAccountRegistry({
|
|
1804
|
+
accountRoot: context.paths.accountRoot,
|
|
1805
|
+
logger,
|
|
1806
|
+
registryFile: context.paths.registryFile
|
|
1807
|
+
});
|
|
1808
|
+
const accounts = await registry.listAccounts();
|
|
1809
|
+
if (accounts.length === 0) {
|
|
1810
|
+
context.io.stdout.write("No accounts configured.\n");
|
|
1811
|
+
logger.info("command.empty");
|
|
1812
|
+
return 0;
|
|
1813
|
+
}
|
|
1814
|
+
const account = resolveAccountBySelector({ accounts, logger, selector });
|
|
1815
|
+
logger.info("command.start", {
|
|
1816
|
+
requestedSelector: selector,
|
|
1817
|
+
resolvedAccountId: account.id,
|
|
1818
|
+
label: account.label
|
|
1819
|
+
});
|
|
1820
|
+
await registry.removeAccount(account.id);
|
|
1821
|
+
await rm3(account.authDirectory, { force: true, recursive: true });
|
|
1822
|
+
context.io.stdout.write(`Removed account "${account.label}" (${account.id}).
|
|
1823
|
+
`);
|
|
1824
|
+
logger.info("command.complete", {
|
|
1825
|
+
requestedSelector: selector,
|
|
1826
|
+
resolvedAccountId: account.id
|
|
1827
|
+
});
|
|
1828
|
+
return 0;
|
|
1829
|
+
}
|
|
1830
|
+
function buildAccountRemoveHelpText() {
|
|
1831
|
+
return [
|
|
1832
|
+
"Usage:",
|
|
1833
|
+
" codexes account remove <account-id-or-label>"
|
|
1834
|
+
].join("\n");
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// src/commands/account-use/run-account-use-command.ts
|
|
1838
|
+
async function runAccountUseCommand(context, argv) {
|
|
1839
|
+
const logger = createLogger({
|
|
1840
|
+
level: context.logging.level,
|
|
1841
|
+
name: "account_use",
|
|
1842
|
+
sink: context.logging.sink
|
|
1843
|
+
});
|
|
1844
|
+
if (argv.includes("--help")) {
|
|
1845
|
+
context.io.stdout.write(`${buildAccountUseHelpText()}
|
|
1846
|
+
`);
|
|
1847
|
+
logger.info("help.rendered");
|
|
1848
|
+
return 0;
|
|
1849
|
+
}
|
|
1850
|
+
const registry = createAccountRegistry({
|
|
1851
|
+
accountRoot: context.paths.accountRoot,
|
|
1852
|
+
logger,
|
|
1853
|
+
registryFile: context.paths.registryFile
|
|
1854
|
+
});
|
|
1855
|
+
const accounts = await registry.listAccounts();
|
|
1856
|
+
if (accounts.length === 0) {
|
|
1857
|
+
context.io.stdout.write(
|
|
1858
|
+
[
|
|
1859
|
+
"No accounts configured.",
|
|
1860
|
+
"Add one with: codexes account add <label>"
|
|
1861
|
+
].join("\n") + "\n"
|
|
1862
|
+
);
|
|
1863
|
+
logger.info("command.empty");
|
|
1864
|
+
return 0;
|
|
1865
|
+
}
|
|
1866
|
+
const selector = argv[0]?.trim() ?? null;
|
|
1867
|
+
if (argv.length > 1) {
|
|
1868
|
+
throw new Error(buildAccountUseHelpText());
|
|
1869
|
+
}
|
|
1870
|
+
let targetAccount = null;
|
|
1871
|
+
if (!selector) {
|
|
1872
|
+
if (accounts.length === 1) {
|
|
1873
|
+
const [singleAccount] = accounts;
|
|
1874
|
+
if (!singleAccount) {
|
|
1875
|
+
throw new Error("No accounts configured.");
|
|
1876
|
+
}
|
|
1877
|
+
targetAccount = singleAccount;
|
|
1878
|
+
logger.info("command.single_account_default", {
|
|
1879
|
+
resolvedAccountId: targetAccount.id,
|
|
1880
|
+
label: targetAccount.label
|
|
1881
|
+
});
|
|
1882
|
+
} else {
|
|
1883
|
+
throw new Error(
|
|
1884
|
+
"Multiple accounts exist. Specify which one to use: codexes account use <account-id-or-label>"
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
} else {
|
|
1888
|
+
targetAccount = resolveAccountBySelector({ accounts, logger, selector });
|
|
1889
|
+
}
|
|
1890
|
+
if (!targetAccount) {
|
|
1891
|
+
throw new Error("Could not resolve the account to use.");
|
|
1892
|
+
}
|
|
1893
|
+
logger.info("command.start", {
|
|
1894
|
+
requestedSelector: selector,
|
|
1895
|
+
resolvedAccountId: targetAccount.id,
|
|
1896
|
+
label: targetAccount.label
|
|
1897
|
+
});
|
|
1898
|
+
const selectedAccount = await registry.selectAccount(targetAccount.id);
|
|
1899
|
+
context.io.stdout.write(
|
|
1900
|
+
`Using account "${selectedAccount.label}" (${selectedAccount.id}) as the default.
|
|
1901
|
+
`
|
|
1902
|
+
);
|
|
1903
|
+
logger.info("command.complete", {
|
|
1904
|
+
requestedSelector: selector,
|
|
1905
|
+
resolvedAccountId: selectedAccount.id
|
|
1906
|
+
});
|
|
1907
|
+
return 0;
|
|
1908
|
+
}
|
|
1909
|
+
function buildAccountUseHelpText() {
|
|
1910
|
+
return [
|
|
1911
|
+
"Usage:",
|
|
1912
|
+
" codexes account use <account-id-or-label>",
|
|
1913
|
+
" codexes account use",
|
|
1914
|
+
"",
|
|
1915
|
+
"When only one account exists, `codexes account use` selects it automatically."
|
|
1916
|
+
].join("\n");
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// src/runtime/lock/runtime-lock.ts
|
|
1920
|
+
import os3 from "node:os";
|
|
1921
|
+
import path10 from "node:path";
|
|
1922
|
+
import { mkdir as mkdir6, readFile as readFile7, rm as rm4, stat as stat5, writeFile as writeFile5 } from "node:fs/promises";
|
|
1923
|
+
var DEFAULT_WAIT_TIMEOUT_MS = 15e3;
|
|
1924
|
+
var DEFAULT_STALE_LOCK_MS = 5 * 60 * 1e3;
|
|
1925
|
+
var DEFAULT_POLL_INTERVAL_MS = 250;
|
|
1926
|
+
async function acquireRuntimeLock(input) {
|
|
1927
|
+
const lockRoot = path10.join(input.runtimeRoot, "lock");
|
|
1928
|
+
const ownerFile = path10.join(lockRoot, "owner.json");
|
|
1929
|
+
const waitTimeoutMs = input.waitTimeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
1930
|
+
const staleLockMs = input.staleLockMs ?? DEFAULT_STALE_LOCK_MS;
|
|
1931
|
+
const pollIntervalMs = input.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
1932
|
+
const startedAt = Date.now();
|
|
1933
|
+
input.logger.info("runtime_lock.acquire.start", {
|
|
1934
|
+
lockRoot,
|
|
1935
|
+
waitTimeoutMs,
|
|
1936
|
+
staleLockMs,
|
|
1937
|
+
pollIntervalMs
|
|
1938
|
+
});
|
|
1939
|
+
while (true) {
|
|
1940
|
+
try {
|
|
1941
|
+
await mkdir6(lockRoot);
|
|
1942
|
+
const owner = {
|
|
1943
|
+
pid: process.pid,
|
|
1944
|
+
host: os3.hostname(),
|
|
1945
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1946
|
+
};
|
|
1947
|
+
await writeFile5(ownerFile, JSON.stringify(owner, null, 2), "utf8");
|
|
1948
|
+
input.logger.info("runtime_lock.acquire.complete", {
|
|
1949
|
+
lockRoot,
|
|
1950
|
+
waitedMs: Date.now() - startedAt,
|
|
1951
|
+
owner
|
|
1952
|
+
});
|
|
1953
|
+
return {
|
|
1954
|
+
async release() {
|
|
1955
|
+
input.logger.info("runtime_lock.release.start", { lockRoot });
|
|
1956
|
+
await rm4(lockRoot, { force: true, recursive: true }).catch(() => void 0);
|
|
1957
|
+
input.logger.info("runtime_lock.release.complete", { lockRoot });
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
} catch (error) {
|
|
1961
|
+
if (!isAlreadyExistsError(error)) {
|
|
1962
|
+
input.logger.error("runtime_lock.acquire.failed", {
|
|
1963
|
+
lockRoot,
|
|
1964
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1965
|
+
});
|
|
1966
|
+
throw error;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
const lockAgeMs = await readLockAgeMs(lockRoot, ownerFile);
|
|
1970
|
+
if (lockAgeMs !== null && lockAgeMs > staleLockMs) {
|
|
1971
|
+
input.logger.warn("runtime_lock.stale_detected", {
|
|
1972
|
+
lockRoot,
|
|
1973
|
+
lockAgeMs
|
|
1974
|
+
});
|
|
1975
|
+
await rm4(lockRoot, { force: true, recursive: true }).catch(() => void 0);
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
const waitedMs = Date.now() - startedAt;
|
|
1979
|
+
input.logger.debug("runtime_lock.acquire.waiting", {
|
|
1980
|
+
lockRoot,
|
|
1981
|
+
waitedMs,
|
|
1982
|
+
lockAgeMs
|
|
1983
|
+
});
|
|
1984
|
+
if (waitedMs >= waitTimeoutMs) {
|
|
1985
|
+
input.logger.error("runtime_lock.acquire.timeout", {
|
|
1986
|
+
lockRoot,
|
|
1987
|
+
waitedMs,
|
|
1988
|
+
lockAgeMs
|
|
1989
|
+
});
|
|
1990
|
+
throw new Error(
|
|
1991
|
+
`Timed out waiting for the shared runtime lock after ${waitTimeoutMs}ms.`
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
await sleep(pollIntervalMs);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
async function readLockAgeMs(lockRoot, ownerFile) {
|
|
1998
|
+
const ownerContents = await readFile7(ownerFile, "utf8").catch(() => null);
|
|
1999
|
+
if (ownerContents) {
|
|
2000
|
+
const parsed = JSON.parse(ownerContents);
|
|
2001
|
+
if (typeof parsed.createdAt === "string") {
|
|
2002
|
+
return Date.now() - new Date(parsed.createdAt).getTime();
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
const lockStats = await stat5(lockRoot).catch(() => null);
|
|
2006
|
+
return lockStats ? Date.now() - lockStats.mtimeMs : null;
|
|
2007
|
+
}
|
|
2008
|
+
function isAlreadyExistsError(error) {
|
|
2009
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
2010
|
+
}
|
|
2011
|
+
function sleep(durationMs) {
|
|
2012
|
+
return new Promise((resolve) => {
|
|
2013
|
+
setTimeout(resolve, durationMs);
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// src/runtime/activate-account/activate-account.ts
|
|
2018
|
+
import { copyFile as copyFile3, cp as cp3, mkdir as mkdir7, readFile as readFile8, rm as rm5, stat as stat6 } from "node:fs/promises";
|
|
2019
|
+
import path11 from "node:path";
|
|
2020
|
+
import { createHash } from "node:crypto";
|
|
2021
|
+
async function activateAccountIntoSharedRuntime(input) {
|
|
2022
|
+
const runtimePaths = resolveAccountRuntimePaths(input.runtimeContract, input.account.id);
|
|
2023
|
+
const accountStateRoot = runtimePaths.accountStateDirectory;
|
|
2024
|
+
const backupRoot = path11.join(runtimePaths.runtimeBackupDirectory, "active");
|
|
2025
|
+
input.logger.info("account_activation.start", {
|
|
2026
|
+
accountId: input.account.id,
|
|
2027
|
+
label: input.account.label,
|
|
2028
|
+
accountStateRoot,
|
|
2029
|
+
sharedCodexHome: input.sharedCodexHome,
|
|
2030
|
+
backupRoot
|
|
2031
|
+
});
|
|
2032
|
+
await rm5(backupRoot, { force: true, recursive: true }).catch(() => void 0);
|
|
2033
|
+
await mkdir7(backupRoot, { recursive: true });
|
|
2034
|
+
const accountRules = input.runtimeContract.fileRules.filter(
|
|
2035
|
+
(rule) => rule.classification === "account"
|
|
2036
|
+
);
|
|
2037
|
+
const authSourcePath = path11.join(accountStateRoot, "auth.json");
|
|
2038
|
+
if (!await pathExists4(authSourcePath)) {
|
|
2039
|
+
input.logger.error("account_activation.missing_auth", {
|
|
2040
|
+
accountId: input.account.id,
|
|
2041
|
+
authSourcePath
|
|
2042
|
+
});
|
|
2043
|
+
throw new Error(
|
|
2044
|
+
`Account "${input.account.label}" has no stored auth.json; add the account again.`
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
try {
|
|
2048
|
+
for (const rule of accountRules) {
|
|
2049
|
+
await backupRuntimeArtifact({
|
|
2050
|
+
backupRoot,
|
|
2051
|
+
logger: input.logger,
|
|
2052
|
+
rule,
|
|
2053
|
+
sharedCodexHome: input.sharedCodexHome
|
|
2054
|
+
});
|
|
2055
|
+
await replaceRuntimeArtifact({
|
|
2056
|
+
accountStateRoot,
|
|
2057
|
+
logger: input.logger,
|
|
2058
|
+
rule,
|
|
2059
|
+
sharedCodexHome: input.sharedCodexHome
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
} catch (error) {
|
|
2063
|
+
input.logger.error("account_activation.failed", {
|
|
2064
|
+
accountId: input.account.id,
|
|
2065
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2066
|
+
});
|
|
2067
|
+
await restoreSharedRuntimeFromBackup({
|
|
2068
|
+
account: input.account,
|
|
2069
|
+
backupRoot,
|
|
2070
|
+
logger: input.logger,
|
|
2071
|
+
runtimeContract: input.runtimeContract,
|
|
2072
|
+
sharedCodexHome: input.sharedCodexHome
|
|
2073
|
+
});
|
|
2074
|
+
throw error;
|
|
2075
|
+
}
|
|
2076
|
+
input.logger.info("account_activation.complete", {
|
|
2077
|
+
accountId: input.account.id,
|
|
2078
|
+
sharedCodexHome: input.sharedCodexHome
|
|
2079
|
+
});
|
|
2080
|
+
return {
|
|
2081
|
+
account: input.account,
|
|
2082
|
+
backupRoot,
|
|
2083
|
+
runtimeContract: input.runtimeContract,
|
|
2084
|
+
sharedCodexHome: input.sharedCodexHome,
|
|
2085
|
+
sourceAccountStateRoot: accountStateRoot
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
async function syncSharedRuntimeBackToAccount(input) {
|
|
2089
|
+
const accountRules = input.session.runtimeContract.fileRules.filter(
|
|
2090
|
+
(rule) => rule.classification === "account"
|
|
2091
|
+
);
|
|
2092
|
+
input.logger.info("account_sync.start", {
|
|
2093
|
+
accountId: input.session.account.id,
|
|
2094
|
+
sharedCodexHome: input.session.sharedCodexHome,
|
|
2095
|
+
accountStateRoot: input.session.sourceAccountStateRoot
|
|
2096
|
+
});
|
|
2097
|
+
for (const rule of accountRules) {
|
|
2098
|
+
await syncRuntimeArtifact({
|
|
2099
|
+
accountStateRoot: input.session.sourceAccountStateRoot,
|
|
2100
|
+
logger: input.logger,
|
|
2101
|
+
rule,
|
|
2102
|
+
sharedCodexHome: input.session.sharedCodexHome
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
input.logger.info("account_sync.complete", {
|
|
2106
|
+
accountId: input.session.account.id
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
async function restoreSharedRuntimeFromBackup(input) {
|
|
2110
|
+
const accountRules = input.runtimeContract.fileRules.filter(
|
|
2111
|
+
(rule) => rule.classification === "account"
|
|
2112
|
+
);
|
|
2113
|
+
input.logger.warn("account_activation.restore.start", {
|
|
2114
|
+
accountId: input.account.id,
|
|
2115
|
+
backupRoot: input.backupRoot,
|
|
2116
|
+
sharedCodexHome: input.sharedCodexHome
|
|
2117
|
+
});
|
|
2118
|
+
for (const rule of accountRules) {
|
|
2119
|
+
await restoreRuntimeArtifact({
|
|
2120
|
+
backupRoot: input.backupRoot,
|
|
2121
|
+
logger: input.logger,
|
|
2122
|
+
rule,
|
|
2123
|
+
sharedCodexHome: input.sharedCodexHome
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
input.logger.warn("account_activation.restore.complete", {
|
|
2127
|
+
accountId: input.account.id
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
async function backupRuntimeArtifact(input) {
|
|
2131
|
+
const sourcePath = path11.join(input.sharedCodexHome, normalizedPattern(input.rule.pathPattern));
|
|
2132
|
+
const backupPath = path11.join(input.backupRoot, normalizedPattern(input.rule.pathPattern));
|
|
2133
|
+
if (!await pathExists4(sourcePath)) {
|
|
2134
|
+
input.logger.debug("account_activation.backup.skip", {
|
|
2135
|
+
pathPattern: input.rule.pathPattern,
|
|
2136
|
+
sourcePath,
|
|
2137
|
+
reason: "missing"
|
|
2138
|
+
});
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
await mkdir7(path11.dirname(backupPath), { recursive: true });
|
|
2142
|
+
if (isDirectoryPattern(input.rule)) {
|
|
2143
|
+
await cp3(sourcePath, backupPath, { recursive: true });
|
|
2144
|
+
} else {
|
|
2145
|
+
await copyFile3(sourcePath, backupPath);
|
|
2146
|
+
}
|
|
2147
|
+
input.logger.debug("account_activation.backup.complete", {
|
|
2148
|
+
pathPattern: input.rule.pathPattern,
|
|
2149
|
+
sourcePath,
|
|
2150
|
+
backupPath
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
async function replaceRuntimeArtifact(input) {
|
|
2154
|
+
const sourcePath = path11.join(input.accountStateRoot, normalizedPattern(input.rule.pathPattern));
|
|
2155
|
+
const targetPath = path11.join(input.sharedCodexHome, normalizedPattern(input.rule.pathPattern));
|
|
2156
|
+
await rm5(targetPath, { force: true, recursive: true }).catch(() => void 0);
|
|
2157
|
+
if (!await pathExists4(sourcePath)) {
|
|
2158
|
+
input.logger.debug("account_activation.replace.skip", {
|
|
2159
|
+
pathPattern: input.rule.pathPattern,
|
|
2160
|
+
sourcePath,
|
|
2161
|
+
reason: "missing"
|
|
2162
|
+
});
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
await mkdir7(path11.dirname(targetPath), { recursive: true });
|
|
2166
|
+
if (isDirectoryPattern(input.rule)) {
|
|
2167
|
+
await cp3(sourcePath, targetPath, { recursive: true });
|
|
2168
|
+
} else {
|
|
2169
|
+
await copyFile3(sourcePath, targetPath);
|
|
2170
|
+
}
|
|
2171
|
+
input.logger.debug("account_activation.replace.complete", {
|
|
2172
|
+
pathPattern: input.rule.pathPattern,
|
|
2173
|
+
sourcePath,
|
|
2174
|
+
targetPath
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
async function syncRuntimeArtifact(input) {
|
|
2178
|
+
const sourcePath = path11.join(input.sharedCodexHome, normalizedPattern(input.rule.pathPattern));
|
|
2179
|
+
const targetPath = path11.join(input.accountStateRoot, normalizedPattern(input.rule.pathPattern));
|
|
2180
|
+
if (!await pathExists4(sourcePath)) {
|
|
2181
|
+
input.logger.debug("account_sync.skip", {
|
|
2182
|
+
pathPattern: input.rule.pathPattern,
|
|
2183
|
+
sourcePath,
|
|
2184
|
+
reason: "missing"
|
|
2185
|
+
});
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
const changed = await hasArtifactChanged(sourcePath, targetPath, isDirectoryPattern(input.rule));
|
|
2189
|
+
if (!changed) {
|
|
2190
|
+
input.logger.debug("account_sync.no_change", {
|
|
2191
|
+
pathPattern: input.rule.pathPattern,
|
|
2192
|
+
sourcePath,
|
|
2193
|
+
targetPath
|
|
2194
|
+
});
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
await rm5(targetPath, { force: true, recursive: true }).catch(() => void 0);
|
|
2198
|
+
await mkdir7(path11.dirname(targetPath), { recursive: true });
|
|
2199
|
+
if (isDirectoryPattern(input.rule)) {
|
|
2200
|
+
await cp3(sourcePath, targetPath, { recursive: true });
|
|
2201
|
+
} else {
|
|
2202
|
+
await copyFile3(sourcePath, targetPath);
|
|
2203
|
+
}
|
|
2204
|
+
input.logger.info("account_sync.updated", {
|
|
2205
|
+
pathPattern: input.rule.pathPattern,
|
|
2206
|
+
sourcePath,
|
|
2207
|
+
targetPath
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
async function restoreRuntimeArtifact(input) {
|
|
2211
|
+
const backupPath = path11.join(input.backupRoot, normalizedPattern(input.rule.pathPattern));
|
|
2212
|
+
const targetPath = path11.join(input.sharedCodexHome, normalizedPattern(input.rule.pathPattern));
|
|
2213
|
+
await rm5(targetPath, { force: true, recursive: true }).catch(() => void 0);
|
|
2214
|
+
if (!await pathExists4(backupPath)) {
|
|
2215
|
+
input.logger.debug("account_activation.restore.skip", {
|
|
2216
|
+
pathPattern: input.rule.pathPattern,
|
|
2217
|
+
backupPath,
|
|
2218
|
+
reason: "missing"
|
|
2219
|
+
});
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
await mkdir7(path11.dirname(targetPath), { recursive: true });
|
|
2223
|
+
if (isDirectoryPattern(input.rule)) {
|
|
2224
|
+
await cp3(backupPath, targetPath, { recursive: true });
|
|
2225
|
+
} else {
|
|
2226
|
+
await copyFile3(backupPath, targetPath);
|
|
2227
|
+
}
|
|
2228
|
+
input.logger.debug("account_activation.restore.complete_artifact", {
|
|
2229
|
+
pathPattern: input.rule.pathPattern,
|
|
2230
|
+
backupPath,
|
|
2231
|
+
targetPath
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
function isDirectoryPattern(rule) {
|
|
2235
|
+
return rule.pathPattern.endsWith("/**");
|
|
2236
|
+
}
|
|
2237
|
+
function normalizedPattern(pattern) {
|
|
2238
|
+
return pattern.endsWith("/**") ? pattern.slice(0, -3) : pattern;
|
|
2239
|
+
}
|
|
2240
|
+
async function hasArtifactChanged(sourcePath, targetPath, isDirectory) {
|
|
2241
|
+
if (!await pathExists4(targetPath)) {
|
|
2242
|
+
return true;
|
|
2243
|
+
}
|
|
2244
|
+
if (isDirectory) {
|
|
2245
|
+
const [sourceHash2, targetHash2] = await Promise.all([
|
|
2246
|
+
hashDirectory(sourcePath),
|
|
2247
|
+
hashDirectory(targetPath)
|
|
2248
|
+
]);
|
|
2249
|
+
return sourceHash2 !== targetHash2;
|
|
2250
|
+
}
|
|
2251
|
+
const [sourceHash, targetHash] = await Promise.all([
|
|
2252
|
+
hashFile(sourcePath),
|
|
2253
|
+
hashFile(targetPath)
|
|
2254
|
+
]);
|
|
2255
|
+
return sourceHash !== targetHash;
|
|
2256
|
+
}
|
|
2257
|
+
async function hashFile(filePath) {
|
|
2258
|
+
const content = await readFile8(filePath);
|
|
2259
|
+
return createHash("sha256").update(content).digest("hex");
|
|
2260
|
+
}
|
|
2261
|
+
async function hashDirectory(directoryPath) {
|
|
2262
|
+
const entries = await collectFiles(directoryPath);
|
|
2263
|
+
const hash = createHash("sha256");
|
|
2264
|
+
for (const entry of entries.sort()) {
|
|
2265
|
+
hash.update(entry.relativePath);
|
|
2266
|
+
hash.update(await readFile8(entry.absolutePath));
|
|
2267
|
+
}
|
|
2268
|
+
return hash.digest("hex");
|
|
2269
|
+
}
|
|
2270
|
+
async function collectFiles(root) {
|
|
2271
|
+
const rootStats = await stat6(root).catch(() => null);
|
|
2272
|
+
if (!rootStats) {
|
|
2273
|
+
return [];
|
|
2274
|
+
}
|
|
2275
|
+
if (!rootStats.isDirectory()) {
|
|
2276
|
+
return [{ absolutePath: root, relativePath: path11.basename(root) }];
|
|
2277
|
+
}
|
|
2278
|
+
const results = [];
|
|
2279
|
+
const stack = [root];
|
|
2280
|
+
while (stack.length > 0) {
|
|
2281
|
+
const current = stack.pop();
|
|
2282
|
+
if (!current) {
|
|
2283
|
+
continue;
|
|
2284
|
+
}
|
|
2285
|
+
const entries = await import("node:fs/promises").then(
|
|
2286
|
+
(fs) => fs.readdir(current, { withFileTypes: true })
|
|
2287
|
+
);
|
|
2288
|
+
for (const entry of entries) {
|
|
2289
|
+
const absolutePath = path11.join(current, entry.name);
|
|
2290
|
+
if (entry.isDirectory()) {
|
|
2291
|
+
stack.push(absolutePath);
|
|
2292
|
+
continue;
|
|
2293
|
+
}
|
|
2294
|
+
if (entry.isFile()) {
|
|
2295
|
+
results.push({
|
|
2296
|
+
absolutePath,
|
|
2297
|
+
relativePath: path11.relative(root, absolutePath).split(path11.sep).join("/")
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
return results;
|
|
2303
|
+
}
|
|
2304
|
+
async function pathExists4(targetPath) {
|
|
2305
|
+
try {
|
|
2306
|
+
await stat6(targetPath);
|
|
2307
|
+
return true;
|
|
2308
|
+
} catch (error) {
|
|
2309
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
2310
|
+
return false;
|
|
2311
|
+
}
|
|
2312
|
+
throw error;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// src/process/spawn-codex-command.ts
|
|
2317
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
2318
|
+
async function spawnCodexCommand(input) {
|
|
2319
|
+
const launchSpec = await resolveCodexLaunchSpec(input.codexBinaryPath, input.argv);
|
|
2320
|
+
input.logger.info("spawn_codex.start", {
|
|
2321
|
+
codexBinaryPath: input.codexBinaryPath,
|
|
2322
|
+
resolvedCommand: launchSpec.command,
|
|
2323
|
+
codexHome: input.codexHome,
|
|
2324
|
+
argv: launchSpec.args,
|
|
2325
|
+
stdinIsTTY: process.stdin.isTTY ?? false,
|
|
2326
|
+
stdoutIsTTY: process.stdout.isTTY ?? false,
|
|
2327
|
+
stderrIsTTY: process.stderr.isTTY ?? false
|
|
2328
|
+
});
|
|
2329
|
+
return new Promise((resolve, reject) => {
|
|
2330
|
+
const child = spawn2(launchSpec.command, launchSpec.args, {
|
|
2331
|
+
env: {
|
|
2332
|
+
...process.env,
|
|
2333
|
+
CODEX_HOME: input.codexHome
|
|
2334
|
+
},
|
|
2335
|
+
shell: false,
|
|
2336
|
+
stdio: "inherit",
|
|
2337
|
+
windowsHide: false
|
|
2338
|
+
});
|
|
2339
|
+
let settled = false;
|
|
2340
|
+
const forwardSignal = (signal) => {
|
|
2341
|
+
input.logger.warn("spawn_codex.parent_signal", {
|
|
2342
|
+
signal,
|
|
2343
|
+
pid: child.pid ?? null
|
|
2344
|
+
});
|
|
2345
|
+
child.kill(signal);
|
|
2346
|
+
};
|
|
2347
|
+
const signalHandlers = {
|
|
2348
|
+
SIGINT: () => forwardSignal("SIGINT"),
|
|
2349
|
+
SIGTERM: () => forwardSignal("SIGTERM")
|
|
2350
|
+
};
|
|
2351
|
+
process.on("SIGINT", signalHandlers.SIGINT);
|
|
2352
|
+
process.on("SIGTERM", signalHandlers.SIGTERM);
|
|
2353
|
+
const cleanup = () => {
|
|
2354
|
+
process.off("SIGINT", signalHandlers.SIGINT);
|
|
2355
|
+
process.off("SIGTERM", signalHandlers.SIGTERM);
|
|
2356
|
+
};
|
|
2357
|
+
child.on("error", (error) => {
|
|
2358
|
+
if (settled) {
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
settled = true;
|
|
2362
|
+
cleanup();
|
|
2363
|
+
input.logger.error("spawn_codex.error", {
|
|
2364
|
+
codexBinaryPath: input.codexBinaryPath,
|
|
2365
|
+
message: error.message
|
|
2366
|
+
});
|
|
2367
|
+
reject(error);
|
|
2368
|
+
});
|
|
2369
|
+
child.on("exit", (exitCode2, signal) => {
|
|
2370
|
+
if (settled) {
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
settled = true;
|
|
2374
|
+
cleanup();
|
|
2375
|
+
input.logger.info("spawn_codex.complete", {
|
|
2376
|
+
codexBinaryPath: input.codexBinaryPath,
|
|
2377
|
+
exitCode: exitCode2,
|
|
2378
|
+
signal
|
|
2379
|
+
});
|
|
2380
|
+
resolve(exitCode2 ?? 1);
|
|
2381
|
+
});
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
// src/selection/account-auth-state.ts
|
|
2386
|
+
import { readFile as readFile9 } from "node:fs/promises";
|
|
2387
|
+
import path12 from "node:path";
|
|
2388
|
+
async function readAccountAuthState(input) {
|
|
2389
|
+
const filePath = path12.join(input.account.authDirectory, "state", "auth.json");
|
|
2390
|
+
input.logger.debug("selection.account_auth_state.read_start", {
|
|
2391
|
+
accountId: input.account.id,
|
|
2392
|
+
label: input.account.label,
|
|
2393
|
+
filePath
|
|
2394
|
+
});
|
|
2395
|
+
try {
|
|
2396
|
+
const raw = await readFile9(filePath, "utf8");
|
|
2397
|
+
const parsed = JSON.parse(raw);
|
|
2398
|
+
if (!isRecord(parsed)) {
|
|
2399
|
+
input.logger.warn("selection.account_auth_state.unsupported_shape", {
|
|
2400
|
+
accountId: input.account.id,
|
|
2401
|
+
label: input.account.label,
|
|
2402
|
+
filePath,
|
|
2403
|
+
topLevelType: typeof parsed
|
|
2404
|
+
});
|
|
2405
|
+
return {
|
|
2406
|
+
ok: false,
|
|
2407
|
+
category: "unsupported-auth-shape",
|
|
2408
|
+
filePath,
|
|
2409
|
+
message: "auth.json is not a JSON object."
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
const accessToken = resolveString(
|
|
2413
|
+
parsed.access_token,
|
|
2414
|
+
getNestedString(parsed, ["tokens", "access_token"]),
|
|
2415
|
+
getNestedString(parsed, ["tokens", "accessToken"])
|
|
2416
|
+
);
|
|
2417
|
+
if (!accessToken) {
|
|
2418
|
+
input.logger.warn("selection.account_auth_state.access_token_missing", {
|
|
2419
|
+
accountId: input.account.id,
|
|
2420
|
+
label: input.account.label,
|
|
2421
|
+
filePath,
|
|
2422
|
+
hasTokensObject: isRecord(parsed.tokens)
|
|
2423
|
+
});
|
|
2424
|
+
return {
|
|
2425
|
+
ok: false,
|
|
2426
|
+
category: "missing-access-token",
|
|
2427
|
+
filePath,
|
|
2428
|
+
message: "auth.json does not contain an access_token."
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
const result = {
|
|
2432
|
+
ok: true,
|
|
2433
|
+
filePath,
|
|
2434
|
+
state: {
|
|
2435
|
+
accessToken,
|
|
2436
|
+
accountId: resolveString(
|
|
2437
|
+
parsed.account_id,
|
|
2438
|
+
parsed.accountId,
|
|
2439
|
+
getNestedString(parsed, ["tokens", "account_id"]),
|
|
2440
|
+
getNestedString(parsed, ["tokens", "accountId"])
|
|
2441
|
+
),
|
|
2442
|
+
authMode: resolveString(
|
|
2443
|
+
parsed.auth_mode,
|
|
2444
|
+
parsed.authMode,
|
|
2445
|
+
getNestedString(parsed, ["tokens", "auth_mode"]),
|
|
2446
|
+
getNestedString(parsed, ["tokens", "authMode"])
|
|
2447
|
+
),
|
|
2448
|
+
lastRefresh: resolveString(
|
|
2449
|
+
parsed.last_refresh,
|
|
2450
|
+
parsed.lastRefresh,
|
|
2451
|
+
parsed.refresh_at,
|
|
2452
|
+
parsed.refreshAt
|
|
2453
|
+
)
|
|
2454
|
+
}
|
|
2455
|
+
};
|
|
2456
|
+
input.logger.debug("selection.account_auth_state.read_complete", {
|
|
2457
|
+
accountId: input.account.id,
|
|
2458
|
+
label: input.account.label,
|
|
2459
|
+
filePath,
|
|
2460
|
+
hasAccessToken: true,
|
|
2461
|
+
authAccountId: result.state.accountId,
|
|
2462
|
+
authMode: result.state.authMode,
|
|
2463
|
+
hasRefreshMetadata: result.state.lastRefresh !== null
|
|
2464
|
+
});
|
|
2465
|
+
return result;
|
|
2466
|
+
} catch (error) {
|
|
2467
|
+
if (isNodeErrorWithCode(error, "ENOENT")) {
|
|
2468
|
+
input.logger.warn("selection.account_auth_state.missing_file", {
|
|
2469
|
+
accountId: input.account.id,
|
|
2470
|
+
label: input.account.label,
|
|
2471
|
+
filePath
|
|
2472
|
+
});
|
|
2473
|
+
return {
|
|
2474
|
+
ok: false,
|
|
2475
|
+
category: "missing-file",
|
|
2476
|
+
filePath,
|
|
2477
|
+
message: "auth.json was not found for the account profile."
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
if (error instanceof SyntaxError) {
|
|
2481
|
+
input.logger.warn("selection.account_auth_state.malformed_json", {
|
|
2482
|
+
accountId: input.account.id,
|
|
2483
|
+
label: input.account.label,
|
|
2484
|
+
filePath,
|
|
2485
|
+
message: error.message
|
|
2486
|
+
});
|
|
2487
|
+
return {
|
|
2488
|
+
ok: false,
|
|
2489
|
+
category: "malformed-json",
|
|
2490
|
+
filePath,
|
|
2491
|
+
message: error.message
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
input.logger.warn("selection.account_auth_state.unsupported_shape", {
|
|
2495
|
+
accountId: input.account.id,
|
|
2496
|
+
label: input.account.label,
|
|
2497
|
+
filePath,
|
|
2498
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2499
|
+
});
|
|
2500
|
+
return {
|
|
2501
|
+
ok: false,
|
|
2502
|
+
category: "unsupported-auth-shape",
|
|
2503
|
+
filePath,
|
|
2504
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
function getNestedString(value, pathParts) {
|
|
2509
|
+
let current = value;
|
|
2510
|
+
for (const part of pathParts) {
|
|
2511
|
+
if (!isRecord(current) || typeof current[part] === "undefined") {
|
|
2512
|
+
return null;
|
|
2513
|
+
}
|
|
2514
|
+
current = current[part];
|
|
2515
|
+
}
|
|
2516
|
+
return typeof current === "string" && current.trim().length > 0 ? current : null;
|
|
2517
|
+
}
|
|
2518
|
+
function resolveString(...values) {
|
|
2519
|
+
for (const value of values) {
|
|
2520
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
2521
|
+
return value;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
return null;
|
|
2525
|
+
}
|
|
2526
|
+
function isRecord(value) {
|
|
2527
|
+
return typeof value === "object" && value !== null;
|
|
2528
|
+
}
|
|
2529
|
+
function isNodeErrorWithCode(error, code) {
|
|
2530
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
// src/selection/usage-normalize.ts
|
|
2534
|
+
function normalizeWhamUsageResponse(input) {
|
|
2535
|
+
input.logger.debug("selection.usage_normalize.start", {
|
|
2536
|
+
accountIdHint: input.accountIdHint ?? null,
|
|
2537
|
+
topLevelKeys: Object.keys(input.raw).sort()
|
|
2538
|
+
});
|
|
2539
|
+
const daily = normalizeUsageWindow({
|
|
2540
|
+
accountIdHint: input.accountIdHint,
|
|
2541
|
+
logger: input.logger,
|
|
2542
|
+
raw: resolveUsageWindow(input.raw, "daily"),
|
|
2543
|
+
window: "daily"
|
|
2544
|
+
});
|
|
2545
|
+
const weekly = normalizeUsageWindow({
|
|
2546
|
+
accountIdHint: input.accountIdHint,
|
|
2547
|
+
logger: input.logger,
|
|
2548
|
+
raw: resolveUsageWindow(input.raw, "weekly"),
|
|
2549
|
+
window: "weekly"
|
|
2550
|
+
});
|
|
2551
|
+
const accountId = pickString(input.raw.account_id, input.raw.accountId, input.accountIdHint);
|
|
2552
|
+
const allowed = typeof input.raw.allowed === "boolean" ? input.raw.allowed : true;
|
|
2553
|
+
const limitReached = typeof input.raw.limit_reached === "boolean" ? input.raw.limit_reached : daily.limitReached || weekly.limitReached;
|
|
2554
|
+
const status = classifyUsageStatus({
|
|
2555
|
+
allowed,
|
|
2556
|
+
dailyRemaining: daily.remaining,
|
|
2557
|
+
limitReached,
|
|
2558
|
+
weeklyRemaining: weekly.remaining
|
|
2559
|
+
});
|
|
2560
|
+
const snapshot = {
|
|
2561
|
+
accountId,
|
|
2562
|
+
allowed,
|
|
2563
|
+
limitReached,
|
|
2564
|
+
dailyRemaining: daily.remaining,
|
|
2565
|
+
weeklyRemaining: weekly.remaining,
|
|
2566
|
+
dailyResetsAt: daily.resetsAt,
|
|
2567
|
+
weeklyResetsAt: weekly.resetsAt,
|
|
2568
|
+
dailyPercentUsed: daily.percentUsed,
|
|
2569
|
+
weeklyPercentUsed: weekly.percentUsed,
|
|
2570
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2571
|
+
status,
|
|
2572
|
+
statusReason: describeUsageStatus(status),
|
|
2573
|
+
windows: {
|
|
2574
|
+
daily,
|
|
2575
|
+
weekly
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
input.logger.debug("selection.usage_normalize.complete", {
|
|
2579
|
+
accountId: snapshot.accountId,
|
|
2580
|
+
allowed: snapshot.allowed,
|
|
2581
|
+
limitReached: snapshot.limitReached,
|
|
2582
|
+
dailyRemaining: snapshot.dailyRemaining,
|
|
2583
|
+
weeklyRemaining: snapshot.weeklyRemaining,
|
|
2584
|
+
dailyResetsAt: snapshot.dailyResetsAt,
|
|
2585
|
+
weeklyResetsAt: snapshot.weeklyResetsAt,
|
|
2586
|
+
status: snapshot.status,
|
|
2587
|
+
statusReason: snapshot.statusReason
|
|
2588
|
+
});
|
|
2589
|
+
return snapshot;
|
|
2590
|
+
}
|
|
2591
|
+
function normalizeUsageWindow(input) {
|
|
2592
|
+
if (!input.raw) {
|
|
2593
|
+
input.logger.debug("selection.usage_normalize.window_missing", {
|
|
2594
|
+
accountIdHint: input.accountIdHint ?? null,
|
|
2595
|
+
window: input.window
|
|
2596
|
+
});
|
|
2597
|
+
return {
|
|
2598
|
+
limit: null,
|
|
2599
|
+
used: null,
|
|
2600
|
+
remaining: null,
|
|
2601
|
+
limitReached: false,
|
|
2602
|
+
resetsAt: null,
|
|
2603
|
+
percentUsed: null,
|
|
2604
|
+
source: null
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
const limit = pickNumber(input.raw.limit);
|
|
2608
|
+
const used = pickNumber(input.raw.used, calculateUsed(limit, pickNumber(input.raw.remaining)));
|
|
2609
|
+
const remaining = pickNumber(
|
|
2610
|
+
input.raw.remaining,
|
|
2611
|
+
calculateRemaining(limit, used)
|
|
2612
|
+
);
|
|
2613
|
+
const limitReached = typeof input.raw.limit_reached === "boolean" ? input.raw.limit_reached : remaining !== null ? remaining <= 0 : false;
|
|
2614
|
+
const percentUsed = pickNumber(
|
|
2615
|
+
input.raw.percent_used,
|
|
2616
|
+
input.raw.percentage_used,
|
|
2617
|
+
calculatePercentUsed(limit, used, remaining)
|
|
2618
|
+
);
|
|
2619
|
+
const resetsAt = normalizeTimestamp(
|
|
2620
|
+
input.raw.reset_at,
|
|
2621
|
+
input.raw.resets_at,
|
|
2622
|
+
input.raw.next_reset_at
|
|
2623
|
+
);
|
|
2624
|
+
const source = resolveWindowSource(input.raw);
|
|
2625
|
+
input.logger.debug("selection.usage_normalize.window_complete", {
|
|
2626
|
+
accountIdHint: input.accountIdHint ?? null,
|
|
2627
|
+
window: input.window,
|
|
2628
|
+
limit,
|
|
2629
|
+
used,
|
|
2630
|
+
remaining,
|
|
2631
|
+
limitReached,
|
|
2632
|
+
percentUsed,
|
|
2633
|
+
resetsAt,
|
|
2634
|
+
source
|
|
2635
|
+
});
|
|
2636
|
+
return {
|
|
2637
|
+
limit,
|
|
2638
|
+
used,
|
|
2639
|
+
remaining,
|
|
2640
|
+
limitReached,
|
|
2641
|
+
resetsAt,
|
|
2642
|
+
percentUsed,
|
|
2643
|
+
source
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2646
|
+
function resolveUsageWindow(raw, window) {
|
|
2647
|
+
const candidates = [raw[window], raw.usage?.[window], raw.quotas?.[window]];
|
|
2648
|
+
for (const candidate of candidates) {
|
|
2649
|
+
if (isRecord2(candidate)) {
|
|
2650
|
+
return candidate;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
return null;
|
|
2654
|
+
}
|
|
2655
|
+
function resolveWindowSource(raw) {
|
|
2656
|
+
if (typeof raw.source === "string") {
|
|
2657
|
+
return raw.source;
|
|
2658
|
+
}
|
|
2659
|
+
if (typeof raw.kind === "string") {
|
|
2660
|
+
return raw.kind;
|
|
2661
|
+
}
|
|
2662
|
+
return null;
|
|
2663
|
+
}
|
|
2664
|
+
function classifyUsageStatus(input) {
|
|
2665
|
+
if (!input.allowed) {
|
|
2666
|
+
return "not-allowed";
|
|
2667
|
+
}
|
|
2668
|
+
if (input.limitReached) {
|
|
2669
|
+
return "limit-reached";
|
|
2670
|
+
}
|
|
2671
|
+
if (input.dailyRemaining === null && input.weeklyRemaining === null) {
|
|
2672
|
+
return "missing-usage-data";
|
|
2673
|
+
}
|
|
2674
|
+
return "usable";
|
|
2675
|
+
}
|
|
2676
|
+
function describeUsageStatus(status) {
|
|
2677
|
+
switch (status) {
|
|
2678
|
+
case "not-allowed":
|
|
2679
|
+
return "usage endpoint reported that the account is not allowed to launch";
|
|
2680
|
+
case "limit-reached":
|
|
2681
|
+
return "usage endpoint reported an exhausted limit window";
|
|
2682
|
+
case "missing-usage-data":
|
|
2683
|
+
return "usage endpoint did not expose enough quota fields to rank this account";
|
|
2684
|
+
case "usable":
|
|
2685
|
+
return "usage endpoint exposed enough quota fields to rank this account";
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
function normalizeTimestamp(...values) {
|
|
2689
|
+
for (const value of values) {
|
|
2690
|
+
if (typeof value === "string") {
|
|
2691
|
+
const parsed = new Date(value);
|
|
2692
|
+
if (!Number.isNaN(parsed.valueOf())) {
|
|
2693
|
+
return parsed.toISOString();
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2697
|
+
const normalizedValue = value > 1e10 ? value : value * 1e3;
|
|
2698
|
+
const parsed = new Date(normalizedValue);
|
|
2699
|
+
if (!Number.isNaN(parsed.valueOf())) {
|
|
2700
|
+
return parsed.toISOString();
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
return null;
|
|
2705
|
+
}
|
|
2706
|
+
function calculateRemaining(limit, used) {
|
|
2707
|
+
if (limit === null || used === null) {
|
|
2708
|
+
return null;
|
|
2709
|
+
}
|
|
2710
|
+
return limit - used;
|
|
2711
|
+
}
|
|
2712
|
+
function calculateUsed(limit, remaining) {
|
|
2713
|
+
if (limit === null || remaining === null) {
|
|
2714
|
+
return null;
|
|
2715
|
+
}
|
|
2716
|
+
return limit - remaining;
|
|
2717
|
+
}
|
|
2718
|
+
function calculatePercentUsed(limit, used, remaining) {
|
|
2719
|
+
if (limit === null || limit <= 0) {
|
|
2720
|
+
return null;
|
|
2721
|
+
}
|
|
2722
|
+
const numerator = used ?? calculateUsed(limit, remaining);
|
|
2723
|
+
if (numerator === null) {
|
|
2724
|
+
return null;
|
|
2725
|
+
}
|
|
2726
|
+
return Number((numerator / limit * 100).toFixed(2));
|
|
2727
|
+
}
|
|
2728
|
+
function pickString(...values) {
|
|
2729
|
+
for (const value of values) {
|
|
2730
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
2731
|
+
return value;
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
return null;
|
|
2735
|
+
}
|
|
2736
|
+
function pickNumber(...values) {
|
|
2737
|
+
for (const value of values) {
|
|
2738
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2739
|
+
return value;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
return null;
|
|
2743
|
+
}
|
|
2744
|
+
function isRecord2(value) {
|
|
2745
|
+
return typeof value === "object" && value !== null;
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
// src/selection/usage-client.ts
|
|
2749
|
+
var WHAM_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
2750
|
+
async function probeAccountUsage(input) {
|
|
2751
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
2752
|
+
input.logger.info("selection.usage_probe.start", {
|
|
2753
|
+
accountId: input.account.id,
|
|
2754
|
+
label: input.account.label,
|
|
2755
|
+
timeoutMs: input.probeConfig.probeTimeoutMs,
|
|
2756
|
+
useAccountIdHeader: input.probeConfig.useAccountIdHeader
|
|
2757
|
+
});
|
|
2758
|
+
const authState = await readAccountAuthState({
|
|
2759
|
+
account: input.account,
|
|
2760
|
+
logger: input.logger
|
|
2761
|
+
});
|
|
2762
|
+
if (!authState.ok) {
|
|
2763
|
+
input.logger.warn("selection.usage_probe.auth_missing", {
|
|
2764
|
+
accountId: input.account.id,
|
|
2765
|
+
label: input.account.label,
|
|
2766
|
+
category: authState.category,
|
|
2767
|
+
filePath: authState.filePath
|
|
2768
|
+
});
|
|
2769
|
+
return {
|
|
2770
|
+
ok: false,
|
|
2771
|
+
account: input.account,
|
|
2772
|
+
category: "auth-missing",
|
|
2773
|
+
message: authState.message,
|
|
2774
|
+
source: "fresh"
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
try {
|
|
2778
|
+
const response = await fetchImpl(WHAM_USAGE_URL, {
|
|
2779
|
+
method: "GET",
|
|
2780
|
+
headers: buildUsageHeaders({
|
|
2781
|
+
accessToken: authState.state.accessToken,
|
|
2782
|
+
accountId: authState.state.accountId,
|
|
2783
|
+
useAccountIdHeader: input.probeConfig.useAccountIdHeader
|
|
2784
|
+
}),
|
|
2785
|
+
signal: AbortSignal.timeout(input.probeConfig.probeTimeoutMs)
|
|
2786
|
+
});
|
|
2787
|
+
input.logger.debug("selection.usage_probe.http_complete", {
|
|
2788
|
+
accountId: input.account.id,
|
|
2789
|
+
label: input.account.label,
|
|
2790
|
+
status: response.status,
|
|
2791
|
+
ok: response.ok
|
|
2792
|
+
});
|
|
2793
|
+
if (!response.ok) {
|
|
2794
|
+
input.logger.warn("selection.usage_probe.http_error", {
|
|
2795
|
+
accountId: input.account.id,
|
|
2796
|
+
label: input.account.label,
|
|
2797
|
+
status: response.status
|
|
2798
|
+
});
|
|
2799
|
+
return {
|
|
2800
|
+
ok: false,
|
|
2801
|
+
account: input.account,
|
|
2802
|
+
category: "http-error",
|
|
2803
|
+
message: `Usage probe returned HTTP ${response.status}.`,
|
|
2804
|
+
source: "fresh"
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
const body = await response.json();
|
|
2808
|
+
if (!isRecord3(body)) {
|
|
2809
|
+
input.logger.warn("selection.usage_probe.invalid_response", {
|
|
2810
|
+
accountId: input.account.id,
|
|
2811
|
+
label: input.account.label,
|
|
2812
|
+
bodyType: typeof body
|
|
2813
|
+
});
|
|
2814
|
+
return {
|
|
2815
|
+
ok: false,
|
|
2816
|
+
account: input.account,
|
|
2817
|
+
category: "invalid-response",
|
|
2818
|
+
message: "Usage probe returned a non-object JSON payload.",
|
|
2819
|
+
source: "fresh"
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2822
|
+
const snapshot = normalizeWhamUsageResponse({
|
|
2823
|
+
accountIdHint: authState.state.accountId ?? input.account.id,
|
|
2824
|
+
logger: input.logger,
|
|
2825
|
+
raw: body
|
|
2826
|
+
});
|
|
2827
|
+
input.logger.info("selection.usage_probe.success", {
|
|
2828
|
+
accountId: input.account.id,
|
|
2829
|
+
label: input.account.label,
|
|
2830
|
+
snapshotStatus: snapshot.status,
|
|
2831
|
+
dailyRemaining: snapshot.dailyRemaining,
|
|
2832
|
+
weeklyRemaining: snapshot.weeklyRemaining,
|
|
2833
|
+
limitReached: snapshot.limitReached
|
|
2834
|
+
});
|
|
2835
|
+
return {
|
|
2836
|
+
ok: true,
|
|
2837
|
+
account: input.account,
|
|
2838
|
+
snapshot,
|
|
2839
|
+
source: "fresh"
|
|
2840
|
+
};
|
|
2841
|
+
} catch (error) {
|
|
2842
|
+
if (isAbortError(error)) {
|
|
2843
|
+
input.logger.warn("selection.usage_probe.timeout", {
|
|
2844
|
+
accountId: input.account.id,
|
|
2845
|
+
label: input.account.label,
|
|
2846
|
+
timeoutMs: input.probeConfig.probeTimeoutMs
|
|
2847
|
+
});
|
|
2848
|
+
return {
|
|
2849
|
+
ok: false,
|
|
2850
|
+
account: input.account,
|
|
2851
|
+
category: "timeout",
|
|
2852
|
+
message: `Usage probe timed out after ${input.probeConfig.probeTimeoutMs}ms.`,
|
|
2853
|
+
source: "fresh"
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
input.logger.error("selection.usage_probe.request_failed", {
|
|
2857
|
+
accountId: input.account.id,
|
|
2858
|
+
label: input.account.label,
|
|
2859
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2860
|
+
});
|
|
2861
|
+
return {
|
|
2862
|
+
ok: false,
|
|
2863
|
+
account: input.account,
|
|
2864
|
+
category: "invalid-response",
|
|
2865
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2866
|
+
source: "fresh"
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
function buildUsageHeaders(input) {
|
|
2871
|
+
const headers = new Headers({
|
|
2872
|
+
accept: "application/json",
|
|
2873
|
+
authorization: `Bearer ${input.accessToken}`,
|
|
2874
|
+
"user-agent": "codexes/0.1 experimental-usage-probe"
|
|
2875
|
+
});
|
|
2876
|
+
if (input.useAccountIdHeader && input.accountId) {
|
|
2877
|
+
headers.set("OpenAI-Account-ID", input.accountId);
|
|
2878
|
+
}
|
|
2879
|
+
return headers;
|
|
2880
|
+
}
|
|
2881
|
+
function isAbortError(error) {
|
|
2882
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
2883
|
+
}
|
|
2884
|
+
function isRecord3(value) {
|
|
2885
|
+
return typeof value === "object" && value !== null;
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// src/selection/usage-cache.ts
|
|
2889
|
+
import { mkdir as mkdir8, readFile as readFile10, rename as rename2, writeFile as writeFile6 } from "node:fs/promises";
|
|
2890
|
+
import path13 from "node:path";
|
|
2891
|
+
var USAGE_CACHE_SCHEMA_VERSION = 1;
|
|
2892
|
+
async function loadUsageCache(input) {
|
|
2893
|
+
try {
|
|
2894
|
+
const raw = await readFile10(input.cacheFilePath, "utf8");
|
|
2895
|
+
const parsed = JSON.parse(raw);
|
|
2896
|
+
const normalized = normalizeUsageCacheDocument(parsed);
|
|
2897
|
+
input.logger.debug("selection.usage_cache.load_success", {
|
|
2898
|
+
cacheFilePath: input.cacheFilePath,
|
|
2899
|
+
entryCount: normalized.entries.length
|
|
2900
|
+
});
|
|
2901
|
+
return normalized.entries;
|
|
2902
|
+
} catch (error) {
|
|
2903
|
+
if (isNodeErrorWithCode2(error, "ENOENT")) {
|
|
2904
|
+
input.logger.debug("selection.usage_cache.missing", {
|
|
2905
|
+
cacheFilePath: input.cacheFilePath
|
|
2906
|
+
});
|
|
2907
|
+
return [];
|
|
2908
|
+
}
|
|
2909
|
+
const backupPath = `${input.cacheFilePath}.corrupt-${Date.now()}`;
|
|
2910
|
+
await rename2(input.cacheFilePath, backupPath).catch(() => void 0);
|
|
2911
|
+
input.logger.warn("selection.usage_cache.corrupt", {
|
|
2912
|
+
cacheFilePath: input.cacheFilePath,
|
|
2913
|
+
backupPath,
|
|
2914
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2915
|
+
});
|
|
2916
|
+
return [];
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
async function persistUsageCache(input) {
|
|
2920
|
+
await mkdir8(path13.dirname(input.cacheFilePath), { recursive: true });
|
|
2921
|
+
const document = {
|
|
2922
|
+
schemaVersion: USAGE_CACHE_SCHEMA_VERSION,
|
|
2923
|
+
entries: input.entries
|
|
2924
|
+
};
|
|
2925
|
+
const tempFile = `${input.cacheFilePath}.tmp`;
|
|
2926
|
+
const serialized = JSON.stringify(document, null, 2);
|
|
2927
|
+
await writeFile6(tempFile, serialized, "utf8");
|
|
2928
|
+
await rename2(tempFile, input.cacheFilePath);
|
|
2929
|
+
input.logger.debug("selection.usage_cache.persisted", {
|
|
2930
|
+
cacheFilePath: input.cacheFilePath,
|
|
2931
|
+
entryCount: input.entries.length
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
function resolveFreshUsageCacheEntry(input) {
|
|
2935
|
+
const entry = input.entries.find((candidate) => candidate.accountId === input.accountId) ?? null;
|
|
2936
|
+
if (!entry) {
|
|
2937
|
+
input.logger.debug("selection.usage_cache.miss", {
|
|
2938
|
+
accountId: input.accountId,
|
|
2939
|
+
ttlMs: input.ttlMs
|
|
2940
|
+
});
|
|
2941
|
+
return null;
|
|
2942
|
+
}
|
|
2943
|
+
const ageMs = input.now - new Date(entry.cachedAt).valueOf();
|
|
2944
|
+
if (!Number.isFinite(ageMs) || ageMs > input.ttlMs) {
|
|
2945
|
+
input.logger.debug("selection.usage_cache.expired", {
|
|
2946
|
+
accountId: input.accountId,
|
|
2947
|
+
cachedAt: entry.cachedAt,
|
|
2948
|
+
ageMs: Number.isFinite(ageMs) ? ageMs : null,
|
|
2949
|
+
ttlMs: input.ttlMs
|
|
2950
|
+
});
|
|
2951
|
+
return null;
|
|
2952
|
+
}
|
|
2953
|
+
input.logger.debug("selection.usage_cache.hit", {
|
|
2954
|
+
accountId: input.accountId,
|
|
2955
|
+
cachedAt: entry.cachedAt,
|
|
2956
|
+
ageMs,
|
|
2957
|
+
ttlMs: input.ttlMs
|
|
2958
|
+
});
|
|
2959
|
+
return entry;
|
|
2960
|
+
}
|
|
2961
|
+
function normalizeUsageCacheDocument(value) {
|
|
2962
|
+
if (!isRecord4(value)) {
|
|
2963
|
+
throw new Error("Usage cache document is not an object.");
|
|
2964
|
+
}
|
|
2965
|
+
const schemaVersion = typeof value.schemaVersion === "number" ? value.schemaVersion : USAGE_CACHE_SCHEMA_VERSION;
|
|
2966
|
+
if (schemaVersion !== USAGE_CACHE_SCHEMA_VERSION) {
|
|
2967
|
+
throw new Error(`Unsupported usage cache schema version ${schemaVersion}.`);
|
|
2968
|
+
}
|
|
2969
|
+
const entries = Array.isArray(value.entries) ? value.entries.filter(isUsageCacheEntry) : [];
|
|
2970
|
+
return {
|
|
2971
|
+
schemaVersion: USAGE_CACHE_SCHEMA_VERSION,
|
|
2972
|
+
entries
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2975
|
+
function isUsageCacheEntry(value) {
|
|
2976
|
+
return isRecord4(value) && typeof value.accountId === "string" && typeof value.accountLabel === "string" && typeof value.cachedAt === "string" && isRecord4(value.snapshot);
|
|
2977
|
+
}
|
|
2978
|
+
function isRecord4(value) {
|
|
2979
|
+
return typeof value === "object" && value !== null;
|
|
2980
|
+
}
|
|
2981
|
+
function isNodeErrorWithCode2(error, code) {
|
|
2982
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
// src/selection/usage-probe-coordinator.ts
|
|
2986
|
+
async function resolveAccountUsageSnapshots(input) {
|
|
2987
|
+
const now = Date.now();
|
|
2988
|
+
input.logger.info("selection.usage_probe_coordinator.start", {
|
|
2989
|
+
accountCount: input.accounts.length,
|
|
2990
|
+
cacheFilePath: input.cacheFilePath,
|
|
2991
|
+
cacheTtlMs: input.probeConfig.cacheTtlMs,
|
|
2992
|
+
timeoutMs: input.probeConfig.probeTimeoutMs
|
|
2993
|
+
});
|
|
2994
|
+
const cacheEntries = await loadUsageCache({
|
|
2995
|
+
cacheFilePath: input.cacheFilePath,
|
|
2996
|
+
logger: input.logger
|
|
2997
|
+
});
|
|
2998
|
+
const freshCacheEntries = [...cacheEntries];
|
|
2999
|
+
const resolutions = await Promise.all(
|
|
3000
|
+
input.accounts.map(async (account) => {
|
|
3001
|
+
const cached = resolveFreshUsageCacheEntry({
|
|
3002
|
+
accountId: account.id,
|
|
3003
|
+
entries: freshCacheEntries,
|
|
3004
|
+
logger: input.logger,
|
|
3005
|
+
now,
|
|
3006
|
+
ttlMs: input.probeConfig.cacheTtlMs
|
|
3007
|
+
});
|
|
3008
|
+
if (cached) {
|
|
3009
|
+
return {
|
|
3010
|
+
ok: true,
|
|
3011
|
+
account,
|
|
3012
|
+
snapshot: cached.snapshot,
|
|
3013
|
+
source: "cache"
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
const fresh = await probeAccountUsage({
|
|
3017
|
+
account,
|
|
3018
|
+
fetchImpl: input.fetchImpl,
|
|
3019
|
+
logger: input.logger,
|
|
3020
|
+
probeConfig: input.probeConfig
|
|
3021
|
+
});
|
|
3022
|
+
if (fresh.ok) {
|
|
3023
|
+
upsertCacheEntry(freshCacheEntries, {
|
|
3024
|
+
accountId: account.id,
|
|
3025
|
+
accountLabel: account.label,
|
|
3026
|
+
cachedAt: new Date(now).toISOString(),
|
|
3027
|
+
snapshot: fresh.snapshot
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
return fresh;
|
|
3031
|
+
})
|
|
3032
|
+
);
|
|
3033
|
+
await persistUsageCache({
|
|
3034
|
+
cacheFilePath: input.cacheFilePath,
|
|
3035
|
+
entries: freshCacheEntries,
|
|
3036
|
+
logger: input.logger
|
|
3037
|
+
});
|
|
3038
|
+
input.logger.info("selection.usage_probe_coordinator.complete", {
|
|
3039
|
+
accountCount: input.accounts.length,
|
|
3040
|
+
cacheHitCount: resolutions.filter((entry) => entry.ok && entry.source === "cache").length,
|
|
3041
|
+
freshSuccessCount: resolutions.filter((entry) => entry.ok && entry.source === "fresh").length,
|
|
3042
|
+
failureCount: resolutions.filter((entry) => !entry.ok).length
|
|
3043
|
+
});
|
|
3044
|
+
return resolutions;
|
|
3045
|
+
}
|
|
3046
|
+
function upsertCacheEntry(entries, nextEntry) {
|
|
3047
|
+
const existingIndex = entries.findIndex((entry) => entry.accountId === nextEntry.accountId);
|
|
3048
|
+
if (existingIndex >= 0) {
|
|
3049
|
+
entries.splice(existingIndex, 1, nextEntry);
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
entries.push(nextEntry);
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
// src/selection/select-account.ts
|
|
3056
|
+
async function selectAccountForExecution(input) {
|
|
3057
|
+
const accounts = await input.registry.listAccounts();
|
|
3058
|
+
input.logger.info("selection.start", {
|
|
3059
|
+
strategy: input.strategy,
|
|
3060
|
+
accountCount: accounts.length
|
|
3061
|
+
});
|
|
3062
|
+
if (accounts.length === 0) {
|
|
3063
|
+
input.logger.warn("selection.none");
|
|
3064
|
+
throw new Error("No accounts configured. Add one with `codexes account add <label>`.");
|
|
3065
|
+
}
|
|
3066
|
+
switch (input.strategy) {
|
|
3067
|
+
case "manual-default":
|
|
3068
|
+
return selectManualDefaultAccount(input.registry, input.logger, accounts);
|
|
3069
|
+
case "single-account":
|
|
3070
|
+
return selectSingleAccountOnly(input.registry, input.logger, accounts);
|
|
3071
|
+
case "remaining-limit-experimental":
|
|
3072
|
+
return selectExperimentalRemainingLimitAccount({
|
|
3073
|
+
accounts,
|
|
3074
|
+
experimentalSelection: input.experimentalSelection,
|
|
3075
|
+
fetchImpl: input.fetchImpl,
|
|
3076
|
+
logger: input.logger,
|
|
3077
|
+
registry: input.registry,
|
|
3078
|
+
selectionCacheFilePath: input.selectionCacheFilePath
|
|
3079
|
+
});
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
async function selectManualDefaultAccount(registry, logger, accounts) {
|
|
3083
|
+
const defaultAccount = await registry.getDefaultAccount();
|
|
3084
|
+
if (defaultAccount) {
|
|
3085
|
+
logger.info("selection.manual_default", {
|
|
3086
|
+
accountId: defaultAccount.id,
|
|
3087
|
+
label: defaultAccount.label
|
|
3088
|
+
});
|
|
3089
|
+
return defaultAccount;
|
|
3090
|
+
}
|
|
3091
|
+
if (accounts.length === 1) {
|
|
3092
|
+
const [singleAccount] = accounts;
|
|
3093
|
+
if (!singleAccount) {
|
|
3094
|
+
throw new Error("No accounts configured.");
|
|
3095
|
+
}
|
|
3096
|
+
logger.info("selection.manual_default_fallback_single", {
|
|
3097
|
+
accountId: singleAccount.id,
|
|
3098
|
+
label: singleAccount.label
|
|
3099
|
+
});
|
|
3100
|
+
return registry.selectAccount(singleAccount.id);
|
|
3101
|
+
}
|
|
3102
|
+
logger.warn("selection.manual_default_missing", {
|
|
3103
|
+
accountCount: accounts.length
|
|
3104
|
+
});
|
|
3105
|
+
throw new Error(
|
|
3106
|
+
"Multiple accounts are configured but no default account is selected. Use `codexes account use <account-id-or-label>` first."
|
|
3107
|
+
);
|
|
3108
|
+
}
|
|
3109
|
+
async function selectSingleAccountOnly(registry, logger, accounts) {
|
|
3110
|
+
if (accounts.length !== 1) {
|
|
3111
|
+
logger.warn("selection.single_account_invalid", {
|
|
3112
|
+
accountCount: accounts.length
|
|
3113
|
+
});
|
|
3114
|
+
throw new Error(
|
|
3115
|
+
"The single-account strategy requires exactly one configured account."
|
|
3116
|
+
);
|
|
3117
|
+
}
|
|
3118
|
+
const [singleAccount] = accounts;
|
|
3119
|
+
if (!singleAccount) {
|
|
3120
|
+
throw new Error("No accounts configured.");
|
|
3121
|
+
}
|
|
3122
|
+
logger.info("selection.single_account", {
|
|
3123
|
+
accountId: singleAccount.id,
|
|
3124
|
+
label: singleAccount.label
|
|
3125
|
+
});
|
|
3126
|
+
const defaultAccount = await registry.getDefaultAccount();
|
|
3127
|
+
if (defaultAccount?.id === singleAccount.id) {
|
|
3128
|
+
return singleAccount;
|
|
3129
|
+
}
|
|
3130
|
+
return registry.selectAccount(singleAccount.id);
|
|
3131
|
+
}
|
|
3132
|
+
async function selectExperimentalRemainingLimitAccount(input) {
|
|
3133
|
+
if (!input.experimentalSelection?.enabled || !input.selectionCacheFilePath) {
|
|
3134
|
+
input.logger.warn("selection.experimental_config_missing", {
|
|
3135
|
+
enabled: input.experimentalSelection?.enabled ?? false,
|
|
3136
|
+
hasSelectionCacheFilePath: Boolean(input.selectionCacheFilePath)
|
|
3137
|
+
});
|
|
3138
|
+
return selectManualDefaultAccount(input.registry, input.logger, input.accounts);
|
|
3139
|
+
}
|
|
3140
|
+
const defaultAccount = await input.registry.getDefaultAccount();
|
|
3141
|
+
const probeResults = await resolveAccountUsageSnapshots({
|
|
3142
|
+
accounts: input.accounts,
|
|
3143
|
+
cacheFilePath: input.selectionCacheFilePath,
|
|
3144
|
+
fetchImpl: input.fetchImpl,
|
|
3145
|
+
logger: input.logger,
|
|
3146
|
+
probeConfig: input.experimentalSelection
|
|
3147
|
+
});
|
|
3148
|
+
const failedProbes = probeResults.filter((entry) => !entry.ok);
|
|
3149
|
+
if (failedProbes.length > 0) {
|
|
3150
|
+
const eventName = failedProbes.length === probeResults.length ? "selection.experimental_fallback_all_probes_failed" : "selection.experimental_fallback_mixed_probe_outcomes";
|
|
3151
|
+
input.logger.warn(eventName, {
|
|
3152
|
+
failedAccountIds: failedProbes.map((entry) => entry.account.id),
|
|
3153
|
+
failureCategories: failedProbes.map((entry) => entry.category),
|
|
3154
|
+
successfulAccountIds: probeResults.filter((entry) => entry.ok).map((entry) => entry.account.id)
|
|
3155
|
+
});
|
|
3156
|
+
return selectManualDefaultAccount(input.registry, input.logger, input.accounts);
|
|
3157
|
+
}
|
|
3158
|
+
const successfulProbes = probeResults.filter((entry) => entry.ok);
|
|
3159
|
+
const candidates = successfulProbes.filter((entry) => entry.snapshot.status === "usable").sort(
|
|
3160
|
+
(left, right) => compareExperimentalCandidates({
|
|
3161
|
+
defaultAccountId: defaultAccount?.id ?? null,
|
|
3162
|
+
left,
|
|
3163
|
+
right,
|
|
3164
|
+
registryOrder: input.accounts
|
|
3165
|
+
})
|
|
3166
|
+
);
|
|
3167
|
+
input.logger.info("selection.experimental_ranked", {
|
|
3168
|
+
candidateOrder: candidates.map((entry) => ({
|
|
3169
|
+
accountId: entry.account.id,
|
|
3170
|
+
label: entry.account.label,
|
|
3171
|
+
dailyRemaining: entry.snapshot.dailyRemaining,
|
|
3172
|
+
weeklyRemaining: entry.snapshot.weeklyRemaining,
|
|
3173
|
+
source: entry.source
|
|
3174
|
+
})),
|
|
3175
|
+
defaultAccountId: defaultAccount?.id ?? null
|
|
3176
|
+
});
|
|
3177
|
+
const selected = candidates[0];
|
|
3178
|
+
if (!selected) {
|
|
3179
|
+
const allExhausted = successfulProbes.every(
|
|
3180
|
+
(entry) => entry.snapshot.limitReached || entry.snapshot.status === "limit-reached"
|
|
3181
|
+
);
|
|
3182
|
+
input.logger.warn(
|
|
3183
|
+
allExhausted ? "selection.experimental_fallback_all_accounts_exhausted" : "selection.experimental_fallback_ambiguous_usage",
|
|
3184
|
+
{
|
|
3185
|
+
usableProbeCount: candidates.length,
|
|
3186
|
+
probeStatuses: successfulProbes.map((entry) => ({
|
|
3187
|
+
accountId: entry.account.id,
|
|
3188
|
+
snapshotStatus: entry.snapshot.status,
|
|
3189
|
+
limitReached: entry.snapshot.limitReached,
|
|
3190
|
+
dailyRemaining: entry.snapshot.dailyRemaining,
|
|
3191
|
+
weeklyRemaining: entry.snapshot.weeklyRemaining
|
|
3192
|
+
}))
|
|
3193
|
+
}
|
|
3194
|
+
);
|
|
3195
|
+
return selectManualDefaultAccount(input.registry, input.logger, input.accounts);
|
|
3196
|
+
}
|
|
3197
|
+
input.logger.info("selection.experimental_selected", {
|
|
3198
|
+
accountId: selected.account.id,
|
|
3199
|
+
label: selected.account.label,
|
|
3200
|
+
dailyRemaining: selected.snapshot.dailyRemaining,
|
|
3201
|
+
weeklyRemaining: selected.snapshot.weeklyRemaining,
|
|
3202
|
+
source: selected.source
|
|
3203
|
+
});
|
|
3204
|
+
return selected.account;
|
|
3205
|
+
}
|
|
3206
|
+
function compareExperimentalCandidates(input) {
|
|
3207
|
+
const dailyDelta = (input.right.snapshot.dailyRemaining ?? Number.NEGATIVE_INFINITY) - (input.left.snapshot.dailyRemaining ?? Number.NEGATIVE_INFINITY);
|
|
3208
|
+
if (dailyDelta !== 0) {
|
|
3209
|
+
return dailyDelta;
|
|
3210
|
+
}
|
|
3211
|
+
const weeklyDelta = (input.right.snapshot.weeklyRemaining ?? Number.NEGATIVE_INFINITY) - (input.left.snapshot.weeklyRemaining ?? Number.NEGATIVE_INFINITY);
|
|
3212
|
+
if (weeklyDelta !== 0) {
|
|
3213
|
+
return weeklyDelta;
|
|
3214
|
+
}
|
|
3215
|
+
const leftIsDefault = input.left.account.id === input.defaultAccountId;
|
|
3216
|
+
const rightIsDefault = input.right.account.id === input.defaultAccountId;
|
|
3217
|
+
if (leftIsDefault !== rightIsDefault) {
|
|
3218
|
+
return leftIsDefault ? -1 : 1;
|
|
3219
|
+
}
|
|
3220
|
+
return input.registryOrder.findIndex((account) => account.id === input.left.account.id) - input.registryOrder.findIndex((account) => account.id === input.right.account.id);
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
// src/commands/root/run-root-command.ts
|
|
3224
|
+
async function runRootCommand(context) {
|
|
3225
|
+
const logger = createLogger({
|
|
3226
|
+
level: context.logging.level,
|
|
3227
|
+
name: "root",
|
|
3228
|
+
sink: context.logging.sink
|
|
3229
|
+
});
|
|
3230
|
+
logger.debug("argv.received", { argv: context.argv });
|
|
3231
|
+
logger.debug("runtime.detected", {
|
|
3232
|
+
sharedCodexHome: context.paths.sharedCodexHome,
|
|
3233
|
+
accountRoot: context.paths.accountRoot,
|
|
3234
|
+
runtimeRoot: context.paths.runtimeRoot,
|
|
3235
|
+
registryFile: context.paths.registryFile,
|
|
3236
|
+
wrapperConfigFile: context.paths.wrapperConfigFile,
|
|
3237
|
+
selectionCacheFile: context.paths.selectionCacheFile,
|
|
3238
|
+
firstRun: context.runtimeInitialization.firstRun,
|
|
3239
|
+
copiedSharedArtifacts: context.runtimeInitialization.copiedSharedArtifacts,
|
|
3240
|
+
createdRuntimeFiles: context.runtimeInitialization.createdFiles,
|
|
3241
|
+
credentialStoreMode: context.wrapperConfig.credentialStoreMode,
|
|
3242
|
+
accountSelectionStrategy: context.wrapperConfig.accountSelectionStrategy,
|
|
3243
|
+
experimentalSelection: context.wrapperConfig.experimentalSelection,
|
|
3244
|
+
codexBinaryPath: context.codexBinary.path,
|
|
3245
|
+
recursionGuardSource: context.executablePath
|
|
3246
|
+
});
|
|
3247
|
+
const runtimeContract = createRuntimeContract({
|
|
3248
|
+
accountRoot: context.paths.accountRoot,
|
|
3249
|
+
credentialStoreMode: context.wrapperConfig.credentialStoreMode,
|
|
3250
|
+
logger,
|
|
3251
|
+
runtimeRoot: context.paths.runtimeRoot,
|
|
3252
|
+
sharedCodexHome: context.paths.sharedCodexHome
|
|
3253
|
+
});
|
|
3254
|
+
const runtimeSummary = summarizeRuntimeContract(runtimeContract);
|
|
3255
|
+
logger.debug("runtime.contract_ready", runtimeSummary);
|
|
3256
|
+
if (context.wrapperConfig.credentialStoreMode !== "file") {
|
|
3257
|
+
logger.warn("credential_store.unsupported", {
|
|
3258
|
+
credentialStoreMode: context.wrapperConfig.credentialStoreMode,
|
|
3259
|
+
codexConfigFile: context.paths.codexConfigFile,
|
|
3260
|
+
reason: context.wrapperConfig.credentialStorePolicyReason
|
|
3261
|
+
});
|
|
3262
|
+
}
|
|
3263
|
+
if (context.argv[0] === "account" && context.argv[1] === "add") {
|
|
3264
|
+
logger.info("command.dispatch", {
|
|
3265
|
+
command: "account add",
|
|
3266
|
+
argv: context.argv.slice(2)
|
|
3267
|
+
});
|
|
3268
|
+
return runAccountAddCommand(context, context.argv.slice(2));
|
|
3269
|
+
}
|
|
3270
|
+
if (context.argv[0] === "account" && context.argv[1] === "list") {
|
|
3271
|
+
logger.info("command.dispatch", {
|
|
3272
|
+
command: "account list",
|
|
3273
|
+
argv: context.argv.slice(2)
|
|
3274
|
+
});
|
|
3275
|
+
return runAccountListCommand(context);
|
|
3276
|
+
}
|
|
3277
|
+
if (context.argv[0] === "account" && context.argv[1] === "remove") {
|
|
3278
|
+
logger.info("command.dispatch", {
|
|
3279
|
+
command: "account remove",
|
|
3280
|
+
argv: context.argv.slice(2)
|
|
3281
|
+
});
|
|
3282
|
+
return runAccountRemoveCommand(context, context.argv.slice(2));
|
|
3283
|
+
}
|
|
3284
|
+
if (context.argv[0] === "account" && context.argv[1] === "use") {
|
|
3285
|
+
logger.info("command.dispatch", {
|
|
3286
|
+
command: "account use",
|
|
3287
|
+
argv: context.argv.slice(2)
|
|
3288
|
+
});
|
|
3289
|
+
return runAccountUseCommand(context, context.argv.slice(2));
|
|
3290
|
+
}
|
|
3291
|
+
if (context.argv.includes("--help")) {
|
|
3292
|
+
context.io.stdout.write(`${buildHelpText()}
|
|
3293
|
+
`);
|
|
3294
|
+
logger.info("help.rendered");
|
|
3295
|
+
return 0;
|
|
3296
|
+
}
|
|
3297
|
+
if (!context.codexBinary.path) {
|
|
3298
|
+
logger.error("command.binary_missing", {
|
|
3299
|
+
candidates: context.codexBinary.candidates,
|
|
3300
|
+
rejectedCandidates: context.codexBinary.rejectedCandidates
|
|
3301
|
+
});
|
|
3302
|
+
throw new Error("Could not find the real `codex` binary on PATH.");
|
|
3303
|
+
}
|
|
3304
|
+
const registry = createAccountRegistry({
|
|
3305
|
+
accountRoot: context.paths.accountRoot,
|
|
3306
|
+
logger,
|
|
3307
|
+
registryFile: context.paths.registryFile
|
|
3308
|
+
});
|
|
3309
|
+
if (context.wrapperConfig.accountSelectionStrategy === "remaining-limit-experimental") {
|
|
3310
|
+
logger.warn("selection.experimental_enabled", {
|
|
3311
|
+
endpoint: "https://chatgpt.com/backend-api/wham/usage",
|
|
3312
|
+
fallbackStrategy: "manual-default",
|
|
3313
|
+
timeoutMs: context.wrapperConfig.experimentalSelection.probeTimeoutMs,
|
|
3314
|
+
cacheTtlMs: context.wrapperConfig.experimentalSelection.cacheTtlMs,
|
|
3315
|
+
useAccountIdHeader: context.wrapperConfig.experimentalSelection.useAccountIdHeader
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3318
|
+
const activeAccount = await selectAccountForExecution({
|
|
3319
|
+
experimentalSelection: context.wrapperConfig.experimentalSelection,
|
|
3320
|
+
fetchImpl: fetch,
|
|
3321
|
+
logger,
|
|
3322
|
+
registry,
|
|
3323
|
+
selectionCacheFilePath: context.paths.selectionCacheFile,
|
|
3324
|
+
strategy: context.wrapperConfig.accountSelectionStrategy
|
|
3325
|
+
});
|
|
3326
|
+
const lock = await acquireRuntimeLock({
|
|
3327
|
+
logger,
|
|
3328
|
+
runtimeRoot: context.paths.runtimeRoot
|
|
3329
|
+
});
|
|
3330
|
+
try {
|
|
3331
|
+
const activation = await activateAccountIntoSharedRuntime({
|
|
3332
|
+
account: activeAccount,
|
|
3333
|
+
logger,
|
|
3334
|
+
runtimeContract,
|
|
3335
|
+
sharedCodexHome: context.paths.sharedCodexHome
|
|
3336
|
+
});
|
|
3337
|
+
try {
|
|
3338
|
+
const exitCode2 = await spawnCodexCommand({
|
|
3339
|
+
argv: context.argv,
|
|
3340
|
+
codexBinaryPath: context.codexBinary.path,
|
|
3341
|
+
codexHome: context.paths.sharedCodexHome,
|
|
3342
|
+
logger
|
|
3343
|
+
});
|
|
3344
|
+
await syncSharedRuntimeBackToAccount({
|
|
3345
|
+
logger,
|
|
3346
|
+
session: activation
|
|
3347
|
+
});
|
|
3348
|
+
return exitCode2;
|
|
3349
|
+
} catch (error) {
|
|
3350
|
+
await restoreSharedRuntimeFromBackup({
|
|
3351
|
+
account: activeAccount,
|
|
3352
|
+
backupRoot: activation.backupRoot,
|
|
3353
|
+
logger,
|
|
3354
|
+
runtimeContract,
|
|
3355
|
+
sharedCodexHome: context.paths.sharedCodexHome
|
|
3356
|
+
});
|
|
3357
|
+
throw error;
|
|
3358
|
+
}
|
|
3359
|
+
} finally {
|
|
3360
|
+
await lock.release();
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
function buildHelpText() {
|
|
3364
|
+
return [
|
|
3365
|
+
"codexes",
|
|
3366
|
+
"",
|
|
3367
|
+
"Transparent multi-account wrapper around the Codex CLI.",
|
|
3368
|
+
"",
|
|
3369
|
+
"Usage:",
|
|
3370
|
+
" codexes [args...]",
|
|
3371
|
+
" codexes account add <label> [--timeout-ms <milliseconds>]",
|
|
3372
|
+
" codexes account list",
|
|
3373
|
+
" codexes account use <account-id-or-label>",
|
|
3374
|
+
" codexes account remove <account-id-or-label>",
|
|
3375
|
+
"",
|
|
3376
|
+
"Runtime model:",
|
|
3377
|
+
" Shared CODEX_HOME is preserved in a wrapper-owned runtime root.",
|
|
3378
|
+
" Account auth state is stored per account and synced back selectively.",
|
|
3379
|
+
"",
|
|
3380
|
+
"Current status:",
|
|
3381
|
+
" Account management and default Codex passthrough are implemented.",
|
|
3382
|
+
" Selection strategies: manual-default, single-account, remaining-limit-experimental.",
|
|
3383
|
+
" Experimental mode probes https://chatgpt.com/backend-api/wham/usage and falls back to manual-default when ranking is unreliable."
|
|
3384
|
+
].join("\n");
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
// src/core/bootstrap.ts
|
|
3388
|
+
async function runCli(argv, io) {
|
|
3389
|
+
const context = await buildAppContext(argv, io);
|
|
3390
|
+
const logger = createLogger({
|
|
3391
|
+
level: context.logging.level,
|
|
3392
|
+
name: "bootstrap",
|
|
3393
|
+
sink: context.logging.sink
|
|
3394
|
+
});
|
|
3395
|
+
logger.debug("bootstrap.start", {
|
|
3396
|
+
argv,
|
|
3397
|
+
cwd: context.environment.cwd,
|
|
3398
|
+
platform: context.environment.platform,
|
|
3399
|
+
runtime: context.environment.runtime
|
|
3400
|
+
});
|
|
3401
|
+
try {
|
|
3402
|
+
const exitCode2 = await runRootCommand(context);
|
|
3403
|
+
logger.debug("bootstrap.exit", { exitCode: exitCode2 });
|
|
3404
|
+
return exitCode2;
|
|
3405
|
+
} catch (error) {
|
|
3406
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
3407
|
+
logger.error("bootstrap.fatal", {
|
|
3408
|
+
message: normalized.message,
|
|
3409
|
+
stack: normalized.stack
|
|
3410
|
+
});
|
|
3411
|
+
context.io.stderr.write(`codexes: ${normalized.message}
|
|
3412
|
+
`);
|
|
3413
|
+
return 1;
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
// src/cli.ts
|
|
3418
|
+
var exitCode = await runCli(process.argv.slice(2), {
|
|
3419
|
+
cwd: process.cwd(),
|
|
3420
|
+
env: process.env,
|
|
3421
|
+
executablePath: process.argv[1] ?? process.execPath,
|
|
3422
|
+
stderr: process.stderr,
|
|
3423
|
+
stdout: process.stdout
|
|
3424
|
+
});
|
|
3425
|
+
process.exit(exitCode);
|
|
3426
|
+
//# sourceMappingURL=cli.js.map
|