Skip to content

Choose the shell's runtime and distribution given OpenTUI's FFI requirement #59

Closed

Description

@Sandstorm831

Part of #2.

Question

OpenTUI's renderer runs only on Node 26.4.0+ with --experimental-ffi, or on Bun (bun:ffi, flag-free) — Node 24 LTS cannot run it at all. This was measured while landing #55 and it contradicts the runtime decision in #21/#47 (Node 24 LTS npm package), which rested on the #6 prototype's now-disproven "Node needs no FFI" conclusion.

Given that, what runtime and distribution should Secant commit to for the interactive shell — a Node 26.4+FFI npm package, a Bun-compiled zero-install binary (OpenCode's model), or a Node SEA — and what does that decision force to change elsewhere (the SQLite driver, the distribution model, the CI Node line, and the tickets/ADRs built on Node 24)?

Activity

  1. Sandstorm831 commented on Sep 10, 2026

    @Sandstorm831
    CollaboratorAuthor

    Starting context

    Curated from the #55 slice, which is where this surfaced. Everything below is evidence for the grilling session; nothing here is decided.

    The forcing fact (measured during #55)

    The options on the table

    1. Node 26.4.0+ npm package + transparent --experimental-ffi re-exec (what Open the shell: Workspace approval and Home in the TUI on the Renderer Port #55 landed provisionally). The CLI re-execs itself with --experimental-ffi --conditions=browser when launching the TUI, so users never type a flag — but they must have Node 26.4.0+ installed. Above the current LTS until 2026-10-28; the flag is experimental (no cross-patch stability guarantee).
    2. Bun-compiled single-file executable (OpenCode's exact, proven model). bun build --compile embeds the Bun runtime and the OpenTUI native lib into one per-platform binary; OpenTUI runs flag-free via bun:ffi. The user installs nothing. Distributed via a curl installer, GitHub-release archives, a thin npm launcher whose per-platform optionalDependencies carry the binary (so npm i -g still works), Homebrew, etc. ~50–100 MB per-platform binaries, cross-compiled from one host.
    3. Node SEA embedding Node 26.4 and baking --experimental-ffi via execArgvExtension:"cli". User installs nothing, but it stacks three experimental features (Node 26.4 runtime + node:ffi + SEA), has no cross-compilation, and is materially more fragile.
    4. @yao-pkg/pkg (ships Node 26.5 prebuilt) — the FFI + flag path is unproven for pkg; treat as fallback only.

    What the choice forces to change

    Other issues / decisions to rethink

    Evidence sources

    The #55 record comment; docs/research/opencode-tui-extraction-constraints.md (the "Bun, Node, OpenTUI, And Solid Constraints" section); Node release notes for 26.1.0 and 26.4.0; OpenTUI's runtime-support docs; Bun's single-file-executable docs; and the direct createTestRenderer-across-Node-versions probe run during #55.

  2. Sandstorm831 commented on Sep 10, 2026

    @Sandstorm831
    CollaboratorAuthor

    💭 Fleeting note (parking-lot, not part of this decision): every milestone should carry one dedicated code-review pass whose remit is architecture — module depth and seams, folder/import topology, dependency direction, and adherence to the standing principles (deep modules, no dead UI, runtime-neutrality, etc.) — kept separate from correctness/spec review. Worth turning into its own milestone-loop step (or a gate) rather than leaving it implicit in per-ticket change review. Recording it here so it isn't lost; it wants its own home later.

  3. self-assigned this
    on Sep 10, 2026
  4. Sandstorm831 commented on Sep 10, 2026

    @Sandstorm831
    CollaboratorAuthor

    Grilling findings (so far, 2026-09-10)

    Not a resolution. This records what fact-finding established before the decision is taken, so the resolution can cite it rather than restate it. Every external claim below was checked against a primary source today; anything unverified is marked.

    1. Corrections to the Starting context

    Starting-context claim What the sources say today
    Bun does not implement node:sqlite Bun 1.4.0 (2026-08-20) implements it fully (compat table, PR #32498). One open Windows bug: close() can leave the file locked (#40001). A Bun path no longer forces the SQLite driver change; Q8 decouples from this decision.
    --experimental-ffi is an unstable flag users are stuck with node:ffi landed in 26.1.0, not 26.4. Node merged "ffi: enable module by default" on 2026-08-28 (PR #65475); not in 26.8.2 (released 2026-09-09), so expect 26.9.0. The flag becomes a no-op; the module still emits an ExperimentalWarning, which --disable-warning=ExperimentalWarning silences. Stability index today: 1 (Experimental).
    OpenTUI's Node path is unofficial OpenTUI's runtime-support page lists Node.js 26.4.0+ with ESM and --experimental-ffi as supported; engines is { bun: ">=1.3.0", node: ">=26.4.0" }. Its Node tests run on Linux only. The N-API request (opentui#2) was closed as completed via the node:ffi route.
    Node SEA is CommonJS-only and cannot cross-compile Node 26 SEA supports ESM (mainFormat: "module", since 25.7.0) and cross-compilation via the executable field (with useCodeCache/useSnapshot off). execArgv bakes --experimental-ffi (verified locally: the bytes appear in the blob). Still Stability 1.1; the native library must be written to a temp dir before dlopen.
    Bun embeds OpenTUI's library cleanly It works but is undocumented: Bun materializes an embedded shared library to a tmpfile before dlopen (PR #30720). Broken from 1.3.14-canary through stable 1.3.14, fixed in 1.4.0. Bun's own FFI page: "Do not rely on it in production" (docs).
    Node 26 LTS 2026-10-28 Confirmed (schedule.json). Node 24 EOL 2028-04-30. Latest 26.x is 26.8.2.

    Two repo facts the Starting context did not carry:

    • ADR 0018 measured that Bun forfeits legacy conhost. Under Bun 1.3.14 the stdin-release workaround does not save the console. The only source is opentui#1405 (filed from this project); its 2026-09-06 follow-up shows that on Node it is closing the stdin handle that saves conhost, not keeping it alive. Bun's destroy() does not release the native reader; the fix is an open, unmerged Bun PR (#35621).
    • A single-file artefact breaks three standing decisions as written: ADR 0029 finds built-in .wfb files beside dist/, scripts/package-smoke.mjs installs an npm tarball, and ADR 0027 publishes to npm from CI.

    2. OpenTUI 0.4.5 on the current Node line (measured today, this repo)

    Node Full suite (node scripts/run-tests.mjs) Direct FFI probe --disable-warning=ExperimentalWarning
    26.4.0 (declared floor) 79/79 ok suppresses
    26.7.0 79/79 ok suppresses
    26.8.2 (latest) 79/79, three runs ok suppresses

    The three node:ffi tightenings in 26.7–26.8 (SharedArrayBuffer pointers, detached ArrayBuffers, calls after close()) are not hit by OpenTUI 0.4.5. Without the flag on 26.8.2 the renderer still fails ("OpenTUI native FFI is not available for this runtime yet"), i.e. unflagging has not shipped.

    3. What the peer CLIs did

    CLI Distribution today Stated reason
    Claude Code Native installer recommended, compiled with Bun (Bun 1.4.1 notes); README calls npm install deprecated (registry does not flag it). Anthropic acquired Bun 2025-12. Docs argue auto-update and npm permission problems; "no Node dependency" is not a documented differentiator (setup).
    Codex Rust binary, curl installer, npm wrapper. Original Rust README: "requires Node.js 22+ … this runtime requirement inhibits adoption: they would be better served by a standalone executable" (31d0d7a3).
    Gemini CLI npm only, Node ≥ 20. None stated.

    Also documented: under nvm, global npm packages live per Node version (nvm README), so a global CLI disappears on version switch; nvm ships reinstall tooling for exactly this.

    4. Bun downsides Node does not have (verified)

    • Windows crashes in compiled Bun TUIs, open and unanswered. bun#27471 (N-API vtable corruption, filed against Claude Code's bundled Bun: 49 crashes in 33 days on Bun vs zero on Node, TUI escape into the host shell) — open, no Bun reply. bun#40302 (1.4.0 Windows standalone segfault in a bun:ffi loop through @opentui/core, filed by an OpenCode maintainer) — probably fixed by the WebKit bump in 1.4.1, still open, unconfirmed. bun#42099 (1.4.1 standalone segfaults) opened 2026-09-09.
    • Legacy conhost as above: real, with an unmerged fix path.
    • Signing is geography-gated. Azure Artifact Signing (what OpenCode uses for Windows) admits organizations in US/CA/EU/UK/AU/NZ/JP/KR/SG/CH/NO/IL only; individuals US/CA only (quickstart); from $9.99/month. macOS needs the $99/yr Developer Program plus notarization. Claude Code signs and notarizes both; OpenCode signs Windows only and ships macOS CLI binaries under Bun's own identity (opencode#46313). curl does not set the macOS quarantine attribute, but XProtect and signature-validity checks run regardless.
    • Patch churn on the developer side. 1.4.0 broke dns.lookup on macOS (#40573); 1.4.1 broke bun build output for a day (#41351); embedded-dylib dlopen broken across 1.3.14; better-sqlite3 v13 aborted Bun until 1.4.1. Users never see this (the binary pins the runtime); every Bun upgrade is a soak.
    • Test toolchain. node:test under bun test is partial (28/81 upstream tests per the 1.4 blog); module.register/registerHooks are silent no-ops, so the Babel/Solid test loader must move to @opentui/solid/preload. Bun.Terminal (1.3.5, Windows via ConPTY in 1.4) could replace test-only node-pty, which does not run on Bun at all (bun#7362, closed not planned).
    • Artefact size. OpenCode v1.18.30: 44–60 MB compressed per platform, 138–176 MB unpacked through npm. Bun's own binary floor is ~59 MB.
    • FFI and embedding are officially experimental/undocumented (section 1).

    5. Node downsides (for symmetry)

    Users must install Node ≥ 26.4 (LTS in seven weeks). nvm users lose the global on version switch. No zero-dependency installer, ever. Everything else about the Node path is temporary (flag → 26.9, warning → one switch, LTS → 2026-10-28).

    6. How OpenCode handles each downside (source read at anomalyco/opencode 1ead9e3d, v1.18.25)

    Downside OpenCode's handling
    Conhost wedge None. Nothing releases stdin; exit is renderer destroy → worker shutdown RPC (5 s timeout) → process.exit, the exact post-destroy() event-loop work #1405 measured as fatal. Eight user reports of "console dead after exit" on Windows (#22003, #23720, #25691, #26480, #27749, #28155, #28673, #30495), all stale-closed; six community fix PRs closed unmerged. Docs recommend WSL on Windows.
    Windows crashes No telemetry, no crash handling, no restart. Response was toolchain: CI moved off Bun canary to stable baseline builds (Feb 2026), native file search disabled on Windows by default (Jun 2026), Bun frozen at 1.3.14 (last pre-Rust release) since 2026-05-18.
    Signing / AV Windows via azure/artifact-signing-action since March; macOS CLI unsigned. Maintainer's stated AV root cause: the binary extracts OpenTUI's library to a temp folder on first start (#7919); signing did not stop the reports.
    Patch churn Ten Bun pin changes in four months incl. two rollbacks, then frozen. minimumReleaseAge 3 days with all @opentui/* exempted. Windows CI needs --linker hoisted for bun#28147. OpenTUI upgrades reverted twice.
    Testing bun test everywhere; TUI tested headlessly with createTestRenderer/testRender (same as this repo). No PTY test, no test of the compiled binary beyond --version. The TUI test package is not in the CI turbo graph.
    Embedding bun install --os="*" --cpu="*" for the native packages, then Bun.build({ compile: { target } }) per target; bunfs root switched per OS; one vendor package hand-patched so the compiler can see its library.

    Claude Code (public tracker/docs only): no report of the wedge symptom; docs claim Windows 10 1809+ and "works in any terminal without configuration"; two 2026 changelog entries about restoring terminal modes on exit; one open conhost rendering bug (#88306); no changelog line about a Windows crash fix; Bun's team redirects Claude Code crash reports to Anthropic because it ships its own Bun build.

    7. Sizing legacy conhost

    Definition in this repo's record (docs/research/opentui-windows-console-destroy.md, ADR 0027): a visible window owned by conhost.exe, not the "Use legacy console" checkbox (deprecated 2023, no longer installed by default).

    • Windows 11 22H2+ (2022-10): Windows Terminal is the default host (announcement). Windows Terminal, VS Code, Cursor, JetBrains, SSH, and WSL launched inside any of those run conhost headless behind ConPTY — the wedge cannot fire there.
    • Still a conhost window on Windows 11: elevated launches (by design, terminal#13392), explicit conhost.exe, handoff failures, users who set the default host back to Console Host.
    • Windows 10: Windows Terminal never preinstalled or default; the default-host setting needs 22H2 + KB5026435. Same for Server 2022/2025 and LTSC.
    • Windows 10 share, Statcounter Aug 2026, all desktop Windows: 30.1 % worldwide, 23.5 % US (source); consumer ESU extended to 2027-10-12.
    • No developer-specific Windows-version or terminal-emulator data exists (Stack Overflow 2025, JetBrains 2025, Octoverse 2025 all lack it). Microsoft has never published a Windows Terminal install base.
    • Microsoft's plan: retire conhost "from its default position", never remove it (roadmap).

    Inference: the exposed population is Windows 10 developers who launch from a bare cmd/PowerShell window rather than an IDE terminal, plus anyone running elevated. 30 % is an upper bound on the first group; the true share is well below it and shrinking. The cheapest handling under Bun is detection (refuse the TUI with a message when not under Windows Terminal/an IDE; headless unaffected) rather than support.

    8. Where the discussion stands

  5. Sandstorm831 commented on Sep 10, 2026

    @Sandstorm831
    CollaboratorAuthor

    Resolution

    Secant's interactive shell ships as a Bun-compiled single-file executable, not a Node npm package. This overturns the runtime and distribution half of #21 ("Node 24 LTS, one npm package, no self-contained executable in v1"), which rested on #6's disproven "Node needs no FFI" measurement. The decision is taken now; ADR 0030 is written when the Windows soak in #60 passes (a fail on Bun 1.4.2 and again on the next Bun release reverts to the Node 26.4+ npm path with the transparent --experimental-ffi re-exec that #55 landed).

    Why Bun over Node 26.4: Node's problems are temporary (the flag is a no-op from 26.9, LTS lands 2026-10-28) except the permanent ones — users must install a specific Node, nvm users lose the global on version switch, and a zero-dependency installer is never possible. Three of the four peer CLIs (Claude Code, Codex, OpenCode) ship standalone binaries and Codex wrote down exactly this reason. Bun's costs are one-time build work plus a Windows crash family that is real, open, and managed by pinning (see the findings); the soak measures it before the ADR.

    Decisions

    1. Runtime and toolchain. Bun pinned exactly (packageManager, 1.4.2 or the release Soak the Bun-compiled shell on a real Windows machine before ADR 0030 #60 passes on); CI reads the pin. A Bun bump goes through a PR that passes the three-OS gate and re-arms the human Windows check, exactly as ADR 0027 treats an OpenTUI bump. One toolchain: bun build --compile, bun test with @opentui/solid/preload, Bun.Terminal for the real-terminal suite; tsup, tsx, the Babel test loader, and test-only node-pty go; tsc, ESLint, Prettier stay. Baseline x64 builds are moot in Bun 1.4 (AVX2 chosen at runtime).
    2. Distribution, v1. GitHub release archives for exactly the three gated targets (Windows x64, macOS arm64, Linux x64; extras are one line each when asked and each is a row someone will assume is tested). curl | sh and PowerShell installers into ~/.secant/bin on every platform — a fixed location, independent of SECANT_HOME (ADR 0023), no override variable, matching OpenCode's fixed ~/.opencode/bin. An npm launcher @secantdev/secant that spawns the binary from a per-platform optionalDependencies package with no postinstall (Claude Code's model, so --ignore-scripts and pnpm cannot break it); Node is needed only to run npm and the shim, any version. No Homebrew, winget, scoop, or Docker in v1. No self-updater and no version check in v1 (unchanged from Choose Crucible's process, runtime, packaging, and distribution architecture #21).
    3. Signing, v1. macOS binaries carry Bun's automatic ad-hoc signature (Apple silicon refuses arm64 code without at least that) and CI runs codesign --verify --deep --strict on every macOS artefact because Bun's re-signing has broken twice (1.3.12, 1.3.13). No Developer ID, no notarization, no Windows Authenticode in v1. curl does not set the quarantine flag and tar does not propagate it, so the curl and npm routes are never Gatekeeper-evaluated; release-page downloads are documented as "run from Terminal, do not double-click". curl-installed Windows executables carry no mark-of-the-web, so SmartScreen does not fire on them (Smart App Control machines excepted).
    4. SQLite. The Catalog and Run Store use bun:sqlite behind their Interfaces. Bun 1.4 implements node:sqlite but its Windows close() file-lock bug hits the Catalog's open-per-command pattern; bun:sqlite is Bun's first-class API with a transaction helper. Q8 (Choose Crucible's process, runtime, packaging, and distribution architecture #21 §6) closes with this decision: better-sqlite3 is moot under Bun (it needed a Bun fix to load at all and is special-cased by filename).
    5. Runtime neutrality. ADR 0018's rule extends from vendored presentation source to all target source, with a named allowlist where bun:/Bun. are permitted: the SQLite adapter and the Windows console guard. The mechanical check in tests/architecture/check-module-boundaries.ts becomes an allowlist instead of a blanket ban. No Node twin implementations (OpenCode's #sqlite/#pty/#fff node sides exist only for its Electron app and are untested). Evidence that this is achievable: OpenCode touches Bun APIs in 12 of 833 source files, 25 of its 33 call sites are Bun.stringWidth, and it cut from 321 sites in Feb 2026 to 48 by replacing Bun calls with Node builtins.
    6. Legacy conhost. Dropped as a support-matrix row. Startup prints a one-line notice when the console window is conhost-owned (bun:ffi GetConsoleWindow + IsWindowVisible; ConPTY's headless conhost has an invisible window) pointing at Windows Terminal; the TUI still runs and headless is unaffected. Rationale: the wedge is exit-only (a dead console window, no Secant data lost), the exposed population is Windows 10 users launching from a bare cmd/PowerShell window plus elevated shells (upper bound 30 % of desktop Windows, true developer share well below), and OpenCode ships with no handling and eight stale-closed bug reports for it — the notice is what prevents that noise. The human release check retargets from conhost to Windows Terminal, run when the Bun pin, the OpenTUI pin, or the renderer changed. When Bun merges the stdin-release fix (bun#35621) the notice can go.
    7. Node fallback condition. Revert to the Node 26 path only if the Soak the Bun-compiled shell on a real Windows machine before ADR 0030 #60 soak fails on Bun 1.4.2 and again on the next Bun release. Nothing else triggers it; churn is handled by the pin.
    8. Gate before the ADR. Soak the Bun-compiled shell on a real Windows machine before ADR 0030 #60 (HITL Task): compile the current shell for bun-windows-x64 on Bun 1.4.2 and run it on a real Windows machine through every launch and exit path in Windows Terminal plus one long session and a 20-launch loop. Conhost is observed and recorded, not deciding.

    What changes in the record (on #60 pass)

    Future-version ledger

    • Developer ID + notarization (US$99/yr program; individuals need no D-U-N-S) and Windows Authenticode (Azure Artifact Signing is geography-gated; otherwise a CA certificate) — required before a Homebrew channel (casks must pass Gatekeeper since 2026-09-01) or browser-download support.
    • secant upgrade — must write a new file and rename, never overwrite in place (macOS caches the signature in the kernel; in-place overwrite crashes on next launch until reboot).
    • Extra targets (macOS x64, Linux arm64/musl, Windows arm64), Homebrew/winget/scoop, passive update notice, dropping the conhost notice once Bun fixes stdin release.

    Follow-on tickets

    Evidence added since the findings comment

    • OpenCode's installer hardcodes $HOME/.opencode/bin (install:68) with no override variable; its data/config/cache/state follow XDG via xdg-basedir (packages/core/src/global.ts); it has no PowerShell installer (Windows: npm/bun/choco/scoop, WSL recommended).
    • OpenCode runtime coupling: 33 Bun.* sites in 12 of 833 files; three bun/node import-map seams in packages/core/package.json; the Node side is exercised only by the Electron desktop build (script/build-node.ts) and untested; 321 → 48 sites since Feb 2026 via two deliberate migration campaigns.
    • Apple silicon requires at least an ad-hoc signature (Platform Security guide); curl/tar do not set or propagate quarantine (Apple DTS); Bun's ad-hoc re-signing regressed in 1.3.12 (#29120) and 1.3.13 (#32159); Homebrew ended support for casks failing Gatekeeper on 2026-09-01 (brew#20755); in-place binary replacement hazard (Apple).
  6. Sandstorm831 commented on Sep 10, 2026

    @Sandstorm831
    CollaboratorAuthor

    Resolved above; ADR 0030 and the record amendments follow #60. Follow-ons: #60 (soak, HITL) and #61 (M1 implementation).

  7. added a commit that references this issue on Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

wayfinder:context-readyWayfinding ticket has curated starting context and may be claimedwayfinder:grillingHITL grilling decision ticket

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions