ubume 0.1.0

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 (182) hide show
  1. package/README.md +88 -0
  2. package/bin/codexa-local-harness-bridge.js +3 -0
  3. package/bin/codexa.js +28 -0
  4. package/bin/ubume-local-harness-bridge.js +325 -0
  5. package/bin/ubume.js +398 -0
  6. package/package.json +66 -0
  7. package/src/app.tsx +5759 -0
  8. package/src/commands/handler.ts +889 -0
  9. package/src/config/appVersion.ts +69 -0
  10. package/src/config/buildInfo.ts +3 -0
  11. package/src/config/launchArgs.ts +196 -0
  12. package/src/config/layeredConfig.ts +853 -0
  13. package/src/config/legacyEnv.ts +16 -0
  14. package/src/config/persistence.ts +377 -0
  15. package/src/config/runtimeConfig.ts +558 -0
  16. package/src/config/settings.ts +405 -0
  17. package/src/config/toml-serialize.ts +109 -0
  18. package/src/config/trustStore.ts +84 -0
  19. package/src/config/updateCheckCache.ts +85 -0
  20. package/src/core/README.md +55 -0
  21. package/src/core/agent/loop.ts +464 -0
  22. package/src/core/agent/protocol.ts +345 -0
  23. package/src/core/agent/tools.ts +423 -0
  24. package/src/core/auth/codexAuth.ts +359 -0
  25. package/src/core/codex/codexExecArgs.ts +166 -0
  26. package/src/core/codex/codexLaunch.ts +163 -0
  27. package/src/core/codex/codexPrompt.ts +429 -0
  28. package/src/core/debug/inputDebug.ts +51 -0
  29. package/src/core/debug/localStreamDebug.ts +50 -0
  30. package/src/core/debug/modelStateDebug.ts +35 -0
  31. package/src/core/executables/antigravityExecutable.ts +48 -0
  32. package/src/core/executables/claudeExecutable.ts +63 -0
  33. package/src/core/executables/codexExecutable.ts +160 -0
  34. package/src/core/executables/executableResolver.ts +164 -0
  35. package/src/core/executables/geminiExecutable.ts +78 -0
  36. package/src/core/models/codexCapabilities.ts +97 -0
  37. package/src/core/models/codexModelCapabilities.ts +624 -0
  38. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  39. package/src/core/models/modelSpecs.ts +303 -0
  40. package/src/core/models/providerModelCache.ts +94 -0
  41. package/src/core/perf/profiler.ts +125 -0
  42. package/src/core/perf/renderDebug.ts +398 -0
  43. package/src/core/process/CommandRunner.ts +280 -0
  44. package/src/core/process/processValidation.ts +111 -0
  45. package/src/core/providerLauncher/launcher.ts +220 -0
  46. package/src/core/providerLauncher/registry.ts +354 -0
  47. package/src/core/providerLauncher/types.ts +95 -0
  48. package/src/core/providerLauncher/workspaceConfig.ts +487 -0
  49. package/src/core/providerRuntime/anthropic.ts +580 -0
  50. package/src/core/providerRuntime/antigravity.ts +500 -0
  51. package/src/core/providerRuntime/capabilityProfile.ts +383 -0
  52. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -0
  53. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  54. package/src/core/providerRuntime/codexaCupy.ts +97 -0
  55. package/src/core/providerRuntime/codexaNative.ts +425 -0
  56. package/src/core/providerRuntime/contextMetadata.ts +397 -0
  57. package/src/core/providerRuntime/gemini.ts +789 -0
  58. package/src/core/providerRuntime/lmstudio.ts +118 -0
  59. package/src/core/providerRuntime/local.ts +770 -0
  60. package/src/core/providerRuntime/localHarness/runtime.ts +1090 -0
  61. package/src/core/providerRuntime/localOutputBudget.ts +17 -0
  62. package/src/core/providerRuntime/mistralVibe.ts +667 -0
  63. package/src/core/providerRuntime/models.ts +175 -0
  64. package/src/core/providerRuntime/reasoning.ts +20 -0
  65. package/src/core/providerRuntime/registry.ts +286 -0
  66. package/src/core/providerRuntime/types.ts +142 -0
  67. package/src/core/providerRuntime/unsloth.ts +216 -0
  68. package/src/core/providers/codexJsonStream.ts +305 -0
  69. package/src/core/providers/codexSubprocess.ts +378 -0
  70. package/src/core/providers/codexTranscript.ts +695 -0
  71. package/src/core/providers/openaiNative.ts +13 -0
  72. package/src/core/providers/registry.ts +21 -0
  73. package/src/core/providers/types.ts +91 -0
  74. package/src/core/shared/attachments.ts +101 -0
  75. package/src/core/shared/cleanupFastFail.ts +67 -0
  76. package/src/core/shared/clipboard.ts +24 -0
  77. package/src/core/shared/clipboardImage.ts +111 -0
  78. package/src/core/shared/githubDiagnostics.ts +222 -0
  79. package/src/core/shared/hollowResponseFormat.ts +39 -0
  80. package/src/core/terminal/clearFrameBoundary.ts +852 -0
  81. package/src/core/terminal/frameLock.ts +110 -0
  82. package/src/core/terminal/inkRenderReset.ts +123 -0
  83. package/src/core/terminal/startupClear.ts +20 -0
  84. package/src/core/terminal/terminalCapabilities.ts +100 -0
  85. package/src/core/terminal/terminalControl.ts +169 -0
  86. package/src/core/terminal/terminalSanitize.ts +147 -0
  87. package/src/core/terminal/terminalTitle.ts +400 -0
  88. package/src/core/version/channel.ts +27 -0
  89. package/src/core/version/packageManager.ts +119 -0
  90. package/src/core/version/updateCheck.ts +203 -0
  91. package/src/core/workspace/appData.ts +107 -0
  92. package/src/core/workspace/conversationStore.ts +335 -0
  93. package/src/core/workspace/launchContext.ts +259 -0
  94. package/src/core/workspace/planStorage.ts +135 -0
  95. package/src/core/workspace/projectInstructions.ts +54 -0
  96. package/src/core/workspace/scratchDir.ts +64 -0
  97. package/src/core/workspace/workspaceActivity.ts +384 -0
  98. package/src/core/workspace/workspaceGuard.ts +377 -0
  99. package/src/core/workspace/workspaceRoot.ts +47 -0
  100. package/src/exec.ts +73 -0
  101. package/src/headless/execArgs.ts +296 -0
  102. package/src/headless/execRunner.ts +304 -0
  103. package/src/index.tsx +270 -0
  104. package/src/legacyEnvBootstrap.ts +5 -0
  105. package/src/session/appSession.ts +771 -0
  106. package/src/session/chatLifecycle.ts +994 -0
  107. package/src/session/conversation.ts +107 -0
  108. package/src/session/liveRenderScheduler.ts +214 -0
  109. package/src/session/persistedResponse.ts +93 -0
  110. package/src/session/planFlow.ts +159 -0
  111. package/src/session/planTranscript.ts +19 -0
  112. package/src/session/promptRunSchedule.ts +26 -0
  113. package/src/session/types.ts +234 -0
  114. package/src/test/runtimeTestUtils.ts +14 -0
  115. package/src/types/react-dom.d.ts +3 -0
  116. package/src/ui/chrome/ActivityBars.tsx +68 -0
  117. package/src/ui/chrome/ActivityIndicator.tsx +58 -0
  118. package/src/ui/chrome/AnimatedStatusText.tsx +69 -0
  119. package/src/ui/chrome/AppShell.tsx +474 -0
  120. package/src/ui/chrome/BottomComposer.tsx +1135 -0
  121. package/src/ui/chrome/DashCard.tsx +82 -0
  122. package/src/ui/chrome/RunFooter.tsx +65 -0
  123. package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
  124. package/src/ui/chrome/Spinner.tsx +25 -0
  125. package/src/ui/chrome/TopHeader.tsx +439 -0
  126. package/src/ui/chrome/UpdateAvailableCard.tsx +42 -0
  127. package/src/ui/chrome/busyStatusAnimation.ts +11 -0
  128. package/src/ui/input/commandNormalize.ts +66 -0
  129. package/src/ui/input/focus.ts +73 -0
  130. package/src/ui/input/imageAttachments.ts +18 -0
  131. package/src/ui/input/inputBuffer.ts +203 -0
  132. package/src/ui/input/pastedContent.ts +75 -0
  133. package/src/ui/input/rawArrowKeys.ts +28 -0
  134. package/src/ui/input/slashCommands.ts +43 -0
  135. package/src/ui/input/useStdinRawModeLease.ts +23 -0
  136. package/src/ui/layout.ts +560 -0
  137. package/src/ui/panels/AttachmentImportPanel.tsx +131 -0
  138. package/src/ui/panels/AuthPanel.tsx +149 -0
  139. package/src/ui/panels/BackendPicker.tsx +28 -0
  140. package/src/ui/panels/ModePicker.tsx +31 -0
  141. package/src/ui/panels/ModelPicker.tsx +31 -0
  142. package/src/ui/panels/ModelPickerScreen.tsx +761 -0
  143. package/src/ui/panels/ModelReasoningPicker.tsx +458 -0
  144. package/src/ui/panels/Panel.tsx +51 -0
  145. package/src/ui/panels/PermissionsPanel.tsx +78 -0
  146. package/src/ui/panels/PlanActionPicker.tsx +187 -0
  147. package/src/ui/panels/ProviderPicker.tsx +753 -0
  148. package/src/ui/panels/ProviderSetupPrompt.tsx +52 -0
  149. package/src/ui/panels/ReasoningPicker.tsx +46 -0
  150. package/src/ui/panels/ResumePicker.tsx +90 -0
  151. package/src/ui/panels/SelectionPanel.tsx +138 -0
  152. package/src/ui/panels/SettingsPanel.tsx +156 -0
  153. package/src/ui/panels/TextEntryPanel.tsx +139 -0
  154. package/src/ui/panels/ThemePicker.tsx +32 -0
  155. package/src/ui/panels/ToolApprovalPanel.tsx +46 -0
  156. package/src/ui/panels/UpdatePromptPanel.tsx +236 -0
  157. package/src/ui/panels/responsivePickerViewport.ts +64 -0
  158. package/src/ui/render/Markdown.tsx +331 -0
  159. package/src/ui/render/diffRenderer.ts +116 -0
  160. package/src/ui/render/logoVariants.ts +113 -0
  161. package/src/ui/render/modeDisplay.ts +52 -0
  162. package/src/ui/render/outputPipeline.ts +64 -0
  163. package/src/ui/render/runtimeDisplay.ts +128 -0
  164. package/src/ui/render/terminalAnswerFormat.ts +128 -0
  165. package/src/ui/render/textLayout.ts +392 -0
  166. package/src/ui/theme.tsx +274 -0
  167. package/src/ui/themeFlow.ts +41 -0
  168. package/src/ui/timeline/ActionRequiredBlock.tsx +38 -0
  169. package/src/ui/timeline/AgentBlock.tsx +130 -0
  170. package/src/ui/timeline/StaticIntroItem.tsx +54 -0
  171. package/src/ui/timeline/ThinkingBlock.tsx +100 -0
  172. package/src/ui/timeline/Timeline.tsx +1410 -0
  173. package/src/ui/timeline/TranscriptShell.tsx +302 -0
  174. package/src/ui/timeline/TurnGroup.tsx +673 -0
  175. package/src/ui/timeline/layoutListWindow.ts +145 -0
  176. package/src/ui/timeline/liveViewportWindow.ts +68 -0
  177. package/src/ui/timeline/progressEntries.ts +156 -0
  178. package/src/ui/timeline/runActivityView.ts +37 -0
  179. package/src/ui/timeline/staticTranscriptCache.ts +174 -0
  180. package/src/ui/timeline/streamCoalesce.ts +53 -0
  181. package/src/ui/timeline/timelineMeasure.ts +3273 -0
  182. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,160 @@
1
+ import { spawn } from "child_process";
2
+ import { join } from "path";
3
+ import { runCommand } from "../process/CommandRunner.js";
4
+ import { buildSpawnSpec, resolveExecutable } from "./executableResolver.js";
5
+
6
+ type CommandRunner = typeof runCommand;
7
+
8
+ let cachedExecutable: string | null = null;
9
+ let resolveInFlight: Promise<string> | null = null;
10
+
11
+ interface SpawnOptions {
12
+ stdio: ["ignore" | "pipe", "pipe", "pipe"];
13
+ }
14
+
15
+ export interface CapturedProcessOutput {
16
+ exitCode: number | null;
17
+ stdout: string;
18
+ stderr: string;
19
+ }
20
+
21
+ export function resetCodexExecutableCacheForTests(): void {
22
+ cachedExecutable = null;
23
+ }
24
+
25
+ export async function resolveCodexExecutable(options?: {
26
+ runCommandImpl?: CommandRunner;
27
+ cwd?: string;
28
+ configuredPath?: string | null;
29
+ }): Promise<string> {
30
+ if (!options?.configuredPath && !options?.runCommandImpl && cachedExecutable !== null) {
31
+ return cachedExecutable;
32
+ }
33
+
34
+ if (!options?.configuredPath && !options?.runCommandImpl) {
35
+ if (resolveInFlight) return resolveInFlight;
36
+
37
+ resolveInFlight = (async () => {
38
+ const result = await doResolveCodexExecutable(options);
39
+ cachedExecutable = result;
40
+ return result;
41
+ })();
42
+
43
+ try {
44
+ return await resolveInFlight;
45
+ } finally {
46
+ resolveInFlight = null;
47
+ }
48
+ }
49
+
50
+ return doResolveCodexExecutable(options);
51
+ }
52
+
53
+ async function doResolveCodexExecutable(options?: {
54
+ runCommandImpl?: CommandRunner;
55
+ cwd?: string;
56
+ configuredPath?: string | null;
57
+ }): Promise<string> {
58
+ const knownFilePaths: string[] = [];
59
+ if (process.platform === "win32" && process.env.LOCALAPPDATA) {
60
+ knownFilePaths.push(join(process.env.LOCALAPPDATA, "Microsoft", "WindowsApps", "codex.exe"));
61
+ }
62
+
63
+ return resolveExecutable({
64
+ runCommandImpl: options?.runCommandImpl,
65
+ cwd: options?.cwd,
66
+ configuredPath: options?.configuredPath,
67
+ envOverrides: ["CODEX_EXECUTABLE"],
68
+ commandNames: process.platform === "win32"
69
+ ? ["codex.cmd", "codex.exe", "codex"]
70
+ : ["codex"],
71
+ knownFilePaths,
72
+ label: "codex",
73
+ });
74
+ }
75
+
76
+ export function formatCodexLaunchError(err: NodeJS.ErrnoException): string {
77
+ const detail = err.message ? `\n\nDetails: ${err.message}` : "";
78
+
79
+ if (err.code === "ENOENT") {
80
+ return [
81
+ "Codex executable was not found in PATH.",
82
+ "Set CODEX_EXECUTABLE to your working command/path, then restart Ubume.",
83
+ "Alternative: install CLI with `npm install -g @openai/codex`.",
84
+ ].join("\n") + detail;
85
+ }
86
+
87
+ if (err.code === "EACCES" || err.code === "EPERM") {
88
+ return [
89
+ "Codex appears installed but this process cannot launch it (permission blocked).",
90
+ "Set CODEX_EXECUTABLE to a working CLI command/path and restart Ubume.",
91
+ "Windows note: Codex docs recommend WSL for the best CLI experience.",
92
+ ].join("\n") + detail;
93
+ }
94
+
95
+ return err.message;
96
+ }
97
+
98
+ export function spawnCodexProcess(
99
+ executable: string,
100
+ args: string[],
101
+ options: SpawnOptions,
102
+ ): ReturnType<typeof spawn> {
103
+ const spec = buildSpawnSpec(executable, args);
104
+ if (!spec.executable) throw new Error("Codex executable path is empty.");
105
+ return spawn(spec.executable, spec.args, { ...options, shell: false });
106
+ }
107
+
108
+ export function captureCodexProcessOutput(
109
+ executable: string,
110
+ args: string[],
111
+ timeoutMs: number,
112
+ ): Promise<CapturedProcessOutput> {
113
+ return new Promise<CapturedProcessOutput>((resolve, reject) => {
114
+ let proc: ReturnType<typeof spawn>;
115
+ try {
116
+ proc = spawnCodexProcess(executable, args, { stdio: ["ignore", "pipe", "pipe"] });
117
+ } catch (error) {
118
+ reject(error);
119
+ return;
120
+ }
121
+
122
+ let stdout = "";
123
+ let stderr = "";
124
+ let settled = false;
125
+
126
+ const finish = (callback: () => void) => {
127
+ if (settled) return;
128
+ settled = true;
129
+ clearTimeout(timer);
130
+ callback();
131
+ };
132
+
133
+ const timer = setTimeout(() => {
134
+ proc.kill();
135
+ const error = new Error(`Timed out waiting for Codex command: ${args.join(" ")}`) as NodeJS.ErrnoException;
136
+ error.code = "ETIME";
137
+ finish(() => reject(error));
138
+ }, timeoutMs);
139
+
140
+ proc.stdout?.on("data", (chunk: Buffer) => {
141
+ stdout += chunk.toString();
142
+ });
143
+
144
+ proc.stderr?.on("data", (chunk: Buffer) => {
145
+ stderr += chunk.toString();
146
+ });
147
+
148
+ proc.on("error", (error) => {
149
+ finish(() => reject(error));
150
+ });
151
+
152
+ proc.on("close", (exitCode) => {
153
+ finish(() => resolve({
154
+ exitCode,
155
+ stdout,
156
+ stderr,
157
+ }));
158
+ });
159
+ });
160
+ }
@@ -0,0 +1,164 @@
1
+ import { existsSync } from "fs";
2
+ import { join } from "path";
3
+ import { runCommand } from "../process/CommandRunner.js";
4
+ import {
5
+ normalizeExecutableValue,
6
+ validateWindowsBatchArgumentForCmd,
7
+ validateWindowsBatchExecutableForCmd,
8
+ } from "../process/processValidation.js";
9
+
10
+ type CommandRunner = typeof runCommand;
11
+
12
+ export interface ExecutableResolverOptions {
13
+ runCommandImpl?: CommandRunner;
14
+ cwd?: string;
15
+ configuredPath?: string | null;
16
+ envOverrides?: string[];
17
+ commandNames: string[];
18
+ knownPathDirectories?: string[];
19
+ knownFilePaths?: string[];
20
+ label: string;
21
+ allowBareFallback?: boolean;
22
+ requireResolvedFile?: boolean;
23
+ }
24
+
25
+ function validateConfiguredExecutable(value: string, label: string, cwd: string): string {
26
+ return normalizeExecutableValue(value, {
27
+ label,
28
+ cwd,
29
+ requireExistingPath: /[\\/]/.test(value) || /^[\s"']*[A-Za-z]:/.test(value),
30
+ allowBareExecutable: true,
31
+ });
32
+ }
33
+
34
+ function validateResolvedExecutable(value: string, label: string, cwd: string): string {
35
+ return normalizeExecutableValue(value, {
36
+ label,
37
+ cwd,
38
+ requireExistingPath: false,
39
+ allowBareExecutable: true,
40
+ });
41
+ }
42
+
43
+ async function resolveWithWhere(
44
+ runCommandImpl: CommandRunner,
45
+ cwd: string,
46
+ query: string,
47
+ requireResolvedFile: boolean,
48
+ ): Promise<string | null> {
49
+ const whereRunner = runCommandImpl({
50
+ executable: "where.exe",
51
+ args: [query],
52
+ cwd,
53
+ timeoutMs: 5000,
54
+ });
55
+ const whereResult = await whereRunner.result;
56
+ if (whereResult.status !== "completed" || whereResult.exitCode !== 0) return null;
57
+ const lines = whereResult.stdout
58
+ .trim()
59
+ .split(/[\r\n]+/)
60
+ .map((l) => l.trim())
61
+ .filter(Boolean);
62
+ for (const line of lines) {
63
+ let candidate: string;
64
+ try {
65
+ candidate = validateResolvedExecutable(line, "where.exe result", cwd);
66
+ } catch {
67
+ continue;
68
+ }
69
+ if (!requireResolvedFile || existsSync(candidate)) return candidate;
70
+ }
71
+ return null;
72
+ }
73
+
74
+ /**
75
+ * Resolves an executable's location.
76
+ *
77
+ * Priority:
78
+ * 1. Configured path override
79
+ * 2. Environment variable overrides (in order)
80
+ * 3. Windows PATH lookup by explicit command names (using where.exe)
81
+ * 4. Windows known-path fallbacks (e.g. %APPDATA%\npm)
82
+ * 5. Explicit known file fallbacks
83
+ * 6. Bare name fallback, unless disabled
84
+ */
85
+ export async function resolveExecutable(options: ExecutableResolverOptions): Promise<string> {
86
+ const runCommandImpl = options.runCommandImpl ?? runCommand;
87
+ const cwd = options.cwd ?? process.cwd();
88
+
89
+ // 1. Configured path override
90
+ if (options.configuredPath?.trim()) {
91
+ return validateConfiguredExecutable(options.configuredPath, `${options.label}CommandPath`, cwd);
92
+ }
93
+
94
+ // 2. Environment variable overrides
95
+ if (options.envOverrides) {
96
+ for (const envVar of options.envOverrides) {
97
+ const envOverride = process.env[envVar]?.trim();
98
+ if (envOverride) {
99
+ return validateConfiguredExecutable(envOverride, envVar, cwd);
100
+ }
101
+ }
102
+ }
103
+
104
+ // 3. Windows PATH lookup by explicit command names (where.exe returns null on non-Windows gracefully)
105
+ for (const candidate of options.commandNames) {
106
+ const resolved = await resolveWithWhere(runCommandImpl, cwd, candidate, options.requireResolvedFile === true);
107
+ if (resolved) return resolved;
108
+ }
109
+
110
+ // 4. Windows known-path fallbacks (existsSync returns false for non-existent paths on any platform)
111
+ if (options.knownPathDirectories) {
112
+ const knownCandidates: string[] = [];
113
+ for (const dir of options.knownPathDirectories) {
114
+ for (const candidate of options.commandNames) {
115
+ knownCandidates.push(join(dir, candidate));
116
+ }
117
+ }
118
+
119
+ for (const candidate of knownCandidates) {
120
+ const validated = validateResolvedExecutable(candidate, `${options.label} known executable`, cwd);
121
+ if (existsSync(validated)) return validated;
122
+ }
123
+ }
124
+
125
+ // 5. Explicit known file fallbacks
126
+ for (const candidate of options.knownFilePaths ?? []) {
127
+ const validated = validateResolvedExecutable(candidate, `${options.label} known executable`, cwd);
128
+ if (existsSync(validated)) return validated;
129
+ }
130
+
131
+ if (options.allowBareFallback === false) {
132
+ throw new Error(`${options.label} executable was not found.`);
133
+ }
134
+
135
+ // 6. Bare name fallback — prefer the name without an extension (works on Unix).
136
+ const bareName = options.commandNames.find((c) => !c.includes(".")) ?? options.commandNames[0];
137
+ return validateResolvedExecutable(bareName!, `${options.label} executable`, cwd);
138
+ }
139
+
140
+ /**
141
+ * Builds the spawn spec for a resolved executable.
142
+ * .cmd and .bat files must be invoked via `cmd.exe /d /s /c` on Windows.
143
+ *
144
+ * `platform` defaults to the host platform; it is injectable so the Windows
145
+ * wrapping branch can be unit-tested from any OS.
146
+ */
147
+ export function buildSpawnSpec(
148
+ executable: string,
149
+ args: string[],
150
+ platform: NodeJS.Platform = process.platform,
151
+ ): { executable: string; args: string[] } {
152
+ const validatedExecutable = validateResolvedExecutable(executable, "Executable", process.cwd());
153
+ if (platform === "win32") {
154
+ const lower = validatedExecutable.toLowerCase();
155
+ if (lower.endsWith(".cmd") || lower.endsWith(".bat")) {
156
+ validateWindowsBatchExecutableForCmd(validatedExecutable, "Windows batch executable");
157
+ for (const argument of args) {
158
+ validateWindowsBatchArgumentForCmd(argument, "Windows batch argument");
159
+ }
160
+ return { executable: "cmd.exe", args: ["/d", "/s", "/c", "call", validatedExecutable, ...args] };
161
+ }
162
+ }
163
+ return { executable: validatedExecutable, args };
164
+ }
@@ -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,97 @@
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
+ image?: boolean;
12
+ }
13
+
14
+ const capabilityCache = new Map<string, Promise<CodexCliCapabilities>>();
15
+
16
+ function escapeRegExp(value: string): string {
17
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18
+ }
19
+
20
+ function normalizeHelpText(text: string): string {
21
+ return stripNonPrintableControls(stripAnsi(text))
22
+ .replace(/\r\n/g, "\n")
23
+ .replace(/\r/g, "\n")
24
+ .toLowerCase();
25
+ }
26
+
27
+ // Uses a hand-rolled word-boundary pattern rather than \b so that flag names
28
+ // containing hyphens (e.g. --ask-for-approval) are matched reliably.
29
+ function hasCliToken(helpText: string, token: string): boolean {
30
+ const pattern = new RegExp(`(^|[^a-z0-9-])${escapeRegExp(token.toLowerCase())}(?=$|[^a-z0-9-])`, "m");
31
+ return pattern.test(helpText);
32
+ }
33
+
34
+ export function parseCodexCliCapabilities(
35
+ execHelpText: string,
36
+ _topLevelHelpText: string,
37
+ ): CodexCliCapabilities {
38
+ const execHelp = normalizeHelpText(execHelpText);
39
+
40
+ return {
41
+ askForApproval: hasCliToken(execHelp, "--ask-for-approval"),
42
+ sandbox: hasCliToken(execHelp, "--sandbox"),
43
+ config: hasCliToken(execHelp, "--config") || hasCliToken(execHelp, "-c"),
44
+ fullAuto: hasCliToken(execHelp, "--full-auto"),
45
+ image: hasCliToken(execHelp, "--image") || hasCliToken(execHelp, "-i"),
46
+ };
47
+ }
48
+
49
+ export async function getCodexCliCapabilities(executable: string): Promise<CodexCliCapabilities> {
50
+ const cached = capabilityCache.get(executable);
51
+ if (cached) {
52
+ return cached;
53
+ }
54
+
55
+ const inFlight = (async () => {
56
+ const [execHelp, topLevelHelp] = await Promise.allSettled([
57
+ captureCodexProcessOutput(executable, ["exec", "--help"], HELP_TIMEOUT_MS),
58
+ captureCodexProcessOutput(executable, ["--help"], HELP_TIMEOUT_MS),
59
+ ]);
60
+
61
+ const outputs: string[] = [];
62
+
63
+ if (execHelp.status === "fulfilled") {
64
+ outputs.push(execHelp.value.stdout, execHelp.value.stderr);
65
+ }
66
+
67
+ if (topLevelHelp.status === "fulfilled") {
68
+ outputs.push(topLevelHelp.value.stdout, topLevelHelp.value.stderr);
69
+ }
70
+
71
+ if (outputs.length === 0) {
72
+ if (execHelp.status === "rejected") {
73
+ throw execHelp.reason;
74
+ }
75
+
76
+ if (topLevelHelp.status === "rejected") {
77
+ throw topLevelHelp.reason;
78
+ }
79
+
80
+ throw new Error("Unable to determine Codex CLI capabilities from help output.");
81
+ }
82
+
83
+ return parseCodexCliCapabilities(
84
+ execHelp.status === "fulfilled" ? `${execHelp.value.stdout}\n${execHelp.value.stderr}` : "",
85
+ topLevelHelp.status === "fulfilled" ? `${topLevelHelp.value.stdout}\n${topLevelHelp.value.stderr}` : "",
86
+ );
87
+ })();
88
+
89
+ capabilityCache.set(executable, inFlight);
90
+
91
+ try {
92
+ return await inFlight;
93
+ } catch (error) {
94
+ capabilityCache.delete(executable);
95
+ throw error;
96
+ }
97
+ }