mirror of
https://github.com/clawdbot/clawdbot.git
synced 2026-01-31 19:37:45 +01:00
fix: bundle node runtime for mac app
This commit is contained in:
@@ -71,6 +71,12 @@ enum GatewayEnvironment {
|
|||||||
return FileManager.default.isExecutableFile(atPath: path) ? path : nil
|
return FileManager.default.isExecutableFile(atPath: path) ? path : nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func bundledNodeExecutable() -> String? {
|
||||||
|
guard let res = Bundle.main.resourceURL else { return nil }
|
||||||
|
let path = res.appendingPathComponent("Relay/node").path
|
||||||
|
return FileManager.default.isExecutableFile(atPath: path) ? path : nil
|
||||||
|
}
|
||||||
|
|
||||||
static func gatewayPort() -> Int {
|
static func gatewayPort() -> Int {
|
||||||
if let raw = ProcessInfo.processInfo.environment["CLAWDBOT_GATEWAY_PORT"] {
|
if let raw = ProcessInfo.processInfo.environment["CLAWDBOT_GATEWAY_PORT"] {
|
||||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
@@ -107,13 +113,15 @@ enum GatewayEnvironment {
|
|||||||
|
|
||||||
if let bundled = self.bundledGatewayExecutable() {
|
if let bundled = self.bundledGatewayExecutable() {
|
||||||
let installed = self.readGatewayVersion(binary: bundled)
|
let installed = self.readGatewayVersion(binary: bundled)
|
||||||
|
let bundledNode = self.bundledNodeExecutable()
|
||||||
|
let bundledNodeVersion = bundledNode.flatMap { self.readNodeVersion(binary: $0) }
|
||||||
if let expected, let installed, !installed.compatible(with: expected) {
|
if let expected, let installed, !installed.compatible(with: expected) {
|
||||||
let message =
|
let message =
|
||||||
"Bundled gateway \(installed.description) is incompatible with app " +
|
"Bundled gateway \(installed.description) is incompatible with app " +
|
||||||
"\(expected.description); rebuild the app bundle."
|
"\(expected.description); rebuild the app bundle."
|
||||||
return GatewayEnvironmentStatus(
|
return GatewayEnvironmentStatus(
|
||||||
kind: .incompatible(found: installed.description, required: expected.description),
|
kind: .incompatible(found: installed.description, required: expected.description),
|
||||||
nodeVersion: nil,
|
nodeVersion: bundledNodeVersion,
|
||||||
gatewayVersion: installed.description,
|
gatewayVersion: installed.description,
|
||||||
requiredGateway: expected.description,
|
requiredGateway: expected.description,
|
||||||
message: message)
|
message: message)
|
||||||
@@ -121,10 +129,10 @@ enum GatewayEnvironment {
|
|||||||
let gatewayVersionText = installed?.description ?? "unknown"
|
let gatewayVersionText = installed?.description ?? "unknown"
|
||||||
return GatewayEnvironmentStatus(
|
return GatewayEnvironmentStatus(
|
||||||
kind: .ok,
|
kind: .ok,
|
||||||
nodeVersion: nil,
|
nodeVersion: bundledNodeVersion,
|
||||||
gatewayVersion: gatewayVersionText,
|
gatewayVersion: gatewayVersionText,
|
||||||
requiredGateway: expected?.description,
|
requiredGateway: expected?.description,
|
||||||
message: "Bundled gateway \(gatewayVersionText) (bun)")
|
message: "Bundled gateway \(gatewayVersionText) (node \(bundledNodeVersion ?? "unknown"))")
|
||||||
}
|
}
|
||||||
|
|
||||||
let projectRoot = CommandResolver.projectRoot()
|
let projectRoot = CommandResolver.projectRoot()
|
||||||
@@ -351,4 +359,26 @@ enum GatewayEnvironment {
|
|||||||
else { return nil }
|
else { return nil }
|
||||||
return Semver.parse(version)
|
return Semver.parse(version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func readNodeVersion(binary: String) -> String? {
|
||||||
|
let process = Process()
|
||||||
|
process.executableURL = URL(fileURLWithPath: binary)
|
||||||
|
process.arguments = ["--version"]
|
||||||
|
process.environment = ["PATH": CommandResolver.preferredPaths().joined(separator: ":")]
|
||||||
|
|
||||||
|
let pipe = Pipe()
|
||||||
|
process.standardOutput = pipe
|
||||||
|
process.standardError = pipe
|
||||||
|
do {
|
||||||
|
try process.run()
|
||||||
|
process.waitUntilExit()
|
||||||
|
let data = pipe.fileHandleForReading.readToEndSafely()
|
||||||
|
let raw = String(data: data, encoding: .utf8)?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.replacingOccurrences(of: "^v", with: "", options: .regularExpression)
|
||||||
|
return raw?.isEmpty == false ? raw : nil
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ Notes:
|
|||||||
- `daemon install` is a no-op when already installed; use `clawdbot daemon install --force` to reinstall (profile/env/path changes).
|
- `daemon install` is a no-op when already installed; use `clawdbot daemon install --force` to reinstall (profile/env/path changes).
|
||||||
|
|
||||||
Bundled mac app:
|
Bundled mac app:
|
||||||
- Clawdbot.app can bundle a bun-compiled gateway binary and install a per-user LaunchAgent labeled `com.clawdbot.gateway`.
|
- Clawdbot.app can bundle a Node-based gateway relay and install a per-user LaunchAgent labeled `com.clawdbot.gateway`.
|
||||||
- To stop it cleanly, use `clawdbot daemon stop` (or `launchctl bootout gui/$UID/com.clawdbot.gateway`).
|
- To stop it cleanly, use `clawdbot daemon stop` (or `launchctl bootout gui/$UID/com.clawdbot.gateway`).
|
||||||
- To restart, use `clawdbot daemon restart` (or `launchctl kickstart -k gui/$UID/com.clawdbot.gateway`).
|
- To restart, use `clawdbot daemon restart` (or `launchctl kickstart -k gui/$UID/com.clawdbot.gateway`).
|
||||||
- `launchctl` only works if the LaunchAgent is installed; otherwise use `clawdbot daemon install` first.
|
- `launchctl` only works if the LaunchAgent is installed; otherwise use `clawdbot daemon install` first.
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ kill -9 <PID> # last resort
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Fix 3: Check embedded gateway**
|
**Fix 3: Check embedded gateway**
|
||||||
Ensure the gateway relay was properly bundled. Run [`./scripts/package-mac-app.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/package-mac-app.sh) and ensure `bun` is installed.
|
Ensure the gateway relay was properly bundled. Run [`./scripts/package-mac-app.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/package-mac-app.sh) and ensure Node is available (the script downloads a bundled runtime by default).
|
||||||
|
|
||||||
## Debug Mode
|
## Debug Mode
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,43 @@
|
|||||||
---
|
---
|
||||||
summary: "Bundled bun gateway: packaging, launchd, signing, and bytecode"
|
summary: "Bundled Node gateway: packaging, launchd, signing, and bundling"
|
||||||
read_when:
|
read_when:
|
||||||
- Packaging Clawdbot.app
|
- Packaging Clawdbot.app
|
||||||
- Debugging the bundled gateway binary
|
- Debugging the bundled gateway binary
|
||||||
- Changing bun build flags or codesigning
|
- Changing relay bundling flags or codesigning
|
||||||
---
|
---
|
||||||
|
|
||||||
# Bundled bun Gateway (macOS)
|
# Bundled Node Gateway (macOS)
|
||||||
|
|
||||||
Goal: ship **Clawdbot.app** with a self-contained relay binary that can run both the CLI and the Gateway daemon. No global `npm install -g clawdbot`, no system Node requirement.
|
Goal: ship **Clawdbot.app** with a self-contained relay that can run the CLI and
|
||||||
|
Gateway daemon. No global `npm install -g clawdbot`, no system Node requirement.
|
||||||
|
|
||||||
## What gets bundled
|
## What gets bundled
|
||||||
|
|
||||||
App bundle layout:
|
App bundle layout:
|
||||||
|
|
||||||
|
- `Clawdbot.app/Contents/Resources/Relay/node`
|
||||||
|
- Node runtime binary (downloaded during packaging, stripped for size)
|
||||||
|
- `Clawdbot.app/Contents/Resources/Relay/dist/`
|
||||||
|
- Compiled CLI/gateway payload from `pnpm exec tsc`
|
||||||
|
- `Clawdbot.app/Contents/Resources/Relay/node_modules/`
|
||||||
|
- Production dependencies staged via `pnpm deploy --prod --no-optional --legacy`
|
||||||
- `Clawdbot.app/Contents/Resources/Relay/clawdbot`
|
- `Clawdbot.app/Contents/Resources/Relay/clawdbot`
|
||||||
- bun `--compile` relay executable built from `dist/macos/relay.js`
|
- Wrapper script that execs the bundled Node + dist entrypoint
|
||||||
- Supports:
|
|
||||||
- `clawdbot …` (CLI)
|
|
||||||
- `clawdbot gateway …` (LaunchAgent daemon)
|
|
||||||
- `Clawdbot.app/Contents/Resources/Relay/package.json`
|
- `Clawdbot.app/Contents/Resources/Relay/package.json`
|
||||||
- tiny “p runtime compatibility” file (see below)
|
- tiny “Pi runtime compatibility” file (see below, includes `"type": "module"`)
|
||||||
|
- `Clawdbot.app/Contents/Resources/Relay/skills/`
|
||||||
|
- Bundled skills payload (required for Pi tools)
|
||||||
- `Clawdbot.app/Contents/Resources/Relay/theme/`
|
- `Clawdbot.app/Contents/Resources/Relay/theme/`
|
||||||
- p TUI theme payload (optional, but strongly recommended)
|
- Pi TUI theme payload (optional, but strongly recommended)
|
||||||
|
- `Clawdbot.app/Contents/Resources/Relay/a2ui/`
|
||||||
|
- A2UI host assets (served by the gateway)
|
||||||
|
- `Clawdbot.app/Contents/Resources/Relay/control-ui/`
|
||||||
|
- Control UI build output (served by the gateway)
|
||||||
|
|
||||||
Why the sidecar files matter:
|
Why the sidecar files matter:
|
||||||
- The embedded p runtime detects “bun binary mode” and then looks for `package.json` + `theme/` **next to `process.execPath`** (i.e. next to `clawdbot`).
|
- The embedded Pi runtime detects “bundled relay mode” and then looks for
|
||||||
- So even if bun can embed assets, the runtime expects filesystem paths. Keep the sidecar files.
|
`package.json` + `theme/` **next to `process.execPath`** (i.e. next to
|
||||||
|
`node`). Keep the sidecar files.
|
||||||
|
|
||||||
## Build pipeline
|
## Build pipeline
|
||||||
|
|
||||||
@@ -36,18 +47,18 @@ Packaging script:
|
|||||||
It builds:
|
It builds:
|
||||||
- TS: `pnpm exec tsc`
|
- TS: `pnpm exec tsc`
|
||||||
- Swift app + helper: `swift build …`
|
- Swift app + helper: `swift build …`
|
||||||
- bun relay: `bun build dist/macos/relay.js --compile --bytecode …`
|
- Relay payload: `pnpm deploy --prod --no-optional --legacy` + copy `dist/`
|
||||||
|
- Node runtime: downloads the latest Node release (override via `NODE_VERSION`)
|
||||||
|
|
||||||
Important bundler flags:
|
Important knobs:
|
||||||
- `--compile`: produces a standalone executable
|
- `NODE_VERSION=22.12.0` → pin a specific Node version
|
||||||
- `--bytecode`: reduces startup time / parsing overhead (works here)
|
- `NODE_DIST_MIRROR=…` → mirror for downloads (default: nodejs.org)
|
||||||
- externals:
|
- `STRIP_NODE=0` → keep symbols (default strips to reduce size)
|
||||||
- `-e electron`
|
- `BUNDLED_RUNTIME=bun` → switch the relay build back to Bun (`bun --compile`)
|
||||||
- Reason: avoid bundling Electron stubs in the relay binary
|
|
||||||
|
|
||||||
Version injection:
|
Version injection:
|
||||||
- `--define "__CLAWDBOT_VERSION__=\"<pkg version>\""`
|
- The relay wrapper exports `CLAWDBOT_BUNDLED_VERSION` so `--version` works
|
||||||
- The relay honors `__CLAWDBOT_VERSION__` / `CLAWDBOT_BUNDLED_VERSION` so `--version` doesn’t depend on reading `package.json` at runtime.
|
without reading `package.json` at runtime.
|
||||||
|
|
||||||
## Launchd (Gateway as LaunchAgent)
|
## Launchd (Gateway as LaunchAgent)
|
||||||
|
|
||||||
@@ -63,40 +74,26 @@ Manager:
|
|||||||
Behavior:
|
Behavior:
|
||||||
- “Clawdbot Active” enables/disables the LaunchAgent.
|
- “Clawdbot Active” enables/disables the LaunchAgent.
|
||||||
- App quit does **not** stop the gateway (launchd keeps it alive).
|
- App quit does **not** stop the gateway (launchd keeps it alive).
|
||||||
- CLI install (`clawdbot daemon install`) writes the same LaunchAgent; `clawdbot daemon install --force` rewrites it.
|
- CLI install (`clawdbot daemon install`) writes the same LaunchAgent; `--force` rewrites it.
|
||||||
- `clawdbot doctor` audits the LaunchAgent config and can update it to current defaults.
|
|
||||||
|
|
||||||
Logging:
|
Logging:
|
||||||
- launchd stdout/err: `/tmp/clawdbot/clawdbot-gateway.log`
|
- launchd stdout/err: `/tmp/clawdbot/clawdbot-gateway.log`
|
||||||
|
|
||||||
Default LaunchAgent env:
|
Default LaunchAgent env:
|
||||||
- `CLAWDBOT_IMAGE_BACKEND=sips` (avoid sharp native addon under bun)
|
- `CLAWDBOT_IMAGE_BACKEND=sips` (avoid sharp native addon inside the bundle)
|
||||||
|
|
||||||
## Codesigning (hardened runtime + bun)
|
## Codesigning (hardened runtime + Node)
|
||||||
|
|
||||||
Symptom (when mis-signed):
|
Node uses JIT. The bundled runtime is signed with:
|
||||||
- `Ran out of executable memory …` on launch
|
- `com.apple.security.cs.allow-jit`
|
||||||
|
- `com.apple.security.cs.allow-unsigned-executable-memory`
|
||||||
|
|
||||||
Fix:
|
This is applied by `scripts/codesign-mac-app.sh`.
|
||||||
- The bun executable needs JIT-ish permissions under hardened runtime.
|
|
||||||
- `scripts/codesign-mac-app.sh` signs `Relay/clawdbot` with:
|
|
||||||
- `com.apple.security.cs.allow-jit`
|
|
||||||
- `com.apple.security.cs.allow-unsigned-executable-memory`
|
|
||||||
|
|
||||||
## Image processing under bun
|
## Image processing
|
||||||
|
|
||||||
Problem:
|
To avoid shipping native `sharp` addons inside the bundle, the gateway defaults
|
||||||
- bun can’t load some native Node addons like `sharp` (and we don’t want to ship native addon trees for the gateway).
|
to `/usr/bin/sips` for image ops when run from the app (via launchd env + wrapper).
|
||||||
|
|
||||||
Solution:
|
|
||||||
- Image operations prefer `/usr/bin/sips` on macOS (especially under bun).
|
|
||||||
- When running in Node/dev, `sharp` is used when available.
|
|
||||||
- This affects inbound/outbound media, screenshots, and tool image sanitization.
|
|
||||||
|
|
||||||
## Browser control server
|
|
||||||
|
|
||||||
The Gateway starts the browser control server (loopback only) from the relay daemon process,
|
|
||||||
so the relay binary includes Playwright deps.
|
|
||||||
|
|
||||||
## Tests / smoke checks
|
## Tests / smoke checks
|
||||||
|
|
||||||
@@ -115,17 +112,3 @@ Then, in another shell:
|
|||||||
```bash
|
```bash
|
||||||
pnpm -s clawdbot gateway call health --url ws://127.0.0.1:18999 --timeout 3000
|
pnpm -s clawdbot gateway call health --url ws://127.0.0.1:18999 --timeout 3000
|
||||||
```
|
```
|
||||||
|
|
||||||
## Repo hygiene
|
|
||||||
|
|
||||||
Bun may leave dotfiles like `*.bun-build` in the repo root or subfolders.
|
|
||||||
- These are ignored via `.gitignore` (`*.bun-build`).
|
|
||||||
|
|
||||||
## DMG styling (human installer)
|
|
||||||
|
|
||||||
`scripts/create-dmg.sh` styles the DMG via Finder AppleScript.
|
|
||||||
|
|
||||||
Rules of thumb:
|
|
||||||
- Use a **72dpi** background image that matches the Finder window size in points.
|
|
||||||
- Preferred asset: `assets/dmg-background-small.png` (**500×320**).
|
|
||||||
- Default icon positions: app `{125,160}`, Applications `{375,160}`.
|
|
||||||
|
|||||||
@@ -13,10 +13,7 @@ Before building the app, ensure you have the following installed:
|
|||||||
|
|
||||||
1. **Xcode 26.2+**: Required for Swift development.
|
1. **Xcode 26.2+**: Required for Swift development.
|
||||||
2. **Node.js & pnpm**: Required for the gateway and CLI components.
|
2. **Node.js & pnpm**: Required for the gateway and CLI components.
|
||||||
3. **Bun**: Required to package the embedded gateway relay.
|
3. **Node**: Required to package the embedded gateway relay (the script can download a bundled runtime).
|
||||||
```bash
|
|
||||||
curl -fsSL https://bun.sh/install | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
## 1. Initialize Submodules
|
## 1. Initialize Submodules
|
||||||
|
|
||||||
@@ -42,6 +39,8 @@ To build the macOS app and package it into `dist/Clawdbot.app`, run:
|
|||||||
./scripts/package-mac-app.sh
|
./scripts/package-mac-app.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Use `BUNDLED_RUNTIME=node|bun` to switch the embedded gateway runtime (default: node).
|
||||||
|
|
||||||
If you don't have an Apple Developer ID certificate, the script will automatically use **ad-hoc signing** (`-`).
|
If you don't have an Apple Developer ID certificate, the script will automatically use **ad-hoc signing** (`-`).
|
||||||
|
|
||||||
> **Note**: Ad-hoc signed apps may trigger security prompts. If the app crashes immediately with "Abort trap 6", see the [Troubleshooting](#troubleshooting) section.
|
> **Note**: Ad-hoc signed apps may trigger security prompts. If the app crashes immediately with "Abort trap 6", see the [Troubleshooting](#troubleshooting) section.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ This app is usually built from [`scripts/package-mac-app.sh`](https://github.com
|
|||||||
- calls [`scripts/codesign-mac-app.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/codesign-mac-app.sh) to sign the main binary, bundled CLI, and app bundle so macOS treats each rebuild as the same signed bundle and keeps TCC permissions (notifications, accessibility, screen recording, mic, speech). For stable permissions, use a real signing identity; ad-hoc is opt-in and fragile (see [`docs/mac/permissions.md`](/platforms/mac/permissions)).
|
- calls [`scripts/codesign-mac-app.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/codesign-mac-app.sh) to sign the main binary, bundled CLI, and app bundle so macOS treats each rebuild as the same signed bundle and keeps TCC permissions (notifications, accessibility, screen recording, mic, speech). For stable permissions, use a real signing identity; ad-hoc is opt-in and fragile (see [`docs/mac/permissions.md`](/platforms/mac/permissions)).
|
||||||
- uses `CODESIGN_TIMESTAMP=auto` by default; it enables trusted timestamps for Developer ID signatures. Set `CODESIGN_TIMESTAMP=off` to skip timestamping (offline debug builds).
|
- uses `CODESIGN_TIMESTAMP=auto` by default; it enables trusted timestamps for Developer ID signatures. Set `CODESIGN_TIMESTAMP=off` to skip timestamping (offline debug builds).
|
||||||
- inject build metadata into Info.plist: `ClawdbotBuildTimestamp` (UTC) and `ClawdbotGitCommit` (short hash) so the About pane can show build, git, and debug/release channel.
|
- inject build metadata into Info.plist: `ClawdbotBuildTimestamp` (UTC) and `ClawdbotGitCommit` (short hash) so the About pane can show build, git, and debug/release channel.
|
||||||
- **Packaging requires Bun**: The embedded gateway relay is compiled using `bun`. Ensure it is installed (`curl -fsSL https://bun.sh/install | bash`).
|
- **Packaging requires Node**: The embedded gateway relay is bundled with Node. Ensure Node is available for the packaging script (or set `NODE_VERSION` to pin the download).
|
||||||
- reads `SIGN_IDENTITY` from the environment. Add `export SIGN_IDENTITY="Apple Development: Your Name (TEAMID)"` (or your Developer ID Application cert) to your shell rc to always sign with your cert. Ad-hoc signing requires explicit opt-in via `ALLOW_ADHOC_SIGNING=1` or `SIGN_IDENTITY="-"` (not recommended for permission testing).
|
- reads `SIGN_IDENTITY` from the environment. Add `export SIGN_IDENTITY="Apple Development: Your Name (TEAMID)"` (or your Developer ID Application cert) to your shell rc to always sign with your cert. Ad-hoc signing requires explicit opt-in via `ALLOW_ADHOC_SIGNING=1` or `SIGN_IDENTITY="-"` (not recommended for permission testing).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|||||||
@@ -88,10 +88,11 @@ Safety:
|
|||||||
- `cd apps/macos && swift build`
|
- `cd apps/macos && swift build`
|
||||||
- `swift run Clawdbot` (or Xcode)
|
- `swift run Clawdbot` (or Xcode)
|
||||||
- Package app + CLI: `scripts/package-mac-app.sh`
|
- Package app + CLI: `scripts/package-mac-app.sh`
|
||||||
|
- Switch bundled gateway runtime with `BUNDLED_RUNTIME=node|bun` (default: node).
|
||||||
|
|
||||||
## Related docs
|
## Related docs
|
||||||
|
|
||||||
- [Gateway runbook](/gateway)
|
- [Gateway runbook](/gateway)
|
||||||
- [Bundled bun Gateway](/platforms/mac/bun)
|
- [Bundled Node Gateway](/platforms/mac/bun)
|
||||||
- [macOS permissions](/platforms/mac/permissions)
|
- [macOS permissions](/platforms/mac/permissions)
|
||||||
- [Canvas](/platforms/mac/canvas)
|
- [Canvas](/platforms/mac/canvas)
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
|
|||||||
- [macOS remote](https://docs.clawd.bot/platforms/mac/remote)
|
- [macOS remote](https://docs.clawd.bot/platforms/mac/remote)
|
||||||
- [macOS signing](https://docs.clawd.bot/platforms/mac/signing)
|
- [macOS signing](https://docs.clawd.bot/platforms/mac/signing)
|
||||||
- [macOS release](https://docs.clawd.bot/platforms/mac/release)
|
- [macOS release](https://docs.clawd.bot/platforms/mac/release)
|
||||||
- [macOS bun gateway](https://docs.clawd.bot/platforms/mac/bun)
|
- [macOS bundled gateway (Node)](https://docs.clawd.bot/platforms/mac/bun)
|
||||||
- [macOS XPC](https://docs.clawd.bot/platforms/mac/xpc)
|
- [macOS XPC](https://docs.clawd.bot/platforms/mac/xpc)
|
||||||
- [macOS skills](https://docs.clawd.bot/platforms/mac/skills)
|
- [macOS skills](https://docs.clawd.bot/platforms/mac/skills)
|
||||||
- [macOS Peekaboo](https://docs.clawd.bot/platforms/mac/peekaboo)
|
- [macOS Peekaboo](https://docs.clawd.bot/platforms/mac/peekaboo)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ TIMESTAMP_MODE="${CODESIGN_TIMESTAMP:-auto}"
|
|||||||
ENT_TMP_BASE=$(mktemp -t clawdbot-entitlements-base.XXXXXX)
|
ENT_TMP_BASE=$(mktemp -t clawdbot-entitlements-base.XXXXXX)
|
||||||
ENT_TMP_APP=$(mktemp -t clawdbot-entitlements-app.XXXXXX)
|
ENT_TMP_APP=$(mktemp -t clawdbot-entitlements-app.XXXXXX)
|
||||||
ENT_TMP_APP_BASE=$(mktemp -t clawdbot-entitlements-app-base.XXXXXX)
|
ENT_TMP_APP_BASE=$(mktemp -t clawdbot-entitlements-app-base.XXXXXX)
|
||||||
ENT_TMP_BUN=$(mktemp -t clawdbot-entitlements-bun.XXXXXX)
|
ENT_TMP_RUNTIME=$(mktemp -t clawdbot-entitlements-runtime.XXXXXX)
|
||||||
|
|
||||||
if [ ! -d "$APP_BUNDLE" ]; then
|
if [ ! -d "$APP_BUNDLE" ]; then
|
||||||
echo "App bundle not found: $APP_BUNDLE" >&2
|
echo "App bundle not found: $APP_BUNDLE" >&2
|
||||||
@@ -150,7 +150,7 @@ cat > "$ENT_TMP_APP_BASE" <<'PLIST'
|
|||||||
</plist>
|
</plist>
|
||||||
PLIST
|
PLIST
|
||||||
|
|
||||||
cat > "$ENT_TMP_BUN" <<'PLIST'
|
cat > "$ENT_TMP_RUNTIME" <<'PLIST'
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
@@ -215,8 +215,11 @@ if [ -d "$APP_BUNDLE/Contents/Resources/Relay" ]; then
|
|||||||
find "$APP_BUNDLE/Contents/Resources/Relay" -type f \( -name "*.node" -o -name "*.dylib" \) -print0 | while IFS= read -r -d '' f; do
|
find "$APP_BUNDLE/Contents/Resources/Relay" -type f \( -name "*.node" -o -name "*.dylib" \) -print0 | while IFS= read -r -d '' f; do
|
||||||
echo "Signing gateway payload: $f"; sign_item "$f" "$ENT_TMP_BASE"
|
echo "Signing gateway payload: $f"; sign_item "$f" "$ENT_TMP_BASE"
|
||||||
done
|
done
|
||||||
|
if [ -f "$APP_BUNDLE/Contents/Resources/Relay/node" ]; then
|
||||||
|
echo "Signing embedded node"; sign_item "$APP_BUNDLE/Contents/Resources/Relay/node" "$ENT_TMP_RUNTIME"
|
||||||
|
fi
|
||||||
if [ -f "$APP_BUNDLE/Contents/Resources/Relay/clawdbot" ]; then
|
if [ -f "$APP_BUNDLE/Contents/Resources/Relay/clawdbot" ]; then
|
||||||
echo "Signing embedded relay"; sign_item "$APP_BUNDLE/Contents/Resources/Relay/clawdbot" "$ENT_TMP_BUN"
|
echo "Signing embedded relay wrapper"; sign_plain_item "$APP_BUNDLE/Contents/Resources/Relay/clawdbot"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -246,5 +249,5 @@ fi
|
|||||||
# Finally sign the bundle
|
# Finally sign the bundle
|
||||||
sign_item "$APP_BUNDLE" "$APP_ENTITLEMENTS"
|
sign_item "$APP_BUNDLE" "$APP_ENTITLEMENTS"
|
||||||
|
|
||||||
rm -f "$ENT_TMP_BASE" "$ENT_TMP_APP_BASE" "$ENT_TMP_APP" "$ENT_TMP_BUN"
|
rm -f "$ENT_TMP_BASE" "$ENT_TMP_APP_BASE" "$ENT_TMP_APP" "$ENT_TMP_RUNTIME"
|
||||||
echo "Codesign complete for $APP_BUNDLE"
|
echo "Codesign complete for $APP_BUNDLE"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ if [[ "${BUILD_ARCHS_VALUE}" == "all" ]]; then
|
|||||||
fi
|
fi
|
||||||
IFS=' ' read -r -a BUILD_ARCHS <<< "$BUILD_ARCHS_VALUE"
|
IFS=' ' read -r -a BUILD_ARCHS <<< "$BUILD_ARCHS_VALUE"
|
||||||
PRIMARY_ARCH="${BUILD_ARCHS[0]}"
|
PRIMARY_ARCH="${BUILD_ARCHS[0]}"
|
||||||
|
BUNDLED_RUNTIME="${BUNDLED_RUNTIME:-node}"
|
||||||
SPARKLE_PUBLIC_ED_KEY="${SPARKLE_PUBLIC_ED_KEY:-AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=}"
|
SPARKLE_PUBLIC_ED_KEY="${SPARKLE_PUBLIC_ED_KEY:-AGCY8w5vHirVfGGDGc8Szc5iuOqupZSh9pMj/Qs67XI=}"
|
||||||
SPARKLE_FEED_URL="${SPARKLE_FEED_URL:-https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml}"
|
SPARKLE_FEED_URL="${SPARKLE_FEED_URL:-https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml}"
|
||||||
AUTO_CHECKS=true
|
AUTO_CHECKS=true
|
||||||
@@ -136,6 +137,116 @@ build_relay_binary() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolve_node_version() {
|
||||||
|
if [[ -n "${NODE_VERSION:-}" ]]; then
|
||||||
|
echo "${NODE_VERSION#v}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local mirror="${NODE_DIST_MIRROR:-https://nodejs.org/dist}"
|
||||||
|
local latest
|
||||||
|
if latest="$(/usr/bin/curl -fsSL "$mirror/index.tab" 2>/dev/null | /usr/bin/awk 'NR==2 {print $1}')" && [[ -n "$latest" ]]; then
|
||||||
|
echo "${latest#v}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v node >/dev/null 2>&1; then
|
||||||
|
node -p "process.versions.node"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "22.12.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
node_dist_filename() {
|
||||||
|
local version="$1"
|
||||||
|
local arch="$2"
|
||||||
|
local node_arch="$arch"
|
||||||
|
if [[ "$arch" == "x86_64" ]]; then
|
||||||
|
node_arch="x64"
|
||||||
|
fi
|
||||||
|
echo "node-v${version}-darwin-${node_arch}.tar.gz"
|
||||||
|
}
|
||||||
|
|
||||||
|
download_node_binary() {
|
||||||
|
local version="$1"
|
||||||
|
local arch="$2"
|
||||||
|
local out="$3"
|
||||||
|
local mirror="${NODE_DIST_MIRROR:-https://nodejs.org/dist}"
|
||||||
|
local tarball
|
||||||
|
tarball="$(node_dist_filename "$version" "$arch")"
|
||||||
|
|
||||||
|
local tmp_dir
|
||||||
|
tmp_dir="$(mktemp -d)"
|
||||||
|
local url="$mirror/v${version}/${tarball}"
|
||||||
|
echo "⬇️ Downloading Node ${version} (${arch})"
|
||||||
|
/usr/bin/curl -fsSL "$url" -o "$tmp_dir/node.tgz"
|
||||||
|
|
||||||
|
/usr/bin/tar -xzf "$tmp_dir/node.tgz" -C "$tmp_dir"
|
||||||
|
local node_arch="$arch"
|
||||||
|
if [[ "$arch" == "x86_64" ]]; then
|
||||||
|
node_arch="x64"
|
||||||
|
fi
|
||||||
|
local node_src="$tmp_dir/node-v${version}-darwin-${node_arch}/bin/node"
|
||||||
|
if [[ ! -f "$node_src" ]]; then
|
||||||
|
echo "ERROR: Node binary missing in $tarball" >&2
|
||||||
|
rm -rf "$tmp_dir"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cp "$node_src" "$out"
|
||||||
|
chmod +x "$out"
|
||||||
|
rm -rf "$tmp_dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
stage_relay_payload() {
|
||||||
|
local relay_dir="$1"
|
||||||
|
|
||||||
|
if [[ "${SKIP_RELAY_DEPS:-0}" != "1" ]]; then
|
||||||
|
local stage_dir="$relay_dir/.relay-deploy"
|
||||||
|
rm -rf "$stage_dir"
|
||||||
|
mkdir -p "$stage_dir"
|
||||||
|
echo "📦 Staging relay dependencies (pnpm deploy --prod --no-optional --legacy)"
|
||||||
|
(cd "$ROOT_DIR" && pnpm --filter . deploy "$stage_dir" --prod --no-optional --legacy)
|
||||||
|
rm -rf "$relay_dir/node_modules"
|
||||||
|
cp -a "$stage_dir/node_modules" "$relay_dir/node_modules"
|
||||||
|
rm -rf "$stage_dir"
|
||||||
|
else
|
||||||
|
echo "📦 Skipping relay dependency staging (SKIP_RELAY_DEPS=1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "📦 Copying relay dist payload"
|
||||||
|
rm -rf "$relay_dir/dist"
|
||||||
|
cp -R "$ROOT_DIR/dist" "$relay_dir/dist"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_relay_wrapper() {
|
||||||
|
local relay_dir="$1"
|
||||||
|
local wrapper="$relay_dir/clawdbot"
|
||||||
|
cat > "$wrapper" <<SH
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
DIR="\$(cd "\$(dirname "\$0")" && pwd)"
|
||||||
|
NODE="\$DIR/node"
|
||||||
|
REL="\$DIR/dist/macos/relay.js"
|
||||||
|
export CLAWDBOT_BUNDLED_VERSION="\${CLAWDBOT_BUNDLED_VERSION:-$PKG_VERSION}"
|
||||||
|
export CLAWDBOT_IMAGE_BACKEND="\${CLAWDBOT_IMAGE_BACKEND:-sips}"
|
||||||
|
NODE_PATH="\$DIR/node_modules\${NODE_PATH:+:\$NODE_PATH}"
|
||||||
|
export NODE_PATH
|
||||||
|
exec "\$NODE" "\$REL" "\$@"
|
||||||
|
SH
|
||||||
|
chmod +x "$wrapper"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_bundled_runtime() {
|
||||||
|
case "$BUNDLED_RUNTIME" in
|
||||||
|
node|bun) return 0 ;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Unsupported BUNDLED_RUNTIME=$BUNDLED_RUNTIME (use node|bun)" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
echo "📦 Ensuring deps (pnpm install)"
|
echo "📦 Ensuring deps (pnpm install)"
|
||||||
(cd "$ROOT_DIR" && pnpm install --no-frozen-lockfile --config.node-linker=hoisted)
|
(cd "$ROOT_DIR" && pnpm install --no-frozen-lockfile --config.node-linker=hoisted)
|
||||||
if [[ "${SKIP_TSC:-0}" != "1" ]]; then
|
if [[ "${SKIP_TSC:-0}" != "1" ]]; then
|
||||||
@@ -249,31 +360,58 @@ fi
|
|||||||
RELAY_DIR="$APP_ROOT/Contents/Resources/Relay"
|
RELAY_DIR="$APP_ROOT/Contents/Resources/Relay"
|
||||||
|
|
||||||
if [[ "${SKIP_GATEWAY_PACKAGE:-0}" != "1" ]]; then
|
if [[ "${SKIP_GATEWAY_PACKAGE:-0}" != "1" ]]; then
|
||||||
if ! command -v bun >/dev/null 2>&1; then
|
validate_bundled_runtime
|
||||||
echo "ERROR: bun missing. Install bun to package the embedded gateway." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "🧰 Building bundled relay (bun --compile)"
|
|
||||||
mkdir -p "$RELAY_DIR"
|
mkdir -p "$RELAY_DIR"
|
||||||
RELAY_OUT="$RELAY_DIR/clawdbot"
|
RELAY_CMD="$RELAY_DIR/clawdbot"
|
||||||
RELAY_BUILD_DIR="$RELAY_DIR/.relay-build"
|
|
||||||
rm -rf "$RELAY_BUILD_DIR"
|
if [[ "$BUNDLED_RUNTIME" == "bun" ]]; then
|
||||||
mkdir -p "$RELAY_BUILD_DIR"
|
if ! command -v bun >/dev/null 2>&1; then
|
||||||
for arch in "${BUILD_ARCHS[@]}"; do
|
echo "ERROR: bun missing. Install bun or set BUNDLED_RUNTIME=node." >&2
|
||||||
RELAY_ARCH_OUT="$RELAY_BUILD_DIR/clawdbot-$arch"
|
exit 1
|
||||||
build_relay_binary "$arch" "$RELAY_ARCH_OUT"
|
fi
|
||||||
chmod +x "$RELAY_ARCH_OUT"
|
|
||||||
done
|
echo "🧰 Building bundled relay (bun --compile)"
|
||||||
if [[ "${#BUILD_ARCHS[@]}" -gt 1 ]]; then
|
RELAY_BUILD_DIR="$RELAY_DIR/.relay-build"
|
||||||
/usr/bin/lipo -create "$RELAY_BUILD_DIR"/clawdbot-* -output "$RELAY_OUT"
|
rm -rf "$RELAY_BUILD_DIR"
|
||||||
|
mkdir -p "$RELAY_BUILD_DIR"
|
||||||
|
for arch in "${BUILD_ARCHS[@]}"; do
|
||||||
|
RELAY_ARCH_OUT="$RELAY_BUILD_DIR/clawdbot-$arch"
|
||||||
|
build_relay_binary "$arch" "$RELAY_ARCH_OUT"
|
||||||
|
chmod +x "$RELAY_ARCH_OUT"
|
||||||
|
done
|
||||||
|
if [[ "${#BUILD_ARCHS[@]}" -gt 1 ]]; then
|
||||||
|
/usr/bin/lipo -create "$RELAY_BUILD_DIR"/clawdbot-* -output "$RELAY_CMD"
|
||||||
|
else
|
||||||
|
cp "$RELAY_BUILD_DIR/clawdbot-${BUILD_ARCHS[0]}" "$RELAY_CMD"
|
||||||
|
fi
|
||||||
|
rm -rf "$RELAY_BUILD_DIR"
|
||||||
else
|
else
|
||||||
cp "$RELAY_BUILD_DIR/clawdbot-${BUILD_ARCHS[0]}" "$RELAY_OUT"
|
NODE_VERSION="$(resolve_node_version)"
|
||||||
|
echo "🧰 Preparing bundled Node runtime (v${NODE_VERSION})"
|
||||||
|
RELAY_NODE="$RELAY_DIR/node"
|
||||||
|
RELAY_NODE_BUILD_DIR="$RELAY_DIR/.node-build"
|
||||||
|
rm -rf "$RELAY_NODE_BUILD_DIR"
|
||||||
|
mkdir -p "$RELAY_NODE_BUILD_DIR"
|
||||||
|
for arch in "${BUILD_ARCHS[@]}"; do
|
||||||
|
NODE_ARCH_OUT="$RELAY_NODE_BUILD_DIR/node-$arch"
|
||||||
|
download_node_binary "$NODE_VERSION" "$arch" "$NODE_ARCH_OUT"
|
||||||
|
done
|
||||||
|
if [[ "${#BUILD_ARCHS[@]}" -gt 1 ]]; then
|
||||||
|
/usr/bin/lipo -create "$RELAY_NODE_BUILD_DIR"/node-* -output "$RELAY_NODE"
|
||||||
|
else
|
||||||
|
cp "$RELAY_NODE_BUILD_DIR/node-${BUILD_ARCHS[0]}" "$RELAY_NODE"
|
||||||
|
fi
|
||||||
|
chmod +x "$RELAY_NODE"
|
||||||
|
if [[ "${STRIP_NODE:-1}" == "1" ]]; then
|
||||||
|
/usr/bin/strip -x "$RELAY_NODE" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
rm -rf "$RELAY_NODE_BUILD_DIR"
|
||||||
|
stage_relay_payload "$RELAY_DIR"
|
||||||
|
write_relay_wrapper "$RELAY_DIR"
|
||||||
fi
|
fi
|
||||||
rm -rf "$RELAY_BUILD_DIR"
|
|
||||||
|
|
||||||
echo "🧪 Verifying bundled relay (version)"
|
echo "🧪 Verifying bundled relay (version)"
|
||||||
"$RELAY_OUT" --version >/dev/null
|
"$RELAY_CMD" --version >/dev/null
|
||||||
|
|
||||||
echo "🎨 Copying gateway A2UI host assets"
|
echo "🎨 Copying gateway A2UI host assets"
|
||||||
rm -rf "$RELAY_DIR/a2ui"
|
rm -rf "$RELAY_DIR/a2ui"
|
||||||
@@ -292,6 +430,7 @@ if [[ "${SKIP_GATEWAY_PACKAGE:-0}" != "1" ]]; then
|
|||||||
{
|
{
|
||||||
"name": "clawdbot-embedded",
|
"name": "clawdbot-embedded",
|
||||||
"version": "$PKG_VERSION",
|
"version": "$PKG_VERSION",
|
||||||
|
"type": "module",
|
||||||
"piConfig": {
|
"piConfig": {
|
||||||
"name": "pi",
|
"name": "pi",
|
||||||
"configDir": ".pi"
|
"configDir": ".pi"
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ function candidateBinDirs(opts: EnsureClawdbotPathOpts): string[] {
|
|||||||
|
|
||||||
const candidates: string[] = [];
|
const candidates: string[] = [];
|
||||||
|
|
||||||
// Bun bundled (macOS app): `clawdbot` lives in the Relay dir (process.execPath).
|
// Bundled macOS app: `clawdbot` lives in the Relay dir (process.execPath).
|
||||||
try {
|
try {
|
||||||
const execDir = path.dirname(execPath);
|
const execDir = path.dirname(execPath);
|
||||||
const siblingClawdbot = path.join(execDir, "clawdbot");
|
const siblingClawdbot = path.join(execDir, "clawdbot");
|
||||||
@@ -95,7 +95,7 @@ function candidateBinDirs(opts: EnsureClawdbotPathOpts): string[] {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Best-effort PATH bootstrap so skills that require the `clawdbot` CLI can run
|
* Best-effort PATH bootstrap so skills that require the `clawdbot` CLI can run
|
||||||
* under launchd/minimal environments (and inside the macOS bun bundle).
|
* under launchd/minimal environments (and inside the macOS app bundle).
|
||||||
*/
|
*/
|
||||||
export function ensureClawdbotCliOnPath(opts: EnsureClawdbotPathOpts = {}) {
|
export function ensureClawdbotCliOnPath(opts: EnsureClawdbotPathOpts = {}) {
|
||||||
if (process.env.CLAWDBOT_PATH_BOOTSTRAPPED === "1") return;
|
if (process.env.CLAWDBOT_PATH_BOOTSTRAPPED === "1") return;
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import process from "node:process";
|
|||||||
declare const __CLAWDBOT_VERSION__: string;
|
declare const __CLAWDBOT_VERSION__: string;
|
||||||
|
|
||||||
const BUNDLED_VERSION =
|
const BUNDLED_VERSION =
|
||||||
typeof __CLAWDBOT_VERSION__ === "string" ? __CLAWDBOT_VERSION__ : "0.0.0";
|
(typeof __CLAWDBOT_VERSION__ === "string" && __CLAWDBOT_VERSION__) ||
|
||||||
|
process.env.CLAWDBOT_BUNDLED_VERSION ||
|
||||||
|
"0.0.0";
|
||||||
|
|
||||||
function argValue(args: string[], flag: string): string | undefined {
|
function argValue(args: string[], flag: string): string | undefined {
|
||||||
const idx = args.indexOf(flag);
|
const idx = args.indexOf(flag);
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import process from "node:process";
|
|||||||
declare const __CLAWDBOT_VERSION__: string | undefined;
|
declare const __CLAWDBOT_VERSION__: string | undefined;
|
||||||
|
|
||||||
const BUNDLED_VERSION =
|
const BUNDLED_VERSION =
|
||||||
typeof __CLAWDBOT_VERSION__ === "string" ? __CLAWDBOT_VERSION__ : "0.0.0";
|
(typeof __CLAWDBOT_VERSION__ === "string" && __CLAWDBOT_VERSION__) ||
|
||||||
|
process.env.CLAWDBOT_BUNDLED_VERSION ||
|
||||||
|
"0.0.0";
|
||||||
|
|
||||||
function hasFlag(args: string[], flag: string): boolean {
|
function hasFlag(args: string[], flag: string): boolean {
|
||||||
return args.includes(flag);
|
return args.includes(flag);
|
||||||
|
|||||||
Reference in New Issue
Block a user