Skip to content

Distribute Stripe CLI via npm (npx @stripe/cli)#1595

Open
johno-stripe wants to merge 3 commits into
masterfrom
johno/install-by-npm
Open

Distribute Stripe CLI via npm (npx @stripe/cli)#1595
johno-stripe wants to merge 3 commits into
masterfrom
johno/install-by-npm

Conversation

@johno-stripe
Copy link
Copy Markdown

Reviewers

r? @
cc @stripe/developer-products

Summary

Adds npm as an install channel for the Stripe CLI. After this ships, developers with Node >= 18 can run:

npx @stripe/cli login
npm install -g @stripe/cli

This doesn't replace existing install paths (Homebrew, apt, Scoop, Docker) — it adds a fast path for the majority of the CLI's audience who already have Node installed.

Motivation

The Stripe CLI's primary audience is web developers integrating a payments API. Most already have Node.js. Today, installing the CLI on Windows requires Scoop (a package manager most developers haven't heard of), and every platform requires a separate tool. npx @stripe/cli is a universal one-liner.

What's included

  • 6 npm packages under the @stripe scope: a wrapper (@stripe/cli) and 5 platform-specific binary packages (darwin-arm64, darwin-x64, linux-x64, linux-arm64, win32-x64)
  • publish-npm job in the release workflow, gated on all three platform builds completing
  • Install-test coverage: 3 new jobs (npm, npm-no-optional, npx) across platforms with PagerDuty alerting
  • Fallback for --no-optional: the wrapper's postinstall downloads directly from GitHub Releases and verifies SHA256 checksums

No changes to Go code. No changes to goreleaser configs. The existing release artifacts are reused as-is. The README will only be updated to reflect this install path once this has been merged and published.

Prerequisites before merging

  • NPM_TOKEN repo secret (npm automation token scoped to @stripe org)

Test plan

  • node --check passes on all JS files
  • node npm/scripts/stamp-version.js 1.41.1 stamps all package.json files correctly
  • VERSION=1.41.1 GITHUB_TOKEN=... node npm/scripts/fetch-binaries.js downloads, verifies, and extracts all 5 platform binaries
  • Shim resolves platform package and launches binary (node npm/wrapper/bin/shim.js --version via local symlink)
  • Postinstall fallback exercises download → checksum → extraction path
  • Post-publish: npm install -g @stripe/cli && stripe --version on all 3 platforms (covered by new install-test jobs)
  • Post-publish: npx --yes @stripe/cli --version exits successfully
  • Post-publish: npm install -g @stripe/cli --no-optional && stripe --version exercises fallback path
### How it works

For anyone unfamiliar with the npm mechanisms at play:

Platform selection via optionalDependencies + os/cpu

npm's package.json supports two fields that declare platform requirements:

{ "os": ["darwin"], "cpu": ["arm64"] }

When a package declares these, npm will skip installing it on non-matching hosts. The wrapper package (@stripe/cli) lists all 5 platform packages as optionalDependencies:

"optionalDependencies": {
  "@stripe/cli-darwin-arm64": "1.41.1",
  "@stripe/cli-darwin-x64": "1.41.1",
  ...
}

optionalDependencies means "install these if you can, don't fail if you can't." Combined with the os/cpu guards, npm installs only the one matching the user's machine (typically 15-50 MB). The other 4 are silently skipped.

The bin field and the shim

The wrapper declares "bin": { "stripe": "bin/shim.js" }. When installed globally (npm install -g), npm symlinks stripe into the user's PATH pointing at shim.js. The shim is 23 lines: it detects the current platform, finds the installed binary package via require.resolve(), and spawns it with all arguments forwarded and stdio: 'inherit' (transparent passthrough).

The postinstall fallback

If npm was run with --no-optional (or a corporate policy strips optional deps), no platform package is installed. The wrapper's "scripts": { "postinstall": "node scripts/postinstall.js" } detects this, downloads the correct archive from GitHub Releases, verifies the SHA256 checksum against goreleaser's published checksum files, and extracts the binary to a vendor/bin/ directory. The shim falls back to this path when require.resolve() fails.

Why platform packages have a bin field too

Each platform package (e.g. @stripe/cli-darwin-arm64) also declares "bin": { "stripe": "bin/stripe" }. This is a safety net: if someone installs a platform package directly, the binary still gets symlinked correctly. It also allows npm to set the executable bit during installation on systems that require it.


</details>

@johno-stripe johno-stripe requested a review from a team as a code owner May 20, 2026 22:27
@johno-stripe johno-stripe force-pushed the johno/install-by-npm branch from 22bfe50 to 84be4b5 Compare May 21, 2026 19:01
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