mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
The desktop app already shipped a bundled sidecar, but only desktop-managed sessions could see it. This change installs a `claude-haha` launcher into the user bin directory, wires PATH setup so new terminals can resolve it, and keeps desktop installer sessions aligned on the same bundled sidecar resolution path. The desktop install surface now also reports whether the launcher is ready or still waiting on a terminal restart. Constraint: The worktree already contains unrelated icon, docs, and UI changes, so this commit stages only the bundled CLI launcher slice Rejected: Tell users to install the official Claude CLI separately | it breaks the desktop out-of-box install story Rejected: Keep the bundled CLI reachable only inside desktop-managed shells | system terminals would still be unable to call the packaged runtime Rejected: Symlink directly into the app bundle instead of copying to user bin | moving or replacing the app bundle would leave a stale launcher behind Confidence: high Scope-risk: moderate Reversibility: clean Directive: Proxy-backed non-Anthropic providers still depend on the desktop server; do not assume this launcher makes every provider fully standalone Tested: bun test src/server/__tests__/desktop-cli-launcher.test.ts src/server/__tests__/settings.test.ts; bun test src/server/__tests__/conversation-service.test.ts; bun test src/utils/shell/bashProvider.test.ts; cd desktop && bun x vitest run sidecars/launcherRouting.test.ts src/components/settings/InstallCenter.test.tsx; cd desktop && bun run lint; cd desktop && bun run build; cargo check --manifest-path desktop/src-tauri/Cargo.toml Not-tested: Manual packaged desktop app install plus real Terminal/iTerm/PowerShell invocation on fresh macOS and Windows machines
139 lines
4.9 KiB
TypeScript
139 lines
4.9 KiB
TypeScript
/**
|
||
* Claude Code 桌面端合并 sidecar 入口。
|
||
*
|
||
* 历史上 server / cli / IM adapters 是各自独立的进程。每个 bun-compile
|
||
* 二进制都要带一份 ~55MB 的 bun runtime,光这一项就重复占了 100MB+。
|
||
* 把所有运行模式合并到同一个二进制里,runtime 只保留一份;调用方通过
|
||
* 第一个 positional 参数选择模式:
|
||
*
|
||
* claude-sidecar server --app-root <path> --host 127.0.0.1 --port 12345
|
||
* claude-sidecar cli --app-root <path> [其它 CLI 参数...]
|
||
* claude-sidecar adapters --app-root <path> [--feishu] [--telegram]
|
||
*
|
||
* 任何模式都必须先做 process.env / process.argv 设置,再 await 进入相应的
|
||
* 子模块树。原因:src/server/index.ts、src/entrypoints/cli.tsx、以及
|
||
* adapters/feishu/index.ts 等顶层都会立即读 process.argv / process.env,
|
||
* 必须在它们求值前 splice 掉 --app-root、mode、--feishu/--telegram 这些
|
||
* launcher-only 参数。
|
||
*/
|
||
|
||
import { parseLauncherArgs, resolveSidecarInvocation } from './launcherRouting'
|
||
|
||
const rawArgs = process.argv.slice(2)
|
||
const invocation = resolveSidecarInvocation(rawArgs)
|
||
if (!invocation.mode) {
|
||
console.error('claude-sidecar: missing mode argument (expected "server", "cli" or "adapters")')
|
||
process.exit(2)
|
||
}
|
||
const mode = invocation.mode
|
||
const restArgs = invocation.restArgs
|
||
|
||
if (mode === 'adapters') {
|
||
await runAdapters(restArgs)
|
||
} else {
|
||
const { appRoot, args } = parseLauncherArgs(restArgs, invocation.defaultAppRoot)
|
||
|
||
process.env.CLAUDE_APP_ROOT = appRoot
|
||
process.env.CALLER_DIR ||= process.cwd()
|
||
process.argv = [process.argv[0]!, process.argv[1]!, ...args]
|
||
|
||
await import('../../preload.ts')
|
||
|
||
if (mode === 'server') {
|
||
const { startServer } = await import('../../src/server/index.ts')
|
||
startServer()
|
||
} else if (mode === 'cli') {
|
||
await import('../../src/entrypoints/cli.tsx')
|
||
} else {
|
||
console.error(`claude-sidecar: unknown mode "${mode}" (expected "server", "cli" or "adapters")`)
|
||
process.exit(2)
|
||
}
|
||
}
|
||
|
||
async function runAdapters(rawArgs: string[]): Promise<void> {
|
||
// adapters 模式的参数解析独立于 server/cli —— 这里只接受 --feishu /
|
||
// --telegram 选择启用哪个适配器,再加可选的 --app-root(透传给
|
||
// adapters/common/config.ts 内的 process.env 读取)。
|
||
let appRoot: string | null = process.env.CLAUDE_APP_ROOT ?? null
|
||
let enableFeishu = false
|
||
let enableTelegram = false
|
||
|
||
for (let i = 0; i < rawArgs.length; i++) {
|
||
const arg = rawArgs[i]
|
||
if (arg === '--app-root') {
|
||
appRoot = rawArgs[i + 1] ?? null
|
||
i += 1
|
||
continue
|
||
}
|
||
if (arg === '--feishu') {
|
||
enableFeishu = true
|
||
continue
|
||
}
|
||
if (arg === '--telegram') {
|
||
enableTelegram = true
|
||
continue
|
||
}
|
||
console.warn(`claude-sidecar adapters: ignoring unknown arg "${arg}"`)
|
||
}
|
||
|
||
if (!enableFeishu && !enableTelegram) {
|
||
console.error(
|
||
'claude-sidecar adapters: must enable at least one of --feishu / --telegram',
|
||
)
|
||
process.exit(2)
|
||
}
|
||
|
||
if (appRoot) {
|
||
process.env.CLAUDE_APP_ROOT = appRoot
|
||
}
|
||
process.env.CALLER_DIR ||= process.cwd()
|
||
|
||
await import('../../preload.ts')
|
||
|
||
// 在 import adapter 之前先用同一份 loadConfig() 检查凭据。adapter 的
|
||
// top-level 代码里已经有 if (!cred) process.exit(1),但那会把整个
|
||
// 进程拖死 —— 包括另一个本来正常的 adapter。这里提前 gate 一下,
|
||
// 缺凭据的 adapter 直接跳过、不 import。
|
||
const { loadConfig } = await import('../../adapters/common/config.ts')
|
||
const config = loadConfig()
|
||
|
||
let started = 0
|
||
|
||
if (enableFeishu) {
|
||
if (!config.feishu.appId || !config.feishu.appSecret) {
|
||
console.warn(
|
||
'[claude-sidecar] --feishu requested but FEISHU_APP_ID / FEISHU_APP_SECRET missing in env or ~/.claude/adapters.json — skipping',
|
||
)
|
||
} else {
|
||
console.log('[claude-sidecar] starting Feishu adapter')
|
||
// 副作用 import:feishu/index.ts 顶层会自动 new WSClient + start()
|
||
await import('../../adapters/feishu/index.ts')
|
||
started += 1
|
||
}
|
||
}
|
||
|
||
if (enableTelegram) {
|
||
if (!config.telegram.botToken) {
|
||
console.warn(
|
||
'[claude-sidecar] --telegram requested but TELEGRAM_BOT_TOKEN missing in env or ~/.claude/adapters.json — skipping',
|
||
)
|
||
} else {
|
||
console.log('[claude-sidecar] starting Telegram adapter')
|
||
// 副作用 import:telegram/index.ts 顶层会自动 bot.start()
|
||
await import('../../adapters/telegram/index.ts')
|
||
started += 1
|
||
}
|
||
}
|
||
|
||
if (started === 0) {
|
||
console.error(
|
||
'[claude-sidecar] no adapter could be started — check credentials in env or ~/.claude/adapters.json',
|
||
)
|
||
process.exit(1)
|
||
}
|
||
|
||
// 让进程保持存活:两个 adapter 都通过 long-lived WebSocket(Lark WSClient
|
||
// / grammY long-polling)持有 event loop,自然不会退出。这里不需要额外
|
||
// setInterval 兜底。两个 adapter 自己注册的 SIGINT handler 都会触发。
|
||
}
|