Skip to content

feat(init): install @ably/cli globally when launched via npx#394

Merged
umair-ably merged 5 commits into
mainfrom
feature/init-global-install
May 13, 2026
Merged

feat(init): install @ably/cli globally when launched via npx#394
umair-ably merged 5 commits into
mainfrom
feature/init-global-install

Conversation

@umair-ably
Copy link
Copy Markdown
Collaborator

Summary

  • ably init now installs @ably/cli globally when launched via npx, so the documented npx @ably/cli init onboarding command actually delivers a persistent CLI rather than landing in an ephemeral npx cache that disappears after init exits.
  • Adds a --no-install flag for users who manage @ably/cli with their own package manager.
  • Failures are non-fatal — init logs a warning with the captured stderr and tells the user to run npm install -g @ably/cli manually; auth and skills install still proceed.
  • --json mode auto-installs (no prompt) and pipes npm's output so the NDJSON stream stays clean for agents.

Behavior matrix

Invocation Result
npx @ably/cli init (TTY) Prompts "Install @ably/cli globally? [Y/n]" (default Yes) → runs npm install -g @ably/cli@latest → succeeds → auth → skills
npx @ably/cli init --json No prompt (agent-friendly), installs silently
npx @ably/cli init --no-install Skips install entirely
ably init (already on PATH) No install attempted — process.argv[1] doesn't contain _npx
Install fails (EACCES, network) Warning with captured stderr; init continues normally

Implementation notes

  • Detection uses process.argv[1].includes(\${path.sep}_npx${path.sep}`), which is scoped to npm's npx cache. pnpm dlx/yarn dlx/bun x` are not supported entry points and are intentionally out of scope.
  • promptForConfirmation gains an optional { defaultYes } parameter for non-destructive prompts. Backward compatible — all existing destructive-prompt callers (auth:keys:revoke, auth:revoke-token, bench:publisher, base-command) use the default defaultYes: false.
  • Three new test hooks on globalThis.__TEST_MOCKS__ (isRunningFromNpx, confirmGlobalInstall, installGlobally) follow the existing runLogin pattern so unit tests never shell out to real npm install -g.

Test plan

  • pnpm prepare — clean build
  • pnpm exec eslint . — 0 errors (10 pre-existing warnings unrelated)
  • pnpm test test/unit/ — 2509 passed, 1 todo, 1 skipped (185 files)
  • Targeted init + prompt-confirmation suites — 37/37 passed
  • Manual smoke test: npx @ably/cli init from a clean machine and verify ably --help works in a new shell afterwards
  • Manual smoke test: npx @ably/cli init --json produces a clean NDJSON stream (no npm output leakage)
  • Manual smoke test: npx @ably/cli init --no-install skips install and continues to auth/skills

🤖 Generated with Claude Code

`ably init` is the documented one-command onboarding entry point, but
`npx @ably/cli init` only landed the CLI in the ephemeral npx cache —
after init exited, `ably` was no longer on PATH. This change makes init
detect the npx invocation, prompt the user (default Y), and run
`npm install -g @ably/cli@latest` so the CLI persists across shells.

- Add `--no-install` flag for users who manage @ably/cli with their own
  package manager.
- Install runs silently in --json mode (no prompt, stdio piped) so the
  NDJSON stream stays clean for agent consumers.
- Install failures are non-fatal: a warning is logged with the captured
  stderr and the user is told to run `npm install -g @ably/cli` manually.
- Extend `promptForConfirmation` with an optional `defaultYes` flag for
  non-destructive prompts (empty answer counts as yes, suffix `[Y/n]`).
- Test hooks (`isRunningFromNpx`, `confirmGlobalInstall`,
  `installGlobally`) follow the existing `runLogin` pattern so unit
  tests never shell out to real `npm install -g`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 13, 2026

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

Project Deployment Actions Updated (UTC)
cli-web-cli Ready Ready Preview, Comment May 13, 2026 10:35am

Request Review

@claude-code-ably-assistant
Copy link
Copy Markdown

Walkthrough

This PR improves the ably init onboarding experience when the CLI is launched via npx @ably/cli init. It detects npx invocation and offers (or in JSON/agent mode, automatically performs) a global install of @ably/cli, so the CLI is available on PATH after the session rather than evaporating from the npx cache. Failures during install are non-fatal — onboarding continues regardless.

Changes

Area Files Summary
Commands src/commands/init.ts Adds npx detection, --no-install flag, global-install prompt/auto-install logic, and three injectable test hooks
Utils src/utils/prompt-confirmation.ts Extends promptForConfirmation with an optional defaultYes parameter for non-destructive prompts (backward-compatible)
Tests test/unit/commands/init.test.ts 37 new tests covering npx detection, --no-install, JSON mode auto-install, user confirmation flow, and graceful install failure
Tests test/unit/utils/prompt-confirmation.test.ts Tests for the new defaultYes option

Review Notes

  • Behavioral change in promptForConfirmation: The new optional { defaultYes } parameter changes the prompt text and default answer. Worth confirming the [Y/n] vs [y/N] rendering matches expectations across all existing callers.
  • npx detection heuristic: Detection relies on process.argv[1] containing _npx as a path segment (path.sep + '_npx' + path.sep). This is an undocumented npx implementation detail — reviewers should confirm it holds across npm versions and platforms (Windows path separators, pnpm dlx, etc.).
  • Injected test hooks: isRunningFromNpx, confirmGlobalInstall, and installGlobally are exposed on the command instance to allow mocking in unit tests. These are test-seam properties on a production class — reviewers may want to verify they're not accidentally callable in ways that affect real behaviour.
  • No E2E coverage: The PR description notes manual smoke tests (npx @ably/cli init on a clean machine, --json output cleanliness, --no-install) are not yet checked off. No automated E2E test covers the actual npm install -g execution path.
  • JSON mode silent install: In JSON/agent mode the install runs without any prompt and any error is swallowed silently. Reviewers should confirm the JSON output remains clean and that a failed silent install still surfaces somewhere (e.g., a structured warning event).

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

Review: feat(init) — install @ably/cli globally when launched via npx

Overview

This PR delivers the missing half of the npx @ably/cli init onboarding story: after running init, users now actually have ably on their PATH. The implementation is clean, the failure path is correctly non-fatal, and JSON/agent mode is handled thoughtfully.


Issues

1. promptForConfirmation: [Y/n] suffix not in the "already has suffix" guard

src/utils/prompt-confirmation.ts — the existing guard that prevents double-appending a suffix checks for [yes/no], [y/n], and [Y/N], but not the new [Y/n] variant this PR introduces:

message.includes("[yes/no]") ||
message.includes("[y/n]") ||
message.includes("[Y/N]")   // ← missing: message.includes("[Y/n]")

If a caller passes "Install? [Y/n]" with { defaultYes: true }, the output becomes "Install? [Y/n] [Y/n]". This PR's own call site doesn't trigger it (the message has no pre-existing suffix), but leaving the inconsistency will bite the next person who adds a defaultYes prompt with a manually crafted message. A one-line fix:

message.includes("[yes/no]") ||
message.includes("[y/n]") ||
message.includes("[Y/n]") ||   // ← add this
message.includes("[Y/N]")

2. Non-JSON install failure gives a redundant/unhelpful warning message

In interactive mode (stdio: "inherit"), when execSync fails, npm has already printed the real error to the terminal. But the warning surfaced by the catch block uses error.message, which for a non-zero exit is just "Command failed: npm install -g @ably/cli@latest" — not the npm error message itself (that went to the inherited stdout/stderr and isn't available on the thrown Error).

So the user sees:

  1. npm's actual error output on the terminal (good)
  2. ⚠ Could not install @ably/cli globally automatically (Command failed: npm install -g @ably/cli@latest). Run: npm install -g @ably/cli (somewhat redundant)

This isn't wrong, but the warning adds noise without adding information. You could simplify the non-JSON error path to just emit a fixed message since npm has already told the user what went wrong:

// Non-JSON mode: npm already printed the error to the terminal.
// Just confirm the fallback action.
this.logWarning(
  "Could not install @ably/cli globally. Run: npm install -g @ably/cli",
  flags,
);

This is a UX improvement, not a correctness bug — up to you whether it's worth changing.


What looks good

  • npx detection via process.argv[1].includes(\${path.sep}_npx${path.sep}`)is solid and cross-platform. Usingpath.sepavoids the/vs` issue.
  • execSync with stdio: "pipe" in JSON mode correctly silences npm's output from the NDJSON stream, and captures stderr to include in the re-thrown error for the logWarning detail.
  • Non-fatal error handlingmaybeInstallGlobally wraps runGlobalInstall in try/catch and continues. Auth and skills install proceed regardless. This is exactly right.
  • Output helpers used correctly: logProgress (silent in JSON), logSuccessMessage (silent in JSON), logWarning (structured in JSON). No console.* or direct chalk calls.
  • promptForConfirmation extension is fully backward compatible — all existing callers omit the options argument and get the old [y/n] / false-default behavior unchanged.
  • Test coverage is thorough: all 5 decision branches covered (not from npx, --no-install, JSON auto-install, user confirms, user declines, install throws).
  • __TEST_MOCKS__ cleanup in afterEach prevents test pollution.
  • Security: the execSync command is fully hardcoded (@ably/cli@latest) — no user input in the shell string.

Summary

The logic is correct, the JSON output contract is respected, and the failure mode is properly non-fatal. Issue #1 (missing [Y/n] in the suffix guard) is a genuine bug, albeit a minor one. Issue #2 is a UX observation. Everything else is good to merge.

- promptForConfirmation: add `[Y/n]` to the no-double-append guard so a
  caller passing both a manual `[Y/n]` suffix and `{ defaultYes: true }`
  doesn't get the suffix doubled. Adds regression test.
- init global-install warning: in non-JSON mode the underlying npm
  process runs with `stdio: "inherit"` so npm has already printed the
  real error to the user's terminal. The thrown error's `.message` is
  just "Command failed: npm install -g @ably/cli@latest" — restating
  that adds no information. Emit a terse warning in non-JSON mode and
  keep the detailed warning (with captured stderr) in JSON mode where
  npm output was piped. Update the failure-path test and add a new
  JSON-mode failure test asserting captured stderr is surfaced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sacOO7
Copy link
Copy Markdown
Contributor

sacOO7 commented May 13, 2026

@claude review

rl.question(promptMessage, (answer) => {
rl.close();
const response = answer.toLowerCase().trim();
if (response === "") {
Copy link
Copy Markdown
Contributor

@sacOO7 sacOO7 May 13, 2026

Choose a reason for hiding this comment

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

Just a question, are we sure response will always be empty string when user directly press enter?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yes, if not, it resolves normally via yes/y

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

Review Summary

Good work overall. The feature is well-scoped, the non-fatal fallback is correct, and the test coverage using existing __TEST_MOCKS__ patterns is solid. Two substantive issues worth addressing before merge.


1. execSync has no timeout — can hang indefinitely during onboarding

File: src/commands/init.tsrunGlobalInstall()

execSync defaults to no timeout (0 = wait forever). A flaky network, a slow npm registry response, or an npm bug could leave ably init hung with no feedback to the user and no way to recover other than killing the process. This is especially bad for a first-run onboarding command where a new user has no idea what's happening.

// Current — no timeout
execSync("npm install -g @ably/cli@latest", { stdio: "pipe" });

// Safer — 2 minutes is generous for a single global package install
execSync("npm install -g @ably/cli@latest", { stdio: "pipe", timeout: 120_000 });

When execSync exceeds its timeout it throws a ETIMEDOUT error, which your existing catch block in maybeInstallGlobally will handle gracefully (logs a warning, continues with auth and skills). The fix is a one-liner and makes the fallback path actually reachable for network failures.


2. JSON mode emits no event on successful install — agents can't confirm install happened

File: src/commands/init.tsmaybeInstallGlobally()

In --json mode, logProgress and logSuccessMessage are both silent (no-ops by design). So when the global install succeeds, the NDJSON stream contains zero install-related output. An agent driving ably init --json will see auth and skills events but has no way to confirm whether the install ran, succeeded, or was silently skipped (e.g. already on PATH via --no-install).

The PR description's behavior matrix describes JSON mode as "installs silently" — but that leaves agents blind to install state. A minimal structured event would fix it:

// After successful install in JSON mode:
this.logJsonEvent({ install: { status: "installed", package: "@ably/cli@latest" } }, flags);

// Or emit an event for all outcomes (skipped, installed, failed):
this.logJsonEvent({ install: { status: "skipped" | "installed" | "failed", ... } }, flags);

Without this, agents running init --json can only detect install failure (via the warning event) but not install success. That asymmetry makes it hard to build reliable automation around the onboarding flow.


Minor observations (no action required)

  • default: false on the --no-install boolean flag is redundant (oclif boolean flags default false). No harm, just noise.
  • _npx path detection is npm-only by design and the PR description says so explicitly — acceptable trade-off.
  • The promptForConfirmation defaultYes addition is backward-compatible and the new tests cover all the meaningful cases.

Address PR review feedback:

- execSync had no timeout, so a flaky network or stalled npm registry could
  leave `ably init` hung indefinitely with no feedback during onboarding.
  Cap the global install at 120s; ETIMEDOUT flows through the existing
  catch block as a non-fatal warning.

- In `--json` mode the install step was invisible to agents on success
  (logProgress / logSuccessMessage are silent by design), so agents could
  only detect install failure, never success or a skip. Emit one
  structured `install` event per init run covering all outcomes:
  installed, skipped (no-install-flag | not-npx), failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread src/commands/init.ts Outdated
Comment thread src/commands/init.ts Outdated
Reorder the early-return guards in maybeInstallGlobally so the npx context
is checked first. --no-install is only meaningful when an install would
otherwise happen (i.e. when running via npx — the flag's own help text
says so). Previously `ably init --no-install --json` from a globally
installed binary would emit { status: "skipped", reason: "no-install-flag" }
even though the real reason it's skipped is that we're not in npx.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

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

Apart from above minor review comments
LGTM 👍

Co-authored-by: sachin shinde <sachinshinde7676@gmail.com>
@umair-ably umair-ably enabled auto-merge (squash) May 13, 2026 10:35
@umair-ably umair-ably merged commit 19ad85c into main May 13, 2026
12 checks passed
@umair-ably umair-ably deleted the feature/init-global-install branch May 13, 2026 10:51
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.

2 participants