Skip to content

feat(aur): publish a Node.js flavored clever-tools package - #1125

Draft
hsablonniere wants to merge 4 commits into
masterfrom
feat/aur-nodejs-package
Draft

feat(aur): publish a Node.js flavored clever-tools package#1125
hsablonniere wants to merge 4 commits into
masterfrom
feat/aur-nodejs-package

Conversation

@hsablonniere

@hsablonniere hsablonniere commented Aug 19, 2026

Copy link
Copy Markdown
Member

Closes #1124

Context

We now own the clever-tools AUR package. It used to be maintained by a third party, and its inherited PKGBUILD rebuilt, from the GitHub sources, the very same @yao-pkg/pkg binary that we already publish as clever-tools-bin: two AUR packages, one artifact, and a bump lag of days to weeks after each release.

It now ships the Node.js flavor of the CLI, running on the system nodejs package, and both packages are published by the release pipeline.

What Arch users get:

  • clever-tools-bin: self-contained binary, no runtime dependency, frozen Node.js runtime
  • clever-tools: Node.js flavor, arch=any, follows the system nodejs, so runtime security fixes are applied by pacman

Proposal

scripts/publish-aur.js now takes a flavor argument (bin or nodejs) and templates live in scripts/templates/aur/<flavor>/. One script, two AUR repositories, two release jobs: publish-aur-bin (unchanged, needs: publish-cellar-archives) and publish-aur-nodejs (needs: publish-npm).

Design decisions

The npm registry tarball as source. This is the URL template prescribed by the Arch Node.js package guidelines: immutable, versioned, and already attested by our npm publish --provenance. It is also what most Node.js CLIs packaged on the AUR use (gemini-cli, netlify, firebase-tools, devcontainer-cli, repomix).

npm ci against the lockfile, not the wiki's npm install -g --prefix. This is the one place where we knowingly deviate from the guidelines, and the reason is worth stating.

npm always strips package-lock.json from published tarballs, even when the file is explicitly listed in the files field, so the minimal recipe resolves the transitive dependency tree to whatever is latest at build time. Measured on 4.11.0: 52 transitive dependencies resolve to versions this release was never tested against, including major bumps (@pnpm/npm-conf 2.3.1 → 3.0.3, ansi-regex 5.0.1 → 6.3.0, @inquirer/core 10.2.0 → 10.3.2). Our direct dependencies are pinned to exact versions, their own dependencies are not.

Three consequences we are not willing to ship:

  • users would not install what we test and release: a regression introduced in a transitive dependency the week after a release lands on Arch users without any version of clever-tools moving
  • the lockfile carries an integrity hash per package; without it nothing is verified, and a compromised transitive dependency published after our release goes straight into the package
  • two people building the same pkgver-pkgrel two weeks apart get different packages

So the PKGBUILD fetches package-lock.json from the matching git tag as a second source, and runs npm ci --omit=dev --ignore-scripts. The rationale is also inlined in the PKGBUILD, since that file is read by AUR users and by whoever edits it next.

--ignore-scripts: no production dependency needs a lifecycle script, so none runs while packaging.

arch=any: verified, no native addon in the production tree.

A thin sh wrapper for /usr/bin/clever, exporting NO_UPDATE_NOTIFIER=1: a pacman-managed CLI must not advertise out-of-band updates. Same approach as command-code, claude-code and sillytavern on the AUR.

No jq cleanup of the _where and man keys: the recipe in the wiki dates back to npm 6, npm 11 no longer writes those references (verified on the built package).

.SRCINFO stays templated, and no containerized validation job was added. makepkg --printsrcinfo output is byte-identical to the templated file for both flavors, and what a build in CI would catch (a broken PKGBUILD) only happens when the template is edited, not on every tag. The local validation procedure is documented in CONTRIBUTING.md instead.

Drive-by fix

namcap reports 3 errors on the current clever-tools-bin: the PKGBUILD declares no dependency at all, while the binary is dynamically linked against glibc, libstdc++ and libgcc. Fixed with depends=('glibc' 'gcc-libs'), plus !debug to stop shipping an empty /usr/src/debug. Both packages now build with zero namcap error.

How to review

  • scripts/templates/aur/nodejs/PKGBUILD first, it carries the whole reasoning
  • then scripts/publish-aur.js for the flavor dispatch, and scripts/lib/npm-registry.js which derives the tarball sha512 from dist.integrity without downloading anything
  • .github/workflows/release.yml last, the new job is a twin of the existing one

How to test

Requires an Arch machine, makepkg and namcap. Against a local bare repository, nothing reaches AUR:

git init --bare -b master /tmp/aur-clever-tools.git
git clone /tmp/aur-clever-tools.git /tmp/aur-seed
git -C /tmp/aur-seed commit --allow-empty -m init
git -C /tmp/aur-seed push origin HEAD:master

AUR_GIT_URL=/tmp/aur-clever-tools.git ./scripts/publish-aur.js 4.11.0 nodejs

mkdir -p /tmp/aur-build && cp git-aur/PKGBUILD /tmp/aur-build/ && cd /tmp/aur-build
makepkg -f
makepkg --printsrcinfo | diff - ~/clever-tools/git-aur/.SRCINFO
namcap PKGBUILD && namcap clever-tools-*.pkg.tar.zst
sudo pacman -U ./clever-tools-4.11.0-1-any.pkg.tar.zst
clever version

Results on 4.11.0: package builds as any, 5.4 MB compressed / 31 MB installed, zero namcap error, clever version and clever help work, bash and zsh completions installed, and installing it correctly prompts to remove clever-tools-bin thanks to the new conflicts.

Same sequence with the bin flavor to check the existing channel still builds.

Out of scope

  • no CI job validating PKGBUILD templates; if we want one later, the right trigger is a PR touching scripts/templates/aur/**, not every tag

Before merging

The AUR_NODEJS_GIT_URL repository variable is already created (ssh://aur@aur.archlinux.org/clever-tools.git). Remaining check: confirm the CI SSH key has push rights on the transferred package, with ssh aur@aur.archlinux.org list-repos.

Add getSha512 next to getSha256, both factored on a private getFileHash.

Add getNpmTarballSha512, which derives a published tarball checksum from the
dist.integrity field of the npm registry, without downloading the tarball. The
registry is written to by npm publish but read through a CDN, so the read is
retried a few times.
We just recovered the ownership of the clever-tools AUR package, which used to
rebuild the very same binary as clever-tools-bin. It now ships the Node.js
flavor of the CLI, running on the system nodejs package.

PKGBUILD templates are split per flavor, and publish-aur.js takes the flavor as
an argument, so a single script publishes both AUR repositories.

The Node.js package is built from the npm registry tarball, as prescribed by the
Arch Node.js package guidelines, plus the package-lock.json of the matching git
tag as a second source. The lockfile is what makes the install reproducible:
without it, npm resolves the transitive dependency tree to whatever is latest at
build time (52 transitive packages differed when measured on 4.11.0), so users
would not install what we test and release.
Present clever-tools and clever-tools-bin side by side in the setup guide, and
document how to validate a PKGBUILD template locally before it reaches AUR.
namcap reports 3 errors on clever-tools-bin: the binary is dynamically linked
against the C and C++ runtimes, but the PKGBUILD declares no dependency at all.
Add glibc and gcc-libs, and !debug to stop shipping an empty /usr/src/debug.

On the Node.js package, drop the man pages vendored by dependencies, which
namcap flags as non FHS.

Both packages now build with zero namcap error.
@hsablonniere
hsablonniere requested a review from a team as a code owner August 19, 2026 14:18
@hsablonniere
hsablonniere marked this pull request as draft August 19, 2026 14:18
@github-actions

Copy link
Copy Markdown

🔎 A preview has been automatically published!

If you created the alias to the preview script, you can run this command to download and install this preview:

clever-preview update feat-aur-nodejs-package

You can also run it from your local repository:

./scripts/preview.js update feat-aur-nodejs-package
OS SHA256 checksum
🐧 linux a6ef6228f31b007a430c1068301e899227675d1bd7b9e577b8ce06e3819f660f
🍏 macos 7a10dddecb42288fd25b7faed4223179b13852bc6e3cab299b856ffc499c2170

This preview will be deleted once this PR is closed.

@hsablonniere

Copy link
Copy Markdown
Member Author

Follow-up: how do we disable the update notifier properly?

This PR ships a sh wrapper exporting NO_UPDATE_NOTIFIER=1. It works, but it papers over the fact that we have no real notion of "this install is managed by a package manager".

How it works today, for the record: nothing at the packaging level disables the notifier. rollup.config.js strips the import from the entry point when bundling:

if (id.endsWith('/bin/clever.js')) {
  return code.replace("import '../src/initial-update-notifier.js';", '');
}

The module and the update-notifier dependency are then tree-shaken. Verified on the generated bundle: only the definition of the --update-notifier CLI option remains, no updateNotifierModule, no tagsUrl. Since every binary comes from that bundle, .deb, .rpm, archives, Homebrew, Docker Hub and clever-tools-bin all inherit that behaviour for free. npm i -g clever-tools is the only distribution where the notifier actually runs.

Which means the disabling is a side effect of the binary build, not a decision. clever-tools (Node.js flavor) is our first distro package that runs the npm sources instead of a binary, so it is the first one where the question has to be answered explicitly, hence the wrapper.

Two things worth fixing beyond this PR:

Options for the "how do we know" part, roughly by decreasing cleanliness:

  1. an installation marker written by each package (a field added to the installed package.json, or a small file next to it), read at runtime. Explicit, works for every channel, and gives Adapt clever-tools update command based on the package manager #996 the information it needs.
  2. entry point path detection (/usr/lib, /usr/share, /opt, the Homebrew Cellar, ...). No packaging change, but heuristic.
  3. an environment variable set by a wrapper, what this PR does. Simple, per package, and stays useful as a fallback whatever we pick.

No decision needed to merge this PR, the wrapper is not a blocker. Opening the discussion so we do not end up with one ad hoc mechanism per packaging channel.

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.

Publish the clever-tools AUR package from our CI

1 participant