@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.
Files changed (206) hide show
  1. package/README.md +396 -100
  2. package/bin/codexa.js +62 -144
  3. package/package.json +14 -8
  4. package/src/app.tsx +596 -306
  5. package/src/commands/handler.ts +6 -6
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/persistence.ts +10 -0
  9. package/src/config/runtimeConfig.ts +1 -1
  10. package/src/config/settings.ts +8 -16
  11. package/src/config/trustStore.ts +1 -1
  12. package/src/config/updateCheckCache.ts +19 -1
  13. package/src/core/README.md +52 -0
  14. package/src/core/agent/loop.ts +282 -0
  15. package/src/core/agent/protocol.ts +211 -0
  16. package/src/core/agent/tools.ts +414 -0
  17. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  18. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  19. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  20. package/src/core/debug/modelStateDebug.ts +34 -0
  21. package/src/core/executables/antigravityExecutable.ts +48 -0
  22. package/src/core/executables/codexExecutable.ts +1 -0
  23. package/src/core/executables/executableResolver.ts +65 -43
  24. package/src/core/perf/renderDebug.ts +10 -6
  25. package/src/core/process/processValidation.ts +9 -5
  26. package/src/core/providerLauncher/launcher.ts +59 -42
  27. package/src/core/providerLauncher/registry.ts +30 -14
  28. package/src/core/providerLauncher/types.ts +11 -9
  29. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  30. package/src/core/providerRuntime/anthropic.ts +7 -1
  31. package/src/core/providerRuntime/antigravity.ts +305 -0
  32. package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
  33. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  34. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  35. package/src/core/providerRuntime/local.ts +129 -51
  36. package/src/core/providerRuntime/models.ts +22 -11
  37. package/src/core/providerRuntime/registry.ts +58 -31
  38. package/src/core/providerRuntime/types.ts +19 -14
  39. package/src/core/providers/codexSubprocess.ts +2 -2
  40. package/src/core/providers/types.ts +1 -1
  41. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  42. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  43. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  44. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  45. package/src/core/terminal/frameLock.ts +109 -0
  46. package/src/core/terminal/inkRenderReset.ts +123 -0
  47. package/src/core/terminal/terminalControl.ts +22 -0
  48. package/src/core/terminal/terminalTitle.ts +16 -102
  49. package/src/core/version/channel.ts +23 -0
  50. package/src/core/version/updateCheck.ts +193 -0
  51. package/src/core/{launchContext.ts → workspace/launchContext.ts} +32 -39
  52. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  53. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  54. package/src/headless/execRunner.ts +2 -2
  55. package/src/index.tsx +43 -89
  56. package/src/session/appSession.ts +10 -7
  57. package/src/session/chatLifecycle.ts +1 -1
  58. package/src/session/liveRenderScheduler.ts +1 -1
  59. package/src/session/types.ts +3 -2
  60. package/src/ui/ActionRequiredBlock.tsx +5 -5
  61. package/src/ui/ActivityBars.tsx +3 -3
  62. package/src/ui/ActivityIndicator.tsx +6 -6
  63. package/src/ui/AgentBlock.tsx +6 -6
  64. package/src/ui/AnimatedStatusText.tsx +1 -1
  65. package/src/ui/AppShell.tsx +670 -719
  66. package/src/ui/AttachmentImportPanel.tsx +8 -8
  67. package/src/ui/AuthPanel.tsx +20 -20
  68. package/src/ui/BottomComposer.tsx +158 -118
  69. package/src/ui/DashCard.tsx +3 -3
  70. package/src/ui/Markdown.tsx +17 -17
  71. package/src/ui/ModelPickerScreen.tsx +222 -42
  72. package/src/ui/ModelReasoningPicker.tsx +15 -15
  73. package/src/ui/Panel.tsx +3 -3
  74. package/src/ui/PlanActionPicker.tsx +6 -6
  75. package/src/ui/PlanReviewPanel.tsx +9 -9
  76. package/src/ui/ProviderPicker.tsx +735 -321
  77. package/src/ui/RunFooter.tsx +3 -3
  78. package/src/ui/RuntimeStatusBar.tsx +108 -0
  79. package/src/ui/SelectionPanel.tsx +8 -4
  80. package/src/ui/SettingsPanel.tsx +9 -9
  81. package/src/ui/Spinner.tsx +1 -1
  82. package/src/ui/TextEntryPanel.tsx +11 -11
  83. package/src/ui/ThinkingBlock.tsx +8 -8
  84. package/src/ui/Timeline.tsx +1625 -1472
  85. package/src/ui/TopHeader.tsx +437 -293
  86. package/src/ui/TranscriptShell.tsx +322 -0
  87. package/src/ui/TurnGroup.tsx +33 -33
  88. package/src/ui/UpdateAvailableCard.tsx +41 -0
  89. package/src/ui/UpdatePromptPanel.tsx +197 -0
  90. package/src/ui/focus.ts +3 -0
  91. package/src/ui/layout.ts +299 -25
  92. package/src/ui/layoutListWindow.ts +145 -0
  93. package/src/ui/logoVariants.ts +103 -0
  94. package/src/ui/modeDisplay.ts +12 -12
  95. package/src/ui/runtimeDisplay.ts +112 -0
  96. package/src/ui/textLayout.ts +15 -4
  97. package/src/ui/theme.tsx +274 -395
  98. package/src/ui/timelineMeasure.ts +218 -136
  99. package/scripts/audit-codexa-capabilities.mjs +0 -466
  100. package/scripts/gen-build-info.mjs +0 -33
  101. package/scripts/smoke-terminal-bench.mjs +0 -35
  102. package/src/appRenderStability.test.ts +0 -131
  103. package/src/commands/handler.test.ts +0 -655
  104. package/src/config/launchArgs.test.ts +0 -189
  105. package/src/config/layeredConfig.test.ts +0 -143
  106. package/src/config/persistence.test.ts +0 -114
  107. package/src/config/runtimeConfig.test.ts +0 -218
  108. package/src/config/settings.test.ts +0 -155
  109. package/src/config/trustStore.test.ts +0 -29
  110. package/src/core/attachments.test.ts +0 -155
  111. package/src/core/auth/codexAuth.test.ts +0 -68
  112. package/src/core/cleanupFastFail.test.ts +0 -76
  113. package/src/core/codex.ts +0 -124
  114. package/src/core/codexExecArgs.test.ts +0 -195
  115. package/src/core/codexLaunch.test.ts +0 -205
  116. package/src/core/codexPrompt.test.ts +0 -252
  117. package/src/core/executables/codexExecutable.test.ts +0 -212
  118. package/src/core/executables/executableResolver.test.ts +0 -129
  119. package/src/core/executables/geminiExecutable.test.ts +0 -116
  120. package/src/core/executables/pathSanityScan.test.ts +0 -47
  121. package/src/core/githubDiagnostics.test.ts +0 -92
  122. package/src/core/hollowResponseFormat.test.ts +0 -58
  123. package/src/core/launchContext.test.ts +0 -157
  124. package/src/core/models/codexCapabilities.test.ts +0 -45
  125. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  126. package/src/core/models/modelSpecs.test.ts +0 -283
  127. package/src/core/perf/renderDebug.test.ts +0 -230
  128. package/src/core/planStorage.test.ts +0 -143
  129. package/src/core/process/CommandRunner.test.ts +0 -105
  130. package/src/core/projectInstructions.test.ts +0 -50
  131. package/src/core/providerLauncher/launcher.test.ts +0 -238
  132. package/src/core/providerLauncher/registry.test.ts +0 -324
  133. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  134. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  135. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  136. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  137. package/src/core/providerRuntime/gemini.test.ts +0 -437
  138. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  139. package/src/core/providerRuntime/local.test.ts +0 -787
  140. package/src/core/providerRuntime/registry.test.ts +0 -233
  141. package/src/core/providers/codexJsonStream.test.ts +0 -148
  142. package/src/core/providers/codexSubprocess.test.ts +0 -68
  143. package/src/core/providers/codexTranscript.test.ts +0 -284
  144. package/src/core/terminal/startupClear.test.ts +0 -55
  145. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  146. package/src/core/terminal/terminalControl.test.ts +0 -75
  147. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  148. package/src/core/terminal/terminalSelection.test.ts +0 -42
  149. package/src/core/terminal/terminalTitle.test.ts +0 -328
  150. package/src/core/updateCheck.test.ts +0 -194
  151. package/src/core/updateCheck.ts +0 -172
  152. package/src/core/workspaceActivity.test.ts +0 -163
  153. package/src/core/workspaceGuard.test.ts +0 -151
  154. package/src/core/workspaceRoot.test.ts +0 -23
  155. package/src/exec.test.ts +0 -13
  156. package/src/headless/execArgs.test.ts +0 -147
  157. package/src/headless/execRunner.test.ts +0 -436
  158. package/src/index.test.tsx +0 -620
  159. package/src/session/appSession.test.ts +0 -897
  160. package/src/session/chatLifecycle.test.ts +0 -64
  161. package/src/session/liveRenderScheduler.test.ts +0 -201
  162. package/src/session/planFlow.test.ts +0 -103
  163. package/src/session/planTranscript.test.ts +0 -65
  164. package/src/session/promptRunSchedule.test.ts +0 -36
  165. package/src/ui/ActivityIndicator.test.tsx +0 -58
  166. package/src/ui/AgentBlock.test.ts +0 -6
  167. package/src/ui/AnimatedStatusText.test.ts +0 -16
  168. package/src/ui/AppShell.test.tsx +0 -1776
  169. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  170. package/src/ui/BottomComposer.test.ts +0 -674
  171. package/src/ui/CodexLogo.tsx +0 -55
  172. package/src/ui/Markdown.test.ts +0 -157
  173. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  174. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  175. package/src/ui/ModelPickerState.test.tsx +0 -151
  176. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  177. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  178. package/src/ui/PromptCardBorder.test.tsx +0 -161
  179. package/src/ui/ProviderPicker.test.tsx +0 -289
  180. package/src/ui/ProviderShortcut.test.tsx +0 -143
  181. package/src/ui/SettingsPanel.test.tsx +0 -233
  182. package/src/ui/StaticTranscriptItem.tsx +0 -56
  183. package/src/ui/Timeline.test.ts +0 -2067
  184. package/src/ui/TimelineNavigation.test.tsx +0 -201
  185. package/src/ui/TopHeader.test.tsx +0 -254
  186. package/src/ui/TurnGroup.test.tsx +0 -365
  187. package/src/ui/busyStatusAnimation.test.ts +0 -30
  188. package/src/ui/commandNormalize.test.ts +0 -142
  189. package/src/ui/diffRenderer.test.ts +0 -102
  190. package/src/ui/focusFlow.test.tsx +0 -1098
  191. package/src/ui/inputBuffer.test.ts +0 -151
  192. package/src/ui/layout.test.ts +0 -146
  193. package/src/ui/modeDisplay.test.ts +0 -42
  194. package/src/ui/runActivityView.test.ts +0 -89
  195. package/src/ui/runLifecycleView.test.tsx +0 -237
  196. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  197. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  198. package/src/ui/textLayout.test.ts +0 -18
  199. package/src/ui/themeFlow.test.ts +0 -53
  200. package/src/ui/timelineMeasureCache.test.ts +0 -986
  201. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  202. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  203. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  204. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  205. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  206. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
@@ -1,212 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { afterEach } from "node:test";
4
- import type { ChildProcess } from "node:child_process";
5
- import { mkdirSync, rmSync, writeFileSync } from "node:fs";
6
- import { join } from "node:path";
7
- import { tmpdir } from "node:os";
8
- import { runCommand, type CommandResult } from "../process/CommandRunner.js";
9
- import {
10
- resolveCodexExecutable,
11
- resetCodexExecutableCacheForTests,
12
- spawnCodexProcess,
13
- } from "./codexExecutable.js";
14
-
15
- function commandResult(overrides: Partial<CommandResult> = {}): CommandResult {
16
- return {
17
- status: "completed",
18
- exitCode: 0,
19
- signal: null,
20
- stdout: "",
21
- stderr: "",
22
- startedAt: 0,
23
- endedAt: 0,
24
- durationMs: 0,
25
- userMessage: "Command completed.",
26
- ...overrides,
27
- };
28
- }
29
-
30
- function mockRunCommand(result: CommandResult, onCall?: (spec: Parameters<typeof runCommand>[0]) => void): typeof runCommand {
31
- return ((spec) => {
32
- onCall?.(spec);
33
- return {
34
- child: null as unknown as ChildProcess,
35
- result: Promise.resolve(result),
36
- cancel: () => undefined,
37
- };
38
- }) as typeof runCommand;
39
- }
40
-
41
- async function withEnv<T>(env: Partial<NodeJS.ProcessEnv>, callback: () => Promise<T>): Promise<T> {
42
- const originalCodexExe = process.env.CODEX_EXECUTABLE;
43
- try {
44
- if ("CODEX_EXECUTABLE" in env) process.env.CODEX_EXECUTABLE = env.CODEX_EXECUTABLE;
45
- else delete process.env.CODEX_EXECUTABLE;
46
- resetCodexExecutableCacheForTests();
47
- return await callback();
48
- } finally {
49
- if (originalCodexExe === undefined) delete process.env.CODEX_EXECUTABLE;
50
- else process.env.CODEX_EXECUTABLE = originalCodexExe;
51
- resetCodexExecutableCacheForTests();
52
- }
53
- }
54
-
55
- afterEach(() => {
56
- resetCodexExecutableCacheForTests();
57
- });
58
-
59
- test("Codex resolver: configuredPath wins and bypasses env and PATH", async () => {
60
- await withEnv({ CODEX_EXECUTABLE: "env-codex.cmd" }, async () => {
61
- let whereCalled = false;
62
- const resolved = await resolveCodexExecutable({
63
- configuredPath: process.execPath,
64
- runCommandImpl: mockRunCommand(commandResult(), () => { whereCalled = true; }),
65
- });
66
-
67
- assert.equal(resolved, process.execPath);
68
- assert.equal(whereCalled, false, "where.exe should not be called when configuredPath is set");
69
- });
70
- });
71
-
72
- test("Codex resolver: CODEX_EXECUTABLE env var used when no configuredPath", async () => {
73
- await withEnv({ CODEX_EXECUTABLE: "env-codex.cmd" }, async () => {
74
- let whereCalled = false;
75
- const resolved = await resolveCodexExecutable({
76
- runCommandImpl: mockRunCommand(commandResult(), () => { whereCalled = true; }),
77
- });
78
-
79
- assert.equal(resolved, "env-codex.cmd");
80
- assert.equal(whereCalled, false, "where.exe should not be called when CODEX_EXECUTABLE is set");
81
- });
82
- });
83
-
84
- test("Codex resolver: rejects unsafe CODEX_EXECUTABLE values", async () => {
85
- await withEnv({ CODEX_EXECUTABLE: "codex.cmd & calc" }, async () => {
86
- await assert.rejects(
87
- () => resolveCodexExecutable({
88
- runCommandImpl: mockRunCommand(commandResult()),
89
- }),
90
- /shell metacharacters|single executable name/i,
91
- );
92
- });
93
- });
94
-
95
- test("Codex resolver: accepts environment executable paths with spaces", async () => {
96
- const tempRoot = join(tmpdir(), `codexa codex resolver ${Date.now()}`);
97
- const codexPath = join(tempRoot, "codex cli.exe");
98
- mkdirSync(tempRoot, { recursive: true });
99
- writeFileSync(codexPath, "");
100
- try {
101
- await withEnv({ CODEX_EXECUTABLE: `"${codexPath}"` }, async () => {
102
- const resolved = await resolveCodexExecutable({
103
- runCommandImpl: mockRunCommand(commandResult()),
104
- });
105
- assert.equal(resolved, codexPath);
106
- });
107
- } finally {
108
- rmSync(tempRoot, { recursive: true, force: true });
109
- }
110
- });
111
-
112
- test("Codex resolver: Windows where.exe PATH lookup used when no env or config", async () => {
113
- if (process.platform !== "win32") return;
114
-
115
- const resolvedPath = join(tmpdir(), "codex.cmd");
116
- writeFileSync(resolvedPath, "");
117
- await withEnv({}, async () => {
118
- const calls: Array<Parameters<typeof runCommand>[0]> = [];
119
- const resolved = await resolveCodexExecutable({
120
- runCommandImpl: mockRunCommand(
121
- commandResult({ stdout: `${resolvedPath}\n` }),
122
- (spec) => calls.push(spec),
123
- ),
124
- });
125
-
126
- assert.equal(resolved, resolvedPath);
127
- const whereCall = calls.find((c) => c.executable === "where.exe");
128
- assert.ok(whereCall, "where.exe should have been called");
129
- });
130
- });
131
-
132
- test("Codex resolver: bare fallback returned when nothing found (non-Windows)", async () => {
133
- if (process.platform === "win32") return;
134
-
135
- await withEnv({}, async () => {
136
- const resolved = await resolveCodexExecutable({
137
- runCommandImpl: mockRunCommand(commandResult({ exitCode: 1, status: "failed" })),
138
- });
139
-
140
- assert.equal(resolved, "codex", "Should fall back to bare codex name");
141
- });
142
- });
143
-
144
- test("Codex resolver: Windows bare fallback when where.exe fails", async () => {
145
- if (process.platform !== "win32") return;
146
-
147
- await withEnv({}, async () => {
148
- const resolved = await resolveCodexExecutable({
149
- runCommandImpl: mockRunCommand(commandResult({ exitCode: 1, status: "failed" })),
150
- });
151
-
152
- assert.ok(["codex.cmd", "codex.exe", "codex"].includes(resolved), `Unexpected fallback: ${resolved}`);
153
- });
154
- });
155
-
156
- test("Codex resolver: cache is populated after first resolution", async () => {
157
- await withEnv({ CODEX_EXECUTABLE: "env-codex.cmd" }, async () => {
158
- const first = await resolveCodexExecutable();
159
- const second = await resolveCodexExecutable();
160
-
161
- assert.equal(first, second);
162
- assert.equal(first, "env-codex.cmd");
163
- });
164
- });
165
-
166
- test("Codex resolver: configuredPath bypasses cache", async () => {
167
- await withEnv({ CODEX_EXECUTABLE: "env-codex.cmd" }, async () => {
168
- const cached = await resolveCodexExecutable();
169
- assert.equal(cached, "env-codex.cmd");
170
-
171
- const withOverride = await resolveCodexExecutable({
172
- configuredPath: process.execPath,
173
- });
174
- assert.equal(withOverride, process.execPath);
175
-
176
- const afterOverride = await resolveCodexExecutable();
177
- assert.equal(afterOverride, "env-codex.cmd", "Cache should not be polluted by configuredPath call");
178
- });
179
- });
180
-
181
- test("Codex resolver: resetCodexExecutableCacheForTests clears state", async () => {
182
- await withEnv({ CODEX_EXECUTABLE: "first.cmd" }, async () => {
183
- const first = await resolveCodexExecutable();
184
- assert.equal(first, "first.cmd");
185
- });
186
-
187
- resetCodexExecutableCacheForTests();
188
-
189
- await withEnv({ CODEX_EXECUTABLE: "second.cmd" }, async () => {
190
- const second = await resolveCodexExecutable();
191
- assert.equal(second, "second.cmd");
192
- });
193
- });
194
-
195
- test("spawnCodexProcess wraps .cmd in cmd.exe on Windows", () => {
196
- if (process.platform !== "win32") return;
197
-
198
- const codexPath = "C:\\Users\\Example\\AppData\\Roaming\\npm\\codex.cmd";
199
- const proc = spawnCodexProcess(codexPath, ["exec", "--help"], { stdio: ["ignore", "pipe", "pipe"] });
200
- proc.kill();
201
-
202
- assert.ok(proc, "Process should have been spawned");
203
- });
204
-
205
- test("spawnCodexProcess uses executable directly on non-Windows", () => {
206
- if (process.platform === "win32") return;
207
-
208
- const proc = spawnCodexProcess("echo", ["hello"], { stdio: ["ignore", "pipe", "pipe"] });
209
- proc.kill();
210
-
211
- assert.ok(proc, "Process should have been spawned");
212
- });
@@ -1,129 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { ChildProcess } from "node:child_process";
4
- import { mkdirSync, rmSync, writeFileSync } from "node:fs";
5
- import { tmpdir } from "node:os";
6
- import { join } from "node:path";
7
- import { runCommand, type CommandResult } from "../process/CommandRunner.js";
8
- import { resolveExecutable, buildSpawnSpec } from "./executableResolver.js";
9
-
10
- function commandResult(overrides: Partial<CommandResult>): CommandResult {
11
- return {
12
- status: "completed",
13
- exitCode: 0,
14
- signal: null,
15
- stdout: "",
16
- stderr: "",
17
- startedAt: 0,
18
- endedAt: 0,
19
- durationMs: 0,
20
- userMessage: "Command completed.",
21
- ...overrides,
22
- };
23
- }
24
-
25
- function mockRunCommand(result: CommandResult, onCall?: (spec: Parameters<typeof runCommand>[0]) => void): typeof runCommand {
26
- return ((spec) => {
27
- onCall?.(spec);
28
- return {
29
- child: null as unknown as ChildProcess,
30
- result: Promise.resolve(result),
31
- cancel: () => undefined,
32
- };
33
- }) as typeof runCommand;
34
- }
35
-
36
- test("resolver: uses configuredPath", async () => {
37
- const resolved = await resolveExecutable({
38
- commandNames: ["test"],
39
- label: "test",
40
- configuredPath: process.execPath,
41
- });
42
- assert.equal(resolved, process.execPath);
43
- });
44
-
45
- test("resolver: uses environment override", async () => {
46
- const original = process.env.TEST_EXECUTABLE;
47
- process.env.TEST_EXECUTABLE = "custom-test";
48
- try {
49
- const resolved = await resolveExecutable({
50
- commandNames: ["test"],
51
- label: "test",
52
- envOverrides: ["TEST_EXECUTABLE"],
53
- });
54
- assert.equal(resolved, "custom-test");
55
- } finally {
56
- if (original === undefined) delete process.env.TEST_EXECUTABLE;
57
- else process.env.TEST_EXECUTABLE = original;
58
- }
59
- });
60
-
61
- test("resolver: rejects environment override with shell metacharacters", async () => {
62
- const original = process.env.TEST_EXECUTABLE;
63
- process.env.TEST_EXECUTABLE = "custom-test & calc";
64
- try {
65
- await assert.rejects(
66
- () => resolveExecutable({
67
- commandNames: ["test"],
68
- label: "test",
69
- envOverrides: ["TEST_EXECUTABLE"],
70
- }),
71
- /shell metacharacters|single executable name/i,
72
- );
73
- } finally {
74
- if (original === undefined) delete process.env.TEST_EXECUTABLE;
75
- else process.env.TEST_EXECUTABLE = original;
76
- }
77
- });
78
-
79
- test("resolver: rejects configured executable values that include arguments", async () => {
80
- await assert.rejects(
81
- () => resolveExecutable({
82
- commandNames: ["test"],
83
- label: "test",
84
- configuredPath: "test --version",
85
- }),
86
- /single executable name/i,
87
- );
88
- });
89
-
90
- test("resolver: accepts quoted executable paths with spaces", async () => {
91
- const tempRoot = join(tmpdir(), `codexa resolver ${Date.now()}`);
92
- const executablePath = join(tempRoot, "tool with spaces.exe");
93
- mkdirSync(tempRoot, { recursive: true });
94
- writeFileSync(executablePath, "");
95
- try {
96
- const resolved = await resolveExecutable({
97
- commandNames: ["test"],
98
- label: "test",
99
- configuredPath: `"${executablePath}"`,
100
- });
101
- assert.equal(resolved, executablePath);
102
- } finally {
103
- rmSync(tempRoot, { recursive: true, force: true });
104
- }
105
- });
106
-
107
- test("resolver: falls back to bare name if not found", async () => {
108
- const mockImpl = mockRunCommand(commandResult({ status: "failed", exitCode: 1 }));
109
- const resolved = await resolveExecutable({
110
- runCommandImpl: mockImpl,
111
- commandNames: ["mytest"],
112
- label: "test",
113
- });
114
- assert.equal(resolved, "mytest");
115
- });
116
-
117
- test("buildSpawnSpec: wraps .cmd files in cmd.exe on Windows", async () => {
118
- if (process.platform !== "win32") return;
119
- const spec = buildSpawnSpec("test.cmd", ["arg1"]);
120
- assert.equal(spec.executable, "cmd.exe");
121
- assert.deepEqual(spec.args, ["/d", "/s", "/c", "call", "test.cmd", "arg1"]);
122
- });
123
-
124
- test("buildSpawnSpec: does not wrap .exe files", async () => {
125
- if (process.platform !== "win32") return;
126
- const spec = buildSpawnSpec("test.exe", ["arg1"]);
127
- assert.equal(spec.executable, "test.exe");
128
- assert.deepEqual(spec.args, ["arg1"]);
129
- });
@@ -1,116 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import type { ChildProcess } from "node:child_process";
4
- import { mkdirSync, rmSync, writeFileSync } from "node:fs";
5
- import { tmpdir } from "node:os";
6
- import { join } from "node:path";
7
- import { buildGeminiSpawnSpec, resetGeminiExecutableCacheForTests, resolveGeminiExecutable } from "./geminiExecutable.js";
8
- import { runCommand, type CommandResult } from "../process/CommandRunner.js";
9
-
10
- function commandResult(overrides: Partial<CommandResult>): CommandResult {
11
- return {
12
- status: "completed",
13
- exitCode: 0,
14
- signal: null,
15
- stdout: "",
16
- stderr: "",
17
- startedAt: 0,
18
- endedAt: 0,
19
- durationMs: 0,
20
- userMessage: "Command completed.",
21
- ...overrides,
22
- };
23
- }
24
-
25
- function mockRunCommand(onCall: (spec: Parameters<typeof runCommand>[0]) => CommandResult): typeof runCommand {
26
- return ((spec) => ({
27
- child: null as unknown as ChildProcess,
28
- result: Promise.resolve(onCall(spec)),
29
- cancel: () => undefined,
30
- })) as typeof runCommand;
31
- }
32
-
33
- async function withEnv<T>(env: Partial<NodeJS.ProcessEnv>, callback: () => Promise<T>): Promise<T> {
34
- const originalExecutable = process.env.GEMINI_EXECUTABLE;
35
- const originalCliPath = process.env.GEMINI_CLI_PATH;
36
- const originalAppData = process.env.APPDATA;
37
- try {
38
- if ("GEMINI_EXECUTABLE" in env) process.env.GEMINI_EXECUTABLE = env.GEMINI_EXECUTABLE;
39
- else delete process.env.GEMINI_EXECUTABLE;
40
- if ("GEMINI_CLI_PATH" in env) process.env.GEMINI_CLI_PATH = env.GEMINI_CLI_PATH;
41
- else delete process.env.GEMINI_CLI_PATH;
42
- if ("APPDATA" in env) process.env.APPDATA = env.APPDATA;
43
- resetGeminiExecutableCacheForTests();
44
- return await callback();
45
- } finally {
46
- if (originalExecutable === undefined) delete process.env.GEMINI_EXECUTABLE;
47
- else process.env.GEMINI_EXECUTABLE = originalExecutable;
48
- if (originalCliPath === undefined) delete process.env.GEMINI_CLI_PATH;
49
- else process.env.GEMINI_CLI_PATH = originalCliPath;
50
- if (originalAppData === undefined) delete process.env.APPDATA;
51
- else process.env.APPDATA = originalAppData;
52
- resetGeminiExecutableCacheForTests();
53
- }
54
- }
55
-
56
- test("Gemini resolver: env GEMINI_EXECUTABLE wins over PATH", async () => {
57
- await withEnv({ GEMINI_EXECUTABLE: "env-gemini.cmd" }, async () => {
58
- let whereCalled = false;
59
- const resolved = await resolveGeminiExecutable({
60
- runCommandImpl: mockRunCommand((spec) => {
61
- if (spec.executable === "where.exe") whereCalled = true;
62
- return commandResult({ stdout: "C:\\Tools\\gemini.cmd\n" });
63
- }),
64
- });
65
-
66
- assert.equal(resolved, "env-gemini.cmd");
67
- assert.equal(whereCalled, false);
68
- });
69
- });
70
-
71
- test("Gemini resolver: APPDATA npm shim fallback works", async () => {
72
- if (process.platform !== "win32") return;
73
- const tempRoot = join(tmpdir(), `codexa-gemini-${Date.now()}`);
74
- const npmDir = join(tempRoot, "npm");
75
- const shim = join(npmDir, "gemini.cmd");
76
- mkdirSync(npmDir, { recursive: true });
77
- writeFileSync(shim, "@echo off\r\n", "utf-8");
78
-
79
- try {
80
- await withEnv({ APPDATA: tempRoot }, async () => {
81
- const resolved = await resolveGeminiExecutable({
82
- runCommandImpl: mockRunCommand(() => commandResult({ status: "failed", exitCode: 1 })),
83
- });
84
- assert.equal(resolved, shim);
85
- });
86
- } finally {
87
- rmSync(tempRoot, { recursive: true, force: true });
88
- }
89
- });
90
-
91
- test("Gemini resolver: PowerShell function text is not accepted as executable path", async () => {
92
- if (process.platform !== "win32") return;
93
- await withEnv({}, async () => {
94
- const tempRoot = join(tmpdir(), `codexa-gemini-real-${Date.now()}`);
95
- const npmDir = join(tempRoot, "npm");
96
- const shim = join(npmDir, "gemini.cmd");
97
- mkdirSync(npmDir, { recursive: true });
98
- writeFileSync(shim, "@echo off\r\n", "utf-8");
99
- process.env.APPDATA = tempRoot;
100
- try {
101
- const resolved = await resolveGeminiExecutable({
102
- runCommandImpl: mockRunCommand(() => commandResult({ stdout: "function gemini { param($p) }\n" })),
103
- });
104
- assert.equal(resolved, shim);
105
- } finally {
106
- rmSync(tempRoot, { recursive: true, force: true });
107
- }
108
- });
109
- });
110
-
111
- test("Gemini spawn spec bypasses PowerShell and targets the resolved executable", () => {
112
- const spec = buildGeminiSpawnSpec("C:\\Users\\Example\\AppData\\Roaming\\npm\\gemini.cmd", ["-p", "Respond with READY only."]);
113
- assert.equal(spec.executable, "C:\\Users\\Example\\AppData\\Roaming\\npm\\gemini.cmd");
114
- assert.deepEqual(spec.args, ["-p", "Respond with READY only."]);
115
- assert.equal(spec.shell, undefined);
116
- });
@@ -1,47 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { readdirSync, readFileSync, statSync } from "node:fs";
4
- import { join } from "node:path";
5
-
6
- function collectTsFiles(dir: string): string[] {
7
- const result: string[] = [];
8
- for (const entry of readdirSync(dir)) {
9
- const full = join(dir, entry);
10
- if (statSync(full).isDirectory()) {
11
- result.push(...collectTsFiles(full));
12
- } else if (full.endsWith(".ts")) {
13
- result.push(full);
14
- }
15
- }
16
- return result;
17
- }
18
-
19
- const SRC_ROOT = join(import.meta.dirname, "../../..");
20
-
21
- // Guard against personal user-specific paths appearing in source.
22
- // Patterns are split across array entries to prevent THIS file from matching itself.
23
- const BANNED_FRAGMENTS: Array<[string, string]> = [
24
- ["C", ":\\\\Users\\\\jorda"],
25
- ["C", ":/Users/jorda"],
26
- ];
27
-
28
- test("no source files contain personal hardcoded user paths", { timeout: 30_000 }, () => {
29
- const patterns = BANNED_FRAGMENTS.map(([a, b]) => new RegExp(a + b));
30
- const files = collectTsFiles(SRC_ROOT);
31
- const violations: string[] = [];
32
-
33
- for (const file of files) {
34
- const content = readFileSync(file, "utf-8");
35
- for (const pattern of patterns) {
36
- if (pattern.test(content)) {
37
- violations.push(`${file}: matches ${pattern}`);
38
- }
39
- }
40
- }
41
-
42
- assert.deepEqual(
43
- violations,
44
- [],
45
- `Personal hardcoded user paths found in source:\n${violations.join("\n")}`,
46
- );
47
- });
@@ -1,92 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import {
4
- parseRepoIdentity,
5
- classifyDiagnostics,
6
- type DiagnosticResult,
7
- type RepoIdentity
8
- } from "./githubDiagnostics.js";
9
-
10
- test("parseRepoIdentity: handles various GitHub URL formats", () => {
11
- const cases = [
12
- {
13
- url: "https://github.com/owner/repo.git",
14
- expected: { owner: "owner", repo: "repo", provider: "github" }
15
- },
16
- {
17
- url: "git@github.com:owner/repo.git",
18
- expected: { owner: "owner", repo: "repo", provider: "github" }
19
- },
20
- {
21
- url: "https://github.com/owner/repo",
22
- expected: { owner: "owner", repo: "repo", provider: "github" }
23
- },
24
- {
25
- url: "ssh://git@github.com/owner/repo.git",
26
- expected: { owner: "owner", repo: "repo", provider: "github" }
27
- }
28
- ];
29
-
30
- for (const { url, expected } of cases) {
31
- const result = parseRepoIdentity(url);
32
- assert.ok(result);
33
- assert.equal(result.owner, expected.owner);
34
- assert.equal(result.repo, expected.repo);
35
- assert.equal(result.provider, expected.provider);
36
- }
37
- });
38
-
39
- test("parseRepoIdentity: handles missing or non-GitHub origins", () => {
40
- assert.equal(parseRepoIdentity(null), null);
41
- assert.equal(parseRepoIdentity(""), null);
42
-
43
- const other = parseRepoIdentity("https://gitlab.com/owner/repo.git");
44
- assert.ok(other);
45
- assert.equal(other.provider, "other");
46
- });
47
-
48
- test("classifyDiagnostics: handles all status combinations", () => {
49
- const repo: RepoIdentity = { owner: "o", repo: "r", provider: "github", remoteUrl: "..." };
50
-
51
- const pass: DiagnosticResult = { path: "p", status: "PASS", evidence: "e", blocker: null, recommendedUse: false };
52
- const fail: DiagnosticResult = { path: "p", status: "FAIL", evidence: "e", blocker: "b", recommendedUse: false };
53
- const partial: DiagnosticResult = { path: "p", status: "PARTIAL", evidence: "e", blocker: "b", recommendedUse: false };
54
-
55
- // 1. All PASS -> Local Git + GH CLI
56
- assert.equal(
57
- classifyDiagnostics(repo, pass, pass, pass, fail),
58
- "Local Git + GH CLI"
59
- );
60
-
61
- // 2. GH CLI missing but connector OK -> Connector-only
62
- assert.equal(
63
- classifyDiagnostics(repo, fail, pass, pass, pass),
64
- "Local Git + connector PR creation"
65
- );
66
-
67
- // 3. .git write fails but connector OK -> Connector-only
68
- assert.equal(
69
- classifyDiagnostics(repo, pass, pass, fail, pass),
70
- "Connector-only"
71
- );
72
-
73
- // 4. Connector read OK but write unknown (treated as PASS if not auth error)
74
- assert.equal(
75
- classifyDiagnostics(repo, fail, fail, fail, pass),
76
- "Connector-only"
77
- );
78
-
79
- // 5. Connector permission rejected
80
- const connectorRejected: DiagnosticResult = { path: "p", status: "PARTIAL", evidence: "e", blocker: "auth error", recommendedUse: false };
81
- assert.equal(
82
- classifyDiagnostics(repo, fail, fail, fail, connectorRejected),
83
- "Cannot publish yet"
84
- );
85
-
86
- // 6. Non-GitHub repo
87
- const otherRepo: RepoIdentity = { owner: "", repo: "", provider: "other", remoteUrl: "..." };
88
- assert.equal(
89
- classifyDiagnostics(otherRepo, pass, pass, pass, pass),
90
- "Cannot publish yet"
91
- );
92
- });
@@ -1,58 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { formatHollowResponse } from "./hollowResponseFormat.js";
4
- import type { HollowResponseResult } from "./codexPrompt.js";
5
-
6
- function makeResult(kind: HollowResponseResult["kind"], reason = ""): HollowResponseResult {
7
- return { isHollow: true, kind, reason };
8
- }
9
-
10
- test("greeting produces exactly 2 lines", () => {
11
- const out = formatHollowResponse(makeResult("greeting"));
12
- const lines = out.split("\n");
13
- assert.equal(lines.length, 2);
14
- assert.match(lines[0]!, /generic greeting/);
15
- assert.match(lines[1]!, /Retry/);
16
- });
17
-
18
- test("filler produces exactly 2 lines", () => {
19
- const out = formatHollowResponse(makeResult("filler"));
20
- const lines = out.split("\n");
21
- assert.equal(lines.length, 2);
22
- assert.match(lines[0]!, /acknowledged without acting/);
23
- });
24
-
25
- test("clarification produces exactly 2 lines", () => {
26
- const out = formatHollowResponse(makeResult("clarification"));
27
- const lines = out.split("\n");
28
- assert.equal(lines.length, 2);
29
- assert.match(lines[0]!, /clarification/);
30
- assert.match(lines[1]!, /suggest mode/);
31
- });
32
-
33
- test("short-no-action produces exactly 2 lines", () => {
34
- const out = formatHollowResponse(makeResult("short-no-action"));
35
- const lines = out.split("\n");
36
- assert.equal(lines.length, 2);
37
- assert.match(lines[0]!, /too brief/);
38
- });
39
-
40
- test("no emoji or warning symbols in any output", () => {
41
- for (const kind of ["greeting", "filler", "clarification", "short-no-action"] as const) {
42
- const out = formatHollowResponse(makeResult(kind));
43
- assert.doesNotMatch(out, /⚠/);
44
- assert.doesNotMatch(out, /---/);
45
- }
46
- });
47
-
48
- test("verbose=false omits raw response", () => {
49
- const out = formatHollowResponse(makeResult("greeting"), "Hello.");
50
- assert.doesNotMatch(out, /Hello\./);
51
- });
52
-
53
- test("verbose=true appends raw response", () => {
54
- const out = formatHollowResponse(makeResult("greeting"), "Hello.", true);
55
- assert.match(out, /Backend response: Hello\./);
56
- const lines = out.split("\n");
57
- assert.equal(lines.length, 4); // 2 message lines + blank + backend response
58
- });