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/profile-routing.mjs
CHANGED
|
@@ -1,103 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
isClaudeCopilotModel,
|
|
4
|
-
} from "./models.mjs";
|
|
5
|
-
import { createRoutePlan } from "./route-plan.mjs";
|
|
6
|
-
|
|
7
|
-
function catalogData(catalog) {
|
|
8
|
-
if (Array.isArray(catalog)) return catalog;
|
|
9
|
-
return Array.isArray(catalog?.data) ? catalog.data : [];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function profileRouting({ claudeMode = "inherited", claudeConfigured = false } = {}) {
|
|
13
|
-
const isolated = claudeMode === "isolated" || claudeConfigured === true;
|
|
14
|
-
return Object.freeze({ responses: "codex", messages: isolated ? "claude" : "codex" });
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function createPmStudioModelRouter({ getCatalog, isClaudeEnabled } = {}) {
|
|
18
|
-
if (typeof getCatalog !== "function") throw new Error("getCatalog is required");
|
|
19
|
-
if (typeof isClaudeEnabled !== "function") throw new Error("isClaudeEnabled is required");
|
|
20
|
-
|
|
21
|
-
const unset = Symbol("unset");
|
|
22
|
-
let cachedCatalog = unset;
|
|
23
|
-
let cachedEnabled = false;
|
|
24
|
-
let cachedAllowed = new Map();
|
|
25
|
-
let cachedKnown = new Set();
|
|
26
|
-
let rebuilds = 0;
|
|
27
|
-
|
|
28
|
-
function availability() {
|
|
29
|
-
const catalog = getCatalog();
|
|
30
|
-
const enabled = isClaudeEnabled() === true;
|
|
31
|
-
if (catalog === cachedCatalog && enabled === cachedEnabled) {
|
|
32
|
-
return { allowed: cachedAllowed, known: cachedKnown };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const allowed = new Map();
|
|
36
|
-
const known = new Set();
|
|
37
|
-
for (const model of catalogData(catalog)) {
|
|
38
|
-
const id = String(model?.id || "").trim();
|
|
39
|
-
if (!id || !isClaudeCopilotCatalogEntry(model)) continue;
|
|
40
|
-
known.add(id);
|
|
41
|
-
if (enabled && isClaudeCopilotModel(model) && !allowed.has(id)) allowed.set(id, model);
|
|
42
|
-
}
|
|
43
|
-
cachedCatalog = catalog;
|
|
44
|
-
cachedEnabled = enabled;
|
|
45
|
-
cachedAllowed = allowed;
|
|
46
|
-
cachedKnown = known;
|
|
47
|
-
rebuilds += 1;
|
|
48
|
-
return { allowed, known };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function classify(modelId) {
|
|
52
|
-
const id = String(modelId || "").trim();
|
|
53
|
-
const { allowed, known } = availability();
|
|
54
|
-
if (allowed.has(id)) return "claude";
|
|
55
|
-
if (known.has(id) || id.toLowerCase().startsWith("claude-")) return "unsupported_claude";
|
|
56
|
-
return "enterprise";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function plan(modelId) {
|
|
60
|
-
const id = String(modelId || "").trim();
|
|
61
|
-
const modelType = classify(id);
|
|
62
|
-
const protocol = "openai-chat-completions";
|
|
63
|
-
if (modelType === "claude") {
|
|
64
|
-
return createRoutePlan({
|
|
65
|
-
disposition: "relay",
|
|
66
|
-
origin: "ccdx",
|
|
67
|
-
profile: "claude",
|
|
68
|
-
protocol,
|
|
69
|
-
model: id,
|
|
70
|
-
surface: "pm-studio",
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
if (modelType === "unsupported_claude") {
|
|
74
|
-
return createRoutePlan({
|
|
75
|
-
disposition: "reject",
|
|
76
|
-
origin: "ccdx",
|
|
77
|
-
profile: "claude",
|
|
78
|
-
protocol,
|
|
79
|
-
model: id || "unknown",
|
|
80
|
-
surface: "pm-studio",
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
return createRoutePlan({
|
|
84
|
-
disposition: "native",
|
|
85
|
-
origin: "pm-studio-native",
|
|
86
|
-
profile: "enterprise",
|
|
87
|
-
protocol,
|
|
88
|
-
model: id || "unknown",
|
|
89
|
-
surface: "pm-studio",
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return Object.freeze({
|
|
94
|
-
plan,
|
|
95
|
-
classify,
|
|
96
|
-
allowedModels() {
|
|
97
|
-
return [...availability().allowed.values()];
|
|
98
|
-
},
|
|
99
|
-
diagnostics() {
|
|
100
|
-
return { rebuilds, allowed: cachedAllowed.size, known: cachedKnown.size };
|
|
101
|
-
},
|
|
102
|
-
});
|
|
1
|
+
export function profileRouting() {
|
|
2
|
+
return Object.freeze({ responses: "codex" });
|
|
103
3
|
}
|
package/src/profile-runtime.mjs
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AUTH_PROFILE_CLAUDE,
|
|
3
2
|
AUTH_PROFILE_CODEX,
|
|
4
|
-
authProfilePaths,
|
|
5
|
-
profileReauthMessage,
|
|
6
3
|
readAuthProfileCredentials,
|
|
7
4
|
} from "./auth-profile.mjs";
|
|
8
|
-
import {
|
|
5
|
+
import { defaultCopilotClient } from "./copilot.mjs";
|
|
9
6
|
import { githubTokenFingerprint } from "./github-identity.mjs";
|
|
10
7
|
|
|
11
8
|
function safeProfileStatus(credentials) {
|
|
@@ -28,75 +25,25 @@ export function createProfileRuntime({
|
|
|
28
25
|
home,
|
|
29
26
|
codexClient = defaultCopilotClient,
|
|
30
27
|
} = {}) {
|
|
31
|
-
|
|
32
|
-
profile: AUTH_PROFILE_CLAUDE,
|
|
33
|
-
configured: true,
|
|
34
|
-
valid: false,
|
|
35
|
-
reason: "credential_read_failed",
|
|
36
|
-
token: "",
|
|
37
|
-
identity: null,
|
|
38
|
-
paths: authProfilePaths(AUTH_PROFILE_CLAUDE, { home }),
|
|
39
|
-
});
|
|
40
|
-
const readCredentials = () => {
|
|
41
|
-
try {
|
|
42
|
-
return readAuthProfileCredentials(AUTH_PROFILE_CLAUDE, { home });
|
|
43
|
-
} catch {
|
|
44
|
-
return unreadableCredentials();
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
const credentials = readCredentials();
|
|
48
|
-
const claudeProfile = safeProfileStatus(credentials);
|
|
49
|
-
const claudeCredentialFingerprint = credentials.valid
|
|
50
|
-
? githubTokenFingerprint(credentials.token)
|
|
51
|
-
: "";
|
|
52
|
-
const isClaudeProfileCurrent = () => {
|
|
53
|
-
if (!claudeCredentialFingerprint) return false;
|
|
54
|
-
const current = readCredentials();
|
|
55
|
-
return current.valid === true
|
|
56
|
-
&& githubTokenFingerprint(current.token) === claudeCredentialFingerprint;
|
|
57
|
-
};
|
|
58
|
-
let codexCredentialFingerprint = "";
|
|
28
|
+
let credentials;
|
|
59
29
|
try {
|
|
60
|
-
|
|
61
|
-
if (codexCredentials.valid) {
|
|
62
|
-
codexCredentialFingerprint = githubTokenFingerprint(codexCredentials.token);
|
|
63
|
-
}
|
|
30
|
+
credentials = readAuthProfileCredentials(AUTH_PROFILE_CODEX, { home });
|
|
64
31
|
} catch {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
claudeMode: "inherited",
|
|
74
|
-
claudeProfile,
|
|
75
|
-
codexCredentialFingerprint,
|
|
76
|
-
claudeCredentialFingerprint: "",
|
|
77
|
-
isClaudeProfileCurrent,
|
|
78
|
-
});
|
|
32
|
+
credentials = {
|
|
33
|
+
profile: AUTH_PROFILE_CODEX,
|
|
34
|
+
configured: true,
|
|
35
|
+
valid: false,
|
|
36
|
+
reason: "credential_read_failed",
|
|
37
|
+
token: "",
|
|
38
|
+
identity: null,
|
|
39
|
+
};
|
|
79
40
|
}
|
|
80
41
|
|
|
81
|
-
const readRuntimeCredentials = credentials.reason === "credential_read_failed"
|
|
82
|
-
? () => credentials
|
|
83
|
-
: readCredentials;
|
|
84
|
-
const claudeClient = createCopilotClient({
|
|
85
|
-
profile: AUTH_PROFILE_CLAUDE,
|
|
86
|
-
home,
|
|
87
|
-
tokenPath: credentials.paths.tokenPath,
|
|
88
|
-
allowTokenDiscovery: false,
|
|
89
|
-
readGithubCredentials: readRuntimeCredentials,
|
|
90
|
-
reauthMessage: (reason) => `${reason}\n${profileReauthMessage(AUTH_PROFILE_CLAUDE, { home })}`,
|
|
91
|
-
});
|
|
92
|
-
|
|
93
42
|
return Object.freeze({
|
|
94
43
|
codexClient,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
claudeCredentialFingerprint,
|
|
100
|
-
isClaudeProfileCurrent,
|
|
44
|
+
codexProfile: safeProfileStatus(credentials),
|
|
45
|
+
codexCredentialFingerprint: credentials.valid
|
|
46
|
+
? githubTokenFingerprint(credentials.token)
|
|
47
|
+
: "",
|
|
101
48
|
});
|
|
102
49
|
}
|
|
@@ -286,7 +286,7 @@ export function createResponsesHandler(options) {
|
|
|
286
286
|
sendJsonError(res, responseError, statusCode || 500);
|
|
287
287
|
return;
|
|
288
288
|
}
|
|
289
|
-
await endStreamWithError(res,
|
|
289
|
+
await endStreamWithError(res, error || new Error(errMsg), abort);
|
|
290
290
|
}, {
|
|
291
291
|
signal: abort.signal,
|
|
292
292
|
abort,
|
package/src/responses-proxy.mjs
CHANGED
|
@@ -258,7 +258,7 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
|
|
|
258
258
|
}
|
|
259
259
|
} catch (e) {
|
|
260
260
|
logRequestFailure("Responses", e, options.abort);
|
|
261
|
-
await endStreamWithError(res,
|
|
261
|
+
await endStreamWithError(res, e, options.abort);
|
|
262
262
|
return { successful: false, compacted: false };
|
|
263
263
|
} finally {
|
|
264
264
|
await reader.cancel().catch(() => {});
|
package/src/running-adapter.mjs
CHANGED
|
@@ -3,11 +3,9 @@ import { localPackageVersion } from "./version.mjs";
|
|
|
3
3
|
|
|
4
4
|
export const ADAPTER_HEALTH_PATH = "/_ccdx/health";
|
|
5
5
|
export const ADAPTER_STATUS_PATH = "/_ccdx/status";
|
|
6
|
-
export const ADAPTER_PROTOCOL_VERSION =
|
|
6
|
+
export const ADAPTER_PROTOCOL_VERSION = 3;
|
|
7
7
|
export const ADAPTER_VERSION = localPackageVersion();
|
|
8
|
-
export const ADAPTER_CAPABILITIES = Object.freeze([
|
|
9
|
-
"pm_studio_split_origin_v1",
|
|
10
|
-
]);
|
|
8
|
+
export const ADAPTER_CAPABILITIES = Object.freeze([]);
|
|
11
9
|
const ADAPTER_INSTANCE_ID = randomUUID();
|
|
12
10
|
|
|
13
11
|
export function adapterHealthPayload() {
|
package/src/stream-errors.mjs
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import { writeOrDrain } from "./http-transport.mjs";
|
|
2
2
|
import { markStreamFailure } from "./stream-performance.mjs";
|
|
3
3
|
|
|
4
|
-
function streamErrorData(
|
|
4
|
+
function streamErrorData(error, abort) {
|
|
5
5
|
const code = abort?.reason || error?.code || "upstream_stream_error";
|
|
6
6
|
const message = `${code}: ${error?.message || "Upstream stream failed"}`;
|
|
7
|
-
if (protocol === "anthropic") {
|
|
8
|
-
return { type: "error", error: { type: "api_error", message } };
|
|
9
|
-
}
|
|
10
7
|
return { type: "error", code, message, param: null };
|
|
11
8
|
}
|
|
12
9
|
|
|
13
|
-
export async function endStreamWithError(res,
|
|
10
|
+
export async function endStreamWithError(res, error, abort) {
|
|
14
11
|
markStreamFailure();
|
|
15
12
|
if (res.destroyed || res.writableEnded) return;
|
|
16
|
-
const data = streamErrorData(
|
|
13
|
+
const data = streamErrorData(error, abort);
|
|
17
14
|
await writeOrDrain(res, `event: error\ndata: ${JSON.stringify(data)}\n\n`).catch(() => false);
|
|
18
15
|
if (!res.destroyed && !res.writableEnded) res.end();
|
|
19
16
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { performance } from "node:perf_hooks";
|
|
2
2
|
import { currentRequestContext } from "./request-context.mjs";
|
|
3
3
|
|
|
4
|
-
const PERFORMANCE_ROUTES = Object.freeze(["responses", "responses_compact"
|
|
4
|
+
const PERFORMANCE_ROUTES = Object.freeze(["responses", "responses_compact"]);
|
|
5
5
|
const TTFT_EDGES_MS = Object.freeze([
|
|
6
6
|
100, 200, 300, 500, 700, 1_000, 1_400, 2_000, 2_800, 4_000,
|
|
7
7
|
5_500, 8_000, 12_000, 16_000, 24_000, 36_000, 60_000, 120_000, 300_000,
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_TERMINAL_ANIMATION_THEME,
|
|
3
|
+
TERMINAL_ANIMATION_THEMES,
|
|
4
|
+
TERMINAL_ANIMATION_TRACK_WIDTH,
|
|
5
|
+
getTerminalAnimationCycleLength,
|
|
6
|
+
getTerminalAnimationFrameDelay,
|
|
7
|
+
isTerminalAnimationTheme,
|
|
8
|
+
renderTerminalAnimationFrame,
|
|
9
|
+
} from "./terminal-animation.mjs";
|
|
10
|
+
|
|
11
|
+
export { renderCometFrame } from "./terminal-animation.mjs";
|
|
12
|
+
|
|
1
13
|
const ESC = "\u001b";
|
|
2
14
|
const ERASE_LINE = `\r${ESC}[2K`;
|
|
3
15
|
const HIDE_CURSOR = `${ESC}[?25l`;
|
|
4
16
|
const SHOW_CURSOR = `${ESC}[?25h`;
|
|
5
17
|
|
|
6
|
-
const TRACK_WIDTH = 20;
|
|
7
|
-
const TAIL_LENGTH = 7;
|
|
8
18
|
const IDLE_DELAY_MS = 800;
|
|
9
|
-
const FRAME_DELAY_MS = 45;
|
|
10
|
-
const LOOP_PAUSE_MS = 200;
|
|
11
|
-
const TAIL_COLORS = [159, 123, 87, 51, 45, 39, 33];
|
|
12
19
|
|
|
13
20
|
function envFlagEnabled(value) {
|
|
14
21
|
const normalized = String(value || "").trim().toLowerCase();
|
|
@@ -21,22 +28,7 @@ function terminalActivityEnabled({ env, output, errorOutput }) {
|
|
|
21
28
|
if (envFlagEnabled(env.CI) || String(env.TERM || "").toLowerCase() === "dumb") return false;
|
|
22
29
|
if (output?.isTTY !== true || errorOutput?.isTTY !== true) return false;
|
|
23
30
|
const columns = Number(output.columns);
|
|
24
|
-
return !Number.isFinite(columns) || columns <= 0 || columns >=
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function renderCometFrame(position) {
|
|
28
|
-
let track = "";
|
|
29
|
-
for (let column = 0; column < TRACK_WIDTH; column += 1) {
|
|
30
|
-
const distance = position - column;
|
|
31
|
-
if (distance === 0) {
|
|
32
|
-
track += `${ESC}[97m:${ESC}[0m`;
|
|
33
|
-
} else if (distance > 0 && distance <= TAIL_LENGTH) {
|
|
34
|
-
track += `${ESC}[38;5;${TAIL_COLORS[distance - 1]}m:${ESC}[0m`;
|
|
35
|
-
} else {
|
|
36
|
-
track += " ";
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return `[${track}]`;
|
|
31
|
+
return !Number.isFinite(columns) || columns <= 0 || columns >= TERMINAL_ANIMATION_TRACK_WIDTH + 2;
|
|
40
32
|
}
|
|
41
33
|
|
|
42
34
|
function disabledIndicator() {
|
|
@@ -53,8 +45,13 @@ export function createTerminalActivityIndicator({
|
|
|
53
45
|
errorOutput = process.stderr,
|
|
54
46
|
consoleObj = console,
|
|
55
47
|
timers = { setTimeout, clearTimeout },
|
|
48
|
+
theme = DEFAULT_TERMINAL_ANIMATION_THEME,
|
|
56
49
|
} = {}) {
|
|
57
50
|
if (!terminalActivityEnabled({ env, output, errorOutput })) return disabledIndicator();
|
|
51
|
+
if (!isTerminalAnimationTheme(theme)) throw new TypeError(`Unknown terminal animation theme: ${String(theme)}`);
|
|
52
|
+
|
|
53
|
+
const themeMetadata = TERMINAL_ANIMATION_THEMES.find(({ id }) => id === theme);
|
|
54
|
+
const cycleLength = getTerminalAnimationCycleLength(theme);
|
|
58
55
|
|
|
59
56
|
const methods = ["log", "warn", "error", "debug"];
|
|
60
57
|
const originals = Object.fromEntries(methods.map((method) => [method, consoleObj[method]]));
|
|
@@ -81,9 +78,12 @@ export function createTerminalActivityIndicator({
|
|
|
81
78
|
clearTimer(frameTimer);
|
|
82
79
|
frameTimer = null;
|
|
83
80
|
framePosition = 0;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
try {
|
|
82
|
+
if (cursorHidden || frameVisible) output.write(`${ERASE_LINE}${SHOW_CURSOR}`);
|
|
83
|
+
} finally {
|
|
84
|
+
cursorHidden = false;
|
|
85
|
+
frameVisible = false;
|
|
86
|
+
}
|
|
87
87
|
};
|
|
88
88
|
|
|
89
89
|
const cancelPendingOutput = () => {
|
|
@@ -98,20 +98,23 @@ export function createTerminalActivityIndicator({
|
|
|
98
98
|
clearAnimation();
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
|
-
if (framePosition >=
|
|
102
|
-
output.write(ERASE_LINE);
|
|
103
|
-
frameVisible = false;
|
|
101
|
+
if (framePosition >= cycleLength) {
|
|
104
102
|
framePosition = 0;
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
if (themeMetadata.loopPauseMs > 0) {
|
|
104
|
+
output.write(ERASE_LINE);
|
|
105
|
+
frameVisible = false;
|
|
106
|
+
frameTimer = schedule(drawNextFrame, themeMetadata.loopPauseMs);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
const frameIndex = themeMetadata.startFrame + framePosition;
|
|
109
112
|
const prefix = cursorHidden ? "" : HIDE_CURSOR;
|
|
110
113
|
cursorHidden = true;
|
|
111
114
|
frameVisible = true;
|
|
112
|
-
output.write(`${prefix}${ERASE_LINE}${
|
|
115
|
+
output.write(`${prefix}${ERASE_LINE}${renderTerminalAnimationFrame(theme, frameIndex)}`);
|
|
113
116
|
framePosition += 1;
|
|
114
|
-
frameTimer = schedule(drawNextFrame,
|
|
117
|
+
frameTimer = schedule(drawNextFrame, getTerminalAnimationFrameDelay(theme, frameIndex));
|
|
115
118
|
};
|
|
116
119
|
|
|
117
120
|
const scheduleAfterIdle = () => {
|
|
@@ -161,9 +164,12 @@ export function createTerminalActivityIndicator({
|
|
|
161
164
|
activeRequests = 0;
|
|
162
165
|
clearTimer(idleTimer);
|
|
163
166
|
idleTimer = null;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
+
try {
|
|
168
|
+
clearAnimation();
|
|
169
|
+
} finally {
|
|
170
|
+
for (const method of methods) {
|
|
171
|
+
if (consoleObj[method] === wrappers[method]) consoleObj[method] = originals[method];
|
|
172
|
+
}
|
|
167
173
|
}
|
|
168
174
|
},
|
|
169
175
|
};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
const ESC = "\u001b";
|
|
2
|
+
|
|
3
|
+
export const TERMINAL_ANIMATION_TRACK_WIDTH = 20;
|
|
4
|
+
export const DEFAULT_TERMINAL_ANIMATION_THEME = "comet";
|
|
5
|
+
|
|
6
|
+
const TAIL_LENGTH = 7;
|
|
7
|
+
const TAIL_COLORS = Object.freeze([159, 123, 87, 51, 45, 39, 33]);
|
|
8
|
+
const BRAILLE_GLYPHS = Object.freeze(["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]);
|
|
9
|
+
|
|
10
|
+
function colorCell(glyph, color) {
|
|
11
|
+
return `${ESC}[${color}m${glyph}${ESC}[0m`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function blankTrack() {
|
|
15
|
+
return Array.from({ length: TERMINAL_ANIMATION_TRACK_WIDTH }, () => " ");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function setCell(cells, index, glyph, color) {
|
|
19
|
+
if (index < 0 || index >= TERMINAL_ANIMATION_TRACK_WIDTH) return;
|
|
20
|
+
cells[index] = colorCell(glyph, color);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function drawComet(cells, head, direction, tailLength = TAIL_LENGTH) {
|
|
24
|
+
for (let distance = tailLength; distance >= 1; distance -= 1) {
|
|
25
|
+
setCell(cells, head - (direction * distance), ":", `38;5;${TAIL_COLORS[distance - 1]}`);
|
|
26
|
+
}
|
|
27
|
+
setCell(cells, head, ":", "97");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function wrapTrack(cells) {
|
|
31
|
+
return `[${cells.join("")}]`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function pingPongState(index) {
|
|
35
|
+
const period = (TERMINAL_ANIMATION_TRACK_WIDTH - 1) * 2;
|
|
36
|
+
const offset = index % period;
|
|
37
|
+
return offset < TERMINAL_ANIMATION_TRACK_WIDTH
|
|
38
|
+
? { position: offset, direction: 1 }
|
|
39
|
+
: { position: period - offset, direction: -1 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const THEME_DEFINITIONS = Object.freeze([
|
|
43
|
+
Object.freeze({
|
|
44
|
+
id: "comet",
|
|
45
|
+
label: "Comet",
|
|
46
|
+
frameCount: TERMINAL_ANIMATION_TRACK_WIDTH + TAIL_LENGTH,
|
|
47
|
+
frameDelayMs: 45,
|
|
48
|
+
loopPauseMs: 200,
|
|
49
|
+
startFrame: 0,
|
|
50
|
+
render: renderCometFrame,
|
|
51
|
+
}),
|
|
52
|
+
Object.freeze({
|
|
53
|
+
id: "twin",
|
|
54
|
+
label: "Twin",
|
|
55
|
+
frameCount: 26,
|
|
56
|
+
frameDelayMs: 60,
|
|
57
|
+
loopPauseMs: 0,
|
|
58
|
+
startFrame: 5,
|
|
59
|
+
render(index) {
|
|
60
|
+
const cells = blankTrack();
|
|
61
|
+
drawComet(cells, index - 3, 1, 4);
|
|
62
|
+
drawComet(cells, TERMINAL_ANIMATION_TRACK_WIDTH + 2 - index, -1, 4);
|
|
63
|
+
return wrapTrack(cells);
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
Object.freeze({
|
|
67
|
+
id: "shuttle",
|
|
68
|
+
label: "Shuttle",
|
|
69
|
+
frameCount: (TERMINAL_ANIMATION_TRACK_WIDTH - 1) * 2,
|
|
70
|
+
frameDelayMs: 55,
|
|
71
|
+
loopPauseMs: 0,
|
|
72
|
+
startFrame: 7,
|
|
73
|
+
frameDelay(index) {
|
|
74
|
+
const { position } = pingPongState(index);
|
|
75
|
+
return position === 0 || position === TERMINAL_ANIMATION_TRACK_WIDTH - 1 ? 160 : 55;
|
|
76
|
+
},
|
|
77
|
+
render(index) {
|
|
78
|
+
const cells = blankTrack();
|
|
79
|
+
const { position, direction } = pingPongState(index);
|
|
80
|
+
drawComet(cells, position, direction);
|
|
81
|
+
return wrapTrack(cells);
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
Object.freeze({
|
|
85
|
+
id: "chase",
|
|
86
|
+
label: "Chase",
|
|
87
|
+
frameCount: TERMINAL_ANIMATION_TRACK_WIDTH + 14,
|
|
88
|
+
frameDelayMs: 65,
|
|
89
|
+
loopPauseMs: 0,
|
|
90
|
+
startFrame: 13,
|
|
91
|
+
render(index) {
|
|
92
|
+
const cells = blankTrack();
|
|
93
|
+
const first = index - 4;
|
|
94
|
+
let second = first - 11;
|
|
95
|
+
if (second < -4) second += TERMINAL_ANIMATION_TRACK_WIDTH + 14;
|
|
96
|
+
drawComet(cells, first, 1, 4);
|
|
97
|
+
drawComet(cells, second, 1, 4);
|
|
98
|
+
return wrapTrack(cells);
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
Object.freeze({
|
|
102
|
+
id: "mirror",
|
|
103
|
+
label: "Mirror",
|
|
104
|
+
frameCount: (Math.floor(TERMINAL_ANIMATION_TRACK_WIDTH / 2) - 1) * 2,
|
|
105
|
+
frameDelayMs: 75,
|
|
106
|
+
loopPauseMs: 0,
|
|
107
|
+
startFrame: 3,
|
|
108
|
+
render(index) {
|
|
109
|
+
const cells = blankTrack();
|
|
110
|
+
const half = Math.floor(TERMINAL_ANIMATION_TRACK_WIDTH / 2);
|
|
111
|
+
const period = (half - 1) * 2;
|
|
112
|
+
const distance = index < half ? index : period - index;
|
|
113
|
+
drawComet(cells, half - 1 - distance, -1, 3);
|
|
114
|
+
drawComet(cells, half + distance, 1, 3);
|
|
115
|
+
return wrapTrack(cells);
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
Object.freeze({
|
|
119
|
+
id: "pulse",
|
|
120
|
+
label: "Pulse",
|
|
121
|
+
frameCount: 8,
|
|
122
|
+
frameDelayMs: 115,
|
|
123
|
+
loopPauseMs: 0,
|
|
124
|
+
startFrame: 2,
|
|
125
|
+
render(index) {
|
|
126
|
+
const cells = blankTrack();
|
|
127
|
+
const radii = [0, 1, 2, 3, 4, 3, 2, 1];
|
|
128
|
+
const radius = radii[index];
|
|
129
|
+
const center = Math.floor((TERMINAL_ANIMATION_TRACK_WIDTH - 1) / 2);
|
|
130
|
+
setCell(cells, center, ":", "97");
|
|
131
|
+
for (let distance = 1; distance <= radius; distance += 1) {
|
|
132
|
+
const color = `38;5;${TAIL_COLORS[distance]}`;
|
|
133
|
+
setCell(cells, center - distance, ":", color);
|
|
134
|
+
setCell(cells, center + distance, ":", color);
|
|
135
|
+
}
|
|
136
|
+
return wrapTrack(cells);
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
Object.freeze({
|
|
140
|
+
id: "braille",
|
|
141
|
+
label: "Braille",
|
|
142
|
+
frameCount: BRAILLE_GLYPHS.length,
|
|
143
|
+
frameDelayMs: 80,
|
|
144
|
+
loopPauseMs: 0,
|
|
145
|
+
startFrame: 0,
|
|
146
|
+
render(index) {
|
|
147
|
+
const cells = blankTrack();
|
|
148
|
+
setCell(cells, Math.floor(TERMINAL_ANIMATION_TRACK_WIDTH / 2), BRAILLE_GLYPHS[index], `38;5;${TAIL_COLORS[2]}`);
|
|
149
|
+
return wrapTrack(cells);
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
const DEFINITIONS_BY_ID = new Map(THEME_DEFINITIONS.map((theme) => [theme.id, theme]));
|
|
155
|
+
|
|
156
|
+
export const TERMINAL_ANIMATION_THEMES = Object.freeze(THEME_DEFINITIONS.map(({
|
|
157
|
+
id,
|
|
158
|
+
label,
|
|
159
|
+
frameCount,
|
|
160
|
+
frameDelayMs,
|
|
161
|
+
loopPauseMs,
|
|
162
|
+
startFrame,
|
|
163
|
+
}) => Object.freeze({ id, label, frameCount, frameDelayMs, loopPauseMs, startFrame })));
|
|
164
|
+
|
|
165
|
+
export function isTerminalAnimationTheme(value) {
|
|
166
|
+
return typeof value === "string" && DEFINITIONS_BY_ID.has(value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function themeDefinition(theme) {
|
|
170
|
+
const definition = DEFINITIONS_BY_ID.get(theme);
|
|
171
|
+
if (!definition) throw new TypeError(`Unknown terminal animation theme: ${String(theme)}`);
|
|
172
|
+
return definition;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizedFrameIndex(frameIndex, frameCount) {
|
|
176
|
+
if (!Number.isSafeInteger(frameIndex)) throw new TypeError("Terminal animation frame index must be an integer");
|
|
177
|
+
return ((frameIndex % frameCount) + frameCount) % frameCount;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function getTerminalAnimationCycleLength(theme) {
|
|
181
|
+
return themeDefinition(theme).frameCount;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function getTerminalAnimationFrameDelay(theme, frameIndex) {
|
|
185
|
+
const definition = themeDefinition(theme);
|
|
186
|
+
const index = normalizedFrameIndex(frameIndex, definition.frameCount);
|
|
187
|
+
return definition.frameDelay?.(index) ?? definition.frameDelayMs;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function renderTerminalAnimationFrame(theme, frameIndex) {
|
|
191
|
+
const definition = themeDefinition(theme);
|
|
192
|
+
return definition.render(normalizedFrameIndex(frameIndex, definition.frameCount));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function renderCometFrame(position) {
|
|
196
|
+
let track = "";
|
|
197
|
+
for (let column = 0; column < TERMINAL_ANIMATION_TRACK_WIDTH; column += 1) {
|
|
198
|
+
const distance = position - column;
|
|
199
|
+
if (distance === 0) {
|
|
200
|
+
track += `${ESC}[97m:${ESC}[0m`;
|
|
201
|
+
} else if (distance > 0 && distance <= TAIL_LENGTH) {
|
|
202
|
+
track += `${ESC}[38;5;${TAIL_COLORS[distance - 1]}m:${ESC}[0m`;
|
|
203
|
+
} else {
|
|
204
|
+
track += " ";
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return `[${track}]`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function outputSupportsPreview(output) {
|
|
211
|
+
if (output?.isTTY !== true) return false;
|
|
212
|
+
const columns = Number(output.columns);
|
|
213
|
+
return !Number.isFinite(columns) || columns <= 0 || columns >= TERMINAL_ANIMATION_TRACK_WIDTH + 2;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function playTerminalAnimationPreview(theme, {
|
|
217
|
+
output = process.stdout,
|
|
218
|
+
timers = { setTimeout, clearTimeout },
|
|
219
|
+
} = {}) {
|
|
220
|
+
const definition = themeDefinition(theme);
|
|
221
|
+
if (!outputSupportsPreview(output)) return false;
|
|
222
|
+
|
|
223
|
+
const eraseLine = `\r${ESC}[2K`;
|
|
224
|
+
let timer = null;
|
|
225
|
+
let previewStarted = false;
|
|
226
|
+
|
|
227
|
+
const wait = (delay) => new Promise((resolve) => {
|
|
228
|
+
let settled = false;
|
|
229
|
+
const nextTimer = timers.setTimeout(() => {
|
|
230
|
+
settled = true;
|
|
231
|
+
timer = null;
|
|
232
|
+
resolve();
|
|
233
|
+
}, delay);
|
|
234
|
+
timer = settled ? null : nextTimer;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
for (let offset = 0; offset < definition.frameCount; offset += 1) {
|
|
239
|
+
const frameIndex = definition.startFrame + offset;
|
|
240
|
+
previewStarted = true;
|
|
241
|
+
output.write(`${eraseLine}${renderTerminalAnimationFrame(theme, frameIndex)}`);
|
|
242
|
+
await wait(getTerminalAnimationFrameDelay(theme, frameIndex));
|
|
243
|
+
}
|
|
244
|
+
return true;
|
|
245
|
+
} finally {
|
|
246
|
+
if (timer) timers.clearTimeout(timer);
|
|
247
|
+
if (previewStarted) output.write(eraseLine);
|
|
248
|
+
}
|
|
249
|
+
}
|