|
| 1 | +import path from "path"; |
| 2 | +import fs from "fs"; |
1 | 3 | import { app } from "electron"; |
2 | 4 | import { getAppSetting } from "./app-settings"; |
| 5 | +import { log } from "./logger"; |
3 | 6 |
|
4 | 7 | // Import the SDK's own types — Query is the return type of sdk.query() |
5 | 8 | import type { Query, query as sdkQueryFn } from "@anthropic-ai/claude-agent-sdk"; |
@@ -40,20 +43,84 @@ export function clientAppEnv(): Record<string, string> { |
40 | 43 | return { CLAUDE_AGENT_SDK_CLIENT_APP: `${clientName}/${app.getVersion()}` }; |
41 | 44 | } |
42 | 45 |
|
| 46 | +const SDK_PACKAGE = ["@anthropic-ai", "claude-agent-sdk"].join("/"); |
| 47 | +const SDK_EMBED_EXPORT = [SDK_PACKAGE, "embed"].join("/"); |
| 48 | + |
43 | 49 | /** |
44 | 50 | * Resolve the SDK's cli.js path for child process spawning. |
45 | | - * In production ASAR builds, the SDK resolves cli.js inside app.asar via import.meta.url, |
46 | | - * but the spawned Node child process has no ASAR patching and can't read it. |
47 | | - * We translate app.asar → app.asar.unpacked so the child process finds the real file. |
| 51 | + * In production ASAR builds, the SDK may resolve cli.js inside app.asar, but the |
| 52 | + * spawned Node child process has no ASAR patching and can't read it. We derive |
| 53 | + * cli.js from the package entrypoint and prefer app.asar.unpacked when packaged. |
48 | 54 | */ |
49 | | -export function getCliPath(): string | undefined { |
| 55 | +export function resolveCliPathFromEntry(entryPath: string, isPackaged: boolean): string { |
| 56 | + const cliPath = path.join(path.dirname(entryPath), "cli.js"); |
| 57 | + if (!isPackaged) return cliPath; |
| 58 | + |
| 59 | + const unpackedCliPath = cliPath.replace(/app\.asar([/\\])/, "app.asar.unpacked$1"); |
| 60 | + return unpackedCliPath; |
| 61 | +} |
| 62 | + |
| 63 | +/** Cached CLI path — resolved once then reused. */ |
| 64 | +let _cachedCliPath: string | undefined; |
| 65 | + |
| 66 | +interface CliPathResolution { |
| 67 | + strategy: "embed" | "package" | "app-path"; |
| 68 | + path: string; |
| 69 | +} |
| 70 | + |
| 71 | +function candidateExists(filePath: string): boolean { |
50 | 72 | try { |
51 | | - // eslint-disable-next-line @typescript-eslint/no-require-imports |
52 | | - const cliPath = require.resolve("@anthropic-ai/claude-agent-sdk/cli.js"); |
53 | | - if (!app.isPackaged) return cliPath; |
54 | | - // asarUnpack puts cli.js in app.asar.unpacked/ — translate for child processes |
55 | | - return cliPath.replace(/app\.asar([/\\])/, "app.asar.unpacked$1"); |
| 73 | + return fs.existsSync(filePath); |
56 | 74 | } catch { |
| 75 | + return false; |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +function tryResolveFromEntry(specifier: string, strategy: CliPathResolution["strategy"]): CliPathResolution | undefined { |
| 80 | + try { |
| 81 | + // eslint-disable-next-line @typescript-eslint/no-require-imports |
| 82 | + const entryPath = require.resolve(specifier); |
| 83 | + const candidatePath = resolveCliPathFromEntry(entryPath, app.isPackaged); |
| 84 | + const exists = candidateExists(candidatePath); |
| 85 | + log("CLI_PATH_RESOLVE", `strategy=${strategy} entry=${entryPath} candidate=${candidatePath} exists=${exists}`); |
| 86 | + if (!exists) return undefined; |
| 87 | + return { strategy, path: candidatePath }; |
| 88 | + } catch (err) { |
| 89 | + const message = err instanceof Error ? err.message : String(err); |
| 90 | + log("CLI_PATH_RESOLVE_ERR", `strategy=${strategy} specifier=${specifier} ${message}`); |
57 | 91 | return undefined; |
58 | 92 | } |
59 | 93 | } |
| 94 | + |
| 95 | +function resolveFromEmbedEntry(): CliPathResolution | undefined { |
| 96 | + return tryResolveFromEntry(SDK_EMBED_EXPORT, "embed"); |
| 97 | +} |
| 98 | + |
| 99 | +function resolveFromPackageEntry(): CliPathResolution | undefined { |
| 100 | + return tryResolveFromEntry(SDK_PACKAGE, "package"); |
| 101 | +} |
| 102 | + |
| 103 | +function resolveFromAppPath(): CliPathResolution | undefined { |
| 104 | + if (!app.isPackaged) return undefined; |
| 105 | + |
| 106 | + const unpackedBase = app.getAppPath().replace(/app\.asar$/, "app.asar.unpacked"); |
| 107 | + const candidatePath = path.join(unpackedBase, "node_modules", "@anthropic-ai", "claude-agent-sdk", "cli.js"); |
| 108 | + const exists = candidateExists(candidatePath); |
| 109 | + log("CLI_PATH_RESOLVE", `strategy=app-path candidate=${candidatePath} exists=${exists}`); |
| 110 | + if (!exists) return undefined; |
| 111 | + return { strategy: "app-path", path: candidatePath }; |
| 112 | +} |
| 113 | + |
| 114 | +export function getCliPath(): string | undefined { |
| 115 | + if (_cachedCliPath) return _cachedCliPath; |
| 116 | + |
| 117 | + const resolution = resolveFromEmbedEntry() ?? resolveFromPackageEntry() ?? resolveFromAppPath(); |
| 118 | + if (resolution) { |
| 119 | + _cachedCliPath = resolution.path; |
| 120 | + log("CLI_PATH_SELECTED", `strategy=${resolution.strategy} path=${resolution.path}`); |
| 121 | + return resolution.path; |
| 122 | + } |
| 123 | + |
| 124 | + log("CLI_PATH_MISSING", "No valid Claude CLI path resolved; SDK fallback may fail in packaged apps"); |
| 125 | + return undefined; |
| 126 | +} |
0 commit comments