codex-copilot-dx 0.6.21 → 0.7.1
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 +142 -323
- package/bin/cli.mjs +26 -130
- package/package.json +2 -8
- package/scripts/benchmark.mjs +0 -114
- package/src/adapter.mjs +22 -107
- package/src/auth-profile.mjs +18 -130
- package/src/cli-animation.mjs +108 -0
- package/src/cli-auth.mjs +44 -398
- package/src/cli-models.mjs +21 -49
- package/src/cli-options.mjs +53 -75
- package/src/cli-status.mjs +6 -23
- package/src/config.mjs +2 -8
- package/src/copilot.mjs +2 -2
- package/src/doctor.mjs +44 -291
- package/src/model-cache.mjs +4 -11
- package/src/models.mjs +0 -184
- package/src/observability.mjs +1 -38
- package/src/profile-model-runtime.mjs +61 -189
- package/src/profile-routing.mjs +2 -102
- package/src/profile-runtime.mjs +15 -68
- package/src/responses-handler.mjs +1 -1
- package/src/responses-proxy.mjs +1 -1
- package/src/running-adapter.mjs +2 -4
- package/src/stream-errors.mjs +3 -6
- package/src/stream-performance.mjs +1 -1
- package/src/terminal-activity.mjs +40 -34
- package/src/terminal-animation.mjs +249 -0
- package/src/usage.mjs +0 -31
- package/src/user-settings.mjs +50 -0
- package/src/anthropic-handler.mjs +0 -195
- package/src/anthropic.mjs +0 -387
- package/src/claude-config.mjs +0 -57
- package/src/claude-desktop-config.mjs +0 -317
- package/src/cli-error.mjs +0 -50
- package/src/pm-studio-asar.mjs +0 -1244
- package/src/pm-studio-bundle-content.mjs +0 -165
- package/src/pm-studio-relay.mjs +0 -563
- package/src/pm-studio-setup-lock.mjs +0 -336
- package/src/pm-studio-setup.mjs +0 -1581
- package/src/pm-studio-status.mjs +0 -369
- package/src/token-counter-service.mjs +0 -211
- package/src/token-counter-worker.mjs +0 -16
package/src/adapter.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
-
import { createAnthropicCountTokensHandler, createAnthropicMessagesHandler } from "./anthropic-handler.mjs";
|
|
3
2
|
import { defaultCopilotClient } from "./copilot.mjs";
|
|
4
3
|
import { isValidModelList } from "./model-cache.mjs";
|
|
5
|
-
import { claudeDesktopModelsResponse } from "./models.mjs";
|
|
6
4
|
import {
|
|
7
5
|
ADAPTER_STATUS_PATH,
|
|
8
6
|
classifyAdapterRoute,
|
|
@@ -11,11 +9,9 @@ import {
|
|
|
11
9
|
runtimeStatusPayload,
|
|
12
10
|
} from "./observability.mjs";
|
|
13
11
|
import { createRequestId, runWithRequestContext } from "./request-context.mjs";
|
|
14
|
-
import { createPmStudioRelayHandler, PM_STUDIO_RELAY_PREFIX } from "./pm-studio-relay.mjs";
|
|
15
12
|
import { createStreamPerformanceMetrics } from "./stream-performance.mjs";
|
|
16
13
|
import { status } from "./status.mjs";
|
|
17
14
|
import { createTerminalActivityIndicator } from "./terminal-activity.mjs";
|
|
18
|
-
import { createTokenCounterService } from "./token-counter-service.mjs";
|
|
19
15
|
import { ADAPTER_HEALTH_PATH, adapterHealthPayload } from "./running-adapter.mjs";
|
|
20
16
|
import { createResponsesCompactHandler, createResponsesHandler } from "./responses-handler.mjs";
|
|
21
17
|
import { createResponsesImagePressureController } from "./responses-image-pressure.mjs";
|
|
@@ -63,17 +59,19 @@ export function requestPath(reqUrl) {
|
|
|
63
59
|
return new URL(reqUrl || "/", "http://localhost").pathname;
|
|
64
60
|
}
|
|
65
61
|
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
62
|
+
function sendRetiredClaudeSurface(res) {
|
|
63
|
+
res.writeHead(410, {
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
"Cache-Control": "no-store",
|
|
66
|
+
});
|
|
67
|
+
res.end(JSON.stringify({
|
|
68
|
+
type: "error",
|
|
69
|
+
error: {
|
|
70
|
+
type: "not_found_error",
|
|
71
|
+
code: "ccdx_claude_retired",
|
|
72
|
+
message: "Claude App and Claude Code support was retired in ccdx 0.7.0.",
|
|
73
|
+
},
|
|
74
|
+
}));
|
|
77
75
|
}
|
|
78
76
|
|
|
79
77
|
function modelListCanUseLastKnownGood(statusCode) {
|
|
@@ -93,50 +91,18 @@ function sendLastKnownGoodModels(res, modelRegistry) {
|
|
|
93
91
|
// Shared request handler. Keeping this separate from the listener makes the
|
|
94
92
|
// complete HTTP routing layer testable without opening a local port.
|
|
95
93
|
export function createAdapterHandler(options = {}) {
|
|
96
|
-
const claudeDesktopApiKey = options.claudeDesktopApiKey
|
|
97
|
-
|| process.env.CCDX_CLAUDE_DESKTOP_API_KEY
|
|
98
|
-
|| process.env.CCDX_PROXY_API_KEY
|
|
99
|
-
|| "";
|
|
100
94
|
const codexClient = options.codexClient || defaultCopilotClient;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
if (options.claudeMode === "isolated"
|
|
106
|
-
&& (!options.claudeClient || options.claudeClient === codexClient)) {
|
|
107
|
-
throw new Error("Isolated Claude mode requires a distinct Claude client");
|
|
108
|
-
}
|
|
109
|
-
if (options.claudeMode === "inherited"
|
|
110
|
-
&& options.claudeClient && options.claudeClient !== codexClient) {
|
|
111
|
-
throw new Error("Inherited Claude mode requires the Codex client");
|
|
112
|
-
}
|
|
113
|
-
const claudeClient = options.claudeClient || codexClient;
|
|
114
|
-
// chatCompletionsFn is retained as a compatibility injection and continues to
|
|
115
|
-
// override both surfaces in existing tests and embedders. New dual-profile
|
|
116
|
-
// callers should pass codexClient/claudeClient instead.
|
|
117
|
-
const codexChatCompletionsFn = options.codexChatCompletionsFn
|
|
95
|
+
// Keep the established injection names because Responses may still use the
|
|
96
|
+
// internal Chat Completions compatibility path for upstream models that need it.
|
|
97
|
+
const chatCompletionsFn = options.codexChatCompletionsFn
|
|
118
98
|
|| options.chatCompletionsFn
|
|
119
99
|
|| codexClient.chatCompletions;
|
|
120
|
-
const claudeChatCompletionsFn = options.claudeChatCompletionsFn
|
|
121
|
-
|| options.chatCompletionsFn
|
|
122
|
-
|| claudeClient.chatCompletions;
|
|
123
100
|
const responsesFn = options.responsesFn || codexClient.responses;
|
|
124
101
|
const responsesCompactFn = options.responsesCompactFn || codexClient.responsesCompact;
|
|
125
102
|
const listModelsFn = options.listModelsFn || codexClient.listModels;
|
|
126
103
|
const getCachedModelEndpointsFn = options.getCachedModelEndpointsFn
|
|
127
104
|
|| codexClient.getCachedModelEndpoints;
|
|
128
|
-
const claudeMode = hasExplicitClaudeMode
|
|
129
|
-
? options.claudeMode
|
|
130
|
-
: claudeClient === codexClient ? "inherited" : "isolated";
|
|
131
|
-
if (claudeMode === "isolated" && options.chatCompletionsFn) {
|
|
132
|
-
throw new Error("Isolated Claude mode cannot use the shared chatCompletionsFn override; pass codexChatCompletionsFn or claudeChatCompletionsFn explicitly");
|
|
133
|
-
}
|
|
134
105
|
const codexModelRegistry = options.codexModelRegistry || options.modelRegistry;
|
|
135
|
-
const claudeModelRegistry = options.claudeModelRegistry || options.modelRegistry || codexModelRegistry;
|
|
136
|
-
if (claudeMode === "isolated"
|
|
137
|
-
&& (!options.claudeModelRegistry || claudeModelRegistry === codexModelRegistry)) {
|
|
138
|
-
throw new Error("Isolated Claude mode requires a distinct Claude model registry");
|
|
139
|
-
}
|
|
140
106
|
const openAIModelEnv = options.openAIModelEnv || process.env;
|
|
141
107
|
const upstreamTimeoutMs = parsePositiveInteger(options.upstreamTimeoutMs, ADAPTER_RUNTIME_CONFIG.upstreamTimeoutMs);
|
|
142
108
|
const streamHandshakeTimeoutMs = parsePositiveInteger(options.streamHandshakeTimeoutMs, ADAPTER_RUNTIME_CONFIG.streamHandshakeTimeoutMs);
|
|
@@ -146,19 +112,10 @@ export function createAdapterHandler(options = {}) {
|
|
|
146
112
|
const requestMetrics = options.requestMetrics || createRequestMetrics();
|
|
147
113
|
const streamPerformanceMetrics = options.streamPerformanceMetrics || createStreamPerformanceMetrics();
|
|
148
114
|
const imagePressure = options.imagePressure || createResponsesImagePressureController();
|
|
149
|
-
const ownsTokenCounterService = !options.tokenCounterService;
|
|
150
|
-
const tokenCounterService = options.tokenCounterService || createTokenCounterService({
|
|
151
|
-
timeoutMs: options.countTokensTimeoutMs,
|
|
152
|
-
maxQueued: options.countTokensMaxQueued,
|
|
153
|
-
});
|
|
154
|
-
const claudeDesktopModelOptions = () => {
|
|
155
|
-
const modelDefs = claudeModelRegistry?.modelDefs || options.claudeDesktopModelDefs;
|
|
156
|
-
return Array.isArray(modelDefs) ? { modelDefs } : {};
|
|
157
|
-
};
|
|
158
115
|
const responsesHandler = createResponsesHandler({
|
|
159
116
|
acquireRequest,
|
|
160
117
|
autoReviewModelResolver: options.autoReviewModelResolver,
|
|
161
|
-
chatCompletionsFn
|
|
118
|
+
chatCompletionsFn,
|
|
162
119
|
getCachedModelEndpointsFn,
|
|
163
120
|
imagePressure,
|
|
164
121
|
modelRegistry: codexModelRegistry,
|
|
@@ -184,34 +141,6 @@ export function createAdapterHandler(options = {}) {
|
|
|
184
141
|
streamIdleTimeoutMs,
|
|
185
142
|
upstreamTimeoutMs,
|
|
186
143
|
});
|
|
187
|
-
const anthropicCountTokensHandler = createAnthropicCountTokensHandler({
|
|
188
|
-
acquireRequest,
|
|
189
|
-
requestBodyTimeoutMs,
|
|
190
|
-
countTokensFn: (body, countOptions) => tokenCounterService.count(body, countOptions),
|
|
191
|
-
});
|
|
192
|
-
const anthropicMessagesHandler = createAnthropicMessagesHandler({
|
|
193
|
-
acquireRequest,
|
|
194
|
-
chatCompletionsFn: claudeChatCompletionsFn,
|
|
195
|
-
environment: process.env,
|
|
196
|
-
modelOptions: claudeDesktopModelOptions,
|
|
197
|
-
requestBodyTimeoutMs,
|
|
198
|
-
streamHandshakeTimeoutMs,
|
|
199
|
-
streamIdleTimeoutMs,
|
|
200
|
-
upstreamTimeoutMs,
|
|
201
|
-
});
|
|
202
|
-
const pmStudioRelayHandler = createPmStudioRelayHandler({
|
|
203
|
-
acquireRequest,
|
|
204
|
-
claudeClient,
|
|
205
|
-
claudeMode,
|
|
206
|
-
claudeProfile: options.claudeProfile,
|
|
207
|
-
claudeModelRegistry,
|
|
208
|
-
fetchImpl: options.pmStudioFetchImpl,
|
|
209
|
-
requestBodyTimeoutMs,
|
|
210
|
-
streamHandshakeTimeoutMs,
|
|
211
|
-
streamIdleTimeoutMs,
|
|
212
|
-
upstreamTimeoutMs,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
144
|
const dispatch = (req, res, pathname) => {
|
|
216
145
|
if (req.method === "GET" && pathname === ADAPTER_STATUS_PATH) {
|
|
217
146
|
if (!isLoopbackAddress(req.socket?.remoteAddress)) {
|
|
@@ -227,11 +156,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
227
156
|
imagePressure,
|
|
228
157
|
modelRegistry: codexModelRegistry,
|
|
229
158
|
codexClient,
|
|
230
|
-
claudeClient,
|
|
231
159
|
codexModelRegistry,
|
|
232
|
-
claudeModelRegistry,
|
|
233
|
-
claudeMode,
|
|
234
|
-
isClaudeProfileCurrent: options.isClaudeProfileCurrent,
|
|
235
160
|
})));
|
|
236
161
|
return;
|
|
237
162
|
}
|
|
@@ -242,10 +167,6 @@ export function createAdapterHandler(options = {}) {
|
|
|
242
167
|
return;
|
|
243
168
|
}
|
|
244
169
|
|
|
245
|
-
if (pathname.startsWith(`${PM_STUDIO_RELAY_PREFIX}/`)) {
|
|
246
|
-
return pmStudioRelayHandler(req, res, pathname);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
170
|
if (req.method === "POST" && pathname === "/v1/responses") {
|
|
250
171
|
return responsesHandler(req, res);
|
|
251
172
|
}
|
|
@@ -255,11 +176,6 @@ export function createAdapterHandler(options = {}) {
|
|
|
255
176
|
}
|
|
256
177
|
|
|
257
178
|
if (req.method === "GET" && pathname === "/v1/models") {
|
|
258
|
-
if (shouldServeClaudeDesktopModels(req, claudeDesktopApiKey)) {
|
|
259
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
260
|
-
res.end(JSON.stringify(claudeDesktopModelsResponse(process.env, claudeDesktopModelOptions())));
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
179
|
const abort = createRequestAbort(req, res);
|
|
264
180
|
abort.setTimeout(upstreamTimeoutMs);
|
|
265
181
|
listModelsFn({ signal: abort.signal })
|
|
@@ -296,11 +212,11 @@ export function createAdapterHandler(options = {}) {
|
|
|
296
212
|
}
|
|
297
213
|
|
|
298
214
|
if (req.method === "POST" && pathname === "/v1/messages/count_tokens") {
|
|
299
|
-
return
|
|
215
|
+
return sendRetiredClaudeSurface(res);
|
|
300
216
|
}
|
|
301
217
|
|
|
302
218
|
if (req.method === "POST" && pathname === "/v1/messages") {
|
|
303
|
-
return
|
|
219
|
+
return sendRetiredClaudeSurface(res);
|
|
304
220
|
}
|
|
305
221
|
|
|
306
222
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -388,9 +304,6 @@ export function createAdapterHandler(options = {}) {
|
|
|
388
304
|
containUnexpectedError(error);
|
|
389
305
|
}
|
|
390
306
|
};
|
|
391
|
-
handler.cleanup = () => {
|
|
392
|
-
if (ownsTokenCounterService) tokenCounterService.close();
|
|
393
|
-
};
|
|
394
307
|
return handler;
|
|
395
308
|
}
|
|
396
309
|
|
|
@@ -398,7 +311,9 @@ export function createAdapterHandler(options = {}) {
|
|
|
398
311
|
|
|
399
312
|
export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
|
|
400
313
|
const ownsTerminalActivity = !Object.hasOwn(options, "terminalActivity");
|
|
401
|
-
const terminalActivity = ownsTerminalActivity
|
|
314
|
+
const terminalActivity = ownsTerminalActivity
|
|
315
|
+
? createTerminalActivityIndicator({ theme: options.terminalAnimationTheme })
|
|
316
|
+
: options.terminalActivity;
|
|
402
317
|
const handler = createAdapterHandler({ ...options, terminalActivity });
|
|
403
318
|
const server = http.createServer(handler);
|
|
404
319
|
const cleanupTerminalActivity = () => {
|
package/src/auth-profile.mjs
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { atomicWriteFilePairSync, atomicWriteFileSync } from "./atomic-file.mjs";
|
|
5
3
|
import {
|
|
6
4
|
githubTokenLockPath,
|
|
7
5
|
githubTokenMetadataPath,
|
|
@@ -11,172 +9,62 @@ import {
|
|
|
11
9
|
githubTokenFingerprint,
|
|
12
10
|
normalizeGithubIdentity,
|
|
13
11
|
} from "./github-identity.mjs";
|
|
14
|
-
import { withFileLock } from "./lock.mjs";
|
|
15
|
-
import { parseSafePositiveInteger, RUNTIME_DEFAULTS } from "./runtime-config.mjs";
|
|
16
12
|
|
|
17
13
|
export const AUTH_PROFILE_CODEX = "codex";
|
|
18
|
-
export const AUTH_PROFILE_CLAUDE = "claude";
|
|
19
|
-
|
|
20
|
-
const AUTH_PROFILES = new Set([AUTH_PROFILE_CODEX, AUTH_PROFILE_CLAUDE]);
|
|
21
14
|
|
|
22
15
|
function checkedProfile(profile) {
|
|
23
16
|
const value = String(profile || "").trim().toLowerCase();
|
|
24
|
-
if (
|
|
17
|
+
if (value !== AUTH_PROFILE_CODEX) {
|
|
18
|
+
throw new Error(`Unsupported authentication profile: ${profile}`);
|
|
19
|
+
}
|
|
25
20
|
return value;
|
|
26
21
|
}
|
|
27
22
|
|
|
28
23
|
export function authProfilePaths(profile, { home = os.homedir() } = {}) {
|
|
29
24
|
const name = checkedProfile(profile);
|
|
30
|
-
if (name === AUTH_PROFILE_CODEX) {
|
|
31
|
-
return {
|
|
32
|
-
profile: name,
|
|
33
|
-
tokenPath: githubTokenPath(home),
|
|
34
|
-
metadataPath: githubTokenMetadataPath(home),
|
|
35
|
-
lockPath: githubTokenLockPath(home),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const directory = path.join(home, ".local", "share", "copilot-api", "profiles", AUTH_PROFILE_CLAUDE);
|
|
40
|
-
const tokenPath = path.join(directory, "github_token");
|
|
41
25
|
return {
|
|
42
26
|
profile: name,
|
|
43
|
-
tokenPath,
|
|
44
|
-
metadataPath:
|
|
45
|
-
lockPath:
|
|
27
|
+
tokenPath: githubTokenPath(home),
|
|
28
|
+
metadataPath: githubTokenMetadataPath(home),
|
|
29
|
+
lockPath: githubTokenLockPath(home),
|
|
46
30
|
};
|
|
47
31
|
}
|
|
48
32
|
|
|
49
33
|
function readOptionalFile(filePath) {
|
|
50
34
|
try {
|
|
51
|
-
return { exists: true, data: fs.readFileSync(filePath)
|
|
35
|
+
return { exists: true, data: fs.readFileSync(filePath) };
|
|
52
36
|
} catch (error) {
|
|
53
|
-
if (error?.code === "ENOENT") return { exists: false, data: null
|
|
37
|
+
if (error?.code === "ENOENT") return { exists: false, data: null };
|
|
54
38
|
throw error;
|
|
55
39
|
}
|
|
56
40
|
}
|
|
57
41
|
|
|
58
42
|
function parsedMetadata(snapshot) {
|
|
59
|
-
if (!snapshot.exists) return
|
|
43
|
+
if (!snapshot.exists) return null;
|
|
60
44
|
try {
|
|
61
|
-
return
|
|
45
|
+
return JSON.parse(snapshot.data.toString("utf8"));
|
|
62
46
|
} catch {
|
|
63
|
-
return
|
|
47
|
+
return null;
|
|
64
48
|
}
|
|
65
49
|
}
|
|
66
50
|
|
|
67
51
|
export function readAuthProfileCredentials(profile, { home = os.homedir() } = {}) {
|
|
68
52
|
const paths = authProfilePaths(profile, { home });
|
|
69
53
|
const tokenSnapshot = readOptionalFile(paths.tokenPath);
|
|
70
|
-
const metadataSnapshot = readOptionalFile(paths.metadataPath);
|
|
71
54
|
const token = tokenSnapshot.exists ? tokenSnapshot.data.toString("utf8").trim() : "";
|
|
72
|
-
const
|
|
73
|
-
const metadataFingerprint = metadataResult.value?.token_fingerprint;
|
|
55
|
+
const metadata = parsedMetadata(readOptionalFile(paths.metadataPath));
|
|
74
56
|
const metadataMatchesToken = Boolean(token)
|
|
75
|
-
&& Boolean(
|
|
76
|
-
&&
|
|
77
|
-
const metadataIdentity = normalizeGithubIdentity(metadataResult.value);
|
|
78
|
-
const identity = metadataMatchesToken ? metadataIdentity : null;
|
|
79
|
-
|
|
80
|
-
if (paths.profile === AUTH_PROFILE_CODEX) {
|
|
81
|
-
const configured = tokenSnapshot.exists;
|
|
82
|
-
return {
|
|
83
|
-
profile: paths.profile,
|
|
84
|
-
configured,
|
|
85
|
-
valid: configured && Boolean(token),
|
|
86
|
-
reason: !configured ? "unconfigured" : token ? "" : "empty_token",
|
|
87
|
-
token,
|
|
88
|
-
identity,
|
|
89
|
-
metadata: metadataResult.value,
|
|
90
|
-
paths,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Metadata is written last, but any credential artifact reserves the Claude
|
|
95
|
-
// profile. A partial write must fail closed instead of borrowing Codex.
|
|
96
|
-
const configured = metadataSnapshot.exists || tokenSnapshot.exists;
|
|
97
|
-
let reason = "";
|
|
98
|
-
if (!configured) reason = "unconfigured";
|
|
99
|
-
else if (!metadataSnapshot.exists) reason = "missing_metadata";
|
|
100
|
-
else if (metadataResult.malformed) reason = "metadata_malformed";
|
|
101
|
-
else if (!tokenSnapshot.exists) reason = "missing_token";
|
|
102
|
-
else if (!token) reason = "empty_token";
|
|
103
|
-
else if (!metadataIdentity) reason = "metadata_identity_missing";
|
|
104
|
-
else if (!metadataFingerprint || metadataFingerprint !== githubTokenFingerprint(token)) reason = "token_metadata_mismatch";
|
|
57
|
+
&& Boolean(metadata?.token_fingerprint)
|
|
58
|
+
&& metadata.token_fingerprint === githubTokenFingerprint(token);
|
|
105
59
|
|
|
106
60
|
return {
|
|
107
61
|
profile: paths.profile,
|
|
108
|
-
configured,
|
|
109
|
-
valid:
|
|
110
|
-
reason,
|
|
62
|
+
configured: tokenSnapshot.exists,
|
|
63
|
+
valid: tokenSnapshot.exists && Boolean(token),
|
|
64
|
+
reason: !tokenSnapshot.exists ? "unconfigured" : token ? "" : "empty_token",
|
|
111
65
|
token,
|
|
112
|
-
identity,
|
|
113
|
-
metadata: metadataResult.value,
|
|
114
|
-
paths,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function lockOptions(env = process.env) {
|
|
119
|
-
return {
|
|
120
|
-
timeoutMs: parseSafePositiveInteger(env.CCDX_TOKEN_LOCK_TIMEOUT_MS, RUNTIME_DEFAULTS.tokenLockTimeoutMs),
|
|
121
|
-
staleMs: parseSafePositiveInteger(env.CCDX_TOKEN_LOCK_STALE_MS, RUNTIME_DEFAULTS.tokenLockStaleMs),
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function withAuthProfileLock(profile, fn, {
|
|
126
|
-
home = os.homedir(),
|
|
127
|
-
env = process.env,
|
|
128
|
-
...options
|
|
129
|
-
} = {}) {
|
|
130
|
-
const { lockPath } = authProfilePaths(profile, { home });
|
|
131
|
-
return withFileLock(lockPath, fn, { ...lockOptions(env), ...options });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function writeClaudeAuthProfile(token, identity, {
|
|
135
|
-
home = os.homedir(),
|
|
136
|
-
now = () => new Date(),
|
|
137
|
-
writeFile = atomicWriteFileSync,
|
|
138
|
-
unlinkFile = fs.unlinkSync,
|
|
139
|
-
} = {}) {
|
|
140
|
-
const cleanToken = String(token || "").trim();
|
|
141
|
-
const normalizedIdentity = normalizeGithubIdentity(identity);
|
|
142
|
-
if (!cleanToken) throw new Error("Claude GitHub token is empty");
|
|
143
|
-
if (!normalizedIdentity) throw new Error("Claude GitHub account identity is missing");
|
|
144
|
-
|
|
145
|
-
const paths = authProfilePaths(AUTH_PROFILE_CLAUDE, { home });
|
|
146
|
-
const directory = path.dirname(paths.tokenPath);
|
|
147
|
-
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
148
|
-
fs.chmodSync(directory, 0o700);
|
|
149
|
-
|
|
150
|
-
const timestamp = now();
|
|
151
|
-
const updatedAt = timestamp instanceof Date ? timestamp.toISOString() : new Date(timestamp).toISOString();
|
|
152
|
-
const metadata = `${JSON.stringify({
|
|
153
|
-
profile: AUTH_PROFILE_CLAUDE,
|
|
154
|
-
...normalizedIdentity,
|
|
155
|
-
token_fingerprint: githubTokenFingerprint(cleanToken),
|
|
156
|
-
updated_at: updatedAt,
|
|
157
|
-
}, null, 2)}\n`;
|
|
158
|
-
|
|
159
|
-
// Commit the activation marker last. Readers never observe a configured
|
|
160
|
-
// Claude profile unless the token and its fingerprint agree.
|
|
161
|
-
atomicWriteFilePairSync(
|
|
162
|
-
paths.tokenPath,
|
|
163
|
-
cleanToken,
|
|
164
|
-
paths.metadataPath,
|
|
66
|
+
identity: metadataMatchesToken ? normalizeGithubIdentity(metadata) : null,
|
|
165
67
|
metadata,
|
|
166
|
-
{ mode: 0o600, writeFile, unlinkFile },
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
profile: AUTH_PROFILE_CLAUDE,
|
|
171
|
-
identity: normalizedIdentity,
|
|
172
68
|
paths,
|
|
173
69
|
};
|
|
174
70
|
}
|
|
175
|
-
|
|
176
|
-
export function profileReauthMessage(profile, { home = os.homedir() } = {}) {
|
|
177
|
-
const name = checkedProfile(profile);
|
|
178
|
-
if (name === AUTH_PROFILE_CLAUDE) {
|
|
179
|
-
return "Claude authentication is unavailable. Run `ccdx auth login claude --reauth`, then restart ccdx.";
|
|
180
|
-
}
|
|
181
|
-
return `Codex authentication is unavailable at ${authProfilePaths(name, { home }).tokenPath}. Run ccdx again to log in.`;
|
|
182
|
-
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_TERMINAL_ANIMATION_THEME,
|
|
5
|
+
playTerminalAnimationPreview,
|
|
6
|
+
renderTerminalAnimationFrame,
|
|
7
|
+
TERMINAL_ANIMATION_THEMES,
|
|
8
|
+
} from "./terminal-animation.mjs";
|
|
9
|
+
import {
|
|
10
|
+
readUserSettings,
|
|
11
|
+
terminalAnimationPreference,
|
|
12
|
+
writeTerminalAnimationTheme,
|
|
13
|
+
} from "./user-settings.mjs";
|
|
14
|
+
|
|
15
|
+
const DISABLED_ENV_VALUES = new Set(["0", "false", "no", "off"]);
|
|
16
|
+
|
|
17
|
+
function markers(theme, currentTheme) {
|
|
18
|
+
const values = [];
|
|
19
|
+
if (theme.id === DEFAULT_TERMINAL_ANIMATION_THEME) values.push("default");
|
|
20
|
+
if (theme.id === currentTheme) values.push("current");
|
|
21
|
+
return values.length ? ` [${values.join(", ")}]` : "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function disabledEnvironmentValue(env) {
|
|
25
|
+
const value = String(env.CCDX_TERMINAL_ANIMATION || "").trim();
|
|
26
|
+
return DISABLED_ENV_VALUES.has(value.toLowerCase()) ? value : "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function runAnimationCommand({
|
|
30
|
+
commandName = "ccdx",
|
|
31
|
+
env = process.env,
|
|
32
|
+
home = os.homedir(),
|
|
33
|
+
input = process.stdin,
|
|
34
|
+
output = process.stdout,
|
|
35
|
+
prompt,
|
|
36
|
+
preview = playTerminalAnimationPreview,
|
|
37
|
+
} = {}) {
|
|
38
|
+
if (!prompt && (!input.isTTY || !output.isTTY)) {
|
|
39
|
+
throw new Error(`${commandName} animation requires an interactive terminal`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
readUserSettings({ env, home, strict: true });
|
|
43
|
+
const current = terminalAnimationPreference({ env, home });
|
|
44
|
+
const currentIndex = TERMINAL_ANIMATION_THEMES.findIndex(({ id }) => id === current.theme);
|
|
45
|
+
|
|
46
|
+
output.write(`${commandName} animation\n`);
|
|
47
|
+
output.write(`Current: ${TERMINAL_ANIMATION_THEMES[currentIndex].label} (${current.source})\n\n`);
|
|
48
|
+
TERMINAL_ANIMATION_THEMES.forEach((theme, index) => {
|
|
49
|
+
const staticPreview = theme.id === DEFAULT_TERMINAL_ANIMATION_THEME
|
|
50
|
+
? ` ${renderTerminalAnimationFrame(theme.id, 10)}`
|
|
51
|
+
: "";
|
|
52
|
+
output.write(` ${index + 1}. ${theme.label}${markers(theme, current.theme)}${staticPreview}\n`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
let readline;
|
|
56
|
+
const ask = prompt || (async (question) => {
|
|
57
|
+
readline ||= createInterface({ input, output });
|
|
58
|
+
return readline.question(question);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
let selectedTheme = current.theme;
|
|
62
|
+
let keepCurrent = false;
|
|
63
|
+
try {
|
|
64
|
+
while (true) {
|
|
65
|
+
const answer = String(await ask(`Select [${currentIndex + 1}], or q to cancel: `) || "").trim();
|
|
66
|
+
if (answer.toLowerCase() === "q") {
|
|
67
|
+
output.write("No changes made.\n");
|
|
68
|
+
return { changed: false, cancelled: true, theme: current.theme };
|
|
69
|
+
}
|
|
70
|
+
if (answer === "") {
|
|
71
|
+
keepCurrent = true;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
const selectedIndex = Number(answer) - 1;
|
|
75
|
+
if (Number.isInteger(selectedIndex)
|
|
76
|
+
&& selectedIndex >= 0
|
|
77
|
+
&& selectedIndex < TERMINAL_ANIMATION_THEMES.length) {
|
|
78
|
+
selectedTheme = TERMINAL_ANIMATION_THEMES[selectedIndex].id;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
output.write(`Enter a number from 1 to ${TERMINAL_ANIMATION_THEMES.length}, or q to cancel.\n`);
|
|
82
|
+
}
|
|
83
|
+
} finally {
|
|
84
|
+
readline?.close();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result = keepCurrent
|
|
88
|
+
? { changed: false, theme: current.theme }
|
|
89
|
+
: writeTerminalAnimationTheme(selectedTheme, { env, home });
|
|
90
|
+
const disabledValue = disabledEnvironmentValue(env);
|
|
91
|
+
if (!disabledValue) {
|
|
92
|
+
try {
|
|
93
|
+
await preview(selectedTheme, { output });
|
|
94
|
+
} catch {
|
|
95
|
+
output.write("[WARN] Preview unavailable; the terminal animation setting was still applied.\n");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const selected = TERMINAL_ANIMATION_THEMES.find(({ id }) => id === selectedTheme);
|
|
100
|
+
output.write(`${result.changed ? "Saved" : "Kept"} terminal animation: ${selected.label}\n`);
|
|
101
|
+
if (disabledValue) {
|
|
102
|
+
output.write(`CCDX_TERMINAL_ANIMATION=${disabledValue} remains effective until it is unset; terminal animation stays disabled.\n`);
|
|
103
|
+
output.write("The saved animation will be used on the next ccdx start after the override is unset.\n");
|
|
104
|
+
} else {
|
|
105
|
+
output.write("The next ccdx start will use this animation.\n");
|
|
106
|
+
}
|
|
107
|
+
return { ...result, cancelled: false, theme: selectedTheme };
|
|
108
|
+
}
|