mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-07-09 17:18:24 +00:00
Fix Claude app launch on macOS
This commit is contained in:
parent
6118d52df5
commit
2410530450
4 changed files with 136 additions and 9 deletions
|
|
@ -46,12 +46,9 @@ const claudeAppDesignCdpConnectTimeoutMs = 15_000;
|
|||
const claudeAppDesignCdpKeepAliveMs = 45_000;
|
||||
const claudeAppDesignCdpPollIntervalMs = 250;
|
||||
|
||||
export function shouldEnableClaudeAppDesignCdp(enabledByConfig = false): boolean {
|
||||
export function shouldEnableClaudeAppDesignCdp(_enabledByConfig = false): boolean {
|
||||
const configured = process.env.CCR_CLAUDE_APP_DESIGN_CDP?.trim().toLowerCase();
|
||||
if (configured === "false" || configured === "0" || configured === "off") {
|
||||
return false;
|
||||
}
|
||||
return enabledByConfig || configured === "true" || configured === "1" || configured === "on";
|
||||
return configured === "true" || configured === "1" || configured === "on";
|
||||
}
|
||||
|
||||
export async function reserveClaudeAppCdpPort(logger: ClaudeAppCdpLogger = console, enabledByConfig = false): Promise<number | undefined> {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export type ClaudeAppLaunchResult = {
|
|||
command: string;
|
||||
cdpPort?: number;
|
||||
claudeDesignProxy?: boolean;
|
||||
pidIsLauncher?: boolean;
|
||||
pid?: number;
|
||||
userDataDir: string;
|
||||
};
|
||||
|
|
@ -52,8 +53,7 @@ export async function launchClaudeAppProfile(configDir: string, profile: Profile
|
|||
const shouldOpenDesign = shouldOpenClaudeAppDesign(config);
|
||||
const cdpPort = await reserveClaudeAppCdpPort(console, shouldOpenDesign);
|
||||
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
const appEnv: Record<string, string> = {
|
||||
...profileEnv(profile),
|
||||
...claudeCodeModelEnv(profile),
|
||||
...(config ? botGatewayProfileEnv(config, profile, "app") : {}),
|
||||
|
|
@ -64,11 +64,16 @@ export async function launchClaudeAppProfile(configDir: string, profile: Profile
|
|||
ELECTRON_ENABLE_LOGGING: "1",
|
||||
...claudeCodeUtcTimezoneEnvOverride()
|
||||
};
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
...appEnv
|
||||
};
|
||||
delete env.ELECTRON_RUN_AS_NODE;
|
||||
|
||||
const designUrl = claudeAppDesignUrl(config);
|
||||
const proxyUrl = claudeAppProxyUrl(config);
|
||||
const child = spawn(lookup.executable, claudeElectronArgs(userDataDir, cdpPort, proxyUrl), {
|
||||
const launch = claudeAppLaunchCommand(lookup.executable, userDataDir, cdpPort, proxyUrl, appEnv);
|
||||
const child = spawn(launch.command, launch.args, {
|
||||
detached: true,
|
||||
env,
|
||||
stdio: "ignore"
|
||||
|
|
@ -84,8 +89,9 @@ export async function launchClaudeAppProfile(configDir: string, profile: Profile
|
|||
return {
|
||||
child,
|
||||
claudeDesignProxy: Boolean(proxyUrl),
|
||||
command: lookup.executable,
|
||||
command: launch.command,
|
||||
...(cdpPort ? { cdpPort } : {}),
|
||||
...(launch.pidIsLauncher ? { pidIsLauncher: launch.pidIsLauncher } : {}),
|
||||
pid: child.pid,
|
||||
userDataDir
|
||||
};
|
||||
|
|
@ -163,6 +169,52 @@ function claudeElectronArgs(userDataDir: string, cdpPort?: number, proxyUrl?: st
|
|||
];
|
||||
}
|
||||
|
||||
export function claudeAppLaunchCommand(
|
||||
executable: string,
|
||||
userDataDir: string,
|
||||
cdpPort: number | undefined,
|
||||
proxyUrl: string | undefined,
|
||||
env: Record<string, string>
|
||||
): { args: string[]; command: string; pidIsLauncher?: boolean } {
|
||||
const args = claudeElectronArgs(userDataDir, cdpPort, proxyUrl);
|
||||
const appBundle = process.platform === "darwin" ? macAppBundleFromExecutable(executable) : undefined;
|
||||
if (appBundle) {
|
||||
return {
|
||||
args: [
|
||||
"-W",
|
||||
"-n",
|
||||
...macOpenEnvArgs(env),
|
||||
appBundle,
|
||||
"--args",
|
||||
...args
|
||||
],
|
||||
command: "/usr/bin/open",
|
||||
pidIsLauncher: true
|
||||
};
|
||||
}
|
||||
return {
|
||||
args,
|
||||
command: executable
|
||||
};
|
||||
}
|
||||
|
||||
function macAppBundleFromExecutable(executable: string): string | undefined {
|
||||
const normalized = path.normalize(executable);
|
||||
const marker = `${path.sep}Contents${path.sep}MacOS${path.sep}`;
|
||||
const markerIndex = normalized.lastIndexOf(marker);
|
||||
if (markerIndex <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
const appBundle = normalized.slice(0, markerIndex);
|
||||
return appBundle.endsWith(".app") && isDirectory(appBundle) ? appBundle : undefined;
|
||||
}
|
||||
|
||||
function macOpenEnvArgs(env: Record<string, string>): string[] {
|
||||
return Object.entries(env)
|
||||
.filter(([key, value]) => isEnvName(key) && typeof value === "string")
|
||||
.flatMap(([key, value]) => ["--env", `${key}=${value}`]);
|
||||
}
|
||||
|
||||
function claudeElectronUserDataDir(settingsDir: string, profile: ProfileConfig): string {
|
||||
return path.join(
|
||||
settingsDir,
|
||||
|
|
|
|||
24
tests/main/claude-app-cdp.test.mjs
Normal file
24
tests/main/claude-app-cdp.test.mjs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { shouldEnableClaudeAppDesignCdp } from "../../src/main/claude-app-cdp.ts";
|
||||
|
||||
test("Claude App Design CDP is opt-in even when Claude Design is configured", (t) => {
|
||||
const previous = process.env.CCR_CLAUDE_APP_DESIGN_CDP;
|
||||
t.after(() => {
|
||||
if (previous === undefined) {
|
||||
delete process.env.CCR_CLAUDE_APP_DESIGN_CDP;
|
||||
} else {
|
||||
process.env.CCR_CLAUDE_APP_DESIGN_CDP = previous;
|
||||
}
|
||||
});
|
||||
|
||||
delete process.env.CCR_CLAUDE_APP_DESIGN_CDP;
|
||||
assert.equal(shouldEnableClaudeAppDesignCdp(true), false);
|
||||
assert.equal(shouldEnableClaudeAppDesignCdp(false), false);
|
||||
|
||||
process.env.CCR_CLAUDE_APP_DESIGN_CDP = "true";
|
||||
assert.equal(shouldEnableClaudeAppDesignCdp(false), true);
|
||||
|
||||
process.env.CCR_CLAUDE_APP_DESIGN_CDP = "off";
|
||||
assert.equal(shouldEnableClaudeAppDesignCdp(true), false);
|
||||
});
|
||||
54
tests/main/claude-app-launch.test.mjs
Normal file
54
tests/main/claude-app-launch.test.mjs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { claudeAppLaunchCommand } from "../../src/main/claude-app-launch.ts";
|
||||
|
||||
test("claudeAppLaunchCommand opens macOS app bundles through LaunchServices", (t) => {
|
||||
const tempDir = mkdtempForTest();
|
||||
t.after(() => rmSync(tempDir, { force: true, recursive: true }));
|
||||
|
||||
const appBundle = path.join(tempDir, "Claude.app");
|
||||
const executable = path.join(appBundle, "Contents", "MacOS", "Claude");
|
||||
mkdirSync(path.dirname(executable), { recursive: true });
|
||||
writeFileSync(executable, "");
|
||||
|
||||
const launch = claudeAppLaunchCommand(
|
||||
executable,
|
||||
path.join(tempDir, "profile data"),
|
||||
49152,
|
||||
"http://127.0.0.1:3456",
|
||||
{
|
||||
"BAD-NAME": "ignored",
|
||||
CLAUDE_CONFIG_DIR: path.join(tempDir, "config"),
|
||||
CCR_PROFILE_SURFACE: "app"
|
||||
}
|
||||
);
|
||||
|
||||
if (process.platform !== "darwin") {
|
||||
assert.equal(launch.command, executable);
|
||||
assert.equal(launch.pidIsLauncher, undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.equal(launch.command, "/usr/bin/open");
|
||||
assert.equal(launch.pidIsLauncher, true);
|
||||
assert.deepEqual(launch.args.slice(0, 2), ["-W", "-n"]);
|
||||
assert.ok(launch.args.includes("--env"));
|
||||
assert.ok(launch.args.includes(`CLAUDE_CONFIG_DIR=${path.join(tempDir, "config")}`));
|
||||
assert.ok(launch.args.includes("CCR_PROFILE_SURFACE=app"));
|
||||
assert.equal(launch.args.some((arg) => arg.startsWith("BAD-NAME=")), false);
|
||||
|
||||
const appIndex = launch.args.indexOf(appBundle);
|
||||
assert.ok(appIndex > 0);
|
||||
assert.equal(launch.args[appIndex + 1], "--args");
|
||||
assert.ok(launch.args.includes("--remote-debugging-port=49152"));
|
||||
assert.ok(launch.args.includes("--remote-debugging-address=127.0.0.1"));
|
||||
assert.ok(launch.args.includes("--proxy-server=http://127.0.0.1:3456"));
|
||||
assert.ok(launch.args.includes(`--user-data-dir=${path.join(tempDir, "profile data")}`));
|
||||
});
|
||||
|
||||
function mkdtempForTest() {
|
||||
return mkdtempSync(path.join(os.tmpdir(), "ccr-claude-app-launch-"));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue