@golba98/codexa 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/README.md +320 -0
  2. package/bin/codexa.js +445 -0
  3. package/package.json +45 -0
  4. package/scripts/audit-codexa-capabilities.mjs +466 -0
  5. package/scripts/smoke-terminal-bench.mjs +35 -0
  6. package/src/app.tsx +4561 -0
  7. package/src/appRenderStability.test.ts +131 -0
  8. package/src/commands/handler.test.ts +643 -0
  9. package/src/commands/handler.ts +875 -0
  10. package/src/config/launchArgs.test.ts +158 -0
  11. package/src/config/launchArgs.ts +186 -0
  12. package/src/config/layeredConfig.test.ts +143 -0
  13. package/src/config/layeredConfig.ts +836 -0
  14. package/src/config/persistence.test.ts +110 -0
  15. package/src/config/persistence.ts +311 -0
  16. package/src/config/runtimeConfig.test.ts +218 -0
  17. package/src/config/runtimeConfig.ts +554 -0
  18. package/src/config/settings.test.ts +155 -0
  19. package/src/config/settings.ts +401 -0
  20. package/src/config/toml-serialize.ts +98 -0
  21. package/src/config/trustStore.test.ts +29 -0
  22. package/src/config/trustStore.ts +68 -0
  23. package/src/core/attachments.test.ts +155 -0
  24. package/src/core/attachments.ts +71 -0
  25. package/src/core/auth/codexAuth.test.ts +68 -0
  26. package/src/core/auth/codexAuth.ts +359 -0
  27. package/src/core/cleanupFastFail.test.ts +76 -0
  28. package/src/core/cleanupFastFail.ts +67 -0
  29. package/src/core/clipboard.ts +24 -0
  30. package/src/core/codex.ts +124 -0
  31. package/src/core/codexExecArgs.test.ts +195 -0
  32. package/src/core/codexExecArgs.ts +152 -0
  33. package/src/core/codexLaunch.test.ts +205 -0
  34. package/src/core/codexLaunch.ts +162 -0
  35. package/src/core/codexPrompt.test.ts +252 -0
  36. package/src/core/codexPrompt.ts +428 -0
  37. package/src/core/executables/claudeExecutable.ts +63 -0
  38. package/src/core/executables/codexExecutable.test.ts +212 -0
  39. package/src/core/executables/codexExecutable.ts +159 -0
  40. package/src/core/executables/executableResolver.test.ts +129 -0
  41. package/src/core/executables/executableResolver.ts +138 -0
  42. package/src/core/executables/geminiExecutable.test.ts +116 -0
  43. package/src/core/executables/geminiExecutable.ts +78 -0
  44. package/src/core/executables/pathSanityScan.test.ts +47 -0
  45. package/src/core/githubDiagnostics.test.ts +92 -0
  46. package/src/core/githubDiagnostics.ts +222 -0
  47. package/src/core/hollowResponseFormat.test.ts +58 -0
  48. package/src/core/hollowResponseFormat.ts +39 -0
  49. package/src/core/inputDebug.ts +51 -0
  50. package/src/core/launchContext.test.ts +157 -0
  51. package/src/core/launchContext.ts +266 -0
  52. package/src/core/models/codexCapabilities.test.ts +45 -0
  53. package/src/core/models/codexCapabilities.ts +95 -0
  54. package/src/core/models/codexModelCapabilities.test.ts +246 -0
  55. package/src/core/models/codexModelCapabilities.ts +571 -0
  56. package/src/core/models/modelSpecs.test.ts +283 -0
  57. package/src/core/models/modelSpecs.ts +300 -0
  58. package/src/core/perf/profiler.ts +125 -0
  59. package/src/core/perf/renderDebug.test.ts +230 -0
  60. package/src/core/perf/renderDebug.ts +373 -0
  61. package/src/core/planStorage.test.ts +143 -0
  62. package/src/core/planStorage.ts +141 -0
  63. package/src/core/process/CommandRunner.test.ts +105 -0
  64. package/src/core/process/CommandRunner.ts +269 -0
  65. package/src/core/process/processValidation.ts +101 -0
  66. package/src/core/projectInstructions.test.ts +50 -0
  67. package/src/core/projectInstructions.ts +54 -0
  68. package/src/core/providerLauncher/launcher.test.ts +238 -0
  69. package/src/core/providerLauncher/launcher.ts +203 -0
  70. package/src/core/providerLauncher/registry.test.ts +324 -0
  71. package/src/core/providerLauncher/registry.ts +253 -0
  72. package/src/core/providerLauncher/types.ts +84 -0
  73. package/src/core/providerLauncher/workspaceConfig.test.ts +638 -0
  74. package/src/core/providerLauncher/workspaceConfig.ts +407 -0
  75. package/src/core/providerRuntime/anthropic.test.ts +1120 -0
  76. package/src/core/providerRuntime/anthropic.ts +576 -0
  77. package/src/core/providerRuntime/capabilityProfile.test.ts +311 -0
  78. package/src/core/providerRuntime/capabilityProfile.ts +288 -0
  79. package/src/core/providerRuntime/claudeCodeDiscovery.ts +446 -0
  80. package/src/core/providerRuntime/contextMetadata.test.ts +468 -0
  81. package/src/core/providerRuntime/contextMetadata.ts +409 -0
  82. package/src/core/providerRuntime/gemini.test.ts +437 -0
  83. package/src/core/providerRuntime/gemini.ts +784 -0
  84. package/src/core/providerRuntime/lmstudio.test.ts +168 -0
  85. package/src/core/providerRuntime/lmstudio.ts +118 -0
  86. package/src/core/providerRuntime/local.test.ts +787 -0
  87. package/src/core/providerRuntime/local.ts +754 -0
  88. package/src/core/providerRuntime/models.ts +150 -0
  89. package/src/core/providerRuntime/reasoning.ts +17 -0
  90. package/src/core/providerRuntime/registry.test.ts +233 -0
  91. package/src/core/providerRuntime/registry.ts +203 -0
  92. package/src/core/providerRuntime/types.ts +103 -0
  93. package/src/core/providers/codexJsonStream.test.ts +148 -0
  94. package/src/core/providers/codexJsonStream.ts +305 -0
  95. package/src/core/providers/codexSubprocess.test.ts +68 -0
  96. package/src/core/providers/codexSubprocess.ts +372 -0
  97. package/src/core/providers/codexTranscript.test.ts +284 -0
  98. package/src/core/providers/codexTranscript.ts +695 -0
  99. package/src/core/providers/openaiNative.ts +13 -0
  100. package/src/core/providers/registry.ts +21 -0
  101. package/src/core/providers/types.ts +59 -0
  102. package/src/core/terminal/terminalCapabilities.test.ts +93 -0
  103. package/src/core/terminal/terminalCapabilities.ts +100 -0
  104. package/src/core/terminal/terminalControl.test.ts +75 -0
  105. package/src/core/terminal/terminalControl.ts +147 -0
  106. package/src/core/terminal/terminalSanitize.test.ts +22 -0
  107. package/src/core/terminal/terminalSanitize.ts +147 -0
  108. package/src/core/terminal/terminalSelection.test.ts +42 -0
  109. package/src/core/terminal/terminalSelection.ts +66 -0
  110. package/src/core/terminal/terminalTitle.test.ts +328 -0
  111. package/src/core/terminal/terminalTitle.ts +483 -0
  112. package/src/core/workspaceActivity.test.ts +163 -0
  113. package/src/core/workspaceActivity.ts +380 -0
  114. package/src/core/workspaceGuard.test.ts +151 -0
  115. package/src/core/workspaceGuard.ts +288 -0
  116. package/src/core/workspaceRoot.test.ts +23 -0
  117. package/src/core/workspaceRoot.ts +47 -0
  118. package/src/exec.test.ts +13 -0
  119. package/src/exec.ts +72 -0
  120. package/src/headless/execArgs.test.ts +147 -0
  121. package/src/headless/execArgs.ts +294 -0
  122. package/src/headless/execRunner.test.ts +434 -0
  123. package/src/headless/execRunner.ts +304 -0
  124. package/src/index.test.tsx +618 -0
  125. package/src/index.tsx +296 -0
  126. package/src/session/appSession.test.ts +897 -0
  127. package/src/session/appSession.ts +761 -0
  128. package/src/session/chatLifecycle.test.ts +64 -0
  129. package/src/session/chatLifecycle.ts +951 -0
  130. package/src/session/liveRenderScheduler.test.ts +201 -0
  131. package/src/session/liveRenderScheduler.ts +214 -0
  132. package/src/session/planFlow.test.ts +103 -0
  133. package/src/session/planFlow.ts +149 -0
  134. package/src/session/planTranscript.test.ts +65 -0
  135. package/src/session/planTranscript.ts +15 -0
  136. package/src/session/promptRunSchedule.test.ts +36 -0
  137. package/src/session/promptRunSchedule.ts +26 -0
  138. package/src/session/types.ts +228 -0
  139. package/src/test/runtimeTestUtils.ts +14 -0
  140. package/src/types/react-dom.d.ts +3 -0
  141. package/src/ui/ActionRequiredBlock.tsx +38 -0
  142. package/src/ui/ActivityBars.tsx +68 -0
  143. package/src/ui/ActivityIndicator.test.tsx +58 -0
  144. package/src/ui/ActivityIndicator.tsx +58 -0
  145. package/src/ui/AgentBlock.test.ts +6 -0
  146. package/src/ui/AgentBlock.tsx +130 -0
  147. package/src/ui/AnimatedStatusText.test.ts +16 -0
  148. package/src/ui/AnimatedStatusText.tsx +69 -0
  149. package/src/ui/AppShell.test.tsx +1739 -0
  150. package/src/ui/AppShell.tsx +698 -0
  151. package/src/ui/AttachmentImportPanel.test.tsx +204 -0
  152. package/src/ui/AttachmentImportPanel.tsx +98 -0
  153. package/src/ui/AuthPanel.tsx +113 -0
  154. package/src/ui/BackendPicker.tsx +28 -0
  155. package/src/ui/BottomComposer.test.ts +674 -0
  156. package/src/ui/BottomComposer.tsx +1028 -0
  157. package/src/ui/CodexLogo.tsx +55 -0
  158. package/src/ui/DashCard.tsx +82 -0
  159. package/src/ui/Markdown.test.ts +157 -0
  160. package/src/ui/Markdown.tsx +310 -0
  161. package/src/ui/ModePicker.tsx +27 -0
  162. package/src/ui/ModelPicker.tsx +31 -0
  163. package/src/ui/ModelPickerProviderScope.test.tsx +411 -0
  164. package/src/ui/ModelPickerScreen.test.tsx +99 -0
  165. package/src/ui/ModelPickerScreen.tsx +416 -0
  166. package/src/ui/ModelPickerState.test.tsx +151 -0
  167. package/src/ui/ModelReasoningPicker.test.tsx +447 -0
  168. package/src/ui/ModelReasoningPicker.tsx +458 -0
  169. package/src/ui/Panel.tsx +51 -0
  170. package/src/ui/PermissionsPanel.tsx +78 -0
  171. package/src/ui/PlanActionPicker.tsx +119 -0
  172. package/src/ui/PlanReviewPanel.test.tsx +267 -0
  173. package/src/ui/PlanReviewPanel.tsx +212 -0
  174. package/src/ui/PromptCardBorder.test.tsx +161 -0
  175. package/src/ui/ProviderPicker.test.tsx +289 -0
  176. package/src/ui/ProviderPicker.tsx +321 -0
  177. package/src/ui/ProviderShortcut.test.tsx +143 -0
  178. package/src/ui/ReasoningPicker.tsx +46 -0
  179. package/src/ui/RunFooter.tsx +65 -0
  180. package/src/ui/SelectionPanel.tsx +67 -0
  181. package/src/ui/SettingsPanel.test.tsx +233 -0
  182. package/src/ui/SettingsPanel.tsx +156 -0
  183. package/src/ui/Spinner.tsx +25 -0
  184. package/src/ui/StaticIntroItem.tsx +54 -0
  185. package/src/ui/StaticTranscriptItem.tsx +56 -0
  186. package/src/ui/TextEntryPanel.tsx +139 -0
  187. package/src/ui/ThemePicker.tsx +31 -0
  188. package/src/ui/ThinkingBlock.tsx +100 -0
  189. package/src/ui/Timeline.test.ts +2067 -0
  190. package/src/ui/Timeline.tsx +1472 -0
  191. package/src/ui/TimelineNavigation.test.tsx +201 -0
  192. package/src/ui/TopHeader.test.tsx +239 -0
  193. package/src/ui/TopHeader.tsx +257 -0
  194. package/src/ui/TurnGroup.test.tsx +365 -0
  195. package/src/ui/TurnGroup.tsx +657 -0
  196. package/src/ui/busyStatusAnimation.test.ts +30 -0
  197. package/src/ui/busyStatusAnimation.ts +11 -0
  198. package/src/ui/commandNormalize.test.ts +142 -0
  199. package/src/ui/commandNormalize.ts +66 -0
  200. package/src/ui/diffRenderer.test.ts +102 -0
  201. package/src/ui/diffRenderer.ts +116 -0
  202. package/src/ui/focus.ts +61 -0
  203. package/src/ui/focusFlow.test.tsx +1098 -0
  204. package/src/ui/inputBuffer.test.ts +151 -0
  205. package/src/ui/inputBuffer.ts +203 -0
  206. package/src/ui/layout.test.ts +145 -0
  207. package/src/ui/layout.ts +287 -0
  208. package/src/ui/modeDisplay.test.ts +42 -0
  209. package/src/ui/modeDisplay.ts +52 -0
  210. package/src/ui/outputPipeline.ts +64 -0
  211. package/src/ui/progressEntries.ts +156 -0
  212. package/src/ui/runActivityView.test.ts +89 -0
  213. package/src/ui/runActivityView.ts +37 -0
  214. package/src/ui/runLifecycleView.test.tsx +237 -0
  215. package/src/ui/slashCommands.ts +41 -0
  216. package/src/ui/statusRenderIsolation.test.tsx +654 -0
  217. package/src/ui/terminalAnswerFormat.test.ts +19 -0
  218. package/src/ui/terminalAnswerFormat.ts +128 -0
  219. package/src/ui/textLayout.test.ts +18 -0
  220. package/src/ui/textLayout.ts +338 -0
  221. package/src/ui/theme.tsx +395 -0
  222. package/src/ui/themeFlow.test.ts +53 -0
  223. package/src/ui/themeFlow.ts +41 -0
  224. package/src/ui/timelineMeasure.ts +3088 -0
  225. package/src/ui/timelineMeasureCache.test.ts +986 -0
  226. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,116 @@
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
+ });
@@ -0,0 +1,78 @@
1
+ import { join } from "path";
2
+ import { runCommand } from "../process/CommandRunner.js";
3
+ import { resolveExecutable } from "./executableResolver.js";
4
+
5
+ type CommandRunner = typeof runCommand;
6
+
7
+ let cachedExecutable: string | null = null;
8
+
9
+ export function resetGeminiExecutableCacheForTests(): void {
10
+ cachedExecutable = null;
11
+ }
12
+
13
+ /**
14
+ * Returns the resolved Gemini CLI executable (full path or bare name).
15
+ *
16
+ * Priority:
17
+ * 1. Configured path override (geminiCommandPath)
18
+ * 2. GEMINI_EXECUTABLE or GEMINI_CLI_PATH env var
19
+ * 3. Windows PATH lookup for real files: gemini.exe, gemini.cmd, gemini.bat, gemini
20
+ * 4. Windows where.exe gemini fallback
21
+ * 5. Common npm/global locations on Windows
22
+ */
23
+ export async function resolveGeminiExecutable(options?: {
24
+ runCommandImpl?: CommandRunner;
25
+ cwd?: string;
26
+ configuredPath?: string | null;
27
+ }): Promise<string> {
28
+ if (!options?.configuredPath && !options?.runCommandImpl && cachedExecutable !== null) {
29
+ return cachedExecutable;
30
+ }
31
+
32
+ const knownPathDirectories: string[] = [];
33
+ const userProfile = process.env.USERPROFILE;
34
+ const appData = process.env.APPDATA;
35
+ const localAppData = process.env.LOCALAPPDATA;
36
+
37
+ if (process.platform === "win32") {
38
+ if (appData) {
39
+ knownPathDirectories.push(join(appData, "npm"));
40
+ }
41
+ if (localAppData) {
42
+ knownPathDirectories.push(join(localAppData, "Programs", "nodejs"));
43
+ }
44
+ }
45
+
46
+ if (userProfile) {
47
+ knownPathDirectories.push(join(userProfile, ".local", "bin"));
48
+ knownPathDirectories.push(join(userProfile, "bin"));
49
+ }
50
+
51
+ const result = await resolveExecutable({
52
+ runCommandImpl: options?.runCommandImpl,
53
+ cwd: options?.cwd,
54
+ configuredPath: options?.configuredPath,
55
+ envOverrides: ["GEMINI_EXECUTABLE", "GEMINI_CLI_PATH"],
56
+ commandNames: ["gemini.exe", "gemini.cmd", "gemini.bat", "gemini"],
57
+ knownPathDirectories,
58
+ knownFilePaths: [],
59
+ label: "gemini",
60
+ allowBareFallback: process.platform !== "win32",
61
+ requireResolvedFile: true,
62
+ });
63
+
64
+ if (!options?.configuredPath && !options?.runCommandImpl) {
65
+ cachedExecutable = result;
66
+ }
67
+ return result;
68
+ }
69
+
70
+ /**
71
+ * Builds the spawn spec for a resolved Gemini executable.
72
+ */
73
+ export function buildGeminiSpawnSpec(
74
+ executable: string,
75
+ args: string[],
76
+ ): { executable: string; args: string[]; shell?: boolean } {
77
+ return { executable, args };
78
+ }
@@ -0,0 +1,47 @@
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
+ });
@@ -0,0 +1,92 @@
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
+ });
@@ -0,0 +1,222 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ export interface RepoIdentity {
6
+ owner: string;
7
+ repo: string;
8
+ provider: "github" | "other";
9
+ remoteUrl: string;
10
+ }
11
+
12
+ export interface DiagnosticResult {
13
+ path: string;
14
+ status: "PASS" | "FAIL" | "PARTIAL";
15
+ evidence: string;
16
+ blocker: string | null;
17
+ recommendedUse: boolean;
18
+ }
19
+
20
+ export interface DiagnosticsReport {
21
+ repo: RepoIdentity | null;
22
+ defaultBranch: string | null;
23
+ ghCliUser: string | null;
24
+ connectorUser: string | null;
25
+ paths: {
26
+ ghCli: DiagnosticResult;
27
+ localGit: DiagnosticResult;
28
+ localGitWrite: DiagnosticResult;
29
+ connector: DiagnosticResult;
30
+ };
31
+ recommendedFlow:
32
+ | "Local Git + GH CLI"
33
+ | "Local Git + connector PR creation"
34
+ | "Connector-only"
35
+ | "Cannot publish yet";
36
+ }
37
+
38
+ export function parseRepoIdentity(remoteUrl: string | undefined | null): RepoIdentity | null {
39
+ if (!remoteUrl) return null;
40
+
41
+ const url = remoteUrl.trim();
42
+
43
+ // HTTPS: https://github.com/owner/repo.git or https://github.com/owner/repo
44
+ const httpsMatch = url.match(/^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?\/?$/i);
45
+ if (httpsMatch) {
46
+ return {
47
+ owner: httpsMatch[1],
48
+ repo: httpsMatch[2],
49
+ provider: "github",
50
+ remoteUrl: url,
51
+ };
52
+ }
53
+
54
+ // SSH: git@github.com:owner/repo.git or ssh://git@github.com/owner/repo.git
55
+ const sshMatch = url.match(/^(?:ssh:\/\/)?git@github\.com[:\/]([^/]+)\/([^/.]+?)(?:\.git)?\/?$/i);
56
+ if (sshMatch) {
57
+ return {
58
+ owner: sshMatch[1],
59
+ repo: sshMatch[2],
60
+ provider: "github",
61
+ remoteUrl: url,
62
+ };
63
+ }
64
+
65
+ return {
66
+ owner: "",
67
+ repo: "",
68
+ provider: "other",
69
+ remoteUrl: url,
70
+ };
71
+ }
72
+
73
+ export function getLocalGitRemoteUrl(): string | null {
74
+ try {
75
+ return execSync("git remote get-url origin", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ export function checkGhCli(): DiagnosticResult {
82
+ const result: DiagnosticResult = {
83
+ path: "GH CLI",
84
+ status: "FAIL",
85
+ evidence: "",
86
+ blocker: null,
87
+ recommendedUse: false,
88
+ };
89
+
90
+ try {
91
+ const version = execSync("gh --version", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\n")[0];
92
+ result.evidence = version ?? "Unknown version";
93
+ } catch {
94
+ result.blocker = "gh CLI not installed or not in PATH";
95
+ return result;
96
+ }
97
+
98
+ try {
99
+ // gh auth status output format is not structured JSON; pattern-match on known strings.
100
+ const authStatus = execSync("gh auth status", { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
101
+ result.evidence += " | Authenticated";
102
+ if (authStatus.includes("Token scopes")) {
103
+ const scopes = authStatus.match(/Token scopes: (.*)/)?.[1];
104
+ if (scopes && !scopes.includes("repo")) {
105
+ result.status = "PARTIAL";
106
+ result.blocker = "Token missing 'repo' scope";
107
+ } else {
108
+ result.status = "PASS";
109
+ }
110
+ } else {
111
+ result.status = "PASS";
112
+ }
113
+ } catch {
114
+ result.blocker = "Not logged in to GitHub CLI";
115
+ }
116
+
117
+ return result;
118
+ }
119
+
120
+ export function checkLocalGitRemote(): DiagnosticResult {
121
+ const result: DiagnosticResult = {
122
+ path: "Local git remote",
123
+ status: "FAIL",
124
+ evidence: "",
125
+ blocker: null,
126
+ recommendedUse: false,
127
+ };
128
+
129
+ try {
130
+ const remote = execSync("git remote -v", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\n")[0];
131
+ result.evidence = remote ?? "No remote found";
132
+
133
+ execSync("git ls-remote origin HEAD", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
134
+ result.status = "PASS";
135
+ } catch {
136
+ result.blocker = "Cannot reach origin remote (check connectivity or remote URL)";
137
+ }
138
+
139
+ return result;
140
+ }
141
+
142
+ export function checkLocalGitWrite(): DiagnosticResult {
143
+ const result: DiagnosticResult = {
144
+ path: "Local .git write",
145
+ status: "FAIL",
146
+ evidence: "",
147
+ blocker: null,
148
+ recommendedUse: false,
149
+ };
150
+
151
+ const indexLock = join(".git", "index.lock");
152
+ if (existsSync(indexLock)) {
153
+ result.blocker = ".git/index.lock exists (git process might be running)";
154
+ return result;
155
+ }
156
+
157
+ try {
158
+ execSync("git update-ref refs/heads/codexa-diagnostic-lock-test HEAD", { stdio: "ignore" });
159
+ execSync("git update-ref -d refs/heads/codexa-diagnostic-lock-test", { stdio: "ignore" });
160
+ result.status = "PASS";
161
+ result.evidence = "Can create/delete refs";
162
+ } catch (error) {
163
+ result.blocker = "Failed to create/delete ref lock (permission denied?)";
164
+ result.evidence = error instanceof Error ? error.message : String(error);
165
+ }
166
+
167
+ return result;
168
+ }
169
+
170
+ export function classifyDiagnostics(
171
+ repo: RepoIdentity | null,
172
+ ghCli: DiagnosticResult,
173
+ localGit: DiagnosticResult,
174
+ localGitWrite: DiagnosticResult,
175
+ connector: DiagnosticResult
176
+ ): DiagnosticsReport["recommendedFlow"] {
177
+ const isGitHub = repo?.provider === "github";
178
+ if (!isGitHub) return "Cannot publish yet";
179
+
180
+ const ghCliOk = ghCli.status === "PASS";
181
+ const gitRemoteOk = localGit.status === "PASS";
182
+ const gitWriteOk = localGitWrite.status === "PASS";
183
+ const connectorOk = connector.status === "PASS" || (connector.status === "PARTIAL" && !connector.blocker?.includes("auth"));
184
+
185
+ if (ghCliOk && gitRemoteOk && gitWriteOk) {
186
+ return "Local Git + GH CLI";
187
+ }
188
+
189
+ if (connectorOk) {
190
+ if (gitWriteOk && gitRemoteOk) {
191
+ return "Local Git + connector PR creation";
192
+ }
193
+ return "Connector-only";
194
+ }
195
+
196
+ return "Cannot publish yet";
197
+ }
198
+
199
+ export function printDiagnosticsTable(report: DiagnosticsReport) {
200
+ const rows = [
201
+ report.paths.ghCli,
202
+ report.paths.localGit,
203
+ report.paths.localGitWrite,
204
+ report.paths.connector,
205
+ ];
206
+
207
+ console.log("\nPath | Status | Evidence | Blocker");
208
+ console.log("--------------------|---------|-------------------------------|---------------------------");
209
+ for (const row of rows) {
210
+ const p = row.path.padEnd(20);
211
+ const s = row.status.padEnd(8);
212
+ const e = (row.evidence || "").substring(0, 30).padEnd(30);
213
+ const b = row.blocker || "";
214
+ console.log(`${p}| ${s}| ${e}| ${b}`);
215
+ }
216
+
217
+ console.log(`\nResolved repo: ${report.repo ? `${report.repo.owner}/${report.repo.repo}` : "Unknown"}`);
218
+ console.log(`Default branch: ${report.defaultBranch || "Unknown"}`);
219
+ console.log(`Authenticated GH CLI user: ${report.ghCliUser || "Unknown"}`);
220
+ console.log(`Authenticated connector user: ${report.connectorUser || "Unknown"}`);
221
+ console.log(`Recommended PR flow: ${report.recommendedFlow}`);
222
+ }
@@ -0,0 +1,58 @@
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
+ });
@@ -0,0 +1,39 @@
1
+ import type { HollowResponseResult } from "./codexPrompt.js";
2
+
3
+ const MESSAGES: Record<string, [string, string]> = {
4
+ greeting: [
5
+ "Task not executed — backend returned a generic greeting.",
6
+ "Retry with a more specific instruction.",
7
+ ],
8
+ filler: [
9
+ "Task not executed — backend acknowledged without acting.",
10
+ "Retry with a more specific instruction.",
11
+ ],
12
+ clarification: [
13
+ "Task not executed — backend asked for clarification instead of acting.",
14
+ "Rephrase with more detail, or switch to suggest mode.",
15
+ ],
16
+ "short-no-action": [
17
+ "No action confirmed — response too brief for a write-intent prompt.",
18
+ "Verify workspace files manually, or retry.",
19
+ ],
20
+ };
21
+
22
+ export function formatHollowResponse(
23
+ result: HollowResponseResult,
24
+ rawResponse?: string,
25
+ verbose?: boolean,
26
+ ): string {
27
+ const lines = MESSAGES[result.kind] ?? [
28
+ "Task not executed — unexpected backend response.",
29
+ "Retry with a more specific instruction.",
30
+ ];
31
+
32
+ let output = lines.join("\n");
33
+
34
+ if (verbose && rawResponse) {
35
+ output += `\n\nBackend response: ${rawResponse}`;
36
+ }
37
+
38
+ return output;
39
+ }