feat(init): install @ably/cli globally when launched via npx#394
Conversation
`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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis PR improves the Changes
Review Notes
|
There was a problem hiding this comment.
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:
- npm's actual error output on the terminal (good)
⚠ 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. -
execSyncwithstdio: "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 handling —
maybeInstallGloballywrapsrunGlobalInstallin 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). Noconsole.*or directchalkcalls. -
promptForConfirmationextension is fully backward compatible — all existing callers omit theoptionsargument 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 inafterEachprevents test pollution. -
Security: the
execSynccommand 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>
|
@claude review |
| rl.question(promptMessage, (answer) => { | ||
| rl.close(); | ||
| const response = answer.toLowerCase().trim(); | ||
| if (response === "") { |
There was a problem hiding this comment.
Just a question, are we sure response will always be empty string when user directly press enter?
There was a problem hiding this comment.
yes, if not, it resolves normally via yes/y
There was a problem hiding this comment.
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.ts — runGlobalInstall()
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.ts — maybeInstallGlobally()
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: falseon the--no-installboolean flag is redundant (oclif boolean flags default false). No harm, just noise._npxpath detection is npm-only by design and the PR description says so explicitly — acceptable trade-off.- The
promptForConfirmationdefaultYesaddition 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>
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>
sacOO7
left a comment
There was a problem hiding this comment.
Apart from above minor review comments
LGTM 👍
Co-authored-by: sachin shinde <sachinshinde7676@gmail.com>
Summary
ably initnow installs@ably/cliglobally when launched vianpx, so the documentednpx @ably/cli initonboarding command actually delivers a persistent CLI rather than landing in an ephemeral npx cache that disappears after init exits.--no-installflag for users who manage@ably/cliwith their own package manager.npm install -g @ably/climanually; auth and skills install still proceed.--jsonmode auto-installs (no prompt) and pipes npm's output so the NDJSON stream stays clean for agents.Behavior matrix
npx @ably/cli init(TTY)npm install -g @ably/cli@latest→ succeeds → auth → skillsnpx @ably/cli init --jsonnpx @ably/cli init --no-installably init(already on PATH)process.argv[1]doesn't contain_npxImplementation notes
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.promptForConfirmationgains 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 defaultdefaultYes: false.globalThis.__TEST_MOCKS__(isRunningFromNpx,confirmGlobalInstall,installGlobally) follow the existingrunLoginpattern so unit tests never shell out to realnpm install -g.Test plan
pnpm prepare— clean buildpnpm exec eslint .— 0 errors (10 pre-existing warnings unrelated)pnpm test test/unit/— 2509 passed, 1 todo, 1 skipped (185 files)npx @ably/cli initfrom a clean machine and verifyably --helpworks in a new shell afterwardsnpx @ably/cli init --jsonproduces a clean NDJSON stream (no npm output leakage)npx @ably/cli init --no-installskips install and continues to auth/skills🤖 Generated with Claude Code