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.
- package/README.md +88 -0
- package/bin/codexa-local-harness-bridge.js +3 -0
- package/bin/codexa.js +28 -0
- package/bin/ubume-local-harness-bridge.js +325 -0
- package/bin/ubume.js +398 -0
- package/package.json +66 -0
- package/src/app.tsx +5759 -0
- package/src/commands/handler.ts +889 -0
- package/src/config/appVersion.ts +69 -0
- package/src/config/buildInfo.ts +3 -0
- package/src/config/launchArgs.ts +196 -0
- package/src/config/layeredConfig.ts +853 -0
- package/src/config/legacyEnv.ts +16 -0
- package/src/config/persistence.ts +377 -0
- package/src/config/runtimeConfig.ts +558 -0
- package/src/config/settings.ts +405 -0
- package/src/config/toml-serialize.ts +109 -0
- package/src/config/trustStore.ts +84 -0
- package/src/config/updateCheckCache.ts +85 -0
- package/src/core/README.md +55 -0
- package/src/core/agent/loop.ts +464 -0
- package/src/core/agent/protocol.ts +345 -0
- package/src/core/agent/tools.ts +423 -0
- package/src/core/auth/codexAuth.ts +359 -0
- package/src/core/codex/codexExecArgs.ts +166 -0
- package/src/core/codex/codexLaunch.ts +163 -0
- package/src/core/codex/codexPrompt.ts +429 -0
- package/src/core/debug/inputDebug.ts +51 -0
- package/src/core/debug/localStreamDebug.ts +50 -0
- package/src/core/debug/modelStateDebug.ts +35 -0
- package/src/core/executables/antigravityExecutable.ts +48 -0
- package/src/core/executables/claudeExecutable.ts +63 -0
- package/src/core/executables/codexExecutable.ts +160 -0
- package/src/core/executables/executableResolver.ts +164 -0
- package/src/core/executables/geminiExecutable.ts +78 -0
- package/src/core/models/codexCapabilities.ts +97 -0
- package/src/core/models/codexModelCapabilities.ts +624 -0
- package/src/core/models/codexModelsCacheSeed.ts +153 -0
- package/src/core/models/modelSpecs.ts +303 -0
- package/src/core/models/providerModelCache.ts +94 -0
- package/src/core/perf/profiler.ts +125 -0
- package/src/core/perf/renderDebug.ts +398 -0
- package/src/core/process/CommandRunner.ts +280 -0
- package/src/core/process/processValidation.ts +111 -0
- package/src/core/providerLauncher/launcher.ts +220 -0
- package/src/core/providerLauncher/registry.ts +354 -0
- package/src/core/providerLauncher/types.ts +95 -0
- package/src/core/providerLauncher/workspaceConfig.ts +487 -0
- package/src/core/providerRuntime/anthropic.ts +580 -0
- package/src/core/providerRuntime/antigravity.ts +500 -0
- package/src/core/providerRuntime/capabilityProfile.ts +383 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -0
- package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
- package/src/core/providerRuntime/codexaCupy.ts +97 -0
- package/src/core/providerRuntime/codexaNative.ts +425 -0
- package/src/core/providerRuntime/contextMetadata.ts +397 -0
- package/src/core/providerRuntime/gemini.ts +789 -0
- package/src/core/providerRuntime/lmstudio.ts +118 -0
- package/src/core/providerRuntime/local.ts +770 -0
- package/src/core/providerRuntime/localHarness/runtime.ts +1090 -0
- package/src/core/providerRuntime/localOutputBudget.ts +17 -0
- package/src/core/providerRuntime/mistralVibe.ts +667 -0
- package/src/core/providerRuntime/models.ts +175 -0
- package/src/core/providerRuntime/reasoning.ts +20 -0
- package/src/core/providerRuntime/registry.ts +286 -0
- package/src/core/providerRuntime/types.ts +142 -0
- package/src/core/providerRuntime/unsloth.ts +216 -0
- package/src/core/providers/codexJsonStream.ts +305 -0
- package/src/core/providers/codexSubprocess.ts +378 -0
- package/src/core/providers/codexTranscript.ts +695 -0
- package/src/core/providers/openaiNative.ts +13 -0
- package/src/core/providers/registry.ts +21 -0
- package/src/core/providers/types.ts +91 -0
- package/src/core/shared/attachments.ts +101 -0
- package/src/core/shared/cleanupFastFail.ts +67 -0
- package/src/core/shared/clipboard.ts +24 -0
- package/src/core/shared/clipboardImage.ts +111 -0
- package/src/core/shared/githubDiagnostics.ts +222 -0
- package/src/core/shared/hollowResponseFormat.ts +39 -0
- package/src/core/terminal/clearFrameBoundary.ts +852 -0
- package/src/core/terminal/frameLock.ts +110 -0
- package/src/core/terminal/inkRenderReset.ts +123 -0
- package/src/core/terminal/startupClear.ts +20 -0
- package/src/core/terminal/terminalCapabilities.ts +100 -0
- package/src/core/terminal/terminalControl.ts +169 -0
- package/src/core/terminal/terminalSanitize.ts +147 -0
- package/src/core/terminal/terminalTitle.ts +400 -0
- package/src/core/version/channel.ts +27 -0
- package/src/core/version/packageManager.ts +119 -0
- package/src/core/version/updateCheck.ts +203 -0
- package/src/core/workspace/appData.ts +107 -0
- package/src/core/workspace/conversationStore.ts +335 -0
- package/src/core/workspace/launchContext.ts +259 -0
- package/src/core/workspace/planStorage.ts +135 -0
- package/src/core/workspace/projectInstructions.ts +54 -0
- package/src/core/workspace/scratchDir.ts +64 -0
- package/src/core/workspace/workspaceActivity.ts +384 -0
- package/src/core/workspace/workspaceGuard.ts +377 -0
- package/src/core/workspace/workspaceRoot.ts +47 -0
- package/src/exec.ts +73 -0
- package/src/headless/execArgs.ts +296 -0
- package/src/headless/execRunner.ts +304 -0
- package/src/index.tsx +270 -0
- package/src/legacyEnvBootstrap.ts +5 -0
- package/src/session/appSession.ts +771 -0
- package/src/session/chatLifecycle.ts +994 -0
- package/src/session/conversation.ts +107 -0
- package/src/session/liveRenderScheduler.ts +214 -0
- package/src/session/persistedResponse.ts +93 -0
- package/src/session/planFlow.ts +159 -0
- package/src/session/planTranscript.ts +19 -0
- package/src/session/promptRunSchedule.ts +26 -0
- package/src/session/types.ts +234 -0
- package/src/test/runtimeTestUtils.ts +14 -0
- package/src/types/react-dom.d.ts +3 -0
- package/src/ui/chrome/ActivityBars.tsx +68 -0
- package/src/ui/chrome/ActivityIndicator.tsx +58 -0
- package/src/ui/chrome/AnimatedStatusText.tsx +69 -0
- package/src/ui/chrome/AppShell.tsx +474 -0
- package/src/ui/chrome/BottomComposer.tsx +1135 -0
- package/src/ui/chrome/DashCard.tsx +82 -0
- package/src/ui/chrome/RunFooter.tsx +65 -0
- package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
- package/src/ui/chrome/Spinner.tsx +25 -0
- package/src/ui/chrome/TopHeader.tsx +439 -0
- package/src/ui/chrome/UpdateAvailableCard.tsx +42 -0
- package/src/ui/chrome/busyStatusAnimation.ts +11 -0
- package/src/ui/input/commandNormalize.ts +66 -0
- package/src/ui/input/focus.ts +73 -0
- package/src/ui/input/imageAttachments.ts +18 -0
- package/src/ui/input/inputBuffer.ts +203 -0
- package/src/ui/input/pastedContent.ts +75 -0
- package/src/ui/input/rawArrowKeys.ts +28 -0
- package/src/ui/input/slashCommands.ts +43 -0
- package/src/ui/input/useStdinRawModeLease.ts +23 -0
- package/src/ui/layout.ts +560 -0
- package/src/ui/panels/AttachmentImportPanel.tsx +131 -0
- package/src/ui/panels/AuthPanel.tsx +149 -0
- package/src/ui/panels/BackendPicker.tsx +28 -0
- package/src/ui/panels/ModePicker.tsx +31 -0
- package/src/ui/panels/ModelPicker.tsx +31 -0
- package/src/ui/panels/ModelPickerScreen.tsx +761 -0
- package/src/ui/panels/ModelReasoningPicker.tsx +458 -0
- package/src/ui/panels/Panel.tsx +51 -0
- package/src/ui/panels/PermissionsPanel.tsx +78 -0
- package/src/ui/panels/PlanActionPicker.tsx +187 -0
- package/src/ui/panels/ProviderPicker.tsx +753 -0
- package/src/ui/panels/ProviderSetupPrompt.tsx +52 -0
- package/src/ui/panels/ReasoningPicker.tsx +46 -0
- package/src/ui/panels/ResumePicker.tsx +90 -0
- package/src/ui/panels/SelectionPanel.tsx +138 -0
- package/src/ui/panels/SettingsPanel.tsx +156 -0
- package/src/ui/panels/TextEntryPanel.tsx +139 -0
- package/src/ui/panels/ThemePicker.tsx +32 -0
- package/src/ui/panels/ToolApprovalPanel.tsx +46 -0
- package/src/ui/panels/UpdatePromptPanel.tsx +236 -0
- package/src/ui/panels/responsivePickerViewport.ts +64 -0
- package/src/ui/render/Markdown.tsx +331 -0
- package/src/ui/render/diffRenderer.ts +116 -0
- package/src/ui/render/logoVariants.ts +113 -0
- package/src/ui/render/modeDisplay.ts +52 -0
- package/src/ui/render/outputPipeline.ts +64 -0
- package/src/ui/render/runtimeDisplay.ts +128 -0
- package/src/ui/render/terminalAnswerFormat.ts +128 -0
- package/src/ui/render/textLayout.ts +392 -0
- package/src/ui/theme.tsx +274 -0
- package/src/ui/themeFlow.ts +41 -0
- package/src/ui/timeline/ActionRequiredBlock.tsx +38 -0
- package/src/ui/timeline/AgentBlock.tsx +130 -0
- package/src/ui/timeline/StaticIntroItem.tsx +54 -0
- package/src/ui/timeline/ThinkingBlock.tsx +100 -0
- package/src/ui/timeline/Timeline.tsx +1410 -0
- package/src/ui/timeline/TranscriptShell.tsx +302 -0
- package/src/ui/timeline/TurnGroup.tsx +673 -0
- package/src/ui/timeline/layoutListWindow.ts +145 -0
- package/src/ui/timeline/liveViewportWindow.ts +68 -0
- package/src/ui/timeline/progressEntries.ts +156 -0
- package/src/ui/timeline/runActivityView.ts +37 -0
- package/src/ui/timeline/staticTranscriptCache.ts +174 -0
- package/src/ui/timeline/streamCoalesce.ts +53 -0
- package/src/ui/timeline/timelineMeasure.ts +3273 -0
- 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
|
+
}
|