Skip to content

feat: add npm distribution for one-command install#563

Merged
greynewell merged 3 commits intomainfrom
claude/npm-distribution-20260303-1310
Mar 3, 2026
Merged

feat: add npm distribution for one-command install#563
greynewell merged 3 commits intomainfrom
claude/npm-distribution-20260303-1310

Conversation

@greynewell
Copy link
Contributor

@greynewell greynewell commented Mar 3, 2026

Summary

  • Add npm package wrapper that downloads Go binaries from GitHub releases
  • Users can now install via npm install -g uncompact or npx uncompact install
  • Install script version-locks to the npm package version for consistency
  • Release workflow now publishes to npm after GoReleaser completes

Files Added

  • package.json - npm package definition with bin entry and postinstall hook
  • npm/install.js - downloads platform-specific binary from GitHub releases
  • npm/run.js - wrapper script that spawns the Go binary
  • .gitignore - ignores build artifacts and npm/bin/

Test Plan

  • Merge this PR
  • Create a new release tag (e.g., v0.2.0)
  • Verify npm publish succeeds in GitHub Actions
  • Test npm install -g uncompact installs correctly
  • Test npx uncompact --help works without global install

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Project is now available as an npm package (uncompact) for streamlined installation and updates.
    • Automated binary installation during npm setup.
  • Documentation

    • Updated installation instructions with npm-based setup option in README.
  • Bug Fixes

    • Improved hook detection logic.
    • Enhanced token budget handling for content rendering.

Closes #562

@coderabbitai
Copy link

coderabbitai bot commented Mar 3, 2026

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

This PR enables npm-based distribution of the Uncompact project by introducing an npm package manifest, automated binary download-and-install scripts, CLI launcher, and supporting GitHub Actions workflows for testing and publishing to npm upon release tags.

Changes

Cohort / File(s) Summary
NPM Package Distribution
package.json, npm/install.js, npm/run.js
Introduces npm package configuration with automatic binary installation (platform/arch detection, GitHub release download, extraction) and CLI launcher that locates and executes the binary with argument forwarding.
Release & CI Workflows
.github/workflows/release.yml, .github/workflows/ci.yml
Adds npm-publish job to release workflow (publishes to npm after goreleaser with version sync) and new CI job validating Go/Node.js builds and npm script execution.
Configuration & VCS
.gitignore
Establishes ignore patterns for Go binaries, npm artifacts, IDE/OS files, and temporary files.
Documentation
README.md
Updates Quick Start section with explicit npm installation instructions ("npm install -g uncompact") and binary download alternatives.
Logic Enhancements
internal/hooks/hooks.go, internal/template/render.go
Expands UserPromptSubmit hook detection to recognize "show-hook.sh" as equivalent to cache-show commands; adds post-append token budget guard in render to re-truncate if total exceeds MaxTokens after note insertion.

Sequence Diagram

sequenceDiagram
    actor User
    participant npm as npm Registry
    participant GitHub as GitHub API
    participant Local as Local Filesystem
    participant Binary as uncompact Binary

    User->>npm: npm install -g uncompact
    npm->>Local: extract package.json
    npm->>Local: run postinstall script
    Local->>Local: invoke npm/install.js
    Local->>GitHub: fetch release info
    GitHub-->>Local: return release metadata
    Local->>GitHub: download binary asset
    GitHub-->>Local: stream binary (tar.gz/zip)
    Local->>Local: extract to npm/bin
    npm-->>User: install complete
    
    User->>Local: uncompact [args]
    Local->>Local: npm/run.js resolves binary path
    Local->>Binary: spawn with arguments
    Binary->>Binary: process data
    Binary-->>User: output result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Poem

📦 One npm install, and the binary flows,
From GitHub's release to your local bin,
No wrestling with downloads—just watch it grow,
A postinstall hook brings uncompact in! ✨
Click once, compile never, the onboarding glows.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Minor out-of-scope changes detected: updates to README.md documentation and internal/template/render.go (token budget logic) and internal/hooks/hooks.go (hook detection) are unrelated to npm distribution. Remove or move to separate PRs the documentation updates in README.md and the unrelated code changes in internal/template/render.go and internal/hooks/hooks.go to keep this PR focused on npm distribution.
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add npm distribution for one-command install' directly and clearly describes the main change: introducing npm distribution for simplified installation.
Linked Issues check ✅ Passed The PR fully implements issue #562 requirements: creates npm-based one-click installer via package.json and npm/install.js, enables npx usage, provides user-facing output during installation and execution via npm/run.js, and ensures version consistency.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/npm-distribution-20260303-1310

Comment @coderabbitai help to get the list of available commands and usage tips.

Add npm package wrapper that downloads the appropriate Go binary from
GitHub releases on postinstall. Users can now install via:

  npm install -g uncompact
  npx uncompact install

The install script reads the package.json version and fetches the
matching GitHub release, ensuring version consistency. The release
workflow now publishes to npm after GoReleaser completes.

Co-Authored-By: Grey Newell <greyshipscode@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Made-with: Cursor
@greynewell greynewell force-pushed the claude/npm-distribution-20260303-1310 branch from 0571e26 to 23a69a8 Compare March 3, 2026 18:13
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (1)
.github/workflows/release.yml (1)

51-54: Make npm publish idempotent for workflow re-runs.

If this workflow is re-run for the same tag, npm publish will fail because that version is already on npm. The suggested patch adds a quick check before publishing: it uses npm view uncompact@$VERSION version to see if that version already exists. If it does, the step exits successfully without publishing (idempotent behavior). If it doesn't, the publish proceeds normally.

This is especially useful for recover-from-failure scenarios—you can just re-trigger the workflow without manual edits.

Suggested patch
       - name: Publish to npm
-        run: npm publish --access public
+        run: |
+          VERSION="${GITHUB_REF_NAME#v}"
+          if npm view "uncompact@$VERSION" version >/dev/null 2>&1; then
+            echo "uncompact@$VERSION already exists on npm; skipping publish."
+            exit 0
+          fi
+          npm publish --access public
         env:
           NODE_AUTH_TOKEN: ${{ secrets.SUPERMODEL_NPM_TOKEN }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 51 - 54, The Publish to npm step
currently always runs "npm publish --access public" which fails if the same
package version already exists; update the "Publish to npm" step to first check
existence using "npm view uncompact@$VERSION version" (or equivalent variable
for the package/version) and, if that command returns a version, exit the step
successfully (skip publishing), otherwise proceed to run "npm publish --access
public"; ensure the step uses the same NODE_AUTH_TOKEN env and that the
existence check returns a zero status when found so the step becomes idempotent
for re-runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/hooks/hooks.go`:
- Around line 189-190: mergeHooks currently fails to treat "show-hook.sh" as
equivalent for hooks["UserPromptSubmit"], causing duplicate hooks in
partial-install merges; update mergeHooks so the equivalence check for
UserPromptSubmit mirrors the detection check by treating "uncompact show-cache"
and "show-hook.sh" as equivalent (use commandExistsInHooks or the same
equivalence list used in detection when comparing hooks["UserPromptSubmit"]).

In `@internal/template/render.go`:
- Around line 145-162: The appended note can still push output over
opts.MaxTokens; after result += note and resultTokens = countTokens(result) add
a final hard-budget guard: if opts.MaxTokens > 0 and resultTokens >
opts.MaxTokens compute budget = max(1, opts.MaxTokens - noteTokens) and call
truncateToTokenBudget(graph, projectName, budget,
graph.Stats.CircularDependencyCycles, opts.WorkingMemory) to get a truncated
result, then rebuild note via buildNote(resultTokens) and reassign result and
resultTokens using countTokens(result) so the final string never exceeds
opts.MaxTokens. Include these checks around the existing result/ note handling
(referencing result, note, resultTokens, noteTokens, opts.MaxTokens,
truncateToTokenBudget, buildNote, countTokens).

In `@npm/install.js`:
- Around line 43-60: The httpsGet function can hang indefinitely on stalled
connections; modify it to call request.setTimeout(20000) immediately after
creating the request so the request will time out after 20s and trigger the
existing error handler (which will cause request.destroy()/error to propagate);
keep the current reject handling intact so the Promise is rejected on timeout
and redirects/responses still work; update references in the httpsGet function
where the request is created to include this timeout.
- Around line 67-79: The getRelease function currently falls back to the
"latest" release when a specific tagged release (computed as tag from version)
is not found, which breaks version-locking; remove the fallback behavior so that
when httpsGetJson for the tag throws in getRelease (and version is provided and
not "0.0.0") the error is propagated instead of calling the releases/latest
endpoint — update getRelease to only try the tagged URL when version is
specified and, on failure, rethrow the error (do not call the latest URL); keep
the behavior for when version is absent so callers requesting latest still work.
- Line 84: Replace shell-string execSync calls with execFileSync and pass
command arguments as an array to avoid shell quoting issues: for the tar
extraction that currently calls execSync(`tar -xzf "${tarPath}" -C
"${destDir}"`, { stdio: "pipe" }), call execFileSync("tar", ["-xzf", tarPath,
"-C", destDir], { stdio: "pipe" }); do the same for the other execSync uses that
invoke unzip/powershell (the other execSync occurrences in this file) by mapping
their command and flags into an array and using execFileSync with identical
stdio/options so paths like tarPath and destDir are passed directly without
shell parsing.

In `@npm/run.js`:
- Around line 54-56: The child process "close" handler currently calls
process.exit(code ?? 0) which masks signal terminations (code === null) and
returns 0; modify the handler for child.on("close", ...) to detect when code is
null and instead propagate the terminating signal to the current process (e.g.,
re-emit or kill with the same signal) and use a defensive numeric fallback of 1
for unexpected states; update the logic around child.on("close", (code, signal)
=> ...) so signal terminations are forwarded and non-signal failures exit with
code || 1.

In `@README.md`:
- Around line 64-68: Replace the non-mutating example shown as "npx uncompact
install" with a harmless/read-only command to avoid duplicating the actual
install step (which is shown later as "uncompact install"); specifically change
the example invocation string 'npx uncompact install' to a non-mutating command
like 'npx uncompact --help' (or any read-only command) in the README Step 1 so
the text "run without installing" correctly demonstrates a non-install action.

---

Nitpick comments:
In @.github/workflows/release.yml:
- Around line 51-54: The Publish to npm step currently always runs "npm publish
--access public" which fails if the same package version already exists; update
the "Publish to npm" step to first check existence using "npm view
uncompact@$VERSION version" (or equivalent variable for the package/version)
and, if that command returns a version, exit the step successfully (skip
publishing), otherwise proceed to run "npm publish --access public"; ensure the
step uses the same NODE_AUTH_TOKEN env and that the existence check returns a
zero status when found so the step becomes idempotent for re-runs.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7115110 and 0571e26.

📒 Files selected for processing (10)
  • .github/workflows/release.yml
  • .gitignore
  • README.md
  • cmd/run.go
  • internal/hooks/hooks.go
  • internal/template/render.go
  • npm/install.js
  • npm/run.js
  • package.json
  • scripts/uncompact-hook.sh

npm/run.js Outdated
Comment on lines +54 to +56
child.on("close", (code) => {
process.exit(code ?? 0);
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "run.js" -path "*/npm/*" | head -20

Repository: supermodeltools/Uncompact

Length of output: 81


🏁 Script executed:

cat -n npm/run.js | sed -n '40,70p'

Repository: supermodeltools/Uncompact

Length of output: 569


🌐 Web query:

Node.js child_process close event code null signal behavior

💡 Result:

In Node.js, the ChildProcess 'close' event is emitted after the child has ended and all its stdio streams have been closed (so it can occur later than 'exit'). It is guaranteed to fire after 'exit', or after 'error' if the process failed to spawn. [1]

The 'close' handler receives (code, signal):

  • code is the numeric exit code if the process exited normally; otherwise it is null. [1]
  • signal is the signal name if the process was terminated by a signal; otherwise it is null. [1]
  • One of code or signal will always be non-null (so code === null implies signal is a string like "SIGTERM" / "SIGKILL"). [1]

Example interpretation

  • 'close'(0, null) : clean success exit.
  • 'close'(null, 'SIGTERM') : terminated by signal, so no numeric exit code. [1]

Source: Node.js child_process docs for 'close' / 'exit'. [1]

References:
[1] Node.js v22.x child_process documentation (ChildProcess events 'close' / 'exit'). (nodejs.org)

Citations:


Propagate signal terminations, don't mask them.

When the child gets terminated by a signal (like SIGTERM), the code parameter is null. Your current code then exits with 0—making it look like everything was fine when it actually wasn't. Here's the concrete problem: if a deployment script kills the child with SIGTERM, your wrapper cheerfully reports success to the orchestrator, even though things went sideways.

The fix is straightforward: check if a signal happened, then propagate that signal to your own process. This lets the parent shell and any monitoring tools know what actually occurred. And change the fallback from 0 to 1 to be defensive about unexpected states.

diff --git a/npm/run.js b/npm/run.js
@@
-  child.on("close", (code) => {
-    process.exit(code ?? 0);
+  child.on("close", (code, signal) => {
+    if (signal) {
+      process.kill(process.pid, signal);
+      return;
+    }
+    process.exit(code ?? 1);
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@npm/run.js` around lines 54 - 56, The child process "close" handler currently
calls process.exit(code ?? 0) which masks signal terminations (code === null)
and returns 0; modify the handler for child.on("close", ...) to detect when code
is null and instead propagate the terminating signal to the current process
(e.g., re-emit or kill with the same signal) and use a defensive numeric
fallback of 1 for unexpected states; update the logic around child.on("close",
(code, signal) => ...) so signal terminations are forwarded and non-signal
failures exit with code || 1.

greynewell and others added 2 commits March 3, 2026 13:19
- Add idempotent npm publish check to release workflow
- Add 20s timeout and remove latest fallback in npm install script
- Use execFileSync to avoid shell quoting issues in install script
- Handle signals in npm run wrapper
- Add final hard-budget guard in render logic to ensure token limit
- Update README npx example to be non-mutating

Co-Authored-By: Grey Newell <greyshipscode@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Made-with: Cursor
- Add .github/workflows/ci.yml to test Go build/vet and Node script syntax on PRs
- Fix mergeHooks in internal/hooks/hooks.go to treat "show-hook.sh" as equivalent
  to "uncompact show-cache" for UserPromptSubmit to avoid duplicates

Co-Authored-By: Grey Newell <greyshipscode@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Made-with: Cursor
@greynewell greynewell merged commit 050f26a into main Mar 3, 2026
2 of 3 checks passed
@greynewell greynewell deleted the claude/npm-distribution-20260303-1310 branch March 3, 2026 18:24
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.

One-click install with readable output

1 participant