@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,51 @@
1
+ import { appendFileSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+
5
+ type DebugDetails = Record<string, unknown>;
6
+
7
+ let sequence = 0;
8
+
9
+ export function isInputDebugEnabled(): boolean {
10
+ return process.env.CODEXA_DEBUG_INPUT === "1";
11
+ }
12
+
13
+ export function getInputDebugLogPath(): string {
14
+ return process.env.CODEXA_DEBUG_INPUT_LOG || join(homedir(), ".codexa-input-debug.log");
15
+ }
16
+
17
+ export function getStdinDebugState(stdin: unknown): DebugDetails {
18
+ const input = stdin as {
19
+ isTTY?: boolean;
20
+ isRaw?: boolean;
21
+ readable?: boolean;
22
+ destroyed?: boolean;
23
+ isPaused?: () => boolean;
24
+ } | null | undefined;
25
+
26
+ return {
27
+ isTTY: input?.isTTY ?? null,
28
+ isRaw: input?.isRaw ?? null,
29
+ readable: input?.readable ?? null,
30
+ destroyed: input?.destroyed ?? null,
31
+ paused: typeof input?.isPaused === "function" ? input.isPaused() : null,
32
+ };
33
+ }
34
+
35
+ export function traceInputDebug(event: string, details: DebugDetails = {}): void {
36
+ if (!isInputDebugEnabled()) {
37
+ return;
38
+ }
39
+
40
+ try {
41
+ const entry = {
42
+ ts: new Date().toISOString(),
43
+ seq: ++sequence,
44
+ event,
45
+ ...details,
46
+ };
47
+ appendFileSync(getInputDebugLogPath(), `${JSON.stringify(entry)}\n`, "utf8");
48
+ } catch {
49
+ // Debug tracing must never affect interactive input.
50
+ }
51
+ }
@@ -0,0 +1,157 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ import test from "node:test";
6
+ import {
7
+ buildWorkspaceStatusMessage,
8
+ createWorkspaceRelaunchPlan,
9
+ guardWorkspaceRelaunch,
10
+ resolveLaunchContext,
11
+ } from "./launchContext.js";
12
+ import { normalizeWorkspaceRoot } from "./workspaceRoot.js";
13
+
14
+ function createTempWorkspace(): string {
15
+ return mkdtempSync(join(tmpdir(), "codexa-launch-context-"));
16
+ }
17
+
18
+ test("uses installed launcher metadata when available", () => {
19
+ const context = resolveLaunchContext({
20
+ workspaceRoot: "C:/target",
21
+ packageRoot: "C:/repo",
22
+ execPath: "C:/Program Files/nodejs/node.exe",
23
+ env: {
24
+ CODEXA_LAUNCH_KIND: "installed-bin",
25
+ CODEXA_PACKAGE_ROOT: "C:/repo",
26
+ CODEXA_LAUNCHER_SCRIPT: "C:/repo/bin/codexa.js",
27
+ CODEXA_RELAUNCH_EXECUTABLE: "C:/Program Files/nodejs/node.exe",
28
+ CODEXA_RELAUNCH_ARGS: JSON.stringify(["C:/repo/bin/codexa.js", "--profile", "review"]),
29
+ },
30
+ });
31
+
32
+ assert.equal(context.launchKind, "installed-bin");
33
+ assert.equal(context.packageRoot, "C:\\repo");
34
+ assert.equal(context.launcherScriptPath, "C:/repo/bin/codexa.js");
35
+ assert.equal(context.relaunchExecutable, "C:/Program Files/nodejs/node.exe");
36
+ assert.deepEqual(context.relaunchArgs, ["C:/repo/bin/codexa.js", "--profile", "review"]);
37
+ });
38
+
39
+ test("falls back to bun repo launch metadata when no installed launcher env exists", () => {
40
+ const context = resolveLaunchContext({
41
+ workspaceRoot: "C:/repo",
42
+ packageRoot: "C:/repo",
43
+ execPath: "C:/tools/bun.exe",
44
+ hasBunRuntime: true,
45
+ forwardArgs: ["--profile", "review", "--config", "model=\"gpt-5.4\""],
46
+ env: {},
47
+ });
48
+
49
+ assert.equal(context.launchKind, "dev-run");
50
+ assert.equal(context.relaunchExecutable, "C:/tools/bun.exe");
51
+ assert.deepEqual(context.relaunchArgs, [
52
+ "run",
53
+ "--silent",
54
+ join("C:\\repo", "src", "index.tsx"),
55
+ "--",
56
+ "--profile",
57
+ "review",
58
+ "--config",
59
+ "model=\"gpt-5.4\"",
60
+ ]);
61
+ });
62
+
63
+ test("creates an installed-bin relaunch plan with normalized target cwd and env", () => {
64
+ const root = createTempWorkspace();
65
+ try {
66
+ const nextWorkspace = join(root, "next");
67
+ mkdirSync(nextWorkspace);
68
+
69
+ const context = resolveLaunchContext({
70
+ workspaceRoot: root,
71
+ packageRoot: "C:/repo",
72
+ execPath: "C:/Program Files/nodejs/node.exe",
73
+ env: {
74
+ CODEXA_LAUNCH_KIND: "installed-bin",
75
+ CODEXA_PACKAGE_ROOT: "C:/repo",
76
+ CODEXA_LAUNCHER_SCRIPT: "C:/repo/bin/codexa.js",
77
+ CODEXA_RELAUNCH_EXECUTABLE: "C:/Program Files/nodejs/node.exe",
78
+ CODEXA_RELAUNCH_ARGS: JSON.stringify(["C:/repo/bin/codexa.js", "--profile", "review"]),
79
+ },
80
+ });
81
+
82
+ const result = createWorkspaceRelaunchPlan(".\\next", context, {});
83
+ assert.equal(result.ok, true);
84
+ if (!result.ok) return;
85
+
86
+ assert.equal(result.plan.executable, "C:/Program Files/nodejs/node.exe");
87
+ assert.deepEqual(result.plan.args, ["C:/repo/bin/codexa.js", "--profile", "review"]);
88
+ assert.equal(result.plan.cwd, normalizeWorkspaceRoot(nextWorkspace));
89
+ assert.equal(result.plan.env.CODEX_WORKSPACE_ROOT, normalizeWorkspaceRoot(nextWorkspace));
90
+ assert.equal(result.plan.env.CODEXA_LAUNCH_KIND, "installed-bin");
91
+ } finally {
92
+ rmSync(root, { recursive: true, force: true });
93
+ }
94
+ });
95
+
96
+ test("rejects relaunch targets that do not exist", () => {
97
+ const root = createTempWorkspace();
98
+ try {
99
+ const context = resolveLaunchContext({
100
+ workspaceRoot: root,
101
+ packageRoot: root,
102
+ execPath: "C:/tools/bun.exe",
103
+ hasBunRuntime: true,
104
+ env: {},
105
+ });
106
+
107
+ const result = createWorkspaceRelaunchPlan(".\\missing", context, {});
108
+ assert.equal(result.ok, false);
109
+ if (result.ok) return;
110
+ assert.match(result.message, /does not exist/i);
111
+ } finally {
112
+ rmSync(root, { recursive: true, force: true });
113
+ }
114
+ });
115
+
116
+ test("rejects relaunch targets that are files instead of directories", () => {
117
+ const root = createTempWorkspace();
118
+ try {
119
+ const fileTarget = join(root, "notes.txt");
120
+ writeFileSync(fileTarget, "hello", "utf8");
121
+ const context = resolveLaunchContext({
122
+ workspaceRoot: root,
123
+ packageRoot: root,
124
+ execPath: "C:/tools/bun.exe",
125
+ hasBunRuntime: true,
126
+ env: {},
127
+ });
128
+
129
+ const result = createWorkspaceRelaunchPlan(".\\notes.txt", context, {});
130
+ assert.equal(result.ok, false);
131
+ if (result.ok) return;
132
+ assert.match(result.message, /not a directory/i);
133
+ } finally {
134
+ rmSync(root, { recursive: true, force: true });
135
+ }
136
+ });
137
+
138
+ test("blocks workspace relaunch while busy", () => {
139
+ assert.deepEqual(guardWorkspaceRelaunch(false), { allowed: true });
140
+ assert.equal(guardWorkspaceRelaunch(true).allowed, false);
141
+ assert.match(guardWorkspaceRelaunch(true).message ?? "", /finish the current run/i);
142
+ });
143
+
144
+ test("describes dev launch mode with install guidance", () => {
145
+ const message = buildWorkspaceStatusMessage(resolveLaunchContext({
146
+ workspaceRoot: "C:/repo",
147
+ packageRoot: "C:/repo",
148
+ execPath: "C:/tools/bun.exe",
149
+ hasBunRuntime: true,
150
+ env: {},
151
+ }));
152
+
153
+ assert.match(message, /Launch mode: dev\/repo launch/i);
154
+ assert.match(message, /npm link/i);
155
+ assert.match(message, /where codexa/i);
156
+ assert.match(message, /\/workspace relaunch <path>/i);
157
+ });
@@ -0,0 +1,266 @@
1
+ import { statSync } from "fs";
2
+ import { basename, join, resolve } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { resolveWorkspacePath } from "./workspaceGuard.js";
5
+ import { normalizeWorkspaceRoot } from "./workspaceRoot.js";
6
+
7
+ export type LaunchKind = "installed-bin" | "dev-run";
8
+
9
+ export interface LaunchContext {
10
+ workspaceRoot: string;
11
+ packageRoot: string;
12
+ launchKind: LaunchKind;
13
+ launcherScriptPath?: string;
14
+ relaunchExecutable: string;
15
+ relaunchArgs: string[];
16
+ }
17
+
18
+ export interface ResolveLaunchContextOptions {
19
+ env?: NodeJS.ProcessEnv;
20
+ workspaceRoot?: string;
21
+ packageRoot?: string;
22
+ execPath?: string;
23
+ hasBunRuntime?: boolean;
24
+ forwardArgs?: string[];
25
+ }
26
+
27
+ export interface WorkspaceCommandContext {
28
+ root: string;
29
+ summaryMessage: string;
30
+ }
31
+
32
+ export interface WorkspaceRelaunchPlan {
33
+ executable: string;
34
+ args: string[];
35
+ cwd: string;
36
+ env: NodeJS.ProcessEnv;
37
+ targetWorkspaceRoot: string;
38
+ }
39
+
40
+ export type WorkspaceRelaunchPlanResult =
41
+ | { ok: true; plan: WorkspaceRelaunchPlan }
42
+ | { ok: false; message: string };
43
+
44
+ const ENV_KEYS = {
45
+ workspaceRoot: "CODEX_WORKSPACE_ROOT",
46
+ launchKind: "CODEXA_LAUNCH_KIND",
47
+ packageRoot: "CODEXA_PACKAGE_ROOT",
48
+ launcherScript: "CODEXA_LAUNCHER_SCRIPT",
49
+ relaunchExecutable: "CODEXA_RELAUNCH_EXECUTABLE",
50
+ relaunchArgs: "CODEXA_RELAUNCH_ARGS",
51
+ } as const;
52
+
53
+ function getDefaultPackageRoot(): string {
54
+ return normalizeWorkspaceRoot(resolve(fileURLToPath(new URL(".", import.meta.url)), "..", ".."));
55
+ }
56
+
57
+ function parseRelaunchArgs(raw: string | undefined): string[] | null {
58
+ const value = raw?.trim();
59
+ if (!value) return null;
60
+
61
+ try {
62
+ const parsed = JSON.parse(value);
63
+ if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === "string")) {
64
+ return null;
65
+ }
66
+
67
+ return parsed;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ function isBunExecutable(execPath: string): boolean {
74
+ return basename(execPath).toLowerCase().startsWith("bun");
75
+ }
76
+
77
+ function getDevRelaunchExecutable(execPath: string, hasBunRuntime: boolean): string {
78
+ if (hasBunRuntime || isBunExecutable(execPath)) {
79
+ return execPath;
80
+ }
81
+
82
+ return "bun";
83
+ }
84
+
85
+ function buildInstalledRelaunchArgs(launcherScriptPath: string, forwardArgs: readonly string[]): string[] {
86
+ return [launcherScriptPath, ...forwardArgs];
87
+ }
88
+
89
+ function buildDevRelaunchArgs(packageRoot: string, forwardArgs: readonly string[]): string[] {
90
+ const args = ["run", "--silent", join(packageRoot, "src", "index.tsx")];
91
+ if (forwardArgs.length > 0) {
92
+ args.push("--", ...forwardArgs);
93
+ }
94
+ return args;
95
+ }
96
+
97
+ export function resolveLaunchContext(options: ResolveLaunchContextOptions = {}): LaunchContext {
98
+ const env = options.env ?? process.env;
99
+ const workspaceRoot = normalizeWorkspaceRoot(
100
+ options.workspaceRoot ?? env[ENV_KEYS.workspaceRoot] ?? process.cwd(),
101
+ );
102
+ const packageRoot = normalizeWorkspaceRoot(
103
+ env[ENV_KEYS.packageRoot] ?? options.packageRoot ?? getDefaultPackageRoot(),
104
+ );
105
+ const launchKind = env[ENV_KEYS.launchKind] === "installed-bin" ? "installed-bin" : "dev-run";
106
+ const launcherScriptPath = env[ENV_KEYS.launcherScript]?.trim() || undefined;
107
+ const envRelaunchExecutable = env[ENV_KEYS.relaunchExecutable]?.trim();
108
+ const envRelaunchArgs = parseRelaunchArgs(env[ENV_KEYS.relaunchArgs]);
109
+ const execPath = options.execPath ?? process.execPath;
110
+ const hasBunRuntime = options.hasBunRuntime ?? Boolean(process.versions.bun);
111
+ const forwardArgs = options.forwardArgs ?? process.argv.slice(2);
112
+
113
+ if (launchKind === "installed-bin") {
114
+ const resolvedLauncherScript = launcherScriptPath ?? join(packageRoot, "bin", "codexa.js");
115
+
116
+ return {
117
+ workspaceRoot,
118
+ packageRoot,
119
+ launchKind,
120
+ launcherScriptPath: resolvedLauncherScript,
121
+ relaunchExecutable: envRelaunchExecutable || execPath,
122
+ relaunchArgs: envRelaunchArgs && envRelaunchArgs.length > 0
123
+ ? envRelaunchArgs
124
+ : buildInstalledRelaunchArgs(resolvedLauncherScript, forwardArgs),
125
+ };
126
+ }
127
+
128
+ return {
129
+ workspaceRoot,
130
+ packageRoot,
131
+ launchKind: "dev-run",
132
+ launcherScriptPath,
133
+ relaunchExecutable: envRelaunchExecutable || getDevRelaunchExecutable(execPath, hasBunRuntime),
134
+ relaunchArgs: envRelaunchArgs && envRelaunchArgs.length > 0
135
+ ? envRelaunchArgs
136
+ : buildDevRelaunchArgs(packageRoot, forwardArgs),
137
+ };
138
+ }
139
+
140
+ export function buildWorkspaceCommandContext(launchContext: LaunchContext): WorkspaceCommandContext {
141
+ return {
142
+ root: launchContext.workspaceRoot,
143
+ summaryMessage: buildWorkspaceStatusMessage(launchContext),
144
+ };
145
+ }
146
+
147
+ export function buildWorkspaceStatusMessage(launchContext: LaunchContext): string {
148
+ const lines = [
149
+ "Active workspace:",
150
+ ` ${launchContext.workspaceRoot}`,
151
+ "",
152
+ `Launch mode: ${launchContext.launchKind === "installed-bin" ? "installed codexa" : "dev/repo launch"}`,
153
+ "",
154
+ ];
155
+
156
+ if (launchContext.launchKind === "dev-run") {
157
+ lines.push(
158
+ "This session was started from a repo/dev launch, so the locked workspace follows that launch directory.",
159
+ "Recommended Windows setup:",
160
+ ` cd ${launchContext.packageRoot}`,
161
+ " npm link",
162
+ " where codexa",
163
+ " cd <target-folder>",
164
+ " codexa",
165
+ "",
166
+ "Quick recovery from here:",
167
+ " /workspace relaunch <path>",
168
+ );
169
+ } else {
170
+ lines.push(
171
+ "This session is locked to the folder where you launched codexa.",
172
+ "Use /workspace relaunch <path> to restart into another folder from inside this UI.",
173
+ );
174
+ }
175
+
176
+ return lines.join("\n");
177
+ }
178
+
179
+ export function buildDevLaunchNotice(launchContext: LaunchContext): string | null {
180
+ if (launchContext.launchKind !== "dev-run") {
181
+ return null;
182
+ }
183
+
184
+ return [
185
+ "This session was started from a repo/dev launch, so the locked workspace follows that launch directory.",
186
+ "Normal Windows flow:",
187
+ ` cd ${launchContext.packageRoot}`,
188
+ " npm link",
189
+ " where codexa",
190
+ " cd <target-folder>",
191
+ " codexa",
192
+ "",
193
+ "Recovery from this session:",
194
+ " /workspace relaunch <path>",
195
+ ].join("\n");
196
+ }
197
+
198
+ export function guardWorkspaceRelaunch(busy: boolean): { allowed: boolean; message?: string } {
199
+ if (!busy) {
200
+ return { allowed: true };
201
+ }
202
+
203
+ return {
204
+ allowed: false,
205
+ message: "Finish the current run before relaunching into another workspace.",
206
+ };
207
+ }
208
+
209
+ function buildRelaunchEnv(
210
+ launchContext: LaunchContext,
211
+ targetWorkspaceRoot: string,
212
+ baseEnv: NodeJS.ProcessEnv,
213
+ ): NodeJS.ProcessEnv {
214
+ return {
215
+ ...baseEnv,
216
+ [ENV_KEYS.workspaceRoot]: targetWorkspaceRoot,
217
+ [ENV_KEYS.launchKind]: launchContext.launchKind,
218
+ [ENV_KEYS.packageRoot]: launchContext.packageRoot,
219
+ [ENV_KEYS.launcherScript]: launchContext.launcherScriptPath ?? "",
220
+ [ENV_KEYS.relaunchExecutable]: launchContext.relaunchExecutable,
221
+ [ENV_KEYS.relaunchArgs]: JSON.stringify(launchContext.relaunchArgs),
222
+ };
223
+ }
224
+
225
+ export function createWorkspaceRelaunchPlan(
226
+ targetInput: string,
227
+ launchContext: LaunchContext,
228
+ baseEnv: NodeJS.ProcessEnv = process.env,
229
+ ): WorkspaceRelaunchPlanResult {
230
+ const trimmedTarget = targetInput.trim();
231
+ if (!trimmedTarget) {
232
+ return { ok: false, message: "Usage: /workspace relaunch <path>" };
233
+ }
234
+
235
+ const targetWorkspaceRoot = normalizeWorkspaceRoot(
236
+ resolveWorkspacePath(trimmedTarget, launchContext.workspaceRoot),
237
+ );
238
+
239
+ let stats: ReturnType<typeof statSync>;
240
+ try {
241
+ stats = statSync(targetWorkspaceRoot);
242
+ } catch {
243
+ return {
244
+ ok: false,
245
+ message: `Workspace relaunch failed: ${targetWorkspaceRoot} does not exist.`,
246
+ };
247
+ }
248
+
249
+ if (!stats.isDirectory()) {
250
+ return {
251
+ ok: false,
252
+ message: `Workspace relaunch failed: ${targetWorkspaceRoot} is not a directory.`,
253
+ };
254
+ }
255
+
256
+ return {
257
+ ok: true,
258
+ plan: {
259
+ executable: launchContext.relaunchExecutable,
260
+ args: launchContext.relaunchArgs,
261
+ cwd: targetWorkspaceRoot,
262
+ env: buildRelaunchEnv(launchContext, targetWorkspaceRoot, baseEnv),
263
+ targetWorkspaceRoot,
264
+ },
265
+ };
266
+ }
@@ -0,0 +1,45 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { parseCodexCliCapabilities } from "./codexCapabilities.js";
4
+
5
+ test("detects exec help flags and strips ANSI noise", () => {
6
+ const capabilities = parseCodexCliCapabilities(
7
+ "\u001B[32mOptions:\u001B[0m\n --ask-for-approval <policy>\n --sandbox <mode>\n -c, --config <key=value>\n",
8
+ "Global options:\n --ask-for-approval <policy>\n",
9
+ );
10
+
11
+ assert.deepEqual(capabilities, {
12
+ askForApproval: true,
13
+ sandbox: true,
14
+ config: true,
15
+ fullAuto: false,
16
+ });
17
+ });
18
+
19
+ test("does not promote top-level approval flags into exec capabilities", () => {
20
+ const capabilities = parseCodexCliCapabilities(
21
+ "Usage: codex exec [OPTIONS]\n --sandbox <mode>\n --full-auto\n",
22
+ "Global options:\n --ask-for-approval <policy>\n --sandbox <mode>\n --full-auto\n",
23
+ );
24
+
25
+ assert.deepEqual(capabilities, {
26
+ askForApproval: false,
27
+ sandbox: true,
28
+ config: false,
29
+ fullAuto: true,
30
+ });
31
+ });
32
+
33
+ test("requires exact option tokens instead of prose guesses", () => {
34
+ const capabilities = parseCodexCliCapabilities(
35
+ "This build documents ask-for-approval compatibility in prose only.\nIt also mentions sandbox_mode in config docs.\n",
36
+ "Use full auto mode for trusted environments.\n",
37
+ );
38
+
39
+ assert.deepEqual(capabilities, {
40
+ askForApproval: false,
41
+ sandbox: false,
42
+ config: false,
43
+ fullAuto: false,
44
+ });
45
+ });
@@ -0,0 +1,95 @@
1
+ import { captureCodexProcessOutput } from "../executables/codexExecutable.js";
2
+ import { stripAnsi, stripNonPrintableControls } from "../providers/codexTranscript.js";
3
+
4
+ const HELP_TIMEOUT_MS = 5000;
5
+
6
+ export interface CodexCliCapabilities {
7
+ askForApproval: boolean;
8
+ sandbox: boolean;
9
+ config: boolean;
10
+ fullAuto: boolean;
11
+ }
12
+
13
+ const capabilityCache = new Map<string, Promise<CodexCliCapabilities>>();
14
+
15
+ function escapeRegExp(value: string): string {
16
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17
+ }
18
+
19
+ function normalizeHelpText(text: string): string {
20
+ return stripNonPrintableControls(stripAnsi(text))
21
+ .replace(/\r\n/g, "\n")
22
+ .replace(/\r/g, "\n")
23
+ .toLowerCase();
24
+ }
25
+
26
+ // Uses a hand-rolled word-boundary pattern rather than \b so that flag names
27
+ // containing hyphens (e.g. --ask-for-approval) are matched reliably.
28
+ function hasCliToken(helpText: string, token: string): boolean {
29
+ const pattern = new RegExp(`(^|[^a-z0-9-])${escapeRegExp(token.toLowerCase())}(?=$|[^a-z0-9-])`, "m");
30
+ return pattern.test(helpText);
31
+ }
32
+
33
+ export function parseCodexCliCapabilities(
34
+ execHelpText: string,
35
+ _topLevelHelpText: string,
36
+ ): CodexCliCapabilities {
37
+ const execHelp = normalizeHelpText(execHelpText);
38
+
39
+ return {
40
+ askForApproval: hasCliToken(execHelp, "--ask-for-approval"),
41
+ sandbox: hasCliToken(execHelp, "--sandbox"),
42
+ config: hasCliToken(execHelp, "--config") || hasCliToken(execHelp, "-c"),
43
+ fullAuto: hasCliToken(execHelp, "--full-auto"),
44
+ };
45
+ }
46
+
47
+ export async function getCodexCliCapabilities(executable: string): Promise<CodexCliCapabilities> {
48
+ const cached = capabilityCache.get(executable);
49
+ if (cached) {
50
+ return cached;
51
+ }
52
+
53
+ const inFlight = (async () => {
54
+ const [execHelp, topLevelHelp] = await Promise.allSettled([
55
+ captureCodexProcessOutput(executable, ["exec", "--help"], HELP_TIMEOUT_MS),
56
+ captureCodexProcessOutput(executable, ["--help"], HELP_TIMEOUT_MS),
57
+ ]);
58
+
59
+ const outputs: string[] = [];
60
+
61
+ if (execHelp.status === "fulfilled") {
62
+ outputs.push(execHelp.value.stdout, execHelp.value.stderr);
63
+ }
64
+
65
+ if (topLevelHelp.status === "fulfilled") {
66
+ outputs.push(topLevelHelp.value.stdout, topLevelHelp.value.stderr);
67
+ }
68
+
69
+ if (outputs.length === 0) {
70
+ if (execHelp.status === "rejected") {
71
+ throw execHelp.reason;
72
+ }
73
+
74
+ if (topLevelHelp.status === "rejected") {
75
+ throw topLevelHelp.reason;
76
+ }
77
+
78
+ throw new Error("Unable to determine Codex CLI capabilities from help output.");
79
+ }
80
+
81
+ return parseCodexCliCapabilities(
82
+ execHelp.status === "fulfilled" ? `${execHelp.value.stdout}\n${execHelp.value.stderr}` : "",
83
+ topLevelHelp.status === "fulfilled" ? `${topLevelHelp.value.stdout}\n${topLevelHelp.value.stderr}` : "",
84
+ );
85
+ })();
86
+
87
+ capabilityCache.set(executable, inFlight);
88
+
89
+ try {
90
+ return await inFlight;
91
+ } catch (error) {
92
+ capabilityCache.delete(executable);
93
+ throw error;
94
+ }
95
+ }