mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
Introduce the Electron desktop shell alongside the existing React renderer and local Bun server boundary. The migration keeps the DesktopHost contract explicit across Tauri, Electron, and browser runtimes while adding Electron main/preload services for dialogs, shell, notifications, updates, tray/window lifecycle, terminal, preview WebContentsView, app mode, and release/package validation.
The commit also carries the latest local main desktop command updates, including agent slash entries and hidden-by-default markdown thinking details, so the packaged Electron build matches the current main UX surface.
Constraint: React renderer, local Bun server, REST/WebSocket, and sidecar boundaries must remain reusable during the migration
Constraint: macOS dev packages are ad-hoc signed and cannot prove Developer ID notarization or Gatekeeper release launch
Rejected: Browser-only smoke validation | it cannot exercise native dialogs, keychain prompts, notification behavior, or packaged app startup
Confidence: medium
Scope-risk: broad
Directive: Do not remove Tauri host support until signed Electron release artifacts pass native OS smoke on macOS, Windows, and Linux
Tested: bun run check:desktop
Tested: cd desktop && bun run check:electron
Tested: CSC_IDENTITY_AUTO_DISCOVERY=false bun run electron📦dir
Tested: bun run test:package-smoke --platform macos --package-kind dir --artifacts-dir desktop/build-artifacts/electron
Tested: Computer Use read packaged Electron app window at desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app
Not-tested: Developer ID signed/notarized Gatekeeper launch
Not-tested: Real OS notification click-to-session action
Not-tested: Windows and Linux packaged app smoke on real hosts
185 lines
6.4 KiB
TypeScript
185 lines
6.4 KiB
TypeScript
import { mkdir } from 'node:fs/promises'
|
||
import path from 'node:path'
|
||
|
||
const desktopRoot = path.resolve(import.meta.dir, '..')
|
||
const repoRoot = path.resolve(desktopRoot, '..')
|
||
const binariesDir = path.join(desktopRoot, 'src-tauri', 'binaries')
|
||
|
||
const targetTriple =
|
||
process.env.SIDECAR_TARGET_TRIPLE ||
|
||
process.env.TAURI_ENV_TARGET_TRIPLE ||
|
||
process.env.CARGO_BUILD_TARGET ||
|
||
(await detectHostTriple())
|
||
|
||
const bunTarget = mapTargetTripleToBun(targetTriple)
|
||
|
||
// 编译前先扫一遍 src/ 把所有缺失的 ant-internal 模块在磁盘上 stub 出来。
|
||
// 见 desktop/scripts/scan-missing-imports.ts。
|
||
console.log('[build-sidecars] scanning for missing imports...')
|
||
const scanProc = Bun.spawn(
|
||
['bun', 'run', path.join(desktopRoot, 'scripts/scan-missing-imports.ts')],
|
||
{ cwd: repoRoot, stdout: 'inherit', stderr: 'inherit' },
|
||
)
|
||
const scanExit = await scanProc.exited
|
||
if (scanExit !== 0) {
|
||
throw new Error(`[build-sidecars] scan-missing-imports failed (exit ${scanExit})`)
|
||
}
|
||
|
||
await mkdir(binariesDir, { recursive: true })
|
||
|
||
// 单一合并 sidecar:server / cli 共享一份 bun runtime + 共享依赖代码。
|
||
// 调用方(Electron sidecar manager / legacy Tauri host / conversationService)
|
||
// 通过第一个 positional 参数选择 'server' 或 'cli' 模式,详见 desktop/sidecars/claude-sidecar.ts。
|
||
await compileExecutable({
|
||
entrypoint: path.join(desktopRoot, 'sidecars/claude-sidecar.ts'),
|
||
outfileBase: path.join(binariesDir, `claude-sidecar-${targetTriple}`),
|
||
productName: 'Claude Code Sidecar',
|
||
bunTarget,
|
||
})
|
||
|
||
console.log(`[build-sidecars] Built desktop sidecar for ${targetTriple} (${bunTarget})`)
|
||
|
||
async function detectHostTriple() {
|
||
const platform = process.platform
|
||
const arch = process.arch
|
||
|
||
if (platform === 'darwin') {
|
||
if (arch === 'arm64') return 'aarch64-apple-darwin'
|
||
if (arch === 'x64') return 'x86_64-apple-darwin'
|
||
}
|
||
|
||
if (platform === 'win32') {
|
||
if (arch === 'x64') return 'x86_64-pc-windows-msvc'
|
||
if (arch === 'arm64') return 'aarch64-pc-windows-msvc'
|
||
}
|
||
|
||
if (platform === 'linux') {
|
||
if (arch === 'x64') return 'x86_64-unknown-linux-gnu'
|
||
if (arch === 'arm64') return 'aarch64-unknown-linux-gnu'
|
||
}
|
||
|
||
throw new Error(`[build-sidecars] Unsupported host platform/arch: ${platform}/${arch}`)
|
||
}
|
||
|
||
function mapTargetTripleToBun(triple: string) {
|
||
switch (triple) {
|
||
case 'aarch64-apple-darwin':
|
||
return 'bun-darwin-arm64'
|
||
case 'x86_64-apple-darwin':
|
||
return 'bun-darwin-x64'
|
||
case 'x86_64-pc-windows-msvc':
|
||
// Prefer baseline on Windows x64 so older CPUs do not crash before the
|
||
// desktop app can even start the local sidecar process.
|
||
return 'bun-windows-x64-baseline'
|
||
case 'aarch64-pc-windows-msvc':
|
||
return 'bun-windows-arm64'
|
||
case 'x86_64-unknown-linux-gnu':
|
||
return 'bun-linux-x64-baseline'
|
||
case 'aarch64-unknown-linux-gnu':
|
||
return 'bun-linux-arm64'
|
||
case 'x86_64-unknown-linux-musl':
|
||
return 'bun-linux-x64-musl'
|
||
case 'aarch64-unknown-linux-musl':
|
||
return 'bun-linux-arm64-musl'
|
||
default:
|
||
throw new Error(`[build-sidecars] Unsupported target triple: ${triple}`)
|
||
}
|
||
}
|
||
|
||
async function compileExecutable({
|
||
entrypoint,
|
||
outfileBase,
|
||
productName,
|
||
bunTarget,
|
||
}: {
|
||
entrypoint: string
|
||
outfileBase: string
|
||
productName: string
|
||
bunTarget: string
|
||
}) {
|
||
const result = await Bun.build({
|
||
entrypoints: [entrypoint],
|
||
// minify whitespace + identifiers + dead-code 大概能省 5-15% 的二进制大小,
|
||
// 代价是 stack trace 里的函数名变成短名 —— 终端用户场景可接受。
|
||
minify: { whitespace: true, identifiers: true, syntax: true },
|
||
sourcemap: 'none',
|
||
target: 'bun',
|
||
// 可选 npm 包:开 telemetry / 用 sharp 图像 / 用 Bedrock/Vertex 等
|
||
// 替代 provider 时才需要,全部不在顶层 package.json 里。标 external
|
||
// 让 bun build 跳过解析;运行时 import 在没装时自然失败,由 try/catch
|
||
// 或 feature() gate 兜底。
|
||
external: [
|
||
// OpenTelemetry exporters(开 OTEL_* env 时才加载)
|
||
'@opentelemetry/exporter-trace-otlp-grpc',
|
||
'@opentelemetry/exporter-trace-otlp-http',
|
||
'@opentelemetry/exporter-trace-otlp-proto',
|
||
'@opentelemetry/exporter-logs-otlp-grpc',
|
||
'@opentelemetry/exporter-logs-otlp-http',
|
||
'@opentelemetry/exporter-logs-otlp-proto',
|
||
'@opentelemetry/exporter-metrics-otlp-grpc',
|
||
'@opentelemetry/exporter-metrics-otlp-http',
|
||
'@opentelemetry/exporter-metrics-otlp-proto',
|
||
'@opentelemetry/exporter-prometheus',
|
||
// 替代 LLM provider —— 默认不用,用户自装
|
||
'@aws-sdk/client-bedrock',
|
||
'@aws-sdk/client-sts',
|
||
'@anthropic-ai/bedrock-sdk',
|
||
'@anthropic-ai/foundry-sdk',
|
||
'@anthropic-ai/vertex-sdk',
|
||
'@azure/identity',
|
||
// ant-internal / 可选工具
|
||
'@anthropic-ai/mcpb',
|
||
'fflate',
|
||
'sharp',
|
||
'react-devtools-core',
|
||
],
|
||
compile: {
|
||
target: bunTarget,
|
||
outfile: outfileBase,
|
||
autoloadTsconfig: true,
|
||
autoloadPackageJson: true,
|
||
windows: {
|
||
title: productName,
|
||
publisher: 'Claude Code',
|
||
description: productName,
|
||
hideConsole: true,
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!result.success) {
|
||
const logs = result.logs.map((log) => log.message).join('\n')
|
||
throw new Error(`[build-sidecars] Failed to compile ${productName}:\n${logs}`)
|
||
}
|
||
|
||
const outputPath = result.outputs[0]?.path ?? outfileBase
|
||
console.log(`[build-sidecars] ${productName} -> ${outputPath}`)
|
||
|
||
// macOS Apple System Policy (ASP) requires valid code signatures on all
|
||
// executables. Bun-compiled binaries ship with an invalid/empty signature
|
||
// that causes "load code signature error 4" and SIGKILL at launch.
|
||
// Fix: strip the broken signature, then ad-hoc sign.
|
||
if (process.platform === 'darwin') {
|
||
await adHocSignMacBinary(outputPath)
|
||
}
|
||
}
|
||
|
||
async function adHocSignMacBinary(outputPath: string) {
|
||
console.log(`[build-sidecars] ad-hoc signing ${outputPath} for macOS ...`)
|
||
const strip = Bun.spawn(['codesign', '--remove-signature', outputPath], {
|
||
stdout: 'inherit',
|
||
stderr: 'inherit',
|
||
})
|
||
await strip.exited
|
||
|
||
const sign = Bun.spawn(
|
||
['codesign', '--sign', '-', '--force', '--timestamp=none', outputPath],
|
||
{ stdout: 'inherit', stderr: 'inherit' },
|
||
)
|
||
const signExit = await sign.exited
|
||
if (signExit !== 0) {
|
||
throw new Error(`[build-sidecars] ad-hoc codesign failed for ${outputPath} (exit ${signExit})`)
|
||
}
|
||
console.log(`[build-sidecars] ad-hoc signed ${outputPath}`)
|
||
}
|