Skip to content

[DX-1453] fix: make npx @ably/cli <command> work without the redundant ably#419

Open
mattheworiordan wants to merge 6 commits into
mainfrom
fix/npx-single-command
Open

[DX-1453] fix: make npx @ably/cli <command> work without the redundant ably#419
mattheworiordan wants to merge 6 commits into
mainfrom
fix/npx-single-command

Conversation

@mattheworiordan

@mattheworiordan mattheworiordan commented Jun 17, 2026

Copy link
Copy Markdown
Member

Whilst watching someone today onboard and try the one click install, it became evident npx -p @ably/cli ably init has unnecessary duplication of ably, and looking at Neon for comparison, they've kept it simpler with npx neonctl init which works well.

Note trying npx @ably/cli init fails with "could not determine executable to run", so you fall back to npx -p @ably/cli ably init. The repeated ably is pointless, and one command is what people expect from npx. We couldn't give them that, which is a smell. This fixes it.

The cause: the package shipped two bins pointing at different files (ably and ably-interactive). npm's resolver only auto-picks an executable when there's a single bin, or they all point at the same target. Two different ones, so npx can't choose. The fix is a single ably bin, same shape as every other scoped CLI (@angular/cli gives you ng, @vue/cli gives you vue). npx @ably/cli <command> now resolves cleanly.

Nothing breaks:

  • npx @ably/cli <command> now works
  • npx -p @ably/cli ably <command> still works
  • npx @ably/cli ably <command> still works (run.js drops the redundant leading ably)
  • npm install -g @ably/cli then ably <command> is unchanged

One thing to coordinate: ably-interactive is no longer a separate global binary, though the wrapper script still ships in the package. The only runtime consumer is our terminal server, which installs @ably/cli@latest, so it needs a one-line Dockerfile symlink to land alongside this. Companion PR links to this PR.

Tests: a unit test pins the single-bin resolution and fails loudly if a second bin creeps back, and an integration test covers the redundant-ably form.

Docs follow-up if we're happy with this: the onboarding that says npx -p @ably/cli ably init can become npx @ably/cli init.

@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cli-web-cli Ready Ready Preview, Comment Jun 23, 2026 4:08pm

Request Review

@claude-code-ably-assistant

Copy link
Copy Markdown

Walkthrough

This PR fixes npx @ably/cli <command> failing with "could not determine executable to run". The root cause was package.json declaring two bin entries pointing at different files (ablyrun.js, ably-interactivebin/ably-interactive), which trips npm's bin resolver. The fix removes ably-interactive from bin, leaving a single entry so npx resolves deterministically. A backwards-compat shim in bin/run.js strips the redundant leading ably token so the old npx @ably/cli ably <command> form keeps working.

Changes

Area Files Summary
Config package.json Remove ably-interactive bin entry; keep only ably → ./bin/run.js
Entrypoint bin/run.js Splice out a redundant leading ably argv token before oclif runs
Tests test/unit/package/npx-bin-resolution.test.ts Re-implements npm's libnpmexec bin-resolver algorithm; regression-guards single-bin shape
Tests test/integration/commands/npx-backwards-compat.test.ts Spawns the real binary to verify plain and ably ably version forms are equivalent
Docs CHANGELOG.md Unreleased entry documenting the npx behaviour change and compatibility guarantees

Review Notes

  • Deployment dependency: ably-interactive is no longer a global binary after npm install -g @ably/cli. The terminal server (which runs @ably/cli@latest) needs a companion Dockerfile symlink — the PR description links to it but reviewers should confirm that companion lands before or alongside this release.
  • Unit test re-implements third-party logic: npx-bin-resolution.test.ts hand-codes the libnpmexec bin-selector algorithm and explicitly says it is "kept in sync intentionally". This is a reasonable tradeoff given the algorithm is small and stable, but it's worth noting that a future npm version bump could silently diverge.
  • bin/run.js token-strip is unconditional: The splice runs on every invocation of run.js where argv[2] === 'ably'. The comment argues this is safe because there is no top-level ably subcommand — worth verifying that constraint stays true if new top-level commands are ever added.
  • Integration test uses spawn directly: This is appropriate since it exercises the real binary path, not the oclif test harness. No auth is needed — tests use ably version which is fully offline.
  • No new runtime dependencies introduced.

@claude-code-ably-assistant claude-code-ably-assistant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review Summary

Clean, well-targeted fix. The root cause analysis is correct (two bins pointing at different targets trips npm's resolver) and the solution is the right one. No bugs found.


What this PR does:

  • Removes the ably-interactive bin entry from package.json, leaving a single ably bin
  • Adds backwards-compat stripping of a redundant leading 'ably' token in bin/run.js so 'npx @ably/cli ably ' keeps working
  • Adds a unit test pinning the npm resolution algorithm and a regression guard if a second bin creeps back
  • Adds an integration test covering the backwards-compat stripping

Correctness checks:

bin/run.js stripping logic: process.argv.splice(2, 1) fires before execute(), so oclif never sees the redundant token. The process.argv.includes('interactive') check runs after the strip, so 'npx @ably/cli ably interactive' correctly sets ABLY_INTERACTIVE_MODE. No issues.

Edge cases covered:

  • No args: process.argv[2] is undefined, comparison is false — safe
  • Double 'ably': only the first is stripped, second becomes the command name, oclif returns 'not found' — covered by test 3
  • 'ably --help' strips to top-level help — correct

The algorithm copy in the unit test: The test reimplements npm's bin resolution from libnpmexec/lib/get-bin-from-manifest.js and the comment explicitly flags it as an intentional copy kept in sync. This is the right tradeoff: you get a regression guard that fails loudly if a second bin returns, at the cost of the algorithm potentially drifting from npm. Worth the call — this algorithm has been stable across npm major versions.


One thing to coordinate before merge:

The PR description flags it already, but worth calling out explicitly: removing ably-interactive as a global bin BREAKS THE TERMINAL SERVER until the companion Dockerfile symlink lands. These two PRs need to be merged and deployed in the right order. If the terminal server picks up this package version before the Dockerfile change, ably-interactive won't be on PATH and interactive mode will stop working.

This is already documented in the PR — just making sure it's not lost.


Verdict: Approve. The code is correct, the tests cover the key cases, and the coordination dependency is documented. No code changes requested.

@AndyTWF AndyTWF left a comment

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.

There's still some places in the code where we suggest people use ably-interactive, but by removing it from package.json it'll no longer go on peoples PATH and won't work.

This also needs a terminal server PR because that relies on running ably-interactive assuming that the CLI has placed it on PATH.

@mattheworiordan

mattheworiordan commented Jun 19, 2026

Copy link
Copy Markdown
Member Author

There's still some places in the code where we suggest people use ably-interactive,

Why do we do that instead of ably interactive?

This also needs a terminal server PR because that relies on running ably-interactive assuming that the CLI has placed it on PATH.

You mean another or this one https://github.com/ably/cli-terminal-server/pull/163? I think that is done

@umair-ably

Copy link
Copy Markdown
Collaborator

thanks for raising this @mattheworiordan - since we have ably interactive as a command (which handles single Ctrl+C to interrupt the currently running command), we can completely strip ably-interactive as a bin. However, I'd stay clear of symlinking it in the terminal-server as that still seems like bit of a smell.

The only purpose of ably-interactive I can see is that for the web terminal sessions, a double Ctrl+C restarts the CLI and lets the user carry on instead of terminating the shell itself. With this in mind, I propose we move the couple of lines that loop/restart the CLI to the terminal server itself. This avoids needing the symlink which just masks this legacy architecture.

I'll fix up this PR with @AndyTWF's comments in mind and raise a new terminal server PR too

@umair-ably umair-ably marked this pull request as draft June 23, 2026 10:34
umair-ably added a commit that referenced this pull request Jun 23, 2026
This repo records CHANGELOG entries per release — `main` has no [Unreleased]
section — so #419's changes are documented at release time, not in the PR.
Restores CHANGELOG.md to match `main`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mattheworiordan and others added 2 commits June 23, 2026 13:03
…oken

`npx @ably/cli init` (and any other command) failed with
"could not determine executable to run", forcing users into the verbose
`npx -p @ably/cli ably init`. People expect a single command with npx; the
fact that we couldn't do that was a packaging smell.

Root cause: the package declared two `bin` entries pointing at different
targets (`ably` -> ./bin/run.js, `ably-interactive` -> ./bin/ably-interactive).
npm's bin resolver (libnpmexec/get-bin-from-manifest) only auto-selects an
executable when either every bin points at the same target, or a bin is named
after the unscoped package name (`cli`). Two differently-targeted bins, neither
named `cli`, hit the "could not determine executable to run" branch.

Fix: expose a single `bin` (`ably`) — the same shape every other scoped CLI
uses (`@angular/cli` -> ng, `@vue/cli` -> vue). `npx @ably/cli <command>` now
resolves deterministically. Verified against npm's own resolver (old manifest
throws the exact error; new manifest returns `ably`) and end-to-end via
`npx <packed tarball> --version`.

Backwards compatible: `npx -p @ably/cli ably <command>` is unaffected, and
run.js now drops a redundant leading `ably` token so the historical
`npx @ably/cli ably <command>` form keeps working too. There is no top-level
`ably` command, so a leading `ably` is unambiguously the repeated bin name.

Global installs (`npm install -g @ably/cli` then `ably ...`) are unchanged.
The `bin/ably-interactive` auto-restart wrapper script is retained in the
package (still used by `ably interactive` and its tests) but is no longer
installed as a separate global executable. The one runtime consumer of that
binary, ably/cli-terminal-server, installs `@ably/cli@latest`, so its image
build needs a companion one-line Dockerfile symlink landed alongside this
change.

Tests: a hermetic unit test re-implements npm's resolver and asserts the
package always resolves to a single `ably` executable; an integration test
covers the redundant-`ably` backwards-compatible invocation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pnpm strips the execute bit from node-pty's prebuilt spawn-helper, so
`pnpm test:tty` fails with 'posix_spawnp failed'. Add a pretest:tty hook
that restores it (no-op on Windows / when already executable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
umair-ably and others added 4 commits June 23, 2026 17:07
… in-process

The ably-interactive bin was a bash wrapper whose only job was to relaunch the
CLI after a Ctrl+C force-quit. `ably interactive` already handles Ctrl+C
in-process (single Ctrl+C interrupts the running command and returns to the
prompt), so the wrapper is redundant — and removing it is what lets the package
expose a single `ably` bin (fixing npx).

- Delete bin/ably-interactive and the never-wired-up bin/ably-interactive.ps1
- Fix the welcome hint: single Ctrl+C interrupts and returns to the prompt; it
  no longer (falsely) warns that Ctrl+C exits the shell or points at a wrapper
- Non-TTY Ctrl+C now delivers an in-process SIGINT instead of process.exit(130)
  (there is no wrapper to restart the process)

Hosts that want the shell to survive a double-Ctrl+C force-quit (e.g. the
terminal server) wrap `ably interactive` in their own restart loop, keyed off
exit codes 42/130 with ABLY_WRAPPER_MODE=1 (contract preserved).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sweep user-facing and architecture references now that the wrapper binary is
gone and ably interactive handles Ctrl+C in-process:

- env-vars: ABLY_HISTORY_FILE example uses `ably interactive`; reword the note
  (HistoryManager already defaults to ~/.ably/history); update coupled unit test
- Troubleshooting/Debugging: accurate Ctrl+C guidance; no wrapper run-commands
- Exit-Codes: reframe the exit-code consumer as a host session restart loop
- Interactive-REPL: status note marking the bash-wrapper design as superseded
- Project-Structure: drop the deleted bin/ably-interactive(.ps1) tree entries
- manual tests: run `node bin/run.js interactive`; keep the exit-42 contract

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The old suite spawned bin/ably-interactive (now deleted) to assert wrapper
restart/EIO behaviour. Rewrite as interactive-integration.test.ts covering the
no-wrapper reality: starts/exits cleanly, a real SIGINT interrupts a running
command and the SAME process re-prompts and stays usable, and `exit` emits 42
under ABLY_WRAPPER_MODE (the host restart-loop contract). Real-terminal SIGINT
and terminal-corruption/EIO coverage lives in
test/tty/commands/interactive-sigint.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hoist the spawn/output-capture/waitFor boilerplate into a single
`startInteractive()` helper used by all four interactive integration tests.

Replace the mid-command 0x03 test with the reachable at-prompt case. A 0x03
(ETX) byte cannot interrupt a *running* command over piped stdin: handleCommand
calls rl.pause() during execution, which pauses the stdin stream so the
process.stdin "data" handler never receives the byte. The at-prompt branch
(emit SIGINT to readline -> "^C" + hint) is the part that actually works in
non-TTY mode. Mid-command interruption stays covered by the real-SIGINT test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@umair-ably umair-ably force-pushed the fix/npx-single-command branch from 50e4676 to 2dcd3f5 Compare June 23, 2026 16:07
@umair-ably umair-ably changed the title fix: make npx @ably/cli <command> work without the redundant ably [DX-1453] fix: make npx @ably/cli <command> work without the redundant ably Jun 23, 2026
@umair-ably

umair-ably commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Update since the original description (which covered just the npx fix):

  • ably-interactive fully retired, not just the bin alias. Ctrl+C is now handled in-process (single = interrupt + re-prompt, double = force-quit 130) and the wrapper script is deleted. This changes the coordination note: the terminal server needs a small restart loop keyed off exit codes 42/130 with ABLY_WRAPPER_MODE=1, not a Dockerfile symlink to the old bin.
  • Docs updated to drop all wrapper references (Exit-Codes, Interactive-REPL, Troubleshooting, env-vars).
  • Tests reworked — replaced the wrapper-integration suite with in-process integration tests on a shared harness (exit 0/42, real-SIGINT interrupt, non-TTY 0x03 path), plus a pretest:tty fix making node-pty's spawn-helper executable under pnpm.

npx behaviour is unchanged from the description; the above is the wrapper-removal follow-through.

🤖 Generated with Claude Code

@umair-ably umair-ably marked this pull request as ready for review June 23, 2026 16:26
@umair-ably

Copy link
Copy Markdown
Collaborator

@claude review

@claude-code-ably-assistant

Copy link
Copy Markdown

Walkthrough

This PR fixes `npx @ably/cli ` by collapsing the package to a single `ably` bin — removing the separate `ably-interactive` binary entry that caused npm's resolver to fail to auto-select an executable. The `bin/ably-interactive` bash/PowerShell wrapper scripts are deleted entirely (Ctrl+C is already handled in-process since a prior refactor), and `bin/run.js` gains backwards-compat logic to strip a redundant leading `ably` token so `npx @ably/cli ably ` keeps working for old docs and scripts. All tests and docs are updated to match the new single-bin, wrapper-free design.

Changes

Area Files Summary
Config package.json Removes ably-interactive from bin map; leaves only ably: ./bin/run.js. Adds pretest:tty hook.
Bin (deleted) bin/ably-interactive, bin/ably-interactive.ps1 Bash/PowerShell restart-loop wrapper scripts removed entirely.
Bin bin/run.js Strips a redundant leading ably token from process.argv for backwards compat.
Scripts scripts/ensure-node-pty-executable.mjs New: restores spawn-helper execute bit stripped by pnpm; runs as pretest:tty hook.
Commands src/commands/interactive.ts Non-TTY 0x03 handler changed from process.exit(130) to process.kill(process.pid, "SIGINT"); "running without wrapper" warning replaced with a dim hint.
Data src/data/env-vars.ts Removes ably-interactive references from ABLY_HISTORY_FILE env var docs.
Tests — Unit test/unit/package/npx-bin-resolution.test.ts New: pins single-bin npm resolver logic; regression-guards against a second bin creeping back.
Tests — Unit test/unit/utils/env-vars-render.test.ts Updates assertion to match new ABLY_HISTORY_FILE doc text.
Tests — Integration test/integration/commands/npx-backwards-compat.test.ts New: tests the redundant ably ably <cmd> strip; includes a "strip only once" guard.
Tests — E2E test/e2e/interactive/interactive-integration.test.ts New: in-process tests for start/exit, SIGINT interruption, 0x03 byte at prompt, and wrapper-mode exit code 42.
Tests — E2E test/e2e/interactive/wrapper-integration.test.ts Deleted — was testing the removed bash wrapper.
Docs docs/Exit-Codes.md, docs/Interactive-REPL.md, docs/Project-Structure.md, docs/Debugging.md, docs/Troubleshooting.md, test/manual/*.md Removes all ably-interactive references; documents the new single-bin, in-process Ctrl+C design.

Review Notes

  • Deployment coordination required. The PR body states the Ably terminal server (which installs @ably/cli@latest) needs a Dockerfile symlink update, with a companion PR in flight. The ably-interactive global binary disappears the moment this version is published — the companion PR must land at the same time or before.

  • PR body vs diff discrepancy. The description says "the wrapper script still ships in the package" but the diff shows bin/ably-interactive and bin/ably-interactive.ps1 are fully deleted. Confirm whether those files were intended to remain as non-bin package assets, or if the description is stale.

  • Behavioral change in interactive.ts. The non-TTY 0x03 handler now calls process.kill(process.pid, "SIGINT") instead of process.exit(130). The inline comment correctly notes this path only fires while stdin is actively flowing during a command (e.g. a y/n confirmation prompt), not during ordinary long-running commands where readline pauses stdin. Worth a second look to confirm no exit-code regression in the y/n-prompt-interrupt path.

  • npx-bin-resolution.test.ts re-implements npm internals. The test copies libnpmexec's get-bin-from-manifest.js resolver and notes it is "kept in sync intentionally." This is slightly fragile — if npm changes the algorithm, this test passes while the real behaviour breaks. Low risk since the algorithm is stable, but worth tracking.

@umair-ably umair-ably requested a review from AndyTWF June 24, 2026 09:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

5 participants