Repository navigation
runnign automate and finalzie #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
53ae749
62daa9b
8652023
dacf981
e2818b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { resolveClaudeCodeExecutable } from "./claudeCodeExecutable"; | ||
|
|
||
| describe("resolveClaudeCodeExecutable", () => { | ||
| it("prefers the explicit env override", () => { | ||
| expect( | ||
| resolveClaudeCodeExecutable({ | ||
| env: { | ||
| CLAUDE_CODE_EXECUTABLE_PATH: "/custom/bin/claude", | ||
| PATH: "/usr/bin:/bin", | ||
| }, | ||
| }), | ||
| ).toEqual({ | ||
| path: "/custom/bin/claude", | ||
| source: "env", | ||
| }); | ||
| }); | ||
|
|
||
| it("uses the detected Claude auth path before falling back to PATH lookup", () => { | ||
| expect( | ||
| resolveClaudeCodeExecutable({ | ||
| auth: [ | ||
| { | ||
| type: "cli-subscription", | ||
| cli: "claude", | ||
| path: "/opt/homebrew/bin/claude", | ||
| authenticated: true, | ||
| verified: true, | ||
| }, | ||
| ], | ||
| env: { | ||
| PATH: "/usr/bin:/bin", | ||
| }, | ||
| }), | ||
| ).toEqual({ | ||
| path: "/opt/homebrew/bin/claude", | ||
| source: "auth", | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import fs from "node:fs"; | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| import type { DetectedAuth } from "./authDetector"; | ||
|
|
||
| export type ClaudeCodeExecutableResolution = { | ||
| path: string; | ||
| source: "env" | "auth" | "path" | "common-dir" | "fallback-command"; | ||
| }; | ||
|
|
||
| const HOME_DIR = os.homedir(); | ||
| const COMMON_BIN_DIRS = [ | ||
| "/opt/homebrew/bin", | ||
| "/opt/homebrew/sbin", | ||
| "/usr/local/bin", | ||
| "/usr/local/sbin", | ||
| "/usr/bin", | ||
| "/bin", | ||
| `${HOME_DIR}/.local/bin`, | ||
| `${HOME_DIR}/.nvm/current/bin`, | ||
| ].filter(Boolean); | ||
|
|
||
| function isExecutableFile(candidatePath: string): boolean { | ||
| try { | ||
| const stat = fs.statSync(candidatePath); | ||
| return stat.isFile() && (process.platform === "win32" || (stat.mode & 0o111) !== 0); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| function resolveFromPathEntries(command: string, pathValue: string | undefined): string | null { | ||
| if (!pathValue) return null; | ||
| for (const entry of pathValue.split(path.delimiter)) { | ||
| const trimmed = entry.trim(); | ||
| if (!trimmed) continue; | ||
| const candidatePath = path.join(trimmed, command); | ||
| if (isExecutableFile(candidatePath)) { | ||
| return candidatePath; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| function findClaudeAuthPath(auth?: DetectedAuth[]): string | null { | ||
| const entry = auth?.find((item) => item.type === "cli-subscription" && item.cli === "claude"); | ||
| if (!entry) return null; | ||
| if (entry.type !== "cli-subscription") return null; | ||
| return entry.path.trim().length > 0 ? entry.path : null; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| export function resolveClaudeCodeExecutable(args?: { | ||
| auth?: DetectedAuth[]; | ||
| env?: NodeJS.ProcessEnv; | ||
| }): ClaudeCodeExecutableResolution { | ||
| const env = args?.env ?? process.env; | ||
| const envPath = env.CLAUDE_CODE_EXECUTABLE_PATH?.trim(); | ||
| if (envPath) { | ||
| return { path: envPath, source: "env" }; | ||
| } | ||
|
|
||
| const authPath = findClaudeAuthPath(args?.auth); | ||
| if (authPath) { | ||
| return { path: authPath, source: "auth" }; | ||
| } | ||
|
|
||
| const pathResolved = resolveFromPathEntries("claude", env.PATH); | ||
| if (pathResolved) { | ||
| return { path: pathResolved, source: "path" }; | ||
| } | ||
|
|
||
| for (const binDir of COMMON_BIN_DIRS) { | ||
| const candidatePath = path.join(binDir, "claude"); | ||
| if (isExecutableFile(candidatePath)) { | ||
| return { path: candidatePath, source: "common-dir" }; | ||
| } | ||
| } | ||
|
|
||
| return { path: "claude", source: "fallback-command" }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,16 @@ | ||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
| import { query as claudeQuery, type SDKMessage } from "@anthropic-ai/claude-agent-sdk"; | ||
| import type { Logger } from "../logging/logger"; | ||
| import { getErrorMessage } from "../shared/utils"; | ||
| import { resolveAdeMcpServerLaunch } from "../orchestrator/unifiedOrchestratorAdapter"; | ||
| import { | ||
| reportProviderRuntimeAuthFailure, | ||
| reportProviderRuntimeFailure, | ||
| reportProviderRuntimeReady, | ||
| } from "./providerRuntimeHealth"; | ||
| import { resolveClaudeCodeExecutable } from "./claudeCodeExecutable"; | ||
| import { normalizeCliMcpServers } from "./providerResolver"; | ||
|
|
||
| const PROBE_TIMEOUT_MS = 20_000; | ||
| const PROBE_CACHE_TTL_MS = 30_000; | ||
|
|
@@ -22,6 +27,7 @@ type ClaudeRuntimeProbeResult = | |
| /** Cache and in-flight probe keyed by projectRoot to avoid cross-project contamination. */ | ||
| const probeCache = new Map<string, { checkedAtMs: number; result: ClaudeRuntimeProbeResult }>(); | ||
| const inFlightProbes = new Map<string, Promise<ClaudeRuntimeProbeResult>>(); | ||
| let runtimeRootCache: string | null = null; | ||
|
|
||
| function normalizeErrorMessage(error: unknown): string { | ||
| const text = getErrorMessage(error).trim(); | ||
|
|
@@ -84,16 +90,52 @@ function cacheResult(projectRoot: string, result: ClaudeRuntimeProbeResult): Cla | |
| return result; | ||
| } | ||
|
|
||
| function publishResult(result: ClaudeRuntimeProbeResult): void { | ||
| if (result.state === "ready") { | ||
| reportProviderRuntimeReady("claude"); | ||
| return; | ||
| function resolveProbeRuntimeRoot(): string { | ||
| if (runtimeRootCache !== null) return runtimeRootCache; | ||
| const startPoints = [process.cwd(), __dirname]; | ||
| for (const start of startPoints) { | ||
| let dir = path.resolve(start); | ||
| for (let i = 0; i < 12; i += 1) { | ||
| if (fs.existsSync(path.join(dir, "apps", "mcp-server", "package.json"))) { | ||
| runtimeRootCache = dir; | ||
| return dir; | ||
| } | ||
| const parent = path.dirname(dir); | ||
| if (parent === dir) break; | ||
| dir = parent; | ||
| } | ||
| } | ||
| if (result.state === "auth-failed") { | ||
| reportProviderRuntimeAuthFailure("claude", result.message); | ||
| return; | ||
| runtimeRootCache = process.cwd(); | ||
| return runtimeRootCache; | ||
| } | ||
|
|
||
| function resolveProbeMcpServers(projectRoot: string): Record<string, Record<string, unknown>> | undefined { | ||
| const launch = resolveAdeMcpServerLaunch({ | ||
| workspaceRoot: projectRoot, | ||
| runtimeRoot: resolveProbeRuntimeRoot(), | ||
| defaultRole: "agent", | ||
| }); | ||
| return normalizeCliMcpServers("claude", { | ||
| ade: { | ||
| command: launch.command, | ||
| args: launch.cmdArgs, | ||
| env: launch.env, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| function publishResult(result: ClaudeRuntimeProbeResult): void { | ||
| switch (result.state) { | ||
| case "ready": | ||
| reportProviderRuntimeReady("claude"); | ||
| break; | ||
| case "auth-failed": | ||
| reportProviderRuntimeAuthFailure("claude", result.message); | ||
| break; | ||
| case "runtime-failed": | ||
| reportProviderRuntimeFailure("claude", result.message); | ||
| break; | ||
| } | ||
| reportProviderRuntimeFailure("claude", result.message); | ||
| } | ||
|
|
||
| export function resetClaudeRuntimeProbeCache(): void { | ||
|
|
@@ -122,12 +164,15 @@ export async function probeClaudeRuntimeHealth(args: { | |
| const probe = (async (): Promise<ClaudeRuntimeProbeResult> => { | ||
| const abortController = new AbortController(); | ||
| const timeout = setTimeout(() => abortController.abort(), PROBE_TIMEOUT_MS); | ||
| const claudeExecutable = resolveClaudeCodeExecutable(); | ||
| const stream = claudeQuery({ | ||
| prompt: "System initialization check. Respond with only the word READY.", | ||
| options: { | ||
| cwd: projectRoot, | ||
| permissionMode: "plan", | ||
| tools: [], | ||
| pathToClaudeCodeExecutable: claudeExecutable.path, | ||
| mcpServers: resolveProbeMcpServers(projectRoot) as any, | ||
| abortController, | ||
| }, | ||
| }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep probe setup inside the guarded path and reuse the resolved executable path.
🛡️ Suggested hardening+ let claudeExecutablePath: string | null = null;
const probe = (async (): Promise<ClaudeRuntimeProbeResult> => {
const abortController = new AbortController();
const timeout = setTimeout(() => abortController.abort(), PROBE_TIMEOUT_MS);
- const claudeExecutable = resolveClaudeCodeExecutable();
- const stream = claudeQuery({
- prompt: "System initialization check. Respond with only the word READY.",
- options: {
- cwd: projectRoot,
- permissionMode: "plan",
- tools: [],
- pathToClaudeCodeExecutable: claudeExecutable.path,
- mcpServers: resolveProbeMcpServers(projectRoot) as any,
- abortController,
- },
- });
+ let stream: ReturnType<typeof claudeQuery> | null = null;
try {
+ claudeExecutablePath = resolveClaudeCodeExecutable().path;
+ stream = claudeQuery({
+ prompt: "System initialization check. Respond with only the word READY.",
+ options: {
+ cwd: projectRoot,
+ permissionMode: "plan",
+ tools: [],
+ pathToClaudeCodeExecutable: claudeExecutablePath,
+ mcpServers: resolveProbeMcpServers(projectRoot) as any,
+ abortController,
+ },
+ });
for await (const message of stream) {
const result = resultFromSdkMessage(message);
if (result) {
return cacheResult(projectRoot, result);
}
@@
} finally {
clearTimeout(timeout);
try {
- stream.close();
+ stream?.close();
} catch {
// Best effort cleanup — avoid leaving the probe subprocess running.
}
}
})();
@@
args.logger?.warn?.("ai.claude_runtime_probe.failed", {
projectRoot,
state: result.state,
message: result.message,
- claudeExecutablePath: resolveClaudeCodeExecutable().path,
+ claudeExecutablePath,
});Also applies to: 180-205, 216-220 🤖 Prompt for AI Agents |
||
|
|
@@ -172,6 +217,7 @@ export async function probeClaudeRuntimeHealth(args: { | |
| projectRoot, | ||
| state: result.state, | ||
| message: result.message, | ||
| claudeExecutablePath: resolveClaudeCodeExecutable().path, | ||
| }); | ||
| } | ||
| } finally { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
In Node.js/Electron on Windows, when manually resolving an executable from PATH, do you need to account for PATHEXT extensions like.exe/.cmdand thePATH/Pathenvironment-variable casing?💡 Result:
Yes, when manually resolving an executable from PATH in Node.js/Electron on Windows, you need to account for both PATHEXT extensions (like .exe, .cmd) and PATH/Path environment variable casing. Node.js child_process.spawn and spawnSync do not automatically handle PATHEXT; they only find .exe files by default and ignore PATHEXT for non-.exe files like .bat/.cmd. For manual resolution matching Windows shell behavior, implement PATH splitting, PATHEXT appending, and file existence checks. The PATH environment variable in process.env may appear as 'PATH' or 'Path' due to Windows case-insensitivity; check both or use case-insensitive lookup (e.g., find key where key.toUpperCase === 'PATH'). Use libraries like 'which' npm package, which properly handles PATH/PATHEXT on Windows. Electron uses the same Node.js child_process, so same rules apply.
Citations:
process.env.PATHcan be undefined on Windows npm/node-which#108🏁 Script executed:
Repository: arul28/ADE
Length of output: 111
🏁 Script executed:
# Read the file to understand the full context cat -n apps/desktop/src/main/services/ai/claudeCodeExecutable.tsRepository: arul28/ADE
Length of output: 2929
🏁 Script executed:
Repository: arul28/ADE
Length of output: 333
🏁 Script executed:
Repository: arul28/ADE
Length of output: 633
Windows executables require extension handling and case-insensitive PATH lookup.
On Windows, the
claudeexecutable is typically installed asclaude.exeorclaude.cmd. The current code only checks for the literal nameclaudewithout extensions, which will fail to find these installations on Windows. Additionally, the code only accessesenv.PATH, but on Windows this environment variable may be cased asPathdue to case-insensitivity.This affects both the PATH probe (line 67) and the
COMMON_BIN_DIRSprobe (lines 72-76), causing the code to fall through to the fallback"claude"which relies on the shell to resolve the executable.Suggested fix
🤖 Prompt for AI Agents