codex-copilot-dx 0.4.23 → 0.4.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -3
- package/bin/cli.mjs +137 -44
- package/package.json +9 -2
- package/src/adapter.mjs +400 -282
- package/src/atomic-file.mjs +63 -0
- package/src/auth.mjs +148 -7
- package/src/claude-config.mjs +7 -7
- package/src/claude-desktop-config.mjs +3 -4
- package/src/cli-options.mjs +69 -0
- package/src/config.mjs +10 -6
- package/src/copilot.mjs +29 -165
- package/src/doctor.mjs +82 -7
- package/src/file-rotation.mjs +30 -0
- package/src/http-transport.mjs +182 -0
- package/src/image-optimization.mjs +151 -0
- package/src/log.mjs +9 -1
- package/src/model-cache.mjs +45 -0
- package/src/response-history.mjs +195 -0
- package/src/running-adapter.mjs +22 -0
- package/src/shutdown.mjs +29 -0
- package/src/startup.mjs +19 -0
- package/src/stream.mjs +2 -1
- package/src/usage.mjs +57 -14
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
function existingMode(filePath, fallback) {
|
|
6
|
+
try {
|
|
7
|
+
return fs.statSync(filePath).mode & 0o777;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
if (e?.code === "ENOENT") return fallback;
|
|
10
|
+
throw e;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function syncDirectory(directory) {
|
|
15
|
+
if (process.platform === "win32") return;
|
|
16
|
+
let fd;
|
|
17
|
+
try {
|
|
18
|
+
fd = fs.openSync(directory, "r");
|
|
19
|
+
fs.fsyncSync(fd);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
if (!["EINVAL", "ENOTSUP", "EBADF"].includes(e?.code)) throw e;
|
|
22
|
+
} finally {
|
|
23
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function atomicWriteFileSync(filePath, data, { mode = 0o600 } = {}) {
|
|
28
|
+
const directory = path.dirname(filePath);
|
|
29
|
+
const finalMode = existingMode(filePath, mode);
|
|
30
|
+
const tempPath = path.join(directory, `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
31
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
32
|
+
|
|
33
|
+
let fd;
|
|
34
|
+
try {
|
|
35
|
+
fd = fs.openSync(tempPath, "wx", finalMode);
|
|
36
|
+
fs.writeFileSync(fd, data);
|
|
37
|
+
fs.fsyncSync(fd);
|
|
38
|
+
fs.closeSync(fd);
|
|
39
|
+
fd = undefined;
|
|
40
|
+
fs.renameSync(tempPath, filePath);
|
|
41
|
+
fs.chmodSync(filePath, finalMode);
|
|
42
|
+
syncDirectory(directory);
|
|
43
|
+
} finally {
|
|
44
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
45
|
+
try {
|
|
46
|
+
fs.unlinkSync(tempPath);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
if (e?.code !== "ENOENT") throw e;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function atomicWriteFileIfChangedSync(filePath, data, options = {}) {
|
|
54
|
+
try {
|
|
55
|
+
const current = fs.readFileSync(filePath);
|
|
56
|
+
const next = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
57
|
+
if (current.equals(next)) return false;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
if (e?.code !== "ENOENT") throw e;
|
|
60
|
+
}
|
|
61
|
+
atomicWriteFileSync(filePath, data, options);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
package/src/auth.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
5
6
|
import { status } from "./status.mjs";
|
|
6
7
|
import { withFileLock } from "./lock.mjs";
|
|
7
8
|
|
|
@@ -22,6 +23,10 @@ export function githubTokenLockPath(home = os.homedir()) {
|
|
|
22
23
|
return `${githubTokenPath(home)}.lock`;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
export function githubTokenMetadataPath(home = os.homedir()) {
|
|
27
|
+
return `${githubTokenPath(home)}.account.json`;
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
export function githubReauthMessage(reason, home = os.homedir()) {
|
|
26
31
|
const tokenPath = githubTokenPath(home);
|
|
27
32
|
return `${reason}
|
|
@@ -210,6 +215,73 @@ function readSavedGithubToken(home = os.homedir()) {
|
|
|
210
215
|
return fs.readFileSync(filePath, "utf8").trim();
|
|
211
216
|
}
|
|
212
217
|
|
|
218
|
+
function normalizeGithubIdentity(identity) {
|
|
219
|
+
if (!identity || typeof identity !== "object") return null;
|
|
220
|
+
const login = typeof identity.login === "string" ? identity.login.trim() : "";
|
|
221
|
+
const id = identity.id === undefined || identity.id === null ? "" : String(identity.id).trim();
|
|
222
|
+
if (!login && !id) return null;
|
|
223
|
+
return { login, id };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function tokenFingerprint(token) {
|
|
227
|
+
return createHash("sha256").update(String(token || "")).digest("hex").slice(0, 24);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function readGithubTokenMetadata(home = os.homedir(), token = null) {
|
|
231
|
+
try {
|
|
232
|
+
const parsed = JSON.parse(fs.readFileSync(githubTokenMetadataPath(home), "utf8"));
|
|
233
|
+
if (token && parsed.token_fingerprint !== tokenFingerprint(token)) return null;
|
|
234
|
+
const identity = normalizeGithubIdentity(parsed);
|
|
235
|
+
return identity ? { ...identity, token_fingerprint: parsed.token_fingerprint || "" } : null;
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function githubIdentityMatches(identity, expected) {
|
|
242
|
+
const actual = normalizeGithubIdentity(identity);
|
|
243
|
+
const wanted = normalizeGithubIdentity(expected);
|
|
244
|
+
if (!wanted) return true;
|
|
245
|
+
if (!actual) return false;
|
|
246
|
+
if (wanted.id && actual.id) return wanted.id === actual.id;
|
|
247
|
+
return Boolean(wanted.login && actual.login && wanted.login.toLowerCase() === actual.login.toLowerCase());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function expectedGithubIdentity(home, env, token) {
|
|
251
|
+
const configuredLogin = String(env.CCDX_GITHUB_LOGIN || "").trim();
|
|
252
|
+
if (configuredLogin) return { login: configuredLogin };
|
|
253
|
+
return token ? readGithubTokenMetadata(home, token) : null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function fetchGithubIdentity(token, { fetchImpl = fetch, signal } = {}) {
|
|
257
|
+
if (typeof token !== "string" || !token.trim()) return { ok: false, reason: "empty_token" };
|
|
258
|
+
try {
|
|
259
|
+
const resp = await fetchImpl(`${GITHUB_API}/user`, {
|
|
260
|
+
headers: { Authorization: `token ${token.trim()}`, Accept: "application/json" },
|
|
261
|
+
signal,
|
|
262
|
+
});
|
|
263
|
+
if (!resp.ok) return { ok: false, status: resp.status, reason: "github_user_failed" };
|
|
264
|
+
const data = await resp.json();
|
|
265
|
+
const identity = normalizeGithubIdentity(data);
|
|
266
|
+
return identity ? { ok: true, ...identity } : { ok: false, reason: "github_identity_missing" };
|
|
267
|
+
} catch (error) {
|
|
268
|
+
return { ok: false, transient: true, reason: "github_user_request_failed", error };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function ensureGithubTokenMetadata(token, {
|
|
273
|
+
home = os.homedir(),
|
|
274
|
+
fetchImpl = fetch,
|
|
275
|
+
signal,
|
|
276
|
+
} = {}) {
|
|
277
|
+
const existing = readGithubTokenMetadata(home, token);
|
|
278
|
+
if (existing) return existing;
|
|
279
|
+
const identity = await fetchGithubIdentity(token, { fetchImpl, signal });
|
|
280
|
+
if (!identity.ok) return null;
|
|
281
|
+
writeGithubTokenMetadata(identity, home, token);
|
|
282
|
+
return normalizeGithubIdentity(identity);
|
|
283
|
+
}
|
|
284
|
+
|
|
213
285
|
export async function validateGithubToken(token, {
|
|
214
286
|
fetchImpl = fetch,
|
|
215
287
|
signal,
|
|
@@ -239,6 +311,7 @@ export async function validateGithubToken(token, {
|
|
|
239
311
|
userData = await userResp.json();
|
|
240
312
|
} catch {}
|
|
241
313
|
const login = typeof userData.login === "string" ? userData.login : "";
|
|
314
|
+
const id = userData.id === undefined || userData.id === null ? "" : String(userData.id);
|
|
242
315
|
|
|
243
316
|
let copilotResp;
|
|
244
317
|
try {
|
|
@@ -267,7 +340,7 @@ export async function validateGithubToken(token, {
|
|
|
267
340
|
return { ok: false, reason: "copilot_token_missing", login };
|
|
268
341
|
}
|
|
269
342
|
|
|
270
|
-
return { ok: true, login, copilotTokenData };
|
|
343
|
+
return { ok: true, login, id, copilotTokenData };
|
|
271
344
|
}
|
|
272
345
|
|
|
273
346
|
export async function discoverGithubToken({
|
|
@@ -276,10 +349,13 @@ export async function discoverGithubToken({
|
|
|
276
349
|
fetchImpl = fetch,
|
|
277
350
|
signal,
|
|
278
351
|
excludeTokens = [],
|
|
352
|
+
expectedIdentity,
|
|
353
|
+
strictExpectedIdentity = false,
|
|
279
354
|
} = {}) {
|
|
280
355
|
const excluded = new Set(excludeTokens);
|
|
281
356
|
const seen = new Set(excluded);
|
|
282
357
|
const failures = [];
|
|
358
|
+
const candidates = [];
|
|
283
359
|
|
|
284
360
|
for (const source of githubTokenSources({ home, env })) {
|
|
285
361
|
let token = "";
|
|
@@ -294,10 +370,40 @@ export async function discoverGithubToken({
|
|
|
294
370
|
seen.add(token);
|
|
295
371
|
|
|
296
372
|
const validation = await validateGithubToken(token, { fetchImpl, signal });
|
|
297
|
-
if (validation.ok)
|
|
373
|
+
if (validation.ok) {
|
|
374
|
+
const candidate = { ok: true, token, source, validation };
|
|
375
|
+
const explicitSource = source.type === "env" || source.type === "token-file";
|
|
376
|
+
if (expectedIdentity && (strictExpectedIdentity || !explicitSource)) {
|
|
377
|
+
if (githubIdentityMatches(validation, expectedIdentity)) return candidate;
|
|
378
|
+
failures.push({ source, validation, reason: "github_account_mismatch" });
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (explicitSource) return candidate;
|
|
382
|
+
candidates.push(candidate);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
298
385
|
failures.push({ source, validation });
|
|
299
386
|
}
|
|
300
387
|
|
|
388
|
+
if (candidates.length) {
|
|
389
|
+
const identities = new Set(candidates.map(({ validation, token }) => {
|
|
390
|
+
if (validation.id) return `id:${validation.id}`;
|
|
391
|
+
if (validation.login) return `login:${validation.login.toLowerCase()}`;
|
|
392
|
+
return `token:${token}`;
|
|
393
|
+
}));
|
|
394
|
+
if (identities.size === 1) return candidates[0];
|
|
395
|
+
return {
|
|
396
|
+
ok: false,
|
|
397
|
+
ambiguous: true,
|
|
398
|
+
candidates: candidates.map(({ source, validation }) => ({
|
|
399
|
+
source,
|
|
400
|
+
login: validation.login,
|
|
401
|
+
id: validation.id,
|
|
402
|
+
})),
|
|
403
|
+
failures,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
301
407
|
return { ok: false, failures };
|
|
302
408
|
}
|
|
303
409
|
|
|
@@ -314,19 +420,34 @@ export async function importDiscoveredGithubToken({
|
|
|
314
420
|
const run = async () => {
|
|
315
421
|
const excluded = new Set(excludeTokens);
|
|
316
422
|
const savedToken = readSavedGithubToken(home);
|
|
423
|
+
const expectedIdentity = expectedGithubIdentity(home, env, savedToken);
|
|
424
|
+
const strictExpectedIdentity = Boolean(String(env.CCDX_GITHUB_LOGIN || "").trim());
|
|
317
425
|
if (savedToken && !excluded.has(savedToken)) {
|
|
318
426
|
if (!validateSavedToken) {
|
|
319
427
|
return { ok: true, token: savedToken, source: { type: "saved-token" }, imported: false };
|
|
320
428
|
}
|
|
321
429
|
const validation = await validateGithubToken(savedToken, { fetchImpl, signal });
|
|
322
|
-
if (validation.ok) {
|
|
430
|
+
if (validation.ok && githubIdentityMatches(validation, expectedIdentity)) {
|
|
431
|
+
writeGithubTokenMetadata(validation, home, savedToken);
|
|
323
432
|
return { ok: true, token: savedToken, source: { type: "saved-token" }, validation, imported: false };
|
|
324
433
|
}
|
|
325
434
|
}
|
|
326
435
|
|
|
327
|
-
const discovered = await discoverGithubToken({
|
|
436
|
+
const discovered = await discoverGithubToken({
|
|
437
|
+
home,
|
|
438
|
+
env,
|
|
439
|
+
fetchImpl,
|
|
440
|
+
signal,
|
|
441
|
+
excludeTokens,
|
|
442
|
+
expectedIdentity,
|
|
443
|
+
strictExpectedIdentity,
|
|
444
|
+
});
|
|
445
|
+
if (discovered.ambiguous) {
|
|
446
|
+
const logins = [...new Set(discovered.candidates.map((candidate) => candidate.login || candidate.id || "unknown"))];
|
|
447
|
+
throw new Error(`Multiple GitHub Copilot accounts were found (${logins.join(", ")}). Set CCDX_GITHUB_LOGIN or CCDX_GITHUB_TOKEN_PATH to select one explicitly.`);
|
|
448
|
+
}
|
|
328
449
|
if (!discovered.ok) return null;
|
|
329
|
-
writeToken(discovered.token, home);
|
|
450
|
+
writeToken(discovered.token, home, discovered.validation);
|
|
330
451
|
const login = discovered.validation.login ? ` for ${discovered.validation.login}` : "";
|
|
331
452
|
log(status("ok", `Imported GitHub token from ${sourceDescription(discovered.source)}${login}`));
|
|
332
453
|
return { ...discovered, imported: true };
|
|
@@ -421,7 +542,8 @@ export async function ensureAuth({
|
|
|
421
542
|
const data = await pollResp.json();
|
|
422
543
|
const r = interpretPoll(data);
|
|
423
544
|
if (r.state === "done") {
|
|
424
|
-
|
|
545
|
+
const identity = await fetchGithubIdentity(r.token, { fetchImpl, signal });
|
|
546
|
+
writeToken(r.token, home, identity.ok ? identity : null);
|
|
425
547
|
log(status("ok", "Login successful"));
|
|
426
548
|
return;
|
|
427
549
|
}
|
|
@@ -432,9 +554,28 @@ export async function ensureAuth({
|
|
|
432
554
|
}, tokenLockOptions(env));
|
|
433
555
|
}
|
|
434
556
|
|
|
435
|
-
export function
|
|
557
|
+
export function writeGithubTokenMetadata(identity, home = os.homedir(), token = "") {
|
|
558
|
+
const normalized = normalizeGithubIdentity(identity);
|
|
559
|
+
if (!normalized) return false;
|
|
560
|
+
const filePath = githubTokenMetadataPath(home);
|
|
561
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
562
|
+
fs.writeFileSync(filePath, `${JSON.stringify({
|
|
563
|
+
...normalized,
|
|
564
|
+
token_fingerprint: tokenFingerprint(token),
|
|
565
|
+
updated_at: new Date().toISOString(),
|
|
566
|
+
}, null, 2)}\n`, { mode: 0o600 });
|
|
567
|
+
fs.chmodSync(filePath, 0o600);
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function writeToken(token, home = os.homedir(), identity = null) {
|
|
436
572
|
const GITHUB_TOKEN_PATH = githubTokenPath(home);
|
|
437
573
|
fs.mkdirSync(path.dirname(GITHUB_TOKEN_PATH), { recursive: true });
|
|
438
574
|
fs.writeFileSync(GITHUB_TOKEN_PATH, token, { mode: 0o600 });
|
|
439
575
|
fs.chmodSync(GITHUB_TOKEN_PATH, 0o600);
|
|
576
|
+
if (normalizeGithubIdentity(identity)) {
|
|
577
|
+
writeGithubTokenMetadata(identity, home, token);
|
|
578
|
+
} else {
|
|
579
|
+
try { fs.unlinkSync(githubTokenMetadataPath(home)); } catch (e) { if (e?.code !== "ENOENT") throw e; }
|
|
580
|
+
}
|
|
440
581
|
}
|
package/src/claude-config.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import { status } from "./status.mjs";
|
|
5
|
+
import { atomicWriteFileIfChangedSync } from "./atomic-file.mjs";
|
|
5
6
|
|
|
6
7
|
const SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
7
8
|
|
|
@@ -23,20 +24,19 @@ export function computeUpdatedSettings(settings, port) {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
// Point Claude Code at the adapter by updating ~/.claude/settings.json.
|
|
26
|
-
export function ensureClaudeConfig(port = 2026) {
|
|
27
|
+
export function ensureClaudeConfig(port = 2026, { filePath = SETTINGS_PATH } = {}) {
|
|
27
28
|
const target = `http://127.0.0.1:${port}`;
|
|
28
29
|
|
|
29
|
-
if (!fs.existsSync(
|
|
30
|
-
fs.mkdirSync(path.dirname(SETTINGS_PATH), { recursive: true });
|
|
30
|
+
if (!fs.existsSync(filePath)) {
|
|
31
31
|
const { json } = computeUpdatedSettings({}, port);
|
|
32
|
-
|
|
32
|
+
atomicWriteFileIfChangedSync(filePath, JSON.stringify(json, null, 2) + "\n");
|
|
33
33
|
console.log(status("ok", `Created ~/.claude/settings.json for Claude Code at ${target}`));
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
let raw, settings;
|
|
38
38
|
try {
|
|
39
|
-
raw = fs.readFileSync(
|
|
39
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
40
40
|
settings = JSON.parse(raw);
|
|
41
41
|
} catch (e) {
|
|
42
42
|
console.log(status("warn", `Could not parse ~/.claude/settings.json: ${e.message}`));
|
|
@@ -50,7 +50,7 @@ export function ensureClaudeConfig(port = 2026) {
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
atomicWriteFileIfChangedSync(`${filePath}.bak`, raw);
|
|
54
|
+
atomicWriteFileIfChangedSync(filePath, JSON.stringify(json, null, 2) + "\n");
|
|
55
55
|
console.log(status("ok", `Configured Claude Code ANTHROPIC_BASE_URL="${target}" and backed up settings.json.bak`));
|
|
56
56
|
}
|
|
@@ -4,6 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { claudeDesktopModelIds } from "./models.mjs";
|
|
6
6
|
import { status } from "./status.mjs";
|
|
7
|
+
import { atomicWriteFileIfChangedSync, atomicWriteFileSync } from "./atomic-file.mjs";
|
|
7
8
|
|
|
8
9
|
const CONFIG_FILE = "claude_desktop_config.json";
|
|
9
10
|
const CONFIG_LIBRARY_DIR = "configLibrary";
|
|
@@ -127,8 +128,7 @@ function readJsonFile(filePath, fallback = {}) {
|
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
function writeJsonFile(filePath, data) {
|
|
130
|
-
|
|
131
|
-
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
|
|
131
|
+
return atomicWriteFileIfChangedSync(filePath, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
function snapshotFiles(files) {
|
|
@@ -148,8 +148,7 @@ function restoreSnapshots(snapshots) {
|
|
|
148
148
|
fs.rmSync(snapshot.filePath, { force: true });
|
|
149
149
|
continue;
|
|
150
150
|
}
|
|
151
|
-
|
|
152
|
-
fs.writeFileSync(snapshot.filePath, snapshot.data, { mode: 0o600 });
|
|
151
|
+
atomicWriteFileSync(snapshot.filePath, snapshot.data, { mode: 0o600 });
|
|
153
152
|
}
|
|
154
153
|
}
|
|
155
154
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const HELP_COMMANDS = new Set(["help", "--help", "-h"]);
|
|
2
|
+
const VERSION_COMMANDS = new Set(["version", "--version", "-v"]);
|
|
3
|
+
|
|
4
|
+
function unexpectedArgs(args) {
|
|
5
|
+
throw new Error(`Unexpected argument${args.length === 1 ? "" : "s"}: ${args.join(" ")}`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function parseCliArgs(args = []) {
|
|
9
|
+
const [command, ...rest] = args;
|
|
10
|
+
if (!command) return { command: "start", configureClaudeDesktop: false, online: false };
|
|
11
|
+
if (HELP_COMMANDS.has(command)) {
|
|
12
|
+
if (rest.length) unexpectedArgs(rest);
|
|
13
|
+
return { command: "help", configureClaudeDesktop: false, online: false };
|
|
14
|
+
}
|
|
15
|
+
if (VERSION_COMMANDS.has(command)) {
|
|
16
|
+
if (rest.length) unexpectedArgs(rest);
|
|
17
|
+
return { command: "version", configureClaudeDesktop: false, online: false };
|
|
18
|
+
}
|
|
19
|
+
if (command === "usage") {
|
|
20
|
+
if (rest.length) unexpectedArgs(rest);
|
|
21
|
+
return { command: "usage", configureClaudeDesktop: false, online: false };
|
|
22
|
+
}
|
|
23
|
+
if (command === "doctor" || command === "--doctor") {
|
|
24
|
+
const invalid = rest.filter((arg) => arg !== "--online");
|
|
25
|
+
if (invalid.length) unexpectedArgs(invalid);
|
|
26
|
+
if (rest.filter((arg) => arg === "--online").length > 1) unexpectedArgs(["--online"]);
|
|
27
|
+
return { command: "doctor", configureClaudeDesktop: false, online: rest.includes("--online") };
|
|
28
|
+
}
|
|
29
|
+
if (command === "--configure-claude-desktop") {
|
|
30
|
+
if (rest.length) unexpectedArgs(rest);
|
|
31
|
+
return { command: "start", configureClaudeDesktop: true, online: false };
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`Unknown command or option: ${command}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function integerEnv(env, name, fallback, { min, max = Number.MAX_SAFE_INTEGER }) {
|
|
37
|
+
const raw = env[name];
|
|
38
|
+
if (raw === undefined || raw === null || String(raw).trim() === "") return fallback;
|
|
39
|
+
const text = String(raw).trim();
|
|
40
|
+
if (!/^\d+$/.test(text)) throw new Error(`${name} must be an integer between ${min} and ${max}`);
|
|
41
|
+
const value = Number(text);
|
|
42
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) {
|
|
43
|
+
throw new Error(`${name} must be an integer between ${min} and ${max}`);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseRuntimeOptions(env = process.env) {
|
|
49
|
+
return {
|
|
50
|
+
adapterPort: integerEnv(env, "ADAPTER_PORT", 2026, { min: 1, max: 65535 }),
|
|
51
|
+
adapterHost: String(env.ADAPTER_HOST || "127.0.0.1").trim() || "127.0.0.1",
|
|
52
|
+
modelRefreshTimeoutMs: integerEnv(env, "CCDX_MODEL_REFRESH_TIMEOUT_MS", 5000, { min: 1 }),
|
|
53
|
+
existingAdapterTimeoutMs: integerEnv(env, "CCDX_EXISTING_ADAPTER_TIMEOUT_MS", 500, { min: 1 }),
|
|
54
|
+
modelRefreshIntervalMs: integerEnv(env, "CCDX_MODEL_REFRESH_INTERVAL_MS", 30 * 60 * 1000, { min: 0 }),
|
|
55
|
+
upstreamTimeoutMs: integerEnv(env, "CCDX_UPSTREAM_TIMEOUT_MS", 120000, { min: 1 }),
|
|
56
|
+
streamHandshakeTimeoutMs: integerEnv(env, "CCDX_STREAM_HANDSHAKE_TIMEOUT_MS", 120000, { min: 1 }),
|
|
57
|
+
streamIdleTimeoutMs: integerEnv(env, "CCDX_STREAM_IDLE_TIMEOUT_MS", 120000, { min: 1 }),
|
|
58
|
+
shutdownTimeoutMs: integerEnv(env, "CCDX_SHUTDOWN_TIMEOUT_MS", 5000, { min: 1 }),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function cliHelp() {
|
|
63
|
+
return `Usage:
|
|
64
|
+
codex-copilot-dx [--configure-claude-desktop]
|
|
65
|
+
codex-copilot-dx doctor [--online]
|
|
66
|
+
codex-copilot-dx usage
|
|
67
|
+
codex-copilot-dx --version
|
|
68
|
+
codex-copilot-dx --help`;
|
|
69
|
+
}
|
package/src/config.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import { status } from "./status.mjs";
|
|
5
|
+
import { atomicWriteFileIfChangedSync } from "./atomic-file.mjs";
|
|
5
6
|
|
|
6
7
|
const CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
|
|
7
8
|
|
|
@@ -74,20 +75,23 @@ OPENAI_API_KEY = "dummy"
|
|
|
74
75
|
`;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
export function ensureCodexConfig(adapterPort = 2026) {
|
|
78
|
+
export function ensureCodexConfig(adapterPort = 2026, { filePath = CONFIG_PATH } = {}) {
|
|
78
79
|
const baseUrl = `http://127.0.0.1:${adapterPort}/v1`;
|
|
79
80
|
|
|
80
|
-
if (!fs.existsSync(
|
|
81
|
+
if (!fs.existsSync(filePath)) {
|
|
81
82
|
// Codex config does not exist yet; create the local proxy defaults.
|
|
82
|
-
|
|
83
|
-
fs.writeFileSync(CONFIG_PATH, initialCodexConfig(adapterPort));
|
|
83
|
+
atomicWriteFileIfChangedSync(filePath, initialCodexConfig(adapterPort));
|
|
84
84
|
console.log(status("ok", "Created ~/.codex/config.toml"));
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
89
89
|
const updated = computeUpdatedCodexConfig(content, adapterPort);
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
if (!updated.changed) {
|
|
92
|
+
console.log(status("ok", `Codex already points to ${baseUrl}`));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
atomicWriteFileIfChangedSync(filePath, updated.content);
|
|
92
96
|
console.log(status("ok", `Configured Codex base URL: ${baseUrl}`));
|
|
93
97
|
}
|