@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,158 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { parseLaunchArgs } from "./launchArgs.js";
4
+
5
+ test("parses profile and repeated config overrides", () => {
6
+ const parsed = parseLaunchArgs([
7
+ "--profile",
8
+ "review",
9
+ "--config",
10
+ "model=\"gpt-5.4\"",
11
+ "-c",
12
+ "codexa.mode=\"suggest\"",
13
+ ]);
14
+
15
+ assert.equal(parsed.ok, true);
16
+ if (!parsed.ok) return;
17
+
18
+ assert.equal(parsed.value.help, false);
19
+ assert.equal(parsed.value.version, false);
20
+ assert.equal(parsed.value.initialPrompt, null);
21
+ assert.equal(parsed.value.profile, "review");
22
+ assert.deepEqual(parsed.value.configOverrides, [
23
+ "model=\"gpt-5.4\"",
24
+ "codexa.mode=\"suggest\"",
25
+ ]);
26
+ assert.deepEqual(parsed.value.passthroughArgs, [
27
+ "--profile",
28
+ "review",
29
+ "--config",
30
+ "model=\"gpt-5.4\"",
31
+ "-c",
32
+ "codexa.mode=\"suggest\"",
33
+ ]);
34
+ });
35
+
36
+ test("rejects missing profile values", () => {
37
+ const parsed = parseLaunchArgs(["--profile"]);
38
+ assert.equal(parsed.ok, false);
39
+ if (parsed.ok) return;
40
+ assert.match(parsed.error, /--profile/i);
41
+ });
42
+
43
+ test("rejects malformed config payloads", () => {
44
+ const parsed = parseLaunchArgs(["--config", "model"]);
45
+ assert.equal(parsed.ok, false);
46
+ if (parsed.ok) return;
47
+ assert.match(parsed.error, /key=value/i);
48
+ });
49
+
50
+ test("parses inline profile and config assignments", () => {
51
+ const parsed = parseLaunchArgs([
52
+ "--profile=review",
53
+ "--config=model=\"gpt-5.4-mini\"",
54
+ "-c=codexa.mode=\"auto-edit\"",
55
+ ]);
56
+
57
+ assert.equal(parsed.ok, true);
58
+ if (!parsed.ok) return;
59
+
60
+ assert.equal(parsed.value.profile, "review");
61
+ assert.deepEqual(parsed.value.configOverrides, [
62
+ "model=\"gpt-5.4-mini\"",
63
+ "codexa.mode=\"auto-edit\"",
64
+ ]);
65
+ assert.deepEqual(parsed.value.passthroughArgs, [
66
+ "--profile=review",
67
+ "--config=model=\"gpt-5.4-mini\"",
68
+ "-c=codexa.mode=\"auto-edit\"",
69
+ ]);
70
+ });
71
+
72
+ test("parses model flags as runtime config overrides", () => {
73
+ const parsed = parseLaunchArgs([
74
+ "--model",
75
+ "gpt-5.3-codex",
76
+ "-m",
77
+ "gpt-5.4-mini",
78
+ "Say",
79
+ "READY",
80
+ "only.",
81
+ ]);
82
+
83
+ assert.equal(parsed.ok, true);
84
+ if (!parsed.ok) return;
85
+
86
+ assert.equal(parsed.value.initialPrompt, "Say READY only.");
87
+ assert.deepEqual(parsed.value.configOverrides, [
88
+ "model=\"gpt-5.3-codex\"",
89
+ "model=\"gpt-5.4-mini\"",
90
+ ]);
91
+ assert.deepEqual(parsed.value.passthroughArgs, [
92
+ "--model",
93
+ "gpt-5.3-codex",
94
+ "-m",
95
+ "gpt-5.4-mini",
96
+ ]);
97
+ // Last --model flag wins (same order as layered config)
98
+ assert.equal(parsed.value.modelOverride, "gpt-5.4-mini");
99
+ });
100
+
101
+ test("parses inline model flag assignment", () => {
102
+ const parsed = parseLaunchArgs(["--model=gpt-5.5", "compare", "this"]);
103
+
104
+ assert.equal(parsed.ok, true);
105
+ if (!parsed.ok) return;
106
+
107
+ assert.equal(parsed.value.initialPrompt, "compare this");
108
+ assert.deepEqual(parsed.value.configOverrides, ["model=\"gpt-5.5\""]);
109
+ assert.deepEqual(parsed.value.passthroughArgs, ["--model=gpt-5.5"]);
110
+ assert.equal(parsed.value.modelOverride, "gpt-5.5");
111
+ });
112
+
113
+ test("modelOverride is null when no --model flag is given", () => {
114
+ const parsed = parseLaunchArgs(["--profile", "review", "some", "prompt"]);
115
+
116
+ assert.equal(parsed.ok, true);
117
+ if (!parsed.ok) return;
118
+
119
+ assert.equal(parsed.value.modelOverride, null);
120
+ });
121
+
122
+ test("rejects missing model flag values", () => {
123
+ const parsed = parseLaunchArgs(["--model"]);
124
+ assert.equal(parsed.ok, false);
125
+ if (parsed.ok) return;
126
+ assert.match(parsed.error, /--model/i);
127
+ });
128
+
129
+ test("parses help and version flags without passthrough", () => {
130
+ const parsed = parseLaunchArgs(["--help", "-v"]);
131
+ assert.equal(parsed.ok, true);
132
+ if (!parsed.ok) return;
133
+
134
+ assert.equal(parsed.value.help, true);
135
+ assert.equal(parsed.value.version, true);
136
+ assert.equal(parsed.value.initialPrompt, null);
137
+ assert.deepEqual(parsed.value.passthroughArgs, []);
138
+ });
139
+
140
+ test("parses positional arguments as an initial prompt", () => {
141
+ const parsed = parseLaunchArgs(["--profile", "review", "explain", "this", "repo"]);
142
+ assert.equal(parsed.ok, true);
143
+ if (!parsed.ok) return;
144
+
145
+ assert.equal(parsed.value.profile, "review");
146
+ assert.equal(parsed.value.initialPrompt, "explain this repo");
147
+ assert.deepEqual(parsed.value.passthroughArgs, ["--profile", "review"]);
148
+ });
149
+
150
+ test("parses arguments after -- as an initial prompt", () => {
151
+ const parsed = parseLaunchArgs(["--", "--help", "as", "text"]);
152
+ assert.equal(parsed.ok, true);
153
+ if (!parsed.ok) return;
154
+
155
+ assert.equal(parsed.value.help, false);
156
+ assert.equal(parsed.value.initialPrompt, "--help as text");
157
+ assert.deepEqual(parsed.value.passthroughArgs, []);
158
+ });
@@ -0,0 +1,186 @@
1
+ const FLAG_HELP = "--help";
2
+ const FLAG_HELP_SHORT = "-h";
3
+ const FLAG_VERSION = "--version";
4
+ const FLAG_VERSION_SHORT = "-v";
5
+ const FLAG_PROFILE = "--profile";
6
+ const FLAG_CONFIG = "--config";
7
+ const FLAG_CONFIG_SHORT = "-c";
8
+ const FLAG_MODEL = "--model";
9
+ const FLAG_MODEL_SHORT = "-m";
10
+
11
+ export interface LaunchArgs {
12
+ help: boolean;
13
+ version: boolean;
14
+ initialPrompt: string | null;
15
+ profile: string | null;
16
+ configOverrides: string[];
17
+ passthroughArgs: string[];
18
+ /** Explicitly set when --model / -m was passed on the command line. Null when no model flag was given. */
19
+ modelOverride: string | null;
20
+ }
21
+
22
+ export type LaunchArgsParseResult =
23
+ | { ok: true; value: LaunchArgs }
24
+ | { ok: false; error: string };
25
+
26
+ function normalizeProfileValue(value: string | undefined): string | null {
27
+ const trimmed = value?.trim();
28
+ return trimmed ? trimmed : null;
29
+ }
30
+
31
+ function parseConfigFlagValue(raw: string | undefined): string | null {
32
+ const trimmed = raw?.trim();
33
+ if (!trimmed) {
34
+ return null;
35
+ }
36
+
37
+ // Require non-empty key and non-empty value on both sides of "="
38
+ const separatorIndex = trimmed.indexOf("=");
39
+ if (separatorIndex <= 0 || separatorIndex === trimmed.length - 1) {
40
+ return null;
41
+ }
42
+
43
+ return trimmed;
44
+ }
45
+
46
+ function quoteTomlString(value: string): string {
47
+ return JSON.stringify(value);
48
+ }
49
+
50
+ export function parseLaunchArgs(argv: readonly string[]): LaunchArgsParseResult {
51
+ const passthroughArgs: string[] = [];
52
+ const configOverrides: string[] = [];
53
+ const promptArgs: string[] = [];
54
+ let help = false;
55
+ let version = false;
56
+ let profile: string | null = null;
57
+ let modelOverride: string | null = null;
58
+
59
+ for (let index = 0; index < argv.length; index += 1) {
60
+ const arg = argv[index];
61
+ if (!arg) {
62
+ continue;
63
+ }
64
+
65
+ if (arg === "--") {
66
+ promptArgs.push(...argv.slice(index + 1));
67
+ break;
68
+ }
69
+
70
+ if (arg === FLAG_HELP || arg === FLAG_HELP_SHORT) {
71
+ help = true;
72
+ continue;
73
+ }
74
+
75
+ if (arg === FLAG_VERSION || arg === FLAG_VERSION_SHORT) {
76
+ version = true;
77
+ continue;
78
+ }
79
+
80
+ // --profile <value> and --profile=<value> both supported.
81
+ if (arg === FLAG_PROFILE) {
82
+ const value = normalizeProfileValue(argv[index + 1]);
83
+ if (!value) {
84
+ return { ok: false, error: `Missing value for ${FLAG_PROFILE}.` };
85
+ }
86
+ profile = value;
87
+ passthroughArgs.push(arg, value);
88
+ index += 1;
89
+ continue;
90
+ }
91
+
92
+ if (arg.startsWith(`${FLAG_PROFILE}=`)) {
93
+ const value = normalizeProfileValue(arg.slice(`${FLAG_PROFILE}=`.length));
94
+ if (!value) {
95
+ return { ok: false, error: `Missing value for ${FLAG_PROFILE}.` };
96
+ }
97
+ profile = value;
98
+ passthroughArgs.push(`${FLAG_PROFILE}=${value}`);
99
+ continue;
100
+ }
101
+
102
+ // --config / -c accept key=value pairs; both space-separated and inline = forms supported.
103
+ if (arg === FLAG_CONFIG_SHORT || arg === FLAG_CONFIG) {
104
+ const value = parseConfigFlagValue(argv[index + 1]);
105
+ if (!value) {
106
+ return { ok: false, error: `Missing key=value payload for ${arg}.` };
107
+ }
108
+ configOverrides.push(value);
109
+ passthroughArgs.push(arg, value);
110
+ index += 1;
111
+ continue;
112
+ }
113
+
114
+ if (arg.startsWith(`${FLAG_CONFIG}=`)) {
115
+ const value = parseConfigFlagValue(arg.slice(`${FLAG_CONFIG}=`.length));
116
+ if (!value) {
117
+ return { ok: false, error: `Missing key=value payload for ${FLAG_CONFIG}.` };
118
+ }
119
+ configOverrides.push(value);
120
+ passthroughArgs.push(`${FLAG_CONFIG}=${value}`);
121
+ continue;
122
+ }
123
+
124
+ if (arg.startsWith(`${FLAG_CONFIG_SHORT}=`)) {
125
+ const value = parseConfigFlagValue(arg.slice(`${FLAG_CONFIG_SHORT}=`.length));
126
+ if (!value) {
127
+ return { ok: false, error: `Missing key=value payload for ${FLAG_CONFIG_SHORT}.` };
128
+ }
129
+ configOverrides.push(value);
130
+ passthroughArgs.push(`${FLAG_CONFIG_SHORT}=${value}`);
131
+ continue;
132
+ }
133
+
134
+ // --model / -m set the model via a config override, quoted for TOML.
135
+ // Also captured in modelOverride so callers can distinguish "was --model given?"
136
+ // from "what does layered config resolve to?" (which always has a default value).
137
+ if (arg === FLAG_MODEL || arg === FLAG_MODEL_SHORT) {
138
+ const value = normalizeProfileValue(argv[index + 1]);
139
+ if (!value) {
140
+ return { ok: false, error: `Missing value for ${arg}.` };
141
+ }
142
+ configOverrides.push(`model=${quoteTomlString(value)}`);
143
+ passthroughArgs.push(arg, value);
144
+ modelOverride = value;
145
+ index += 1;
146
+ continue;
147
+ }
148
+
149
+ if (arg.startsWith(`${FLAG_MODEL}=`)) {
150
+ const value = normalizeProfileValue(arg.slice(`${FLAG_MODEL}=`.length));
151
+ if (!value) {
152
+ return { ok: false, error: `Missing value for ${FLAG_MODEL}.` };
153
+ }
154
+ configOverrides.push(`model=${quoteTomlString(value)}`);
155
+ passthroughArgs.push(`${FLAG_MODEL}=${value}`);
156
+ modelOverride = value;
157
+ continue;
158
+ }
159
+
160
+ if (!arg.startsWith("-")) {
161
+ promptArgs.push(arg, ...argv.slice(index + 1));
162
+ break;
163
+ }
164
+
165
+ passthroughArgs.push(arg);
166
+ }
167
+
168
+ const initialPrompt = promptArgs
169
+ .map((part) => part.trim())
170
+ .filter(Boolean)
171
+ .join(" ")
172
+ .trim() || null;
173
+
174
+ return {
175
+ ok: true,
176
+ value: {
177
+ help,
178
+ version,
179
+ initialPrompt,
180
+ profile,
181
+ configOverrides,
182
+ passthroughArgs,
183
+ modelOverride,
184
+ },
185
+ };
186
+ }
@@ -0,0 +1,143 @@
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
+
7
+ function writeText(filePath: string, contents: string): void {
8
+ mkdirSync(join(filePath, ".."), { recursive: true });
9
+ writeFileSync(filePath, contents, "utf-8");
10
+ }
11
+
12
+ test("resolves user config, trusted project config, profiles, and CLI overrides deterministically", async () => {
13
+ const tempRoot = mkdtempSync(join(tmpdir(), "codexa-layered-config-"));
14
+ const tempHome = join(tempRoot, "home");
15
+ const workspaceRoot = join(tempRoot, "repo", "packages", "app");
16
+ const projectRoot = join(tempRoot, "repo");
17
+ const previousCodexHome = process.env.CODEX_HOME;
18
+ process.env.CODEX_HOME = tempHome;
19
+
20
+ try {
21
+ mkdirSync(join(projectRoot, ".git"), { recursive: true });
22
+ mkdirSync(join(workspaceRoot, ".codex"), { recursive: true });
23
+ mkdirSync(join(projectRoot, ".codex"), { recursive: true });
24
+ mkdirSync(tempHome, { recursive: true });
25
+
26
+ writeFileSync(join(tempHome, "config.toml"), [
27
+ "model = \"gpt-5.2\"",
28
+ "[codexa]",
29
+ "backend = \"openai-native\"",
30
+ "",
31
+ "[profiles.review]",
32
+ "service_tier = \"fast\"",
33
+ ].join("\n"), "utf-8");
34
+
35
+ writeFileSync(join(projectRoot, ".codex", "config.toml"), [
36
+ "model = \"gpt-5.4\"",
37
+ "profile = \"review\"",
38
+ "[codexa]",
39
+ "mode = \"suggest\"",
40
+ "",
41
+ "[profiles.review]",
42
+ "approval_policy = \"never\"",
43
+ "",
44
+ "[profiles.review.sandbox_workspace_write]",
45
+ "network_access = true",
46
+ "writable_roots = [\"./roots/project\"]",
47
+ ].join("\n"), "utf-8");
48
+
49
+ writeFileSync(join(workspaceRoot, ".codex", "config.toml"), [
50
+ "personality = \"pragmatic\"",
51
+ "",
52
+ "[profiles.review]",
53
+ "model_reasoning_effort = \"high\"",
54
+ ].join("\n"), "utf-8");
55
+
56
+ const trustStore = await import(`./trustStore.js?layered-trust=${Date.now()}`);
57
+ trustStore.setProjectTrust(projectRoot, true);
58
+
59
+ const layeredConfig = await import(`./layeredConfig.js?layered=${Date.now()}`);
60
+ const result = layeredConfig.resolveLayeredConfig({
61
+ workspaceRoot,
62
+ launchArgs: {
63
+ help: false,
64
+ version: false,
65
+ initialPrompt: null,
66
+ profile: null,
67
+ configOverrides: [
68
+ "model=\"gpt-5.4-mini\"",
69
+ "codexa.mode=\"full-auto\"",
70
+ "mcp.enabled=true",
71
+ ],
72
+ passthroughArgs: [],
73
+ modelOverride: "gpt-5.4-mini",
74
+ },
75
+ });
76
+
77
+ assert.equal(result.runtime.provider, "openai-native");
78
+ assert.equal(result.runtime.model, "gpt-5.4-mini");
79
+ assert.equal(result.runtime.reasoningLevel, "high");
80
+ assert.equal(result.runtime.mode, "full-auto");
81
+ assert.equal(result.runtime.policy.approvalPolicy, "never");
82
+ assert.equal(result.runtime.policy.networkAccess, "enabled");
83
+ assert.equal(result.runtime.policy.serviceTier, "fast");
84
+ assert.equal(result.runtime.policy.personality, "pragmatic");
85
+ assert.equal(result.diagnostics.selectedProfile, "review");
86
+ assert.equal(result.diagnostics.selectedProfileSource, "Project config");
87
+ assert.equal(result.diagnostics.projectTrusted, true);
88
+ assert.match(result.runtime.policy.writableRoots[0] ?? "", /roots[\\/]project/i);
89
+ assert.match(result.diagnostics.fieldSources.model, /CLI override/i);
90
+ assert.match(result.diagnostics.fieldSources["policy.approvalPolicy"], /Profile review from Project config/i);
91
+ assert.ok(result.diagnostics.ignoredEntries.some((entry: string) => /mcp\.enabled/i.test(entry)));
92
+ } finally {
93
+ if (previousCodexHome === undefined) {
94
+ delete process.env.CODEX_HOME;
95
+ } else {
96
+ process.env.CODEX_HOME = previousCodexHome;
97
+ }
98
+ rmSync(tempRoot, { recursive: true, force: true });
99
+ }
100
+ });
101
+
102
+ test("blocks project config when the detected project root is untrusted", async () => {
103
+ const tempRoot = mkdtempSync(join(tmpdir(), "codexa-layered-untrusted-"));
104
+ const tempHome = join(tempRoot, "home");
105
+ const workspaceRoot = join(tempRoot, "repo", "app");
106
+ const projectRoot = join(tempRoot, "repo");
107
+ const previousCodexHome = process.env.CODEX_HOME;
108
+ process.env.CODEX_HOME = tempHome;
109
+
110
+ try {
111
+ mkdirSync(join(projectRoot, ".git"), { recursive: true });
112
+ mkdirSync(join(projectRoot, ".codex"), { recursive: true });
113
+ mkdirSync(tempHome, { recursive: true });
114
+
115
+ writeFileSync(join(tempHome, "config.toml"), "model = \"gpt-5.2\"\n", "utf-8");
116
+ writeFileSync(join(projectRoot, ".codex", "config.toml"), "model = \"gpt-5.4-mini\"\n", "utf-8");
117
+
118
+ const layeredConfig = await import(`./layeredConfig.js?untrusted=${Date.now()}`);
119
+ const result = layeredConfig.resolveLayeredConfig({
120
+ workspaceRoot,
121
+ launchArgs: {
122
+ help: false,
123
+ version: false,
124
+ initialPrompt: null,
125
+ profile: null,
126
+ configOverrides: [],
127
+ passthroughArgs: [],
128
+ modelOverride: null,
129
+ },
130
+ });
131
+
132
+ assert.equal(result.runtime.model, "gpt-5.2");
133
+ assert.equal(result.diagnostics.projectTrusted, false);
134
+ assert.ok(result.diagnostics.layers.some((layer: { status: string }) => layer.status === "blocked"));
135
+ } finally {
136
+ if (previousCodexHome === undefined) {
137
+ delete process.env.CODEX_HOME;
138
+ } else {
139
+ process.env.CODEX_HOME = previousCodexHome;
140
+ }
141
+ rmSync(tempRoot, { recursive: true, force: true });
142
+ }
143
+ });