@golba98/codexa 1.0.2 → 1.0.4
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 +396 -100
- package/bin/codexa.js +62 -144
- package/package.json +14 -8
- package/src/app.tsx +596 -306
- package/src/commands/handler.ts +6 -6
- package/src/config/buildInfo.ts +2 -2
- package/src/config/layeredConfig.ts +1 -1
- package/src/config/persistence.ts +10 -0
- package/src/config/runtimeConfig.ts +1 -1
- package/src/config/settings.ts +8 -16
- package/src/config/trustStore.ts +1 -1
- package/src/config/updateCheckCache.ts +19 -1
- package/src/core/README.md +52 -0
- package/src/core/agent/loop.ts +282 -0
- package/src/core/agent/protocol.ts +211 -0
- package/src/core/agent/tools.ts +414 -0
- package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
- package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
- package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
- package/src/core/debug/modelStateDebug.ts +34 -0
- package/src/core/executables/antigravityExecutable.ts +48 -0
- package/src/core/executables/codexExecutable.ts +1 -0
- package/src/core/executables/executableResolver.ts +65 -43
- package/src/core/perf/renderDebug.ts +10 -6
- package/src/core/process/processValidation.ts +9 -5
- package/src/core/providerLauncher/launcher.ts +59 -42
- package/src/core/providerLauncher/registry.ts +30 -14
- package/src/core/providerLauncher/types.ts +11 -9
- package/src/core/providerLauncher/workspaceConfig.ts +41 -26
- package/src/core/providerRuntime/anthropic.ts +7 -1
- package/src/core/providerRuntime/antigravity.ts +305 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
- package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
- package/src/core/providerRuntime/contextMetadata.ts +12 -24
- package/src/core/providerRuntime/local.ts +129 -51
- package/src/core/providerRuntime/models.ts +22 -11
- package/src/core/providerRuntime/registry.ts +58 -31
- package/src/core/providerRuntime/types.ts +19 -14
- package/src/core/providers/codexSubprocess.ts +2 -2
- package/src/core/providers/types.ts +1 -1
- package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
- package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
- package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
- package/src/core/terminal/clearFrameBoundary.ts +814 -0
- package/src/core/terminal/frameLock.ts +109 -0
- package/src/core/terminal/inkRenderReset.ts +123 -0
- package/src/core/terminal/terminalControl.ts +22 -0
- package/src/core/terminal/terminalTitle.ts +16 -102
- package/src/core/version/channel.ts +23 -0
- package/src/core/version/updateCheck.ts +193 -0
- package/src/core/{launchContext.ts → workspace/launchContext.ts} +32 -39
- package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
- package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
- package/src/headless/execRunner.ts +2 -2
- package/src/index.tsx +43 -89
- package/src/session/appSession.ts +10 -7
- package/src/session/chatLifecycle.ts +1 -1
- package/src/session/liveRenderScheduler.ts +1 -1
- package/src/session/types.ts +3 -2
- package/src/ui/ActionRequiredBlock.tsx +5 -5
- package/src/ui/ActivityBars.tsx +3 -3
- package/src/ui/ActivityIndicator.tsx +6 -6
- package/src/ui/AgentBlock.tsx +6 -6
- package/src/ui/AnimatedStatusText.tsx +1 -1
- package/src/ui/AppShell.tsx +670 -719
- package/src/ui/AttachmentImportPanel.tsx +8 -8
- package/src/ui/AuthPanel.tsx +20 -20
- package/src/ui/BottomComposer.tsx +158 -118
- package/src/ui/DashCard.tsx +3 -3
- package/src/ui/Markdown.tsx +17 -17
- package/src/ui/ModelPickerScreen.tsx +222 -42
- package/src/ui/ModelReasoningPicker.tsx +15 -15
- package/src/ui/Panel.tsx +3 -3
- package/src/ui/PlanActionPicker.tsx +6 -6
- package/src/ui/PlanReviewPanel.tsx +9 -9
- package/src/ui/ProviderPicker.tsx +735 -321
- package/src/ui/RunFooter.tsx +3 -3
- package/src/ui/RuntimeStatusBar.tsx +108 -0
- package/src/ui/SelectionPanel.tsx +8 -4
- package/src/ui/SettingsPanel.tsx +9 -9
- package/src/ui/Spinner.tsx +1 -1
- package/src/ui/TextEntryPanel.tsx +11 -11
- package/src/ui/ThinkingBlock.tsx +8 -8
- package/src/ui/Timeline.tsx +1625 -1472
- package/src/ui/TopHeader.tsx +437 -293
- package/src/ui/TranscriptShell.tsx +322 -0
- package/src/ui/TurnGroup.tsx +33 -33
- package/src/ui/UpdateAvailableCard.tsx +41 -0
- package/src/ui/UpdatePromptPanel.tsx +197 -0
- package/src/ui/focus.ts +3 -0
- package/src/ui/layout.ts +299 -25
- package/src/ui/layoutListWindow.ts +145 -0
- package/src/ui/logoVariants.ts +103 -0
- package/src/ui/modeDisplay.ts +12 -12
- package/src/ui/runtimeDisplay.ts +112 -0
- package/src/ui/textLayout.ts +15 -4
- package/src/ui/theme.tsx +274 -395
- package/src/ui/timelineMeasure.ts +218 -136
- package/scripts/audit-codexa-capabilities.mjs +0 -466
- package/scripts/gen-build-info.mjs +0 -33
- package/scripts/smoke-terminal-bench.mjs +0 -35
- package/src/appRenderStability.test.ts +0 -131
- package/src/commands/handler.test.ts +0 -655
- package/src/config/launchArgs.test.ts +0 -189
- package/src/config/layeredConfig.test.ts +0 -143
- package/src/config/persistence.test.ts +0 -114
- package/src/config/runtimeConfig.test.ts +0 -218
- package/src/config/settings.test.ts +0 -155
- package/src/config/trustStore.test.ts +0 -29
- package/src/core/attachments.test.ts +0 -155
- package/src/core/auth/codexAuth.test.ts +0 -68
- package/src/core/cleanupFastFail.test.ts +0 -76
- package/src/core/codex.ts +0 -124
- package/src/core/codexExecArgs.test.ts +0 -195
- package/src/core/codexLaunch.test.ts +0 -205
- package/src/core/codexPrompt.test.ts +0 -252
- package/src/core/executables/codexExecutable.test.ts +0 -212
- package/src/core/executables/executableResolver.test.ts +0 -129
- package/src/core/executables/geminiExecutable.test.ts +0 -116
- package/src/core/executables/pathSanityScan.test.ts +0 -47
- package/src/core/githubDiagnostics.test.ts +0 -92
- package/src/core/hollowResponseFormat.test.ts +0 -58
- package/src/core/launchContext.test.ts +0 -157
- package/src/core/models/codexCapabilities.test.ts +0 -45
- package/src/core/models/codexModelCapabilities.test.ts +0 -246
- package/src/core/models/modelSpecs.test.ts +0 -283
- package/src/core/perf/renderDebug.test.ts +0 -230
- package/src/core/planStorage.test.ts +0 -143
- package/src/core/process/CommandRunner.test.ts +0 -105
- package/src/core/projectInstructions.test.ts +0 -50
- package/src/core/providerLauncher/launcher.test.ts +0 -238
- package/src/core/providerLauncher/registry.test.ts +0 -324
- package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
- package/src/core/providerRuntime/anthropic.test.ts +0 -1120
- package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
- package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
- package/src/core/providerRuntime/gemini.test.ts +0 -437
- package/src/core/providerRuntime/lmstudio.test.ts +0 -168
- package/src/core/providerRuntime/local.test.ts +0 -787
- package/src/core/providerRuntime/registry.test.ts +0 -233
- package/src/core/providers/codexJsonStream.test.ts +0 -148
- package/src/core/providers/codexSubprocess.test.ts +0 -68
- package/src/core/providers/codexTranscript.test.ts +0 -284
- package/src/core/terminal/startupClear.test.ts +0 -55
- package/src/core/terminal/terminalCapabilities.test.ts +0 -93
- package/src/core/terminal/terminalControl.test.ts +0 -75
- package/src/core/terminal/terminalSanitize.test.ts +0 -22
- package/src/core/terminal/terminalSelection.test.ts +0 -42
- package/src/core/terminal/terminalTitle.test.ts +0 -328
- package/src/core/updateCheck.test.ts +0 -194
- package/src/core/updateCheck.ts +0 -172
- package/src/core/workspaceActivity.test.ts +0 -163
- package/src/core/workspaceGuard.test.ts +0 -151
- package/src/core/workspaceRoot.test.ts +0 -23
- package/src/exec.test.ts +0 -13
- package/src/headless/execArgs.test.ts +0 -147
- package/src/headless/execRunner.test.ts +0 -436
- package/src/index.test.tsx +0 -620
- package/src/session/appSession.test.ts +0 -897
- package/src/session/chatLifecycle.test.ts +0 -64
- package/src/session/liveRenderScheduler.test.ts +0 -201
- package/src/session/planFlow.test.ts +0 -103
- package/src/session/planTranscript.test.ts +0 -65
- package/src/session/promptRunSchedule.test.ts +0 -36
- package/src/ui/ActivityIndicator.test.tsx +0 -58
- package/src/ui/AgentBlock.test.ts +0 -6
- package/src/ui/AnimatedStatusText.test.ts +0 -16
- package/src/ui/AppShell.test.tsx +0 -1776
- package/src/ui/AttachmentImportPanel.test.tsx +0 -204
- package/src/ui/BottomComposer.test.ts +0 -674
- package/src/ui/CodexLogo.tsx +0 -55
- package/src/ui/Markdown.test.ts +0 -157
- package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
- package/src/ui/ModelPickerScreen.test.tsx +0 -99
- package/src/ui/ModelPickerState.test.tsx +0 -151
- package/src/ui/ModelReasoningPicker.test.tsx +0 -447
- package/src/ui/PlanReviewPanel.test.tsx +0 -267
- package/src/ui/PromptCardBorder.test.tsx +0 -161
- package/src/ui/ProviderPicker.test.tsx +0 -289
- package/src/ui/ProviderShortcut.test.tsx +0 -143
- package/src/ui/SettingsPanel.test.tsx +0 -233
- package/src/ui/StaticTranscriptItem.tsx +0 -56
- package/src/ui/Timeline.test.ts +0 -2067
- package/src/ui/TimelineNavigation.test.tsx +0 -201
- package/src/ui/TopHeader.test.tsx +0 -254
- package/src/ui/TurnGroup.test.tsx +0 -365
- package/src/ui/busyStatusAnimation.test.ts +0 -30
- package/src/ui/commandNormalize.test.ts +0 -142
- package/src/ui/diffRenderer.test.ts +0 -102
- package/src/ui/focusFlow.test.tsx +0 -1098
- package/src/ui/inputBuffer.test.ts +0 -151
- package/src/ui/layout.test.ts +0 -146
- package/src/ui/modeDisplay.test.ts +0 -42
- package/src/ui/runActivityView.test.ts +0 -89
- package/src/ui/runLifecycleView.test.tsx +0 -237
- package/src/ui/statusRenderIsolation.test.tsx +0 -654
- package/src/ui/terminalAnswerFormat.test.ts +0 -19
- package/src/ui/textLayout.test.ts +0 -18
- package/src/ui/themeFlow.test.ts +0 -53
- package/src/ui/timelineMeasureCache.test.ts +0 -986
- /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
- /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
- /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
- /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
- /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
- /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isTerminalResizing } from "./terminalControl.js";
|
|
4
|
+
|
|
5
|
+
export interface FrameLockOptions {
|
|
6
|
+
stdout: any;
|
|
7
|
+
env: Record<string, string | undefined>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const resizeFrameCacheResetters = new WeakMap<object, () => void>();
|
|
11
|
+
|
|
12
|
+
export function resetFrameLockForResize(stdout: object): void {
|
|
13
|
+
resizeFrameCacheResetters.get(stdout)?.();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Wraps the stdout stream to enforce frame-level deduplication, a flush lock,
|
|
18
|
+
* and width-safe row padding via ANSI clear-to-EOL (\x1b[K) injection.
|
|
19
|
+
*/
|
|
20
|
+
export function wrapStdoutWithFrameLock({
|
|
21
|
+
stdout,
|
|
22
|
+
env,
|
|
23
|
+
}: FrameLockOptions) {
|
|
24
|
+
let lastFrame = "";
|
|
25
|
+
let isFlushing = false;
|
|
26
|
+
let debugLogStream: fs.WriteStream | null = null;
|
|
27
|
+
|
|
28
|
+
if (env.CODEXA_RENDER_DEBUG === "1") {
|
|
29
|
+
try {
|
|
30
|
+
const logPath = env.CODEXA_RENDER_DEBUG_FILE?.trim()
|
|
31
|
+
|| path.join(process.cwd(), ".codexa", "debug", "render-status.log");
|
|
32
|
+
const logDir = path.dirname(logPath);
|
|
33
|
+
if (!fs.existsSync(logDir)) {
|
|
34
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
debugLogStream = fs.createWriteStream(logPath, { flags: "a" });
|
|
37
|
+
} catch (e) {
|
|
38
|
+
// Gracefully fall back if logging fails (e.g. read-only filesystem)
|
|
39
|
+
void e;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const logDebug = (msg: string) => {
|
|
44
|
+
if (debugLogStream) {
|
|
45
|
+
debugLogStream.write(`${new Date().toISOString()} ${msg}\n`);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const originalWrite = stdout.write.bind(stdout);
|
|
50
|
+
|
|
51
|
+
// A terminal resize can make an otherwise identical Ink frame meaningful:
|
|
52
|
+
// rows reflow at a new width and height-driven spacer/layout changes must be
|
|
53
|
+
// written even when their source text did not change. src/index.tsx owns the
|
|
54
|
+
// single resize listener and invokes this reset through the exported helper.
|
|
55
|
+
resizeFrameCacheResetters.set(stdout, () => {
|
|
56
|
+
lastFrame = "";
|
|
57
|
+
logDebug("Frame cache reset: terminal resize");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
stdout.write = (chunk: string | Uint8Array) => {
|
|
61
|
+
if (typeof chunk !== "string") {
|
|
62
|
+
return originalWrite(chunk);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Task 7: Frame Lock
|
|
66
|
+
// Ensure only one render flush can write to stdout at a time.
|
|
67
|
+
if (isFlushing) {
|
|
68
|
+
logDebug("Frame dropped: lock active (concurrent flush)");
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Task 8: Full-frame Deduplication
|
|
73
|
+
// Skip if identical to last frame.
|
|
74
|
+
if (chunk === lastFrame) {
|
|
75
|
+
logDebug("Frame dropped: identical to last frame");
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Task 4: Width-Safe Injection
|
|
80
|
+
// Ensure every row clears to the end of the line. This fixes artifacts
|
|
81
|
+
// left behind when a shorter row replaces a longer row or when terminal
|
|
82
|
+
// wrapping occurs during resize.
|
|
83
|
+
//
|
|
84
|
+
// We inject \x1b[K before every newline and at the end of the frame.
|
|
85
|
+
// padding via \x1b[K injection.
|
|
86
|
+
// Only apply if the chunk is non-empty and doesn't look like a control-only
|
|
87
|
+
// sequence (e.g. terminal title OSC).
|
|
88
|
+
let processed = chunk;
|
|
89
|
+
const isControlSequence = chunk.includes("\x1b]");
|
|
90
|
+
if (chunk.length > 0 && !isControlSequence) {
|
|
91
|
+
processed = chunk.replace(/\n/g, "\x1b[K\n");
|
|
92
|
+
if (!processed.endsWith("\x1b[K") && !processed.endsWith("\x1b[K\n")) {
|
|
93
|
+
processed += "\x1b[K";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
isFlushing = true;
|
|
98
|
+
try {
|
|
99
|
+
lastFrame = chunk;
|
|
100
|
+
const resizing = isTerminalResizing();
|
|
101
|
+
logDebug(`Frame written: length=${processed.length} original=${chunk.length} resizing=${resizing}`);
|
|
102
|
+
return originalWrite(processed);
|
|
103
|
+
} finally {
|
|
104
|
+
isFlushing = false;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return stdout;
|
|
109
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as renderDebug from "../perf/renderDebug.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Typed subset of the internal Ink class instance we reach into.
|
|
5
|
+
*
|
|
6
|
+
* `unsubscribeResize` is used at startup to disable Ink's competing resize
|
|
7
|
+
* handler (see src/index.tsx). The remaining fields are Ink's per-frame render
|
|
8
|
+
* caches; we reset them on the explicit /clear boundary so the next frame is
|
|
9
|
+
* written authoritatively from a clean baseline — exactly like the first frame
|
|
10
|
+
* after a cold startup — rather than diffed against pre-clear output.
|
|
11
|
+
*
|
|
12
|
+
* All fields are optional so the type also describes the minimal capability
|
|
13
|
+
* needed by callers that only disable resize, and so resolution degrades
|
|
14
|
+
* gracefully across Ink versions / test mocks.
|
|
15
|
+
*/
|
|
16
|
+
export interface InkRenderInstance {
|
|
17
|
+
unsubscribeResize?: () => void;
|
|
18
|
+
renderInteractiveFrame?: (output: string, outputHeight: number, staticOutput: string) => void;
|
|
19
|
+
lastOutput?: string;
|
|
20
|
+
lastOutputToRender?: string;
|
|
21
|
+
lastOutputHeight?: number;
|
|
22
|
+
lastTerminalWidth?: number;
|
|
23
|
+
fullStaticOutput?: string;
|
|
24
|
+
log?: { reset?: () => void };
|
|
25
|
+
throttledOnRender?: { cancel?: () => void };
|
|
26
|
+
throttledLog?: { cancel?: () => void };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AppStdoutLike {
|
|
30
|
+
columns?: number;
|
|
31
|
+
rows?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the real Ink class instance via Ink's internal WeakMap<stdout, Ink>.
|
|
36
|
+
* Returns null if resolution fails (e.g. different Ink version, test mocks).
|
|
37
|
+
*
|
|
38
|
+
* Bun doesn't resolve bare subpath imports like "ink/build/instances.js" so we
|
|
39
|
+
* resolve ink's main entry first, then derive the sibling path.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveInkRenderInstance(stdout: object): InkRenderInstance | null {
|
|
42
|
+
try {
|
|
43
|
+
const { createRequire } = require("node:module");
|
|
44
|
+
const req = createRequire(import.meta.url);
|
|
45
|
+
const inkMain = req.resolve("ink");
|
|
46
|
+
const instancesPath = inkMain.replace(/index\.js$/, "instances.js");
|
|
47
|
+
const instances = req(instancesPath);
|
|
48
|
+
const weakMap: WeakMap<object, InkRenderInstance> =
|
|
49
|
+
instances.default ?? instances;
|
|
50
|
+
return weakMap.get(stdout) ?? null;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ResetInkOutputOptions {
|
|
57
|
+
instance: InkRenderInstance | null;
|
|
58
|
+
/** Current terminal column count, used to reseat lastTerminalWidth. */
|
|
59
|
+
columns?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reset Ink's render-state caches to the fresh-startup baseline.
|
|
64
|
+
*
|
|
65
|
+
* Call this immediately AFTER the terminal has been physically cleared (e.g. by
|
|
66
|
+
* terminalControl.clearTranscript on /clear) and BEFORE the post-clear React
|
|
67
|
+
* render commits. It mirrors what a brand-new Ink instance looks like at cold
|
|
68
|
+
* startup: empty frame caches and zero previous height, so the next frame is
|
|
69
|
+
* authoritative and the subsequent resize math (Ink's renderInteractiveFrame /
|
|
70
|
+
* shouldClearTerminalForFrame) is evaluated against truthful state.
|
|
71
|
+
*
|
|
72
|
+
* This does NOT emit any clear/erase escape sequences itself — the terminal is
|
|
73
|
+
* already physically cleared, and Ink's log.reset() zeroes log-update's
|
|
74
|
+
* accounting without writing. Returns false (graceful no-op) if no live Ink
|
|
75
|
+
* instance is available.
|
|
76
|
+
*/
|
|
77
|
+
export function resetInkOutputForFreshFrame({ instance, columns }: ResetInkOutputOptions): boolean {
|
|
78
|
+
if (!instance) {
|
|
79
|
+
renderDebug.traceEvent("terminal", "clearRenderReset", { instanceResolved: false });
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const before = {
|
|
84
|
+
lastOutputLength: instance.lastOutput?.length ?? 0,
|
|
85
|
+
lastOutputToRenderLength: instance.lastOutputToRender?.length ?? 0,
|
|
86
|
+
lastOutputHeight: instance.lastOutputHeight ?? 0,
|
|
87
|
+
fullStaticOutputLength: instance.fullStaticOutput?.length ?? 0,
|
|
88
|
+
lastTerminalWidth: instance.lastTerminalWidth,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Drop any pending throttled render/log so a stale pre-clear frame can't be
|
|
92
|
+
// flushed after we reset the caches.
|
|
93
|
+
instance.throttledOnRender?.cancel?.();
|
|
94
|
+
instance.throttledLog?.cancel?.();
|
|
95
|
+
|
|
96
|
+
// Zero log-update's previousOutput/previousLineCount WITHOUT emitting escape
|
|
97
|
+
// sequences (the terminal was already physically cleared).
|
|
98
|
+
instance.log?.reset?.();
|
|
99
|
+
|
|
100
|
+
// Reset Ink's frame caches to the constructor/startup state.
|
|
101
|
+
instance.lastOutput = "";
|
|
102
|
+
instance.lastOutputToRender = "";
|
|
103
|
+
instance.lastOutputHeight = 0;
|
|
104
|
+
instance.fullStaticOutput = "";
|
|
105
|
+
if (typeof columns === "number" && Number.isFinite(columns)) {
|
|
106
|
+
instance.lastTerminalWidth = columns;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
renderDebug.traceEvent("terminal", "clearRenderReset", {
|
|
110
|
+
instanceResolved: true,
|
|
111
|
+
columns,
|
|
112
|
+
before,
|
|
113
|
+
after: {
|
|
114
|
+
lastOutputLength: instance.lastOutput.length,
|
|
115
|
+
lastOutputToRenderLength: instance.lastOutputToRender.length,
|
|
116
|
+
lastOutputHeight: instance.lastOutputHeight,
|
|
117
|
+
fullStaticOutputLength: instance.fullStaticOutput.length,
|
|
118
|
+
lastTerminalWidth: instance.lastTerminalWidth,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
@@ -14,11 +14,24 @@ export const TERMINAL_SEQUENCES = {
|
|
|
14
14
|
mouseEnable: "\x1b[?1000h\x1b[?1006h",
|
|
15
15
|
mouseDisable: "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l",
|
|
16
16
|
title: `\x1b]0;${TERMINAL_TITLE}\x07\x1b]2;${TERMINAL_TITLE}\x07`,
|
|
17
|
+
alternateScreenEnable: "\x1b[?1049h",
|
|
18
|
+
alternateScreenDisable: "\x1b[?1049l",
|
|
17
19
|
} as const;
|
|
18
20
|
|
|
19
21
|
export type TerminalWrite = (chunk: string) => boolean | void;
|
|
20
22
|
export type TerminalChannel = "stdout" | "stderr";
|
|
21
23
|
|
|
24
|
+
// Tracks whether the terminal is currently in a live resize debounce.
|
|
25
|
+
let terminalResizing = false;
|
|
26
|
+
|
|
27
|
+
export function setTerminalResizing(resizing: boolean): void {
|
|
28
|
+
terminalResizing = resizing;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isTerminalResizing(): boolean {
|
|
32
|
+
return terminalResizing;
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
// Tracks current UI state kind for diagnostic assertions.
|
|
23
36
|
let currentUIStateKind: string = "IDLE";
|
|
24
37
|
|
|
@@ -107,12 +120,14 @@ export interface TerminalModeController {
|
|
|
107
120
|
clearViewport(source: string): void;
|
|
108
121
|
setMouseReporting(enabled: boolean, source: string): void;
|
|
109
122
|
setBracketedPaste(enabled: boolean, source: string): void;
|
|
123
|
+
setAlternateScreen(enabled: boolean, source: string): void;
|
|
110
124
|
resetModes(): void;
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
export function createTerminalModeController(write: TerminalWrite): TerminalModeController {
|
|
114
128
|
let mouseReporting: boolean | null = null;
|
|
115
129
|
let bracketedPaste: boolean | null = null;
|
|
130
|
+
let alternateScreen: boolean | null = null;
|
|
116
131
|
|
|
117
132
|
const writeStdout = (sequence: string, source: string) =>
|
|
118
133
|
writeTerminalControl(write, "stdout", source, sequence);
|
|
@@ -135,13 +150,20 @@ export function createTerminalModeController(write: TerminalWrite): TerminalMode
|
|
|
135
150
|
bracketedPaste = enabled;
|
|
136
151
|
writeStdout(enabled ? TERMINAL_SEQUENCES.bracketedPasteEnable : TERMINAL_SEQUENCES.bracketedPasteDisable, source);
|
|
137
152
|
},
|
|
153
|
+
setAlternateScreen(enabled, source) {
|
|
154
|
+
if (alternateScreen === enabled) return;
|
|
155
|
+
alternateScreen = enabled;
|
|
156
|
+
writeStdout(enabled ? TERMINAL_SEQUENCES.alternateScreenEnable : TERMINAL_SEQUENCES.alternateScreenDisable, source);
|
|
157
|
+
},
|
|
138
158
|
resetModes() {
|
|
139
159
|
renderDebug.traceEvent("terminal", "resetModes", {
|
|
140
160
|
mouseReporting,
|
|
141
161
|
bracketedPaste,
|
|
162
|
+
alternateScreen,
|
|
142
163
|
});
|
|
143
164
|
mouseReporting = null;
|
|
144
165
|
bracketedPaste = null;
|
|
166
|
+
alternateScreen = null;
|
|
145
167
|
},
|
|
146
168
|
};
|
|
147
169
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { APP_NAME, type TerminalTitleMode, formatTerminalTitlePath } from "../../config/settings.js";
|
|
2
|
-
import { appendFileSync, mkdirSync } from "fs";
|
|
3
|
-
import { dirname, join } from "path";
|
|
1
|
+
import { APP_NAME, type TerminalTitleMode, formatTerminalTitlePath } from "../../config/settings.js";
|
|
2
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import * as renderDebug from "../perf/renderDebug.js";
|
|
4
5
|
|
|
5
6
|
export const DEFAULT_TERMINAL_TITLE = APP_NAME;
|
|
6
7
|
|
|
@@ -49,16 +50,17 @@ function findIncompleteOscTitleStart(text: string): number {
|
|
|
49
50
|
return -1;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
const TERMINAL_TITLE_DEBUG_LOG_PATH = process.env["CODEXA_TERMINAL_TITLE_DEBUG_FILE"]?.trim()
|
|
53
|
-
||
|
|
53
|
+
const TERMINAL_TITLE_DEBUG_LOG_PATH = process.env["CODEXA_TERMINAL_TITLE_DEBUG_FILE"]?.trim()
|
|
54
|
+
|| process.env["CODEXA_RENDER_DEBUG_FILE"]?.trim()
|
|
55
|
+
|| join(process.cwd(), ".codexa", "debug", "render-status.log");
|
|
54
56
|
|
|
55
57
|
let terminalTitleLifecycleState = "unknown";
|
|
56
58
|
|
|
57
|
-
function debugLog(msg: string): void {
|
|
58
|
-
if (DEBUG_TERMINAL_TITLE) {
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
}
|
|
59
|
+
function debugLog(msg: string): void {
|
|
60
|
+
if (DEBUG_TERMINAL_TITLE) {
|
|
61
|
+
writeTerminalTitleDebugRecord({ event: "debug", message: msg });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
62
64
|
|
|
63
65
|
function writeTerminalTitleDebugRecord(fields: Record<string, unknown>): void {
|
|
64
66
|
if (!DEBUG_TERMINAL_TITLE) return;
|
|
@@ -144,8 +146,9 @@ export function writeCodexaTerminalTitle(title: string, options?: TerminalTitleO
|
|
|
144
146
|
|
|
145
147
|
lastWrittenTerminalTitle = cleanTitle;
|
|
146
148
|
|
|
147
|
-
const sequence = buildTerminalTitleSequence(cleanTitle);
|
|
148
|
-
|
|
149
|
+
const sequence = buildTerminalTitleSequence(cleanTitle);
|
|
150
|
+
renderDebug.traceTerminalWrite("stdout", `terminalTitle:${options?.reason ?? "unknown"}`, sequence);
|
|
151
|
+
writeTerminalTitleDebugRecord({
|
|
149
152
|
event: "codexaTitleWrite",
|
|
150
153
|
title: cleanTitle,
|
|
151
154
|
reason: options?.reason ?? "unknown",
|
|
@@ -353,76 +356,7 @@ export function writeGuardedTerminalOutput(
|
|
|
353
356
|
return write(safeText) !== false;
|
|
354
357
|
}
|
|
355
358
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Schedules a sequence of title assertions to outlast Windows Terminal's
|
|
360
|
-
* shell integration, which overwrites the process title at unpredictable
|
|
361
|
-
* points during startup (shell prompt init, profile scripts, etc.).
|
|
362
|
-
* The retries at 50/250/500/1000ms are intentional: no single delay is
|
|
363
|
-
* reliable across all terminal configurations, so we assert multiple times.
|
|
364
|
-
*/
|
|
365
|
-
export function beginColdStartSequence(title: string, options?: { write?: (chunk: string) => void }) {
|
|
366
|
-
setIntendedTerminalTitle(title, { force: true, write: options?.write, reason: "cold-start-immediate" });
|
|
367
|
-
|
|
368
|
-
const timers: ReturnType<typeof setTimeout>[] = [];
|
|
369
|
-
let cancelled = false;
|
|
370
|
-
|
|
371
|
-
const scheduleRetry = (delayMs: number) => {
|
|
372
|
-
const id = setTimeout(() => {
|
|
373
|
-
if (!cancelled) {
|
|
374
|
-
debugLog(`cold-start retry t=${delayMs}ms fired`);
|
|
375
|
-
reassertIntendedTerminalTitle({ write: options?.write, reason: `cold-start-retry-${delayMs}ms` });
|
|
376
|
-
}
|
|
377
|
-
}, delayMs);
|
|
378
|
-
timers.push(id);
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
scheduleRetry(50);
|
|
382
|
-
scheduleRetry(250);
|
|
383
|
-
scheduleRetry(500);
|
|
384
|
-
scheduleRetry(1000);
|
|
385
|
-
|
|
386
|
-
return () => {
|
|
387
|
-
cancelled = true;
|
|
388
|
-
timers.forEach(clearTimeout);
|
|
389
|
-
debugLog("cold-start sequence cancelled");
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
export function startTerminalTitleStartupGuard(options?: {
|
|
394
|
-
write?: (chunk: string) => void;
|
|
395
|
-
intervalMs?: number;
|
|
396
|
-
durationMs?: number;
|
|
397
|
-
reason?: string;
|
|
398
|
-
}): () => void {
|
|
399
|
-
const intervalMs = options?.intervalMs ?? 150;
|
|
400
|
-
const durationMs = options?.durationMs ?? 3500;
|
|
401
|
-
const reason = options?.reason ?? "startup-guard";
|
|
402
|
-
let stopped = false;
|
|
403
|
-
const startedAt = Date.now();
|
|
404
|
-
|
|
405
|
-
reassertIntendedTerminalTitle({ write: options?.write, reason: `${reason}-start` });
|
|
406
|
-
const interval = setInterval(() => {
|
|
407
|
-
if (stopped) return;
|
|
408
|
-
if (Date.now() - startedAt >= durationMs) {
|
|
409
|
-
stopped = true;
|
|
410
|
-
clearInterval(interval);
|
|
411
|
-
reassertIntendedTerminalTitle({ write: options?.write, reason: `${reason}-end` });
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
reassertIntendedTerminalTitle({ write: options?.write, reason });
|
|
415
|
-
}, intervalMs);
|
|
416
|
-
interval.unref?.();
|
|
417
|
-
|
|
418
|
-
return () => {
|
|
419
|
-
if (stopped) return;
|
|
420
|
-
stopped = true;
|
|
421
|
-
clearInterval(interval);
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
export function sanitizeTerminalTitle(title: string): string {
|
|
359
|
+
export function sanitizeTerminalTitle(title: string): string {
|
|
426
360
|
return title
|
|
427
361
|
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
428
362
|
.replace(/\u001b/g, "")
|
|
@@ -461,23 +395,3 @@ export function reassertTerminalTitle(
|
|
|
461
395
|
write(buildTerminalTitleSequence(title));
|
|
462
396
|
}
|
|
463
397
|
|
|
464
|
-
/**
|
|
465
|
-
* Acquire a run-scoped title guard that immediately asserts the title,
|
|
466
|
-
* reasserts it periodically while work is active, and emits one final
|
|
467
|
-
* assertion when released.
|
|
468
|
-
*/
|
|
469
|
-
export function acquireTerminalTitleGuard(
|
|
470
|
-
intervalMs = 250,
|
|
471
|
-
reassert: () => void = () => reassertTerminalTitle(),
|
|
472
|
-
): () => void {
|
|
473
|
-
let released = false;
|
|
474
|
-
reassert();
|
|
475
|
-
const timer = setInterval(reassert, intervalMs);
|
|
476
|
-
timer.unref?.();
|
|
477
|
-
return () => {
|
|
478
|
-
if (released) return;
|
|
479
|
-
released = true;
|
|
480
|
-
clearInterval(timer);
|
|
481
|
-
reassert();
|
|
482
|
-
};
|
|
483
|
-
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { APP_VERSION } from "../../config/settings.js";
|
|
2
|
+
|
|
3
|
+
export const CODEXA_CHANNEL_ENV = "CODEXA_CHANNEL";
|
|
4
|
+
export const LOCAL_DEV_CHANNEL = "local-dev";
|
|
5
|
+
|
|
6
|
+
export function getCodexaChannel(env: NodeJS.ProcessEnv = process.env): string {
|
|
7
|
+
return env[CODEXA_CHANNEL_ENV]?.trim() || "published";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isLocalDevChannel(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
11
|
+
return getCodexaChannel(env) === LOCAL_DEV_CHANNEL;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatCodexaVersionLabel(
|
|
15
|
+
version: string = APP_VERSION,
|
|
16
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
17
|
+
): string {
|
|
18
|
+
return isLocalDevChannel(env) ? `${version}-dev local` : version;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatCodexaBrandLabel(env: NodeJS.ProcessEnv = process.env): string {
|
|
22
|
+
return `Codexa v${formatCodexaVersionLabel(APP_VERSION, env)}`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { APP_VERSION } from "../../config/settings.js";
|
|
2
|
+
import { isLocalDevChannel } from "./channel.js";
|
|
3
|
+
|
|
4
|
+
export const CODEXA_NPM_PACKAGE = "@golba98/codexa";
|
|
5
|
+
export const CODEXA_NPM_REGISTRY_URL = "https://registry.npmjs.org/@golba98%2Fcodexa";
|
|
6
|
+
export const CODEXA_UPDATE_COMMAND = `npm install -g ${CODEXA_NPM_PACKAGE}@latest`;
|
|
7
|
+
|
|
8
|
+
export type UpdateStatus = "up-to-date" | "update-available" | "unknown" | "error";
|
|
9
|
+
|
|
10
|
+
export interface NpmRegistryMetadata {
|
|
11
|
+
"dist-tags"?: { latest?: unknown };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UpdateCheckResult {
|
|
15
|
+
status: UpdateStatus;
|
|
16
|
+
currentVersion: string;
|
|
17
|
+
latestVersion: string | null;
|
|
18
|
+
errorMessage?: string;
|
|
19
|
+
checkedAt: number;
|
|
20
|
+
source?: "npm" | "cache";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const FETCH_TIMEOUT_MS = 5000;
|
|
24
|
+
|
|
25
|
+
/** Strip a leading "v" so "v1.0.2" and "1.0.2" are treated as equal. */
|
|
26
|
+
export function normalizeVersion(v: string): string {
|
|
27
|
+
return v.startsWith("v") ? v.slice(1) : v;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatVersionLabel(version: string): string {
|
|
31
|
+
const normalized = normalizeVersion(version.trim());
|
|
32
|
+
return normalized ? `v${normalized}` : version;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
36
|
+
|
|
37
|
+
/** Returns true for valid semver strings with or without a leading "v". */
|
|
38
|
+
export function isValidSemver(v: string): boolean {
|
|
39
|
+
return SEMVER_RE.test(normalizeVersion(v));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function shouldRunStartupUpdateCheck(
|
|
43
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
44
|
+
enabled = true,
|
|
45
|
+
): boolean {
|
|
46
|
+
return enabled && !isLocalDevChannel(env);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Compares two semver strings numerically. Returns negative if a < b, 0 if equal, positive if a > b.
|
|
50
|
+
// Pre-release versions (e.g. 1.0.2-beta.1) sort below their release counterpart (1.0.2 > 1.0.2-beta.1).
|
|
51
|
+
// Leading "v" is stripped before comparison.
|
|
52
|
+
export function compareSemver(a: string, b: string): number {
|
|
53
|
+
const parseParts = (v: string): { numeric: number[]; prerelease: string | null } => {
|
|
54
|
+
const norm = normalizeVersion(v);
|
|
55
|
+
const dashIdx = norm.indexOf("-");
|
|
56
|
+
const base = dashIdx === -1 ? norm : norm.slice(0, dashIdx);
|
|
57
|
+
const prerelease = dashIdx === -1 ? null : norm.slice(dashIdx + 1);
|
|
58
|
+
const numeric = base.split(".").map((p) => parseInt(p, 10) || 0);
|
|
59
|
+
return { numeric, prerelease };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const pa = parseParts(a);
|
|
63
|
+
const pb = parseParts(b);
|
|
64
|
+
const len = Math.max(pa.numeric.length, pb.numeric.length);
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < len; i++) {
|
|
67
|
+
const diff = (pa.numeric[i] ?? 0) - (pb.numeric[i] ?? 0);
|
|
68
|
+
if (diff !== 0) return diff;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Same numeric version: no pre-release > has pre-release
|
|
72
|
+
if (pa.prerelease === null && pb.prerelease !== null) return 1;
|
|
73
|
+
if (pa.prerelease !== null && pb.prerelease === null) return -1;
|
|
74
|
+
if (pa.prerelease !== null && pb.prerelease !== null) {
|
|
75
|
+
return pa.prerelease < pb.prerelease ? -1 : pa.prerelease > pb.prerelease ? 1 : 0;
|
|
76
|
+
}
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isNewerVersion(candidate: string, current: string): boolean {
|
|
81
|
+
return compareSemver(candidate, current) > 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface UpdateCheckOverrides {
|
|
85
|
+
currentVersion?: string;
|
|
86
|
+
fetchNpmMetadataFn?: (url: string) => Promise<NpmRegistryMetadata>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function defaultFetchNpmMetadata(url: string): Promise<NpmRegistryMetadata> {
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch(url, {
|
|
94
|
+
signal: controller.signal,
|
|
95
|
+
headers: { "User-Agent": `${CODEXA_NPM_PACKAGE}-update-checker/1.0` },
|
|
96
|
+
});
|
|
97
|
+
if (!res.ok) throw new Error(`npm registry returned HTTP ${res.status}`);
|
|
98
|
+
return await res.json() as NpmRegistryMetadata;
|
|
99
|
+
} finally {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function checkForUpdates(
|
|
105
|
+
opts?: { enabled?: boolean },
|
|
106
|
+
overrides?: UpdateCheckOverrides,
|
|
107
|
+
): Promise<UpdateCheckResult> {
|
|
108
|
+
const currentVersion = normalizeVersion(overrides?.currentVersion ?? APP_VERSION);
|
|
109
|
+
|
|
110
|
+
if (opts?.enabled === false) {
|
|
111
|
+
return { status: "unknown", currentVersion, latestVersion: null, checkedAt: Date.now(), source: "npm" };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const fetchFn = overrides?.fetchNpmMetadataFn ?? defaultFetchNpmMetadata;
|
|
116
|
+
const metadata = await fetchFn(CODEXA_NPM_REGISTRY_URL);
|
|
117
|
+
const rawLatest = metadata["dist-tags"]?.latest;
|
|
118
|
+
|
|
119
|
+
if (typeof rawLatest !== "string" || !rawLatest.trim()) {
|
|
120
|
+
return {
|
|
121
|
+
status: "error",
|
|
122
|
+
currentVersion,
|
|
123
|
+
latestVersion: null,
|
|
124
|
+
errorMessage: "npm registry response did not include dist-tags.latest",
|
|
125
|
+
checkedAt: Date.now(),
|
|
126
|
+
source: "npm",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const latestVersion = normalizeVersion(rawLatest.trim());
|
|
131
|
+
|
|
132
|
+
if (!isValidSemver(latestVersion) || !isValidSemver(currentVersion)) {
|
|
133
|
+
return {
|
|
134
|
+
status: "unknown",
|
|
135
|
+
currentVersion,
|
|
136
|
+
latestVersion: rawLatest,
|
|
137
|
+
errorMessage: `Invalid semver — current: "${currentVersion}", latest: "${rawLatest}"`,
|
|
138
|
+
checkedAt: Date.now(),
|
|
139
|
+
source: "npm",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const status = isNewerVersion(latestVersion, currentVersion) ? "update-available" : "up-to-date";
|
|
144
|
+
return { status, currentVersion, latestVersion, checkedAt: Date.now(), source: "npm" };
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return {
|
|
147
|
+
status: "error",
|
|
148
|
+
currentVersion,
|
|
149
|
+
latestVersion: null,
|
|
150
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
151
|
+
checkedAt: Date.now(),
|
|
152
|
+
source: "npm",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function formatUpdateInstructions(result: UpdateCheckResult | null): string {
|
|
158
|
+
const current = result?.currentVersion ?? APP_VERSION;
|
|
159
|
+
const latest = result?.latestVersion ?? "unknown";
|
|
160
|
+
|
|
161
|
+
if (result?.status === "error") {
|
|
162
|
+
return [
|
|
163
|
+
`Current installed version: ${current}`,
|
|
164
|
+
`npm latest version: ${latest}`,
|
|
165
|
+
`Error checking npm update status: ${result.errorMessage ?? "unknown error"}`,
|
|
166
|
+
].join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let statusLine: string;
|
|
170
|
+
if (result?.status === "update-available" && result.latestVersion) {
|
|
171
|
+
statusLine = `Update available: Codexa ${formatVersionLabel(result.latestVersion)}`;
|
|
172
|
+
} else if (result?.status === "up-to-date") {
|
|
173
|
+
statusLine = "Already up to date.";
|
|
174
|
+
} else {
|
|
175
|
+
statusLine = "Status unknown — could not reach npm registry.";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return [
|
|
179
|
+
`Current installed version: ${current}`,
|
|
180
|
+
`npm latest version: ${latest}`,
|
|
181
|
+
`Status: ${statusLine}`,
|
|
182
|
+
"",
|
|
183
|
+
`Run: ${CODEXA_UPDATE_COMMAND}`,
|
|
184
|
+
].join("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function formatLocalDevUpdateStatus(): string {
|
|
188
|
+
return [
|
|
189
|
+
"Running local-dev Codexa.",
|
|
190
|
+
"Automatic published npm update prompts are disabled for this channel.",
|
|
191
|
+
"Run /update check to explicitly check the published npm package.",
|
|
192
|
+
].join("\n");
|
|
193
|
+
}
|