mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-21 20:18:11 +02:00
fix(code-mode): make packaged code mode work and drop ~460MB bundled engines (#614)
* fix(code-mode): ship ACP adapters in packaged builds
Code mode spawns the Claude/Codex ACP adapters as separate `node <entry>`
processes resolved at runtime, so each must exist as a real file on disk.
esbuild can't inline them (dynamic require.resolve + spawn target), and Forge's
`ignore: /node_modules/` rule strips the workspace node_modules — so packaged
builds threw `Cannot find module '@agentclientprotocol/claude-agent-acp'` and
code mode was broken in every release. Dev worked only because the pnpm symlink
was present.
Stage the two adapters and their full production dependency closure into
.package/acp/node_modules during generateAssets, reconstructing an npm-style
nested layout: nest on version conflict (claude and codex keep their own
@agentclientprotocol/sdk; the @openai/codex launcher keeps its platform binary)
and skip platform-optional deps not installed for the build OS, so each OS ships
its own native binary. Exempt .package from the node_modules ignore rule, and
make the adapter resolver check the staged location first, falling back to
node_modules in dev.
Resolve dependency directories by walking node_modules directly rather than
require.resolve(`${pkg}/package.json`): the latter throws for packages whose
`exports` map doesn't expose package.json (e.g. @anthropic-ai/claude-agent-sdk),
which would silently drop them and their subtrees from the staged closure.
* fix(code-mode): drive agents from local install, drop bundled engines
Skip the platform-native engine packages (~230 MB each) when staging the ACP adapters and point each adapter at the user's local claude/codex via CLAUDE_CODE_EXECUTABLE / CODEX_PATH, erroring clearly when neither is installed. The codex resolver mirrors claude's login-shell probe so nvm/fnm installs resolve on macOS/Linux. Shrinks each installer ~460 MB.
* fix(code-mode): surface agent startup failures instead of hanging forever
- 60s deadline on adapter initialize / session create+load: an engine that
launches but never completes the SDK handshake (e.g. an outdated local
CLI) now fails with the adapter's stderr attached instead of leaving the
turn (pending...) indefinitely; prompts themselves stay un-timed
- dispose the adapter client when startup fails so the spawned process
does not leak
- set DEBUG_CLAUDE_AGENT_SDK=1 so the SDK logs the exact spawn command and
claude's stderr to ~/.claude/debug/sdk-*.txt and startup errors point at
that file (engine stderr is otherwise discarded entirely)
- graft the user's login-shell PATH onto the adapter env on macOS/Linux:
GUI launches inherit launchd's stripped PATH, which breaks node-shebang
claude launchers (nvm/npm installs) and the engines' own subprocess spawns
* ci(code-mode): cross-platform smoke matrix + one-shot diagnose script
- x-code-mode-smoke.yml: on apps/x PRs, package the app on mac/linux/windows
and run acp-smoke.mjs, which asserts (1) adapters staged + native engines
stripped, (2) each staged adapter boots from the packaged app via the
packaged Electron binary and answers ACP initialize, (3) a fake engine that
launches but never responds is converted into a clear startup-timeout error
instead of hanging forever (the silent-hang class)
- diagnose-code-mode.sh: colleagues run one command and send one blob
(engine versions/paths/types, login-shell vs GUI PATH, auth presence,
stream-json probe, newest SDK debug log) — one round trip instead of five
- forge.config.cjs: only sign/notarize when APPLE_ID is set, so unsigned
local mac builds and the CI smoke matrix can package deterministically
- client.ts: startup timeout overridable via ROWBOAT_ACP_STARTUP_TIMEOUT_MS
(CI uses 10s; also an escape hatch for slow MCP-heavy setups)
Verified on Windows: all smoke checks pass, including the end-to-end
fake-hanging-engine timeout (fails in 10.0s with the stderr-enriched error).
* fix(ci): make smoke fake engines executable — codex-acp spawns CODEX_PATH directly on unix
A bare .js with no shebang/exec bit fails with EACCES and crashes the adapter
on mac/linux. Split into an exec-bit exit-0 fake for the handshake check and
the hanging fake for the timeout check.
* fix(code-mode): resolve claude/codex installed via version managers
commonInstallPaths only checked ~/.nvm/versions/node/<binary>, never
descending into nvm's versioned vX.Y.Z/bin subdirs, so an nvm-installed
codex/claude showed "not installed" in Settings and failed code-mode
chat with "CLI not found" on GUI launches (where the login-shell PATH
isn't inherited).
Enumerate each installed Node version's bin dir for nvm/fnm/asdf, and
add pnpm global (PNPM_HOME / platform default) and Claude Code's legacy
~/.claude/local install location. Shared by both the Settings status
check and the chat/tab binary resolvers, so it covers all code-mode
entry points.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(code-mode): remove code-mode smoke test workflow and script
Only needed during cross-platform verification of the packaged code-mode
fix; drop the CI workflow and its acp-smoke.mjs helper now that it's done.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f04e85670b
commit
33c15cfbd9
8 changed files with 500 additions and 51 deletions
88
apps/x/scripts/diagnose-code-mode.sh
Normal file
88
apps/x/scripts/diagnose-code-mode.sh
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
#!/usr/bin/env bash
|
||||
# One-shot code-mode diagnostics (macOS / Linux).
|
||||
#
|
||||
# When code mode misbehaves on your machine (stuck runs, "CLI not found",
|
||||
# startup timeouts), run this and send the FULL output back — it collects
|
||||
# everything needed to diagnose in one round trip:
|
||||
#
|
||||
# bash apps/x/scripts/diagnose-code-mode.sh
|
||||
#
|
||||
# Read-only except for one tiny `claude -p` probe (a single short API call).
|
||||
|
||||
section() { printf '\n=== %s ===\n' "$1"; }
|
||||
|
||||
# Portable timeout: mac has no `timeout` binary by default.
|
||||
run_with_timeout() {
|
||||
local secs="$1"; shift
|
||||
"$@" & local pid=$!
|
||||
( sleep "$secs" && kill "$pid" 2>/dev/null ) & local killer=$!
|
||||
wait "$pid" 2>/dev/null; local rc=$?
|
||||
kill "$killer" 2>/dev/null
|
||||
return $rc
|
||||
}
|
||||
|
||||
describe_binary() { # $1 = name
|
||||
local p
|
||||
p="$(/bin/sh -lc "command -v $1" 2>/dev/null)"
|
||||
if [ -z "$p" ]; then
|
||||
echo "$1: NOT on login-shell PATH"
|
||||
return
|
||||
fi
|
||||
echo "$1: $p"
|
||||
[ -L "$p" ] && echo " symlink -> $(readlink "$p")"
|
||||
# Node-shebang script vs native binary — the distinction that matters for
|
||||
# GUI launches (shebang scripts need `node` on the SPAWNING process's PATH).
|
||||
local head1
|
||||
head1="$(head -c 64 "$p" 2>/dev/null | head -n 1 | tr -d '\0')"
|
||||
case "$head1" in
|
||||
'#!'*) echo " type: script ($head1)" ;;
|
||||
*) echo " type: native binary" ;;
|
||||
esac
|
||||
echo " version: $(run_with_timeout 15 "$1" --version 2>&1 | head -n 1)"
|
||||
}
|
||||
|
||||
section "system"
|
||||
echo "os: $(uname -sr) ($(uname -m))"
|
||||
echo "shell: ${SHELL:-unset}"
|
||||
echo "date: $(date)"
|
||||
|
||||
section "engines"
|
||||
describe_binary claude
|
||||
describe_binary codex
|
||||
describe_binary node
|
||||
|
||||
section "PATH: login shell vs GUI"
|
||||
echo "login-shell PATH:"
|
||||
/bin/sh -lc 'echo " $PATH"'
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
echo "launchd (GUI) PATH:"
|
||||
echo " $(launchctl getenv PATH 2>/dev/null || echo '(unset — GUI apps get the system default)')"
|
||||
fi
|
||||
|
||||
section "auth presence (no secrets printed)"
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
if security find-generic-password -s "Claude Code-credentials" >/dev/null 2>&1; then
|
||||
echo "claude: keychain credential present"
|
||||
else
|
||||
echo "claude: NO keychain credential (signed in?)"
|
||||
fi
|
||||
fi
|
||||
[ -f "$HOME/.claude/.credentials.json" ] && echo "claude: ~/.claude/.credentials.json present"
|
||||
[ -f "$HOME/.codex/auth.json" ] && echo "codex: ~/.codex/auth.json present" || echo "codex: NO ~/.codex/auth.json"
|
||||
|
||||
section "claude stream-json probe (what the app does under the hood)"
|
||||
# A healthy claude prints a `system`/`init` JSON line within seconds. A hang or
|
||||
# error here reproduces the in-app failure WITHOUT the app.
|
||||
run_with_timeout 45 claude -p "reply with exactly: ok" --output-format stream-json --verbose 2>&1 | head -n 3
|
||||
echo "(probe exit: $? — 143 means it hung and was killed after 45s)"
|
||||
|
||||
section "newest SDK debug log (~/.claude/debug)"
|
||||
latest="$(ls -t "$HOME/.claude/debug"/sdk-*.txt 2>/dev/null | head -n 1)"
|
||||
if [ -n "$latest" ]; then
|
||||
echo "$latest:"
|
||||
tail -n 40 "$latest"
|
||||
else
|
||||
echo "(none found)"
|
||||
fi
|
||||
|
||||
printf '\ndone — send everything above.\n'
|
||||
Loading…
Add table
Add a link
Reference in a new issue