@golba98/codexa 1.0.5 → 1.0.7
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 +119 -14
- package/bin/codexa.js +15 -4
- package/package.json +1 -1
- package/src/app.tsx +65 -81
- package/src/commands/handler.ts +13 -11
- package/src/config/appVersion.ts +65 -0
- package/src/config/buildInfo.ts +2 -2
- package/src/config/runtimeConfig.ts +1 -1
- package/src/config/settings.ts +5 -1
- package/src/config/updateCheckCache.ts +18 -8
- package/src/core/debug/modelStateDebug.ts +3 -2
- package/src/core/models/providerModelCache.ts +9 -25
- package/src/core/perf/renderDebug.ts +5 -4
- package/src/core/providerLauncher/launcher.ts +1 -1
- package/src/core/providerLauncher/workspaceConfig.ts +27 -13
- package/src/core/providerRuntime/antigravity.ts +12 -1
- package/src/core/providerRuntime/registry.ts +10 -6
- package/src/core/shared/attachments.ts +10 -10
- package/src/core/terminal/frameLock.ts +2 -1
- package/src/core/terminal/terminalTitle.ts +3 -2
- package/src/core/version/packageManager.ts +119 -0
- package/src/core/version/updateCheck.ts +15 -9
- package/src/core/workspace/appData.ts +45 -0
- package/src/core/workspace/planStorage.ts +7 -13
- package/src/test/runtimeTestUtils.ts +0 -90
- package/src/ui/chrome/TopHeader.tsx +4 -2
- package/src/ui/chrome/UpdateAvailableCard.tsx +3 -2
- package/src/ui/input/slashCommands.ts +1 -0
- package/src/ui/panels/AttachmentImportPanel.tsx +4 -3
- package/src/ui/panels/UpdatePromptPanel.tsx +53 -43
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
1
|
import { resolveRuntimeConfig, type RuntimeConfig, DEFAULT_RUNTIME_CONFIG, type ResolvedRuntimeConfig } from "../config/runtimeConfig.js";
|
|
5
|
-
import type { ProviderId } from "../core/providerLauncher/types.js";
|
|
6
|
-
import { resetAnthropicRouteValidationCacheForTests } from "../core/providerRuntime/anthropic.js";
|
|
7
|
-
import { resetAntigravityRouteValidationCacheForTests } from "../core/providerRuntime/antigravity.js";
|
|
8
|
-
import type { ProviderModel } from "../core/providerRuntime/types.js";
|
|
9
2
|
|
|
10
3
|
export function makeResolvedRuntime(overrides: Partial<RuntimeConfig> = {}): ResolvedRuntimeConfig {
|
|
11
4
|
return resolveRuntimeConfig({
|
|
@@ -19,86 +12,3 @@ export function makeResolvedRuntime(overrides: Partial<RuntimeConfig> = {}): Res
|
|
|
19
12
|
}
|
|
20
13
|
|
|
21
14
|
export const TEST_RUNTIME = makeResolvedRuntime();
|
|
22
|
-
|
|
23
|
-
function agyModel(id: string, label: string): ProviderModel {
|
|
24
|
-
return {
|
|
25
|
-
id,
|
|
26
|
-
modelId: id,
|
|
27
|
-
label,
|
|
28
|
-
description: null,
|
|
29
|
-
defaultReasoningLevel: null,
|
|
30
|
-
supportedReasoningLevels: null,
|
|
31
|
-
source: "discovered",
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The catalog `agy models` reports on a configured machine. Antigravity resolves
|
|
37
|
-
* its status, backend kind, and model labels from whatever discovery last found,
|
|
38
|
-
* so tests that assert on any of those need a catalog seeded rather than
|
|
39
|
-
* whatever happens to be installed.
|
|
40
|
-
*/
|
|
41
|
-
export const ANTIGRAVITY_FIXTURE_MODELS: readonly ProviderModel[] = [
|
|
42
|
-
agyModel("gemini-3.5-flash", "Gemini 3.5 Flash"),
|
|
43
|
-
agyModel("gemini-3.1-pro", "Gemini 3.1 Pro"),
|
|
44
|
-
agyModel("claude-sonnet-4-6", "Claude Sonnet 4.6"),
|
|
45
|
-
agyModel("claude-sonnet-4-6-think", "Claude Sonnet 4.6 (Thinking)"),
|
|
46
|
-
agyModel("gpt-oss-120b", "GPT-OSS 120B"),
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Drops every provider's in-memory discovery result.
|
|
51
|
-
*
|
|
52
|
-
* Bun shares the module registry across test files, so a catalog discovered by
|
|
53
|
-
* one test (some probe the real `claude` / `agy` CLIs) otherwise survives into
|
|
54
|
-
* the next and takes precedence over both the cache and the fallback list —
|
|
55
|
-
* making assertions depend on file order and on what is installed locally.
|
|
56
|
-
*/
|
|
57
|
-
export function resetProviderDiscoveryState(): void {
|
|
58
|
-
resetAntigravityRouteValidationCacheForTests();
|
|
59
|
-
resetAnthropicRouteValidationCacheForTests();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Runs `fn` with HOME pointed at a throwaway directory holding only the provider
|
|
64
|
-
* model cache built from `seed`.
|
|
65
|
-
*
|
|
66
|
-
* Provider discovery reads `~/.codexa-model-cache.json`, so a test that calls it
|
|
67
|
-
* without this is asserting against the developer's own machine — it passes with
|
|
68
|
-
* a warm cache and fails on a cold one (or in CI). HOME is read per call (see
|
|
69
|
-
* `getProviderModelCacheFile`), so redirecting the env var here is enough.
|
|
70
|
-
*
|
|
71
|
-
* Antigravity's in-memory discovery is reset around the body as well: Bun shares
|
|
72
|
-
* the module registry across test files, so a catalog discovered by one file
|
|
73
|
-
* otherwise leaks into the next and takes precedence over the seeded cache,
|
|
74
|
-
* making results depend on file order. Seed exactly what the assertions need.
|
|
75
|
-
*/
|
|
76
|
-
export function withSeededModelCache<T>(
|
|
77
|
-
seed: Partial<Record<ProviderId, readonly ProviderModel[]>>,
|
|
78
|
-
fn: () => T,
|
|
79
|
-
): T {
|
|
80
|
-
const tempHome = mkdtempSync(join(tmpdir(), "codexa-test-home-"));
|
|
81
|
-
const previousHome = process.env.HOME;
|
|
82
|
-
const previousUserProfile = process.env.USERPROFILE;
|
|
83
|
-
try {
|
|
84
|
-
process.env.HOME = tempHome;
|
|
85
|
-
delete process.env.USERPROFILE;
|
|
86
|
-
const providers = Object.fromEntries(
|
|
87
|
-
Object.entries(seed).map(([providerId, models]) => [providerId, { discoveredAt: 1, models }]),
|
|
88
|
-
);
|
|
89
|
-
writeFileSync(
|
|
90
|
-
join(tempHome, ".codexa-model-cache.json"),
|
|
91
|
-
`${JSON.stringify({ version: 1, providers }, null, 2)}\n`,
|
|
92
|
-
"utf8",
|
|
93
|
-
);
|
|
94
|
-
resetProviderDiscoveryState();
|
|
95
|
-
return fn();
|
|
96
|
-
} finally {
|
|
97
|
-
if (previousHome === undefined) delete process.env.HOME;
|
|
98
|
-
else process.env.HOME = previousHome;
|
|
99
|
-
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
|
100
|
-
else process.env.USERPROFILE = previousUserProfile;
|
|
101
|
-
rmSync(tempHome, { recursive: true, force: true });
|
|
102
|
-
resetProviderDiscoveryState();
|
|
103
|
-
}
|
|
104
|
-
}
|
|
@@ -2,7 +2,7 @@ import React, { memo } from "react";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../../config/settings.js";
|
|
4
4
|
import { formatCodexaBrandLabel } from "../../core/version/channel.js";
|
|
5
|
-
import { formatVersionLabel } from "../../core/version/updateCheck.js";
|
|
5
|
+
import { CODEXA_UPDATE_COMMAND, formatVersionLabel } from "../../core/version/updateCheck.js";
|
|
6
6
|
import type { RuntimeSummary } from "../../config/runtimeConfig.js";
|
|
7
7
|
import type { CodexAuthState } from "../../core/auth/codexAuth.js";
|
|
8
8
|
import { getAuthStateLabel } from "../../core/auth/codexAuth.js";
|
|
@@ -58,6 +58,7 @@ type HeaderMetadataLine = { key: string; text: string; color: string; bold: bool
|
|
|
58
58
|
export interface UpdateAvailableInfo {
|
|
59
59
|
latestVersion: string;
|
|
60
60
|
currentVersion: string;
|
|
61
|
+
updateCommand?: string;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
interface TopHeaderProps {
|
|
@@ -343,6 +344,7 @@ export function TopHeader({
|
|
|
343
344
|
<UpdateAvailableCard
|
|
344
345
|
latestVersion={updateAvailable.latestVersion}
|
|
345
346
|
currentVersion={updateAvailable.currentVersion}
|
|
347
|
+
updateCommand={updateAvailable.updateCommand}
|
|
346
348
|
width={metadataWidth}
|
|
347
349
|
/>
|
|
348
350
|
<Box height={UPDATE_CARD_GAP_ROWS} />
|
|
@@ -359,7 +361,7 @@ export function TopHeader({
|
|
|
359
361
|
)}
|
|
360
362
|
{metadataColumn}
|
|
361
363
|
{updateAvailable && (
|
|
362
|
-
<Text color={theme.warning} wrap="truncate">{`Update available: Codexa ${formatVersionLabel(updateAvailable.latestVersion)} — Run:
|
|
364
|
+
<Text color={theme.warning} wrap="truncate">{`Update available: Codexa ${formatVersionLabel(updateAvailable.latestVersion)} — Run: ${updateAvailable.updateCommand ?? CODEXA_UPDATE_COMMAND}`}</Text>
|
|
363
365
|
)}
|
|
364
366
|
</Box>
|
|
365
367
|
)}
|
|
@@ -10,13 +10,14 @@ export const UPDATE_CARD_ROWS = UPDATE_CARD_CONTENT_ROWS + 2; // +2 for top/bott
|
|
|
10
10
|
export interface UpdateAvailableCardProps {
|
|
11
11
|
latestVersion: string;
|
|
12
12
|
currentVersion: string;
|
|
13
|
+
updateCommand?: string;
|
|
13
14
|
/** Total box width including borders. Long lines are truncated to fit. */
|
|
14
15
|
width?: number;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
export function UpdateAvailableCard({ latestVersion, currentVersion, width }: UpdateAvailableCardProps) {
|
|
18
|
+
export function UpdateAvailableCard({ latestVersion, currentVersion, updateCommand = CODEXA_UPDATE_COMMAND, width }: UpdateAvailableCardProps) {
|
|
18
19
|
const theme = useTheme();
|
|
19
|
-
const command = `Run: ${
|
|
20
|
+
const command = `Run: ${updateCommand}`;
|
|
20
21
|
// Inner content width = boxWidth - 2 (left/right border cols)
|
|
21
22
|
const innerWidth = width !== undefined ? Math.max(8, width - 2) : undefined;
|
|
22
23
|
|
|
@@ -24,6 +24,7 @@ export const SLASH_COMMANDS = [
|
|
|
24
24
|
{ cmd: "/auth", desc: "Manage authentication" },
|
|
25
25
|
{ cmd: "/workspace", desc: "Show the locked workspace" },
|
|
26
26
|
{ cmd: "/copy", desc: "Copy the full conversation transcript to clipboard" },
|
|
27
|
+
{ cmd: "/update", desc: "Check for updates and install the latest Codexa" },
|
|
27
28
|
{ cmd: "/mouse", desc: "Toggle mouse capture for wheel scrolling (off by default)" },
|
|
28
29
|
{ cmd: "/exit", desc: "Quit the application" },
|
|
29
30
|
] as const satisfies readonly SlashCommandSuggestion[];
|
|
@@ -43,7 +43,8 @@ export function AttachmentImportPanel({
|
|
|
43
43
|
}
|
|
44
44
|
}, { isActive: isFocused });
|
|
45
45
|
|
|
46
|
-
const relativeAttachmentsDir = path.relative(workspaceRoot, attachmentsDir).replace(/\\/g, "/");
|
|
46
|
+
const relativeAttachmentsDir = path.relative(workspaceRoot, attachmentsDir).replace(/\\/g, "/");
|
|
47
|
+
const displayedAttachmentsDir = relativeAttachmentsDir.startsWith("..") ? attachmentsDir : relativeAttachmentsDir;
|
|
47
48
|
const hasImages = files.some((f) => f.isImage);
|
|
48
49
|
const showVisionWarning = hasImages && modelSupportsVision === false;
|
|
49
50
|
const fileLabel = files.length === 1 ? "file" : "files";
|
|
@@ -59,7 +60,7 @@ export function AttachmentImportPanel({
|
|
|
59
60
|
>
|
|
60
61
|
<Text color={theme.accent} bold>IMPORT FILE </Text>
|
|
61
62
|
<Text color={theme.textMuted}>
|
|
62
|
-
Copy {files.length} outside-workspace {fileLabel} into
|
|
63
|
+
Copy {files.length} outside-workspace {fileLabel} into {displayedAttachmentsDir}?
|
|
63
64
|
</Text>
|
|
64
65
|
</Box>
|
|
65
66
|
|
|
@@ -76,7 +77,7 @@ export function AttachmentImportPanel({
|
|
|
76
77
|
<Box key={i} flexDirection="column" marginBottom={i < files.length - 1 ? 1 : 0}>
|
|
77
78
|
<Text color={theme.text}>{path.basename(file.srcPath)}</Text>
|
|
78
79
|
<Text color={theme.textDim}>
|
|
79
|
-
{"→ "}{
|
|
80
|
+
{"→ "}{displayedAttachmentsDir}/{file.destFilename}
|
|
80
81
|
</Text>
|
|
81
82
|
</Box>
|
|
82
83
|
))}
|
|
@@ -1,31 +1,45 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { Box, Text, useFocus, useInput } from "ink";
|
|
3
|
-
import { spawn } from "child_process";
|
|
4
3
|
import { useTheme } from "../theme.js";
|
|
5
|
-
import { CODEXA_NPM_PACKAGE,
|
|
4
|
+
import { CODEXA_NPM_PACKAGE, formatVersionLabel } from "../../core/version/updateCheck.js";
|
|
5
|
+
import {
|
|
6
|
+
formatPermissionGuidance,
|
|
7
|
+
getUpdateCommand,
|
|
8
|
+
isPermissionError,
|
|
9
|
+
runUpdateCommand,
|
|
10
|
+
type GlobalPackageManager,
|
|
11
|
+
} from "../../core/version/packageManager.js";
|
|
12
|
+
import type { CommandResult, CommandStreamHandlers } from "../../core/process/CommandRunner.js";
|
|
6
13
|
|
|
7
14
|
type Phase = "menu" | "running" | "done" | "error";
|
|
8
15
|
|
|
16
|
+
export type RunUpdateFn = (
|
|
17
|
+
pm: GlobalPackageManager,
|
|
18
|
+
handlers?: CommandStreamHandlers,
|
|
19
|
+
) => { result: Promise<CommandResult>; cancel: () => void };
|
|
20
|
+
|
|
9
21
|
const MENU_ITEMS = [
|
|
10
22
|
{ label: "Update now" },
|
|
11
|
-
{ label: "
|
|
12
|
-
{ label: "Skip until next version" },
|
|
23
|
+
{ label: "Later" },
|
|
13
24
|
] as const;
|
|
14
25
|
|
|
15
26
|
interface UpdatePromptPanelProps {
|
|
16
27
|
focusId: string;
|
|
17
28
|
currentVersion: string;
|
|
18
29
|
latestVersion: string;
|
|
30
|
+
packageManager: GlobalPackageManager;
|
|
31
|
+
/** Test seam — defaults to the real cross-platform runner. */
|
|
32
|
+
runUpdate?: RunUpdateFn;
|
|
19
33
|
onSkip: () => void;
|
|
20
|
-
onSkipUntilNextVersion: (version: string) => void;
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
export function UpdatePromptPanel({
|
|
24
37
|
focusId,
|
|
25
38
|
currentVersion,
|
|
26
39
|
latestVersion,
|
|
40
|
+
packageManager,
|
|
41
|
+
runUpdate,
|
|
27
42
|
onSkip,
|
|
28
|
-
onSkipUntilNextVersion,
|
|
29
43
|
}: UpdatePromptPanelProps) {
|
|
30
44
|
const theme = useTheme();
|
|
31
45
|
const { isFocused } = useFocus({ id: focusId, autoFocus: true });
|
|
@@ -35,7 +49,7 @@ export function UpdatePromptPanel({
|
|
|
35
49
|
const [outputLines, setOutputLines] = useState<string[]>([]);
|
|
36
50
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
37
51
|
|
|
38
|
-
const
|
|
52
|
+
const runStartedRef = useRef(false);
|
|
39
53
|
|
|
40
54
|
useInput((input, key) => {
|
|
41
55
|
if (key.escape) {
|
|
@@ -54,10 +68,8 @@ export function UpdatePromptPanel({
|
|
|
54
68
|
if (key.return) {
|
|
55
69
|
if (selectedIndex === 0) {
|
|
56
70
|
setPhase("running");
|
|
57
|
-
} else if (selectedIndex === 1) {
|
|
58
|
-
onSkip();
|
|
59
71
|
} else {
|
|
60
|
-
|
|
72
|
+
onSkip();
|
|
61
73
|
}
|
|
62
74
|
return;
|
|
63
75
|
}
|
|
@@ -70,42 +82,42 @@ export function UpdatePromptPanel({
|
|
|
70
82
|
|
|
71
83
|
useEffect(() => {
|
|
72
84
|
if (phase !== "running") return;
|
|
73
|
-
if (
|
|
74
|
-
|
|
85
|
+
if (runStartedRef.current) return;
|
|
86
|
+
runStartedRef.current = true;
|
|
75
87
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const appendLines = (buf: Buffer) => {
|
|
82
|
-
const lines = buf.toString("utf8").split(/\r?\n/).filter(Boolean);
|
|
88
|
+
const appendLines = (text: string) => {
|
|
89
|
+
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
83
90
|
if (lines.length > 0) {
|
|
84
91
|
setOutputLines((prev) => [...prev, ...lines]);
|
|
85
92
|
}
|
|
86
93
|
};
|
|
87
94
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
setErrorMessage(err.message);
|
|
93
|
-
setPhase("error");
|
|
95
|
+
const runner = runUpdate ?? runUpdateCommand;
|
|
96
|
+
const { result, cancel } = runner(packageManager, {
|
|
97
|
+
onStdout: appendLines,
|
|
98
|
+
onStderr: appendLines,
|
|
94
99
|
});
|
|
95
100
|
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
let disposed = false;
|
|
102
|
+
void result.then((res) => {
|
|
103
|
+
if (disposed) return;
|
|
104
|
+
if (res.status === "completed" && res.exitCode === 0) {
|
|
98
105
|
setPhase("done");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (isPermissionError(res)) {
|
|
109
|
+
setErrorMessage(formatPermissionGuidance(packageManager));
|
|
99
110
|
} else {
|
|
100
|
-
setErrorMessage(
|
|
101
|
-
setPhase("error");
|
|
111
|
+
setErrorMessage(res.userMessage);
|
|
102
112
|
}
|
|
113
|
+
setPhase("error");
|
|
103
114
|
});
|
|
104
115
|
|
|
105
116
|
return () => {
|
|
106
|
-
|
|
117
|
+
disposed = true;
|
|
118
|
+
cancel();
|
|
107
119
|
};
|
|
108
|
-
}, [phase]);
|
|
120
|
+
}, [phase, packageManager, runUpdate]);
|
|
109
121
|
|
|
110
122
|
const footerText = phase === "menu"
|
|
111
123
|
? "Esc to close · Enter to confirm"
|
|
@@ -122,16 +134,16 @@ export function UpdatePromptPanel({
|
|
|
122
134
|
flexDirection="column"
|
|
123
135
|
>
|
|
124
136
|
<Box>
|
|
125
|
-
<Text color={theme.accent} bold>{`Update available: Codexa ${
|
|
137
|
+
<Text color={theme.accent} bold>{`Update available: Codexa ${latestVersion}`}</Text>
|
|
126
138
|
</Box>
|
|
127
139
|
<Box marginTop={1}>
|
|
128
|
-
<Text color={theme.text}>{
|
|
140
|
+
<Text color={theme.text}>{`Current version: ${currentVersion}`}</Text>
|
|
129
141
|
</Box>
|
|
130
142
|
<Box>
|
|
131
143
|
<Text color={theme.textMuted}>{`Package: ${CODEXA_NPM_PACKAGE}`}</Text>
|
|
132
144
|
</Box>
|
|
133
145
|
<Box>
|
|
134
|
-
<Text color={theme.textMuted}>{`Run: ${
|
|
146
|
+
<Text color={theme.textMuted}>{`Run: ${getUpdateCommand(packageManager).displayCommand}`}</Text>
|
|
135
147
|
</Box>
|
|
136
148
|
</Box>
|
|
137
149
|
|
|
@@ -146,19 +158,17 @@ export function UpdatePromptPanel({
|
|
|
146
158
|
>
|
|
147
159
|
{phase === "menu" && (
|
|
148
160
|
<>
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
<Text color={index === selectedIndex ? theme.accent : theme.textMuted}>
|
|
152
|
-
{index === selectedIndex ? "› " : " "}
|
|
153
|
-
</Text>
|
|
161
|
+
<Box>
|
|
162
|
+
{MENU_ITEMS.map((item, index) => (
|
|
154
163
|
<Text
|
|
164
|
+
key={item.label}
|
|
155
165
|
color={index === selectedIndex ? theme.text : theme.textMuted}
|
|
156
166
|
bold={index === selectedIndex}
|
|
157
167
|
>
|
|
158
|
-
{
|
|
168
|
+
{`[ ${item.label} ]${index === 0 ? " " : ""}`}
|
|
159
169
|
</Text>
|
|
160
|
-
|
|
161
|
-
|
|
170
|
+
))}
|
|
171
|
+
</Box>
|
|
162
172
|
</>
|
|
163
173
|
)}
|
|
164
174
|
|
|
@@ -173,7 +183,7 @@ export function UpdatePromptPanel({
|
|
|
173
183
|
|
|
174
184
|
{phase === "done" && (
|
|
175
185
|
<>
|
|
176
|
-
<Text color={theme.success}>{
|
|
186
|
+
<Text color={theme.success}>{`Codexa ${formatVersionLabel(latestVersion)} installed successfully.`}</Text>
|
|
177
187
|
<Text color={theme.textMuted}>{"Restart Codexa to use the new version."}</Text>
|
|
178
188
|
</>
|
|
179
189
|
)}
|