diff --git a/docs/design-npm-global-install-fixes.html b/docs/design-npm-global-install-fixes.html new file mode 100644 index 000000000..f1d216e7e --- /dev/null +++ b/docs/design-npm-global-install-fixes.html @@ -0,0 +1,718 @@ + + +
+ + ++ The target onboarding experience for Agent Orchestrator is truly two commands + from a fresh machine: +
+ +npm install -g @composio/ao && ao start
+
+
+ This does not work. Multiple bugs cause ao start to fail after
+ a global npm install. Beyond the bugs, there is an architectural issue: ao start
+ hardcodes runtime: "tmux" in the generated config, making tmux a hidden
+ prerequisite that breaks the "two commands" promise.
+
The failure chain on a fresh machine:
+ +checkBuilt() hardcoded path fails for npm hoisted layoutruntime: "tmux", orchestrator creation fails hard if tmux is missing, kills the dashboard| # | Bug | Severity | Issue | Affects |
|---|---|---|---|---|
| 1 | +checkBuilt() hardcoded single-level path |
+ P0 | +#619 | +All npm/yarn global installs | +
| 2 | +node-pty spawn-helper missing execute bit |
+ P0 | +#624 | +macOS + Linux npm installs | +
| 3 | +tmux is a hidden prerequisite — hardcoded runtime: "tmux", no auto-install, no fallback |
+ P0 | +— | +Any fresh machine without tmux | +
| 4 | +Terminal server crash is permanent (no restart), bookkeeping bug on restart | +P1 | +Related to #489 | +linux-arm64 (AWS Graviton) | +
+ preflight.ts verifies @composio/ao-core exists using a single hardcoded path:
+
// BEFORE: only checks one level deep
+const nodeModules = resolve(webDir, "node_modules", "@composio", "ao-core");
+
+
+ npm hoists dependencies to a parent node_modules/.
+ The check always fails for global installs because ao-core lives one or more
+ levels up from ao-web.
+
packages/web/
+ node_modules/
+ @composio/ao-core -> ../../core
+ /usr/lib/node_modules/@composio/
+ ao-web/ # webDir
+ ao-core/ # sibling, not child
+
+ Replace with findPackageUp() — walks up directories checking
+ node_modules/@composio/ao-core at each level, mirroring Node's own resolution:
+
function findPackageUp(startDir: string, ...segments: string[]): string | null {
+ let dir = resolve(startDir);
+ while (true) {
+ const candidate = resolve(dir, "node_modules", ...segments);
+ if (existsSync(candidate)) return candidate;
+ const parent = dirname(dir);
+ if (parent === dir) break;
+ dir = parent;
+ }
+ return null;
+}
+
+ Error messages distinguish install method:
+node_modules → "npm install -g @composio/ao@latest""pnpm install && pnpm build"
+ node-pty@1.1.0 ships spawn-helper without the execute bit.
+ The monorepo's scripts/rebuild-node-pty.js fixes this via node-gyp rebuild,
+ but it hardcodes pnpm paths and never runs for npm global installs.
+ Upstream fix (microsoft/node-pty#866)
+ is only in 1.2.0-beta.
+
+ New packages/ao/bin/postinstall.js runs after npm install -g @composio/ao.
+ Uses the same findPackageUp pattern to locate node-pty, then chmod 0o755
+ on spawn-helper. Skips on Windows, silent on failure.
+
node-pty ships stable release with the upstream fix.
+ + This was the biggest gap in the "two-step" promise. Three compounding issues: +
+start.ts:299 — Config generation hardcodes runtime: "tmux"start.ts:581-592 — Orchestrator creation fails hard if tmux runtime fails, then kills the dashboardpreflight.ts:checkTmux() — Only showed "brew install tmux" (macOS-only), didn't attempt installOn a fresh machine without tmux, the user saw:
+$ ao start
+✓ Config created: agent-orchestrator.yaml
+⚠ tmux not found — install with: brew install tmux
+...
+✗ Failed to setup orchestrator: tmux: command not found
+
+ checkTmux() now attempts installation before throwing:
async function autoInstallTmux(): Promise<boolean> {
+ const attempts =
+ process.platform === "darwin"
+ ? [{ cmd: "brew", args: ["install", "tmux"] }]
+ : process.platform === "linux"
+ ? [
+ { cmd: "sudo", args: ["apt-get", "install", "-y", "tmux"] },
+ { cmd: "sudo", args: ["dnf", "install", "-y", "tmux"] },
+ ]
+ : [];
+
+ for (const { cmd, args } of attempts) {
+ try {
+ await exec(cmd, args);
+ await exec("tmux", ["-V"]); // verify
+ return true;
+ } catch { /* try next */ }
+ }
+ return false;
+}
+
+
+ start.ts config generation now checks env.hasTmux:
+
const runtime = env.hasTmux ? "tmux" : "process";
+
+
+ The process runtime (@composio/ao-plugin-runtime-process) runs agents as
+ plain child processes. No tmux dependency. Users get a working AO immediately — they
+ can install tmux later and update the config for the full experience.
+
If auto-install fails and the user needs manual action:
+brew install tmuxsudo apt install tmux (Debian/Ubuntu) or sudo dnf install tmux (Fedora)tmux is not available on Windows. Use WSL: wsl --install, then: sudo apt install tmux
+ start-all.ts spawns three child processes. If a terminal server crashes,
+ it logs the exit and never restarts.
+
+ Auto-restart logic with correct bookkeeping: +
+ +
+ The initial version had a bug: launch() always called children.push(child),
+ creating duplicate entries on restart. The cleanup() function counts
+ children.length and waits for exits, so stale handles would skew the alive count
+ and trigger the 5-second force-shutdown timer.
+
Fix: each process gets a slot index assigned on first launch. Restarts replace in-place:
+ +let slotIndex = -1;
+
+function launch(): ChildProcess {
+ const child = spawn(command, args, { ... });
+
+ child.on("exit", (code) => {
+ if (!shuttingDown && opts?.restart && code !== 0 && restarts < maxRestarts) {
+ restarts++;
+ const replacement = launch();
+ children[slotIndex] = replacement; // replace in-place
+ }
+ });
+
+ // Only push on first launch; restarts replace the existing slot
+ if (slotIndex === -1) {
+ slotIndex = children.length;
+ children.push(child);
+ }
+
+ return child;
+}
+
+ Investigated but not broken:
+ +| Suspected Blocker | Actual State |
|---|---|
| Port 3000 conflict | +Already fixed — findFreePort(port + 1) auto-increments up to 100 ports |
+
| node-pty crash kills dashboard | +Already handled — direct-terminal-ws.ts catches import failure, gracefully rejects connections |
+
findWebDir() in global install |
+ Already handled — uses require.resolve() with proper walk-up |
+
concurrently at runtime |
+ Already fixed — production uses node dist-server/start-all.js |
+
After this PR, npm install -g @composio/ao && ao start works on:
| Platform | tmux Available | tmux Not Available |
|---|---|---|
| macOS (Intel + Apple Silicon) | +Full experience runtime: tmux |
+ Auto-installs via brew Falls back to process if brew missing |
+
| Linux x64 | +Full experience runtime: tmux |
+ Auto-installs via apt/dnf Falls back to process if sudo unavailable |
+
| Linux arm64 (Graviton) | +Works Direct terminal unavailable (no node-pty prebuild) |
+ Falls back to process Functional but no tmux attach |
+
| Windows | +N/A | +Falls back to process Error message guides to WSL for full experience |
+
+ Key improvement: No platform requires manual prerequisite installation before
+ ao start. The worst case is runtime: "process" — a degraded
+ but functional experience.
+
| Check | Result |
|---|---|
| TypeScript typecheck | Passes (CLI + web server) |
| ESLint | No new errors on changed files |
| Preflight tests | 15/15 pass (expanded from 13) |
| CLI tests | 222/223 pass (1 = pre-existing flaky doctor-script.test.ts) |
| Test | What It Verifies |
|---|---|
| pnpm layout (direct match) | +findPackageUp finds ao-core in webDir/node_modules on first check |
+
| npm hoisted layout (walk-up) | +findPackageUp misses first level, finds ao-core one level up |
+
| npm hint on missing deps | +Global install path shows npm install -g hint |
+
| pnpm hint on missing deps | +Monorepo path shows pnpm install && pnpm build hint |
+
| Missing dist | +ao-core found but dist/index.js missing shows build hint |
+
| tmux auto-install success | +When tmux -V fails initially but install succeeds, checkTmux passes | +
| tmux auto-install failure | +When all install attempts fail, throws with platform-specific instructions | +
process runtime runs agents as child processes. It lacks tmux session attach
+ and the dashboard terminal panel, but agents work correctly. Users see
+ "Using process runtime (tmux not available)" and can upgrade later.
+ children array on first launch.
+ Restarts replace in-place (children[slotIndex] = replacement) instead of pushing,
+ preventing duplicate entries that would break the cleanup countdown.
+ spawn-helper chmod workaround will be removed once node-pty ships
+ a stable release with the upstream fix (microsoft/node-pty#866).
+