Skip to content

fix(cli): chmod +x bundled spawn-helper + add live PTY spawn smoke test#4058

Merged
saddlepaddle merged 2 commits into
mainfrom
fix/cli-darwin-spawn-helper-exec-bit
May 4, 2026
Merged

fix(cli): chmod +x bundled spawn-helper + add live PTY spawn smoke test#4058
saddlepaddle merged 2 commits into
mainfrom
fix/cli-darwin-spawn-helper-exec-bit

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented May 4, 2026

Why

User reported on cli-v0.2.5:

[terminal] open cfcc863f-...: spawn failed (shell=/bin/zsh cwd=/Users/codetown/code/superset): posix_spawnp failed.

The dist tarball ships lib/node_modules/node-pty/prebuilds/darwin-<arch>/spawn-helper directly from node-pty's npm package, which lands with mode 0644 (npm strips exec bits on tarball pack). On darwin, node-pty posix_spawnps spawn-helper as the actual fork helper for every terminal. Without +x the kernel returns EACCES and the failure surfaces as the cryptic posix_spawnp failed with no errno. The normal install path runs npm rebuild node-pty which restores the bit; we ship the raw prebuild so we have to fix it ourselves.

Why earlier smoke tests didn't catch it

  1. The smoke test only asserted require() worked, not that PTY spawn worked.
  2. When I added a pty.spawn smoke test in an earlier iteration, I ran it from the workspace cwd. Node's module resolution walks up from cwd before consulting NODE_PATH, so node-pty resolved to the host repo's node_modules/node-pty (electron-rebuilt by the desktop app, with proper +x). The smoke test passed against a broken bundle.

What changes

packages/cli/scripts/build-dist.ts

After the existing darwin node-pty/build/ removal step (which forces the loader onto the prebuild path), chmodSync(spawn-helper, 0o755).

Smoke test (build-cli.yml + build-dist-linux-docker.sh)

  • cd /tmp before running the smoke test scripts so Node module resolution doesn't walk up into the host repo.
  • Assert require.resolve("node-pty/lib/unixTerminal") starts with the expected dist path — host-tree leaks now hard-fail the build instead of silently passing.
  • Static test -x on spawn-helper for darwin targets.
  • Live pty.spawn("/bin/sh", ["-c", "echo SPAWN_OK"]) against the bundled Node + bundled prebuild. Asserts the spawn actually emits the expected bytes and exits 0.

Verification

Reproduced and fixed against the published cli-v0.2.5 tarball locally:

=== fixed mode (0755) ===
--- pty-spawn smoke (expect OK):
pty spawn OK
exit:0

=== broken mode (0644) ===
--- pty-spawn smoke (expect FAIL):
caught: posix_spawnp failed.
exit:1

The "broken mode" output matches the user's production error verbatim — meaning the new smoke test would have caught this exact bug, and would catch any future regression in the same class (spawn-helper, pty.node ABI mismatch, arch mismatch).

Test plan

  • CI green on this PR — the new smoke test runs on all three matrix targets
  • After merge, cut cli-v0.2.6, push tag, confirm release artifacts pass smoke test in CI
  • On a fresh machine, superset start → open a terminal → succeeds (no posix_spawnp failed)

Summary by cubic

Fixes macOS terminal spawn failures by making the bundled node-pty spawn-helper executable. Adds and hardens a live PTY spawn smoke test to catch exec-bit, ABI/arch, and host-tree leak issues.

  • Bug Fixes
    • Build: set 0755 on lib/node_modules/node-pty/prebuilds/darwin-*/spawn-helper during dist packaging.
    • CI smoke: resolve DIST as an absolute path before cd /tmp; assert node-pty resolves from the dist; verify spawn-helper is +x on darwin; run pty.spawn("/bin/sh","-c","echo SPAWN_OK") and expect output and exit 0; defer the check a tick to avoid onExit/onData race; fix Docker script to print "$DIST.tar.gz" instead of a relative path.

Written for commit ff9fe0a. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed macOS CLI runtime permission preventing bundled terminal helper from executing.
  • Chores

    • Strengthened CLI build/post-build validation with stricter runtime checks to ensure bundled modules load correctly.
    • Added a runtime integration test that verifies terminal spawn behavior and command execution.
    • Improved Linux Docker build validation with explicit module presence checks.

User reported `[terminal] open ...: posix_spawnp failed.` opening
terminals in cli-v0.2.5. The dist tarball ships
`lib/node_modules/node-pty/prebuilds/darwin-{arch}/spawn-helper` from
node-pty's npm package, which lands on disk with mode 0644 (npm strips
exec bits). node-pty `posix_spawnp`'s spawn-helper as the fork helper
on darwin; without +x the kernel returns EACCES and node-pty surfaces
it as the cryptic "posix_spawnp failed" with no errno. Normal install
paths run `npm rebuild node-pty` which restores the bit; we ship the
raw prebuild so we have to fix it ourselves.

Reproduced locally against the published 0.2.5 tarball:
- chmod 0644 spawn-helper → `pty.spawn(...)` throws `posix_spawnp failed.`
- chmod 0755 spawn-helper → spawn works

Fixes:
- build-dist.ts: chmodSync(spawn-helper, 0o755) on darwin after
  `fixNativeBinariesForNode` removes node-pty's `build/` (which forces
  the bindings loader to use the prebuild path).
- Smoke test (build-cli.yml + build-dist-linux-docker.sh): static
  `test -x` assertion on darwin, plus an end-to-end `pty.spawn` against
  the bundled Node + bundled prebuild that asserts the bundled node-pty
  resolves (no host-tree leak via cwd-walking) and that the spawn
  actually emits expected output. Run from /tmp because Node's resolver
  walks up from cwd before consulting NODE_PATH — staying in the
  workspace silently masked this exact bug in earlier versions of the
  smoke test (the host repo's electron-rebuilt node-pty has +x and made
  the test pass against a broken bundle).
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8eb54613-5d10-48b0-8d6e-2e50cf120fff

📥 Commits

Reviewing files that changed from the base of the PR and between 032b3bd and ff9fe0a.

📒 Files selected for processing (2)
  • .github/workflows/build-cli.yml
  • packages/cli/scripts/build-dist-linux-docker.sh
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/cli/scripts/build-dist-linux-docker.sh
  • .github/workflows/build-cli.yml

📝 Walkthrough

Walkthrough

Post-build and CI smoke-test validation are strengthened for the bundled CLI: DIST paths are made absolute, Node module resolution is isolated to the bundled lib, macOS node-pty spawn-helper is ensured executable, and an integration PTY spawn test asserts resolution, output, exit code, and enforces a 5s timeout.

Changes

CLI Bundle Validation Hardening

Layer / File(s) Summary
Data Shape / Env
packages/cli/scripts/build-dist-linux-docker.sh, .github/workflows/build-cli.yml
DIST is computed as an absolute path and exported into the test environment; NODE_PATH is set to "$DIST/lib/node_modules" for isolated module resolution.
Permission Fix
packages/cli/scripts/build-dist.ts
On darwin, locate node-pty/prebuilds/darwin-{arch}/spawn-helper in staged node_modules and chmod(..., 0o755) when present to make the spawn helper executable.
Existence Checks
packages/cli/scripts/build-dist-linux-docker.sh, .github/workflows/build-cli.yml
Add hard-fail checks that "$DIST/lib/host-service.js" and "$DIST/lib/pty-daemon.js" exist before running runtime checks.
Isolation & Module Load Validation
packages/cli/scripts/build-dist-linux-docker.sh, .github/workflows/build-cli.yml
Smoke tests change cwd to /tmp to avoid workspace resolution and run the bundled Node with NODE_PATH to require() better-sqlite3, node-pty, @parcel/watcher, and libsql.
Runtime PTY Spawn Integration
packages/cli/scripts/build-dist-linux-docker.sh, .github/workflows/build-cli.yml
Add script that asserts require.resolve("node-pty/lib/unixTerminal") resolves inside DIST, spawns /bin/sh -c "echo SPAWN_OK" via node-pty, verifies output and exit code, and fails on spawn errors or a 5s timeout.
CI Output Path Fix
packages/cli/scripts/build-dist-linux-docker.sh
Update tarball summary to reference "$DIST.tar.gz".

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant CI as CI / Workflow
  participant FS as Filesystem (DIST)
  participant Node as Bundled Node
  participant node-pty as node-pty module
  participant Shell as /bin/sh

  CI->>FS: compute absolute DIST, ensure files present
  CI->>Node: run script from /tmp with NODE_PATH="$DIST/lib/node_modules"
  Node->>FS: require.resolve("node-pty/lib/unixTerminal")
  FS-->>Node: path (must be inside DIST)
  Node->>node-pty: spawn("/bin/sh -c 'echo SPAWN_OK'")
  node-pty->>Shell: fork & exec
  Shell-->>node-pty: "SPAWN_OK" + exit 0
  node-pty-->>Node: onData + onExit(0)
  Node-->>CI: success or fail (on timeout/error/output mismatch)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰
I hopped through builds with nimble feet,
Made spawn-helpers ready, tidy, neat,
From /tmp the bundled Node did sing,
“SPAWN_OK” echoed — what joy I bring!
A little chmod, a test that’s sweet.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main changes: fixing chmod permissions on bundled spawn-helper and adding a live PTY spawn smoke test.
Description check ✅ Passed The PR description follows the template structure with all key sections populated: detailed 'Why' explanation, changes breakdown, verification details, and test plan.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cli-darwin-spawn-helper-exec-bit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 4, 2026

Greptile Summary

This PR fixes a production posix_spawnp failed crash on macOS by chmod +x-ing spawn-helper in the bundled node-pty prebuild, and hardens the smoke tests by running them from /tmp (preventing host-tree node_modules shadowing) and adding a live PTY spawn assertion.

  • P1 — build-dist-linux-docker.sh line 102: the final echo still uses a relative dist/superset-${TARGET}.tar.gz path that now resolves under /tmp after the new cd /tmp step, breaking every local linux build under set -euxc.
  • P2 — PTY smoke test race: onExit can fire before the last onData chunk is delivered on Linux; deferring the assertion by one event-loop tick would eliminate the flake risk.

Confidence Score: 3/5

Safe to merge after fixing the broken relative path in the docker script; core fix in build-dist.ts is correct.

One P1 defect in build-dist-linux-docker.sh where cd /tmp leaves the final ls echo using a relative path that breaks under set -euxc. The chmodSync fix in build-dist.ts is clean and the CI workflow changes are correct.

packages/cli/scripts/build-dist-linux-docker.sh — line 102 relative path broken by cd /tmp

Important Files Changed

Filename Overview
packages/cli/scripts/build-dist.ts Adds chmodSync(spawn-helper, 0o755) for darwin targets after the existing node-pty build/ removal step; chmodSync was already imported, logic is guarded by existsSync, and arch extraction reuses the existing targetParts helper.
.github/workflows/build-cli.yml Adds absolute-path DIST resolution, test -x spawn-helper for darwin, and a live PTY spawn smoke test with a potential onData/onExit race that could cause intermittent CI failures on Linux.
packages/cli/scripts/build-dist-linux-docker.sh Mirrors CI smoke-test additions but the final echo line still uses a relative dist/... path that resolves to /tmp/dist/... after the new cd /tmp step, breaking the script under -euxc.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[build-dist.ts: fixNativeBinariesForNode] --> B{platform === darwin?}
    B -- yes --> C[Remove node-pty build/ dir\nforces prebuild path]
    C --> D[Resolve spawn-helper path\nprebuilds/darwin-arch/spawn-helper]
    D --> E{existsSync?}
    E -- yes --> F[chmodSync 0o755\nnew fix]
    E -- no --> G[skip]
    B -- no --> H[Linux: no spawn-helper needed]

    subgraph Smoke Tests
        I[cd /tmp\nprevent host node_modules leak]
        I --> J[require smoke: better-sqlite3\nnode-pty, watcher, libsql]
        J --> K{darwin target?}
        K -- yes --> L[test -x spawn-helper]
        K -- no --> M[skip]
        L --> N[Live PTY spawn\necho SPAWN_OK]
        M --> N
        N --> O{onExit: got SPAWN_OK and exitCode === 0?}
        O -- yes --> P[pty spawn OK]
        O -- no --> Q[FAIL]
    end
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/cli/scripts/build-dist-linux-docker.sh:102
**Relative path breaks after `cd /tmp`**

`cd /tmp` is now run before this `echo`, so `dist/superset-${TARGET}.tar.gz` resolves to `/tmp/dist/...` instead of `/work/packages/cli/dist/...`. With `set -euxc` active in the Docker heredoc the failing `$(ls -la ...)` substitution will cause the entire script to exit non-zero, turning every linux-target local build into a failure.

```suggestion
    echo "[docker-build] tarball: $(ls -la "$DIST.tar.gz")"
```

### Issue 2 of 2
.github/workflows/build-cli.yml:113-119
**`onExit` / `onData` ordering race on Linux PTY**

node-pty's `onExit` can fire before the final `onData` callback is delivered, because the kernel's EOF on the pty master and the SIGCHLD for the child process are independent events. For a short-lived command like `echo SPAWN_OK` this is unlikely but not impossible under CI load. If the last chunk containing `"SPAWN_OK"` arrives after `onExit`, the check fails and the build fails with `pty spawn FAIL exit=0 got=""`. Deferring the assertion by one event-loop tick (`setTimeout(check, 0)`) would eliminate the flake risk. The same applies to the equivalent block in `build-dist-linux-docker.sh`.

Reviews (1): Last reviewed commit: "fix(cli): chmod +x bundled spawn-helper ..." | Re-trigger Greptile

});
setTimeout(() => { console.error(\"pty spawn timeout\"); process.exit(1); }, 5000);
"
echo "[docker-build] tarball: $(ls -la dist/superset-${TARGET}.tar.gz)"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Relative path breaks after cd /tmp

cd /tmp is now run before this echo, so dist/superset-${TARGET}.tar.gz resolves to /tmp/dist/... instead of /work/packages/cli/dist/.... With set -euxc active in the Docker heredoc the failing $(ls -la ...) substitution will cause the entire script to exit non-zero, turning every linux-target local build into a failure.

Suggested change
echo "[docker-build] tarball: $(ls -la dist/superset-${TARGET}.tar.gz)"
echo "[docker-build] tarball: $(ls -la "$DIST.tar.gz")"
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/cli/scripts/build-dist-linux-docker.sh
Line: 102

Comment:
**Relative path breaks after `cd /tmp`**

`cd /tmp` is now run before this `echo`, so `dist/superset-${TARGET}.tar.gz` resolves to `/tmp/dist/...` instead of `/work/packages/cli/dist/...`. With `set -euxc` active in the Docker heredoc the failing `$(ls -la ...)` substitution will cause the entire script to exit non-zero, turning every linux-target local build into a failure.

```suggestion
    echo "[docker-build] tarball: $(ls -la "$DIST.tar.gz")"
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread .github/workflows/build-cli.yml Outdated
Comment on lines +113 to +119
term.onExit(({ exitCode }) => {
if (got.includes("SPAWN_OK") && exitCode === 0) {
console.log("pty spawn OK");
process.exit(0);
}
console.error("pty spawn FAIL exit=" + exitCode + " got=" + JSON.stringify(got));
process.exit(1);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 onExit / onData ordering race on Linux PTY

node-pty's onExit can fire before the final onData callback is delivered, because the kernel's EOF on the pty master and the SIGCHLD for the child process are independent events. For a short-lived command like echo SPAWN_OK this is unlikely but not impossible under CI load. If the last chunk containing "SPAWN_OK" arrives after onExit, the check fails and the build fails with pty spawn FAIL exit=0 got="". Deferring the assertion by one event-loop tick (setTimeout(check, 0)) would eliminate the flake risk. The same applies to the equivalent block in build-dist-linux-docker.sh.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/build-cli.yml
Line: 113-119

Comment:
**`onExit` / `onData` ordering race on Linux PTY**

node-pty's `onExit` can fire before the final `onData` callback is delivered, because the kernel's EOF on the pty master and the SIGCHLD for the child process are independent events. For a short-lived command like `echo SPAWN_OK` this is unlikely but not impossible under CI load. If the last chunk containing `"SPAWN_OK"` arrives after `onExit`, the check fails and the build fails with `pty spawn FAIL exit=0 got=""`. Deferring the assertion by one event-loop tick (`setTimeout(check, 0)`) would eliminate the flake risk. The same applies to the equivalent block in `build-dist-linux-docker.sh`.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/scripts/build-dist-linux-docker.sh`:
- Around line 71-102: After the script does cd /tmp the relative path
dist/superset-${TARGET}.tar.gz no longer resolves; capture an absolute tarball
path before changing directory (e.g. set
TARBALL="$(pwd)/dist/superset-${TARGET}.tar.gz" or use an existing DIST variable
to build an absolute path) and then replace the final echo that uses ls -la
dist/superset-${TARGET}.tar.gz with ls -la "$TARBALL" (or ls -la "$DIST/...") so
the file lookup works after cd /tmp; update references to the tarball
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 52dfaaac-84b0-4dd7-9f35-d687311057b8

📥 Commits

Reviewing files that changed from the base of the PR and between 98bae93 and 032b3bd.

📒 Files selected for processing (3)
  • .github/workflows/build-cli.yml
  • packages/cli/scripts/build-dist-linux-docker.sh
  • packages/cli/scripts/build-dist.ts

Comment thread packages/cli/scripts/build-dist-linux-docker.sh Outdated
Two issues caught by greptile + coderabbit on #4058:

1. build-dist-linux-docker.sh: the final `echo "tarball: ..."` line still
   used the relative path `dist/superset-${TARGET}.tar.gz`, which after
   the new `cd /tmp` step resolves to `/tmp/dist/...` and fails under
   `bash -euxc` — bricking every local linux build. Fix: build the
   tarball path off `$DIST` (already absolute).

2. node-pty's `onExit` and `onData` are independent events (SIGCHLD on
   the child + EOF on the pty master). For `echo SPAWN_OK` it almost
   always lands data-then-exit, but under CI load `onExit` can fire
   before the final `onData` — flake risk. Defer the assertion one
   tick after `onExit` so any in-flight `onData` callback runs first.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

@saddlepaddle
Copy link
Copy Markdown
Collaborator Author

Both issues fixed in ff9fe0a:

  • Greptile P1 / CodeRabbit: build-dist-linux-docker.sh final echo now uses $DIST.tar.gz (absolute) instead of the relative path that broke after cd /tmp.
  • Greptile P2: onExit/onData race — assertion now runs 100ms after onExit via a setTimeout(check, 100) so any in-flight onData callback delivers first. Same fix mirrored in the GH Actions workflow.

Re-verified locally on darwin-arm64: smoke test still passes (pty spawn OK, exit 0).

@saddlepaddle saddlepaddle merged commit 15221c9 into main May 4, 2026
15 checks passed
@saddlepaddle saddlepaddle mentioned this pull request May 4, 2026
3 tasks
saddlepaddle added a commit that referenced this pull request May 4, 2026
Single bug fixed since v0.2.5:
- spawn-helper bundled without exec bit on darwin → terminals crashed
  with `posix_spawnp failed.` on every open. chmod +x in build-dist
  + live PTY spawn smoke test that catches this regression class
  (#4058).

Push cli-v0.2.6 after this lands to fire the release pipeline.
@Kitenite Kitenite deleted the fix/cli-darwin-spawn-helper-exec-bit branch May 6, 2026 04:51
MocA-Love added a commit to MocA-Love/superset that referenced this pull request May 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant