source dump of claude code
at main 136 lines 4.9 kB view raw
1/** 2 * Protocol Handler 3 * 4 * Entry point for `claude --handle-uri <url>`. When the OS invokes claude 5 * with a `claude-cli://` URL, this module: 6 * 1. Parses the URI into a structured action 7 * 2. Detects the user's terminal emulator 8 * 3. Opens a new terminal window running claude with the appropriate args 9 * 10 * This runs in a headless context (no TTY) because the OS launches the binary 11 * directly — there is no terminal attached. 12 */ 13 14import { homedir } from 'os' 15import { logForDebugging } from '../debug.js' 16import { 17 filterExistingPaths, 18 getKnownPathsForRepo, 19} from '../githubRepoPathMapping.js' 20import { jsonStringify } from '../slowOperations.js' 21import { readLastFetchTime } from './banner.js' 22import { parseDeepLink } from './parseDeepLink.js' 23import { MACOS_BUNDLE_ID } from './registerProtocol.js' 24import { launchInTerminal } from './terminalLauncher.js' 25 26/** 27 * Handle an incoming deep link URI. 28 * 29 * Called from the CLI entry point when `--handle-uri` is passed. 30 * This function parses the URI, resolves the claude binary, and 31 * launches it in the user's terminal. 32 * 33 * @param uri - The raw URI string (e.g., "claude-cli://prompt?q=hello+world") 34 * @returns exit code (0 = success) 35 */ 36export async function handleDeepLinkUri(uri: string): Promise<number> { 37 logForDebugging(`Handling deep link URI: ${uri}`) 38 39 let action 40 try { 41 action = parseDeepLink(uri) 42 } catch (error) { 43 const message = error instanceof Error ? error.message : String(error) 44 // biome-ignore lint/suspicious/noConsole: intentional error output 45 console.error(`Deep link error: ${message}`) 46 return 1 47 } 48 49 logForDebugging(`Parsed deep link action: ${jsonStringify(action)}`) 50 51 // Always the running executable — no PATH lookup. The OS launched us via 52 // an absolute path (bundle symlink / .desktop Exec= / registry command) 53 // baked at registration time, and we want the terminal-launched Claude to 54 // be the same binary. process.execPath is that binary. 55 const { cwd, resolvedRepo } = await resolveCwd(action) 56 // Resolve FETCH_HEAD age here, in the trampoline process, so main.tsx 57 // stays await-free — the launched instance receives it as a precomputed 58 // flag instead of statting the filesystem on its own startup path. 59 const lastFetch = resolvedRepo ? await readLastFetchTime(cwd) : undefined 60 const launched = await launchInTerminal(process.execPath, { 61 query: action.query, 62 cwd, 63 repo: resolvedRepo, 64 lastFetchMs: lastFetch?.getTime(), 65 }) 66 if (!launched) { 67 // biome-ignore lint/suspicious/noConsole: intentional error output 68 console.error( 69 'Failed to open a terminal. Make sure a supported terminal emulator is installed.', 70 ) 71 return 1 72 } 73 74 return 0 75} 76 77/** 78 * Handle the case where claude was launched as the app bundle's executable 79 * by macOS (via URL scheme). Uses the NAPI module to receive the URL from 80 * the Apple Event, then handles it normally. 81 * 82 * @returns exit code (0 = success, 1 = error, null = not a URL launch) 83 */ 84export async function handleUrlSchemeLaunch(): Promise<number | null> { 85 // LaunchServices overwrites __CFBundleIdentifier with the launching bundle's 86 // ID. This is a precise positive signal — it's set to our exact bundle ID 87 // if and only if macOS launched us via the URL handler .app bundle. 88 // (`open` from a terminal passes the caller's env through, so negative 89 // heuristics like !TERM don't work — the terminal's TERM leaks in.) 90 if (process.env.__CFBundleIdentifier !== MACOS_BUNDLE_ID) { 91 return null 92 } 93 94 try { 95 const { waitForUrlEvent } = await import('url-handler-napi') 96 const url = waitForUrlEvent(5000) 97 if (!url) { 98 return null 99 } 100 return await handleDeepLinkUri(url) 101 } catch { 102 // NAPI module not available, or handleDeepLinkUri rejected — not a URL launch 103 return null 104 } 105} 106 107/** 108 * Resolve the working directory for the launched Claude instance. 109 * Precedence: explicit cwd > repo lookup (MRU clone) > home. 110 * A repo that isn't cloned locally is not an error — fall through to home 111 * so a web link referencing a repo the user doesn't have still opens Claude. 112 * 113 * Returns the resolved cwd, and the repo slug if (and only if) the MRU 114 * lookup hit — so the launched instance can show which clone was selected 115 * and its git freshness. 116 */ 117async function resolveCwd(action: { 118 cwd?: string 119 repo?: string 120}): Promise<{ cwd: string; resolvedRepo?: string }> { 121 if (action.cwd) { 122 return { cwd: action.cwd } 123 } 124 if (action.repo) { 125 const known = getKnownPathsForRepo(action.repo) 126 const existing = await filterExistingPaths(known) 127 if (existing[0]) { 128 logForDebugging(`Resolved repo ${action.repo}${existing[0]}`) 129 return { cwd: existing[0], resolvedRepo: action.repo } 130 } 131 logForDebugging( 132 `No local clone found for repo ${action.repo}, falling back to home`, 133 ) 134 } 135 return { cwd: homedir() } 136}