Skip to content

Scaffold the auditable CLI package - #1

Open
raghubetina wants to merge 1 commit into
mainfrom
codex/auditable-cli-scaffold
Open

Scaffold the auditable CLI package#1
raghubetina wants to merge 1 commit into
mainfrom
codex/auditable-cli-scaffold

Conversation

@raghubetina

@raghubetina raghubetina commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • create a source-executed ESM JavaScript CLI shell with deterministic help, version, exit status, and non-echoing errors
  • keep the published artifact free of runtime dependencies, lifecycle hooks, telemetry, update checks, and implicit network activity
  • pin Node 22.0 as the runtime floor while running strict quality tooling on Node 24
  • verify exact pack contents and install and execute the tarball offline
  • run the floor-version matrix on Linux and Windows, including the bundled npm-shell workaround required by Node 22.0 on Windows

Boundary

This intentionally adds no Foundation Plan, API, authentication, or release behavior. Those arrive as separate reviewed increments.

Verification

  • npm run check on Node 24.18.0
  • test runner invoked directly from another working directory on Node 22.0.0
  • exact tarball allowlist and offline install/execution smoke
  • local review loop complete with no open findings

@raghubetina

Copy link
Copy Markdown
Contributor Author

Review: technical

First review in this repo, single commit 61d1b8d..4250215. I ran everything locally on the pinned Node 24.18.0 and probed the runtime behavior directly.

Verified

  • npm run check passes end to end — typecheck, lint, format, the 11 node:test cases, the pack-content assertion, and the offline tarball smoke.
  • Behavior probes match the contract: bare invocation and --help print help to stdout with exit 0; --version prints 0.0.0; an unknown flag exits 2 with "Invalid arguments." and — the detail I was checking for — without echoing the flag, which carries the non-echoing discipline from the Rails loader diagnostics into this repo; an unknown positional gets its own "Unknown command." message, also exit 2; piping --help into a closed pipe does not crash, so the EPIPE handler works.
  • The auditability claims are enforced, not aspirational. package.json has zero runtime dependencies and a six-path files allowlist; scripts/check-pack.js asserts the exact tarball listing with assert.deepEqual, so an accidentally included file fails CI rather than shipping; .npmrc sets ignore-scripts=true (protecting contributors from dependency lifecycle scripts, the current supply-chain attack of choice) plus save-exact; and the smoke test installs the real packed tarball offline and executes it, on Linux and Windows in CI.
  • Both CI actions are SHA-pinned with version comments, matching the parent repo's convention.
  • The injected-writer design (run({argv, stdout, stderr}) returning an exit code) keeps the whole surface testable without subprocess spawning, and the tests use it; the subprocess path is still covered by the smoke script. Right split.

1. Claim the npm name before someone else does

npm view firstdraft returns 404 — the bare name is unclaimed. That is good news with a clock on it: this PR deliberately defers release automation, and an unclaimed name on the public registry is claimable by anyone in the interim, at which point the package, its bin name, and the README's install instructions all need renaming. Publishing a placeholder 0.0.0 (or at minimum creating the npm org/package entry) is a ten-minute task worth doing ahead of the rest of the release story. Nothing in this PR blocks on it; the risk just accrues daily until someone does it.

2. Small

  • src/version.js throws at import time if package.json lacks a string version — fail-loud at boot, consistent with the digest-pin habit in the parent repo.
  • check-pack.js requires npm_execpath and asserts it, so running the script outside npm fails with a message instead of mysterious behavior.
  • Node floor >=22.0.0 in engines with development pinned to 24 via .tool-versions, and CI exercising the floor — the claim in the PR body matches the workflow matrix.

Summary

The scaffold does what its title says: the published artifact is six readable files with no dependencies, no scripts, no network behavior, and CI that proves the tarball contains exactly those files and runs offline on both platforms. Finding 1 is outside the diff but time-sensitive. Nothing in the diff to fix.

@raghubetina

Copy link
Copy Markdown
Contributor Author

Lesson: what this PR actually did

A new repo, a new language, and eighty lines of actual program. The interesting content is everything wrapped around those eighty lines.

The one-sentence version

This creates the firstdraft command-line tool — which so far can only print its help and version — plus the machinery that proves the published package contains exactly six auditable files and nothing else.

Why the program is deliberately boring

The CLI right now handles --help, --version, and errors. That's it. No Plan authoring, no API calls, no login — the PR body defers all of it. The point of this increment is to settle the container before any cargo arrives: what ships in the package, what can run on install, what the error discipline is, which platforms are proven. Those decisions are much easier to review now, on eighty lines, than later inside a feature PR where nobody is looking at .npmrc.

The word doing the work in the PR title is "auditable." For a tool that users will run against their own projects with their own credentials someday, the promise is: anyone can read the entire published artifact in one sitting. Here that is enforced three ways:

  • package.json lists zero runtime dependencies — the install graph is one node. Every dependency you add is code your users run that you didn't write; this package starts from none and every future addition will be a visible, reviewable diff.
  • The files allowlist plus a CI script that asserts the packed tarball equals exactly LICENSE, README.md, bin/firstdraft.js, package.json, src/cli.js, src/version.js. If a stray file sneaks into a future tarball, CI fails. (Plenty of real-world npm incidents are "the package accidentally shipped .env" — this makes that a test failure instead of an incident.)
  • A smoke test that packs the real tarball, installs it offline in a temp directory, and runs it — on Linux and Windows. Offline matters: it proves the package needs nothing from the network at install or run time, which is also why there's no telemetry or update-check code to audit.

One more line worth knowing about, in .npmrc: ignore-scripts=true. npm packages can declare scripts that run automatically when installed; hijacked dependencies use exactly that hook to run malware on developer machines. This repo's own npm install refuses to run any of them. You can adopt that line in any project today.

Small choices that carry the house style

If you've been reading the parent-repo lessons, three familiar habits show up here in JavaScript clothing:

Errors don't echo input. Run firstdraft --secret-password-oops and the reply is "Invalid arguments." — the flag is never repeated. Same rule as the Rails loader's diagnostics (#134): anything a user types can end up in logs and screenshots, so error text describes the situation without quoting the input. Exit code 2 for usage errors, 0 for success, so scripts can branch on the result.

Fail loudly at startup. src/version.js reads the version out of package.json and throws if it isn't a string — the same reflex as the schema digest pins: validate your own integrity at boot, before doing work.

Pin what executes. The GitHub Actions are referenced by full commit SHA (with a human-readable version comment), so a compromised action tag can't silently change CI. The dev toolchain is locked (save-exact, package-lock, .tool-versions), while the supported floor is Node 22 — and CI actually runs the floor, not just the latest.

Design for tests. The whole CLI is one function: run({argv, stdout, stderr}) returns an exit code. Tests call it with fake writers and assert on strings — no subprocesses, no captured global state. The subprocess reality (shebang line, Windows shims, broken pipes) is covered separately by the smoke script. When something is annoying to test, the fix is usually this shape: move the logic into a function that takes its dependencies as arguments, and keep the thin shell that wires up the real world.

There's even a tiny lesson in the shell: bin/firstdraft.js swallows EPIPE errors on stdout. That's what makes firstdraft --help | head -1 exit quietly instead of crashing when head closes the pipe early — a detail almost every first CLI gets wrong and users experience as random stack traces.

The one thing the PR can't test

The package is named firstdraft, and as of this review that name is unclaimed on the npm registry. Unclaimed means available; it also means anyone else could take it tomorrow. Since release automation is deliberately deferred, there's a window where the name this whole package is built around isn't reserved. The review recommends claiming it now — a placeholder publish is ten minutes — because "check the outside world's assumptions, not just your own code" is part of reviewing too.

@raghubetina

Copy link
Copy Markdown
Contributor Author

Follow-up on the npm-name suggestion: I researched the current registry path and am deliberately not publishing the help-only 0.0.0 shell. npm policy says package names are for immediate active use, so a functionless reservation release creates an avoidable squatting-policy risk. Also, trusted publishing and staged publishing cannot bootstrap an unclaimed package today; npm/cli#8544 remains open.

The plan is to claim firstdraft with the first genuinely useful 0.1.0 Plan operation. That one bootstrap release will use a short-lived granular token in a protected GitHub environment plus GitHub provenance; immediately afterward we will configure tokenless trusted publishing/staged release, revoke the token, and disallow package tokens.

Sources: package-name policy, trusted-publisher prerequisites, initial-publish issue, and provenance guidance.

Create a source-executed ESM command shell for the unscoped firstdraft package with deterministic help, version, exit status, and non-echoing usage errors.

Keep the runtime dependency-free and free of installation hooks, telemetry, update checks, or implicit network access. Allowlist and inspect the exact tarball, then install and execute it offline as part of the checks.

Pin development and release work to Node 24 while proving the Node 22.0 runtime floor on Linux and Windows. Scope test discovery to the repository test tree and use the working npm shell shim on the affected Windows floor runtime.
@raghubetina
raghubetina force-pushed the codex/auditable-cli-scaffold branch from 3491e19 to 749a90e Compare July 28, 2026 20:20
@raghubetina

Copy link
Copy Markdown
Contributor Author

Finding closed: npm-name plan is better than the suggestion

The research in the follow-up settles it, and I'm withdrawing the placeholder-publish recommendation: a functionless 0.0.0 is itself the thing npm's package-name policy disputes, so the reservation would create the risk it was meant to remove, and npm/cli#8544 means trusted publishing could not bootstrap it anyway. Claiming the name with the first useful 0.1.0 via a short-lived granular token plus provenance, then converting to tokenless trusted publishing and revoking package tokens, is the right sequence — and writing it down now, with sources, means the bootstrap release will follow a plan instead of improvising under time pressure.

Also verified the 3491e19..749a90e amendment: npm run check passes end to end here, the Windows workaround carries its upstream citation (nodejs/node#52682) in a comment where the next person will find it, and the new scripts/run-tests.js closes a quiet failure mode — a bare node --test with no matching files exits green, while the runner now asserts the discovered test list is non-empty before running it. A vacuously passing suite is the kind of bug nobody notices for months; nice catch. Nothing open on this PR.

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