Skip to content

Distribute CLI through npm with checksum-verified postinstall - #139

Merged
yourconscience merged 1 commit into
mainfrom
feat/npm-wrapper
Aug 21, 2026
Merged

Distribute CLI through npm with checksum-verified postinstall#139
yourconscience merged 1 commit into
mainfrom
feat/npm-wrapper

Conversation

@yourconscience

@yourconscience yourconscience commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Adds an npm distribution channel: npm i -g dotagents / npx dotagents.

  • npm/ wrapper package: bin shim spawns the platform binary; postinstall downloads the goreleaser asset for darwin/linux amd64+arm64 from GitHub Releases, verifies sha256 against the release checksums.txt, and extracts with system tar. No archive code is executed; unsupported platforms get a clear error pointing at brew/install.sh.
  • Environments with scripts disabled still get a helpful shim error instead of a broken bin.
  • Release workflow gains an npm-publish job (needs goreleaser, so assets exist first): sets the version from the tag, skips if already published, publishes with provenance. Requires the NPM_TOKEN secret.
  • CI runs the wrapper's node:test suite (platform mapping, asset naming, checksum parsing).
  • README quick start lists npm.

Smoke-tested end to end against the live v0.4.0 release: download, checksum verify, extract, and dotagents help all pass.

Summary by Sourcery

Distribute the dotagents CLI through npm with checksum-verified release binaries and automated, provenance-enabled publishing.

New Features:

  • Add npm installation support for macOS and Linux on amd64 and arm64 via a platform-aware CLI package.
  • Verify downloaded release archives against published SHA-256 checksums before installation and provide actionable errors for unsupported platforms or disabled install scripts.

Enhancements:

  • Add a resilient npm command shim that reports missing installed binaries clearly.
  • Document npm as an additional CLI installation option.

CI:

  • Add CI coverage for npm wrapper platform mapping, release asset naming, URL generation, checksum calculation, and checksum parsing.

Deployment:

  • Publish the npm package from tagged releases after release assets are built, with duplicate-version detection and provenance.

Tests:

  • Add Node.js tests for npm wrapper behavior and release metadata handling.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@sourcery-ai

sourcery-ai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds an npm-distributed wrapper package for the dotagents CLI that downloads a platform-specific binary from GitHub Releases during postinstall with checksum verification, wires it into an executable bin shim, tests the wrapper via node:test in CI, and publishes it from the release workflow if not already on npm.

Sequence diagram for npm postinstall and CLI execution

sequenceDiagram
  actor Developer
  participant npm as npm
  participant install_js as install_js
  participant GitHub as GitHub_Releases
  participant tar as tar
  participant bin_shim as bin_dotagents_js
  participant dotagents as dotagents_binary

  Developer->>npm: npm install -g dotagents
  npm->>install_js: run postinstall
  install_js->>GitHub: fetchBuffer(checksums.txt)
  install_js->>GitHub: fetchBuffer(dotagents_version_platform.tar.gz)
  install_js-->>install_js: sha256(archive)
  install_js-->>install_js: expectedChecksum(checksums, filename)
  install_js-->>install_js: compare checksum
  install_js->>tar: spawnSync(tar -xzf archive -C bin)
  tar-->>install_js: extracted dotagents
  install_js-->>Developer: binary installed in npm/bin

  Developer->>bin_shim: dotagents [args]
  bin_shim-->>bin_shim: fs.existsSync(binary)
  bin_shim->>dotagents: spawnSync(binary, args)
  dotagents-->>Developer: CLI output
Loading

File-Level Changes

Change Details Files
Introduce npm wrapper package that installs and runs the platform-specific dotagents binary downloaded from GitHub Releases with checksum verification.
  • Add Node-based postinstall script that maps Node platform/arch to GoReleaser targets, computes asset names and URLs, downloads the archive and checksums.txt, verifies the tarball sha256, and extracts the binary with system tar into a bin directory.
  • Implement bin shim that locates the downloaded dotagents binary, prints a helpful error if it is missing (e.g., scripts disabled), and forwards CLI arguments to it via spawnSync while preserving stdio and exit codes.
  • Export helper functions (platformTarget, assetName, releaseAssetUrl, sha256, expectedChecksum, install) from the installer module for reuse and testing.
npm/install.js
npm/bin/dotagents.js
Add automated tests and package metadata for the npm wrapper, and ensure only the necessary files are published.
  • Create node:test suite to validate platform mapping, asset naming, release URL construction, sha256 implementation, and checksum parsing behavior.
  • Define npm package metadata including name, dev version, repository, bin mapping, postinstall/test scripts, Node engines requirement, and restrict published files to the bin shim and installer script.
npm/install.test.js
npm/package.json
Integrate npm wrapper tests into CI and add an npm publish job to the release workflow.
  • Extend CI workflow with a job that sets up Node 20 and runs the npm wrapper tests using node --test against the installer test file.
  • Add npm-publish job to release workflow that runs after goreleaser, checks out the repo in the npm directory, configures Node 20 with npm registry auth, skips publishing when the tagged version is already on npm, otherwise bumps the npm package version to match the tag and publishes with provenance using NPM_TOKEN.
.github/workflows/ci.yml
.github/workflows/release.yml
Document npm installation path and update gitignore for npm artifacts.
  • Update README quick start section to include npm global installation as an alternative to Homebrew and the install script.
  • Expand .gitignore to exclude npm-related build or dependency artifacts from version control (exact patterns not reviewed in detail).
README.md
.gitignore

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai 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.

Hey - I've found 2 issues, and left some high level feedback:

  • The installer assumes tar is available on PATH; consider checking for its presence up front and emitting a clearer error message if it's missing rather than surfacing a generic spawn or non‑zero exit error.
  • In install.js, the error message for unsupported platforms currently uses the raw options.platform/arch or process.platform/arch; it may be helpful to explicitly mention that only darwin/linux on x64/arm64 are supported to avoid confusion for users on other combinations.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The installer assumes `tar` is available on PATH; consider checking for its presence up front and emitting a clearer error message if it's missing rather than surfacing a generic spawn or non‑zero exit error.
- In `install.js`, the error message for unsupported platforms currently uses the raw `options.platform`/`arch` or `process.platform`/`arch`; it may be helpful to explicitly mention that only darwin/linux on x64/arm64 are supported to avoid confusion for users on other combinations.

## Individual Comments

### Comment 1
<location path="npm/bin/dotagents.js" line_range="17-22" />
<code_context>
+  process.exit(1);
+}
+
+const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" });
+if (result.error) {
+  console.error(result.error.message);
+  process.exit(1);
+}
+process.exit(result.status === null ? 1 : result.status);
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Preserve child signal termination instead of treating it as a generic exit code 1.

When the child exits via a signal (e.g., SIGINT from Ctrl+C), `spawnSync` sets `status` to `null` and `signal` to the signal name. Converting `null` to exit code `1` hides that signal and diverges from typical CLI behavior. You could instead propagate the signal and only fall back to `1` when neither `status` nor `signal` is set:

```js
const { status, error, signal } = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" });
if (error) {
  console.error(error.message);
  process.exit(1);
}
if (signal) {
  process.kill(process.pid, signal);
} else {
  process.exit(status ?? 1);
}
```
</issue_to_address>

### Comment 2
<location path="npm/install.js" line_range="49-58" />
<code_context>
+function fetchBuffer(url, redirects = 0) {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Add a timeout to network downloads to avoid hanging installs on bad connections.

`fetchBuffer` currently uses `https.get` without a timeout, so a stalled connection could cause `npm install` to hang indefinitely. Please add a per-request timeout (e.g., `request.setTimeout(...)` and aborting on expiry) so the installer fails with a clear, timely error instead of hanging.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread npm/bin/dotagents.js
Comment on lines +17 to +22
const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" });
if (result.error) {
console.error(result.error.message);
process.exit(1);
}
process.exit(result.status === null ? 1 : result.status);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Preserve child signal termination instead of treating it as a generic exit code 1.

When the child exits via a signal (e.g., SIGINT from Ctrl+C), spawnSync sets status to null and signal to the signal name. Converting null to exit code 1 hides that signal and diverges from typical CLI behavior. You could instead propagate the signal and only fall back to 1 when neither status nor signal is set:

const { status, error, signal } = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" });
if (error) {
  console.error(error.message);
  process.exit(1);
}
if (signal) {
  process.kill(process.pid, signal);
} else {
  process.exit(status ?? 1);
}

Comment thread npm/install.js
Comment on lines +49 to +58
function fetchBuffer(url, redirects = 0) {
return new Promise((resolve, reject) => {
https
.get(url, (response) => {
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
response.resume();
if (redirects >= MAX_REDIRECTS) {
reject(new Error(`too many redirects fetching ${url}`));
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Add a timeout to network downloads to avoid hanging installs on bad connections.

fetchBuffer currently uses https.get without a timeout, so a stalled connection could cause npm install to hang indefinitely. Please add a per-request timeout (e.g., request.setTimeout(...) and aborting on expiry) so the installer fails with a clear, timely error instead of hanging.

@yourconscience
yourconscience merged commit 9d3ec9b into main Aug 21, 2026
5 checks passed
@yourconscience
yourconscience deleted the feat/npm-wrapper branch August 21, 2026 16:11
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