Skip to content

feat: install the launcher's CLI version locally from a tested lockfile - #540

Merged
theCodeDrift merged 11 commits into
pinned-cli-wrapperfrom
launcher-cache
Oct 20, 2026
Merged

theCodeDrift merged 11 commits into
pinned-cli-wrapperfrom
launcher-cache

Conversation

@theCodeDrift

@theCodeDrift theCodeDrift commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Stack (root → tip):

The project launcher from #537 still ran every command through npx --yes <package>@<version>. With this change it installs the recorded version once into a git-ignored .taskless/.cache/, from a lockfile the release build derived from our own pnpm-lock.yaml, then runs it from there with the same node. After the first run, a Taskless command touches neither the network nor npm, and every machine installs the dependency tree CI tested.

Why a lockfile, and why not a shrinkwrap

A published package carries only its package.json ranges, so a fresh install of 0.13.1 resolved tsx 4.23.15 and esbuild 0.28.x, where we test 4.21.0 and 0.27.3 (#538). A published npm-shrinkwrap.json can't fix that for this package. Measured with npm 11.8 through a local registry:

  • A full shrinkwrap installs every platform's ast-grep, Vale and esbuild binaries: 1.0 GB instead of 110 MB.
  • One without the platform packages installs no binaries at all.

npm unpacks a dependency's shrinkwrap after its platform check, which is exactly the case our per-platform optional packages hit. Rewriting package.json to exact versions at publish was ruled out.

What changes

  • Build: nightly and release builds derive an npm v3 lockfile for their own package from pnpm-lock.yaml, offline, and embed it (scripts/install-lockfile.ts).
    • Every dependency is pinned by version and integrity, with its os/cpu.
    • The CLI's own entry has no integrity, since the lockfile ships inside that tarball. The exact version pins it instead.
    • The build fails if a dependency appears at two versions or has no integrity.
    • package.json keeps its ranges, so development stays loose.
  • init: writes .taskless/bin/cli.lock.json (or cli-nightly.lock.json) beside the launcher, reported as created/restored like the launcher, never touching the other package's pair. The --json envelope gains a lockfile field.
    • It also prunes cache entries that install.launchers no longer records.
    • It downloads nothing.
  • Launcher:
    • On a cache miss, it runs npm ci --ignore-scripts from its lockfile into a temporary directory, renamed into place only when complete. A concurrent first run that loses the rename uses the winner's copy.
    • It refuses to install when the lockfile is missing or names another version.
    • npm's output goes to standard error.
    • It writes .taskless/.cache/.gitignore (*) and prunes after a new install.
    • It runs the CLI with process.execPath and no shell, so cmd.exe quoting is now needed only for npm.
  • Private registries: the launcher copies the nearest .npmrc at or above the project into the install directory, then removes it. npm reads project settings only from the directory it installs into.
    • It doesn't ask npm prefix: npm masks token-like strings in its output, UUID path segments included. The end-to-end run caught it printing …/***/… and silently using the public registry.
  • Recipes and skill: the ci recipe (topic v8) caches .taskless/.cache, keyed on runner.os and hashFiles('.taskless/bin/cli.lock.json'). The init recipe (topic v12) documents lockfile. The hooks recipe (topic v5) and the skill describe the one-time install.

Verification

  • Checks: pnpm typecheck and pnpm lint clean, and 2,460 tests pass. New tests:
    • lockfile derivation over fixtures;
    • the launcher against a stand-in npm: first and second run, clean --json, lost rename race, missing or mismatched lockfile, npm failing or missing, .npmrc copied from the project and from a parent and not kept, self-ignore, pruning;
    • init writing, restoring and pruning, for both the release and the nightly.
  • Real tree: the lockfile derived from pnpm-lock.yaml for 0.13.1, run through real npm ci, installed tsx 4.21.0, esbuild 0.27.3 and get-tsconfig 4.13.6, only darwin-arm64 binaries, 109 MB.
  • End to end: the built CLI's init in a scratch project, then ./.taskless/bin/cli.mjs check from the real registry.
    • The first run installed and reported an ast-grep finding and a Vale finding.
    • The second run did the same with an npm on PATH that records any call; it was never called.
    • git check-ignore confirms the cache is ignored and the lockfile is not.
    • With a project .npmrc pointing at an unreachable registry and an empty npm cache, the install failed with npm's ECONNREFUSED and left no temporary directory or .npmrc.
  • Archive: checked against the main specs, with no scenario dropped: cli-project-launcher 13 → 27, cli-init 151 → 156, cli-agent 66 → 67.

Follow-ups (not blocking)

  • A Windows run of a first install and a later run.
  • No runtime rule was exercised end to end; tsx resolution is unchanged and its version is now the tested one.

Delivery shape: single PR, stacked on #537, merging forward after it. It depends on the launcher and is safe on its own once #537 lands. The changeset extends .changeset/pinned-cli-wrapper.md rather than adding a second one.

Fixes #538

@theCodeDrift

Copy link
Copy Markdown
Member Author

@claude /review

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @theCodeDrift's task in 5m 33s —— View job


Review complete

Review mode: incremental — read 0 prior review thread(s) before reviewing. .prior-review.json held no review threads, review summaries, or comments beyond the trigger comment itself, so this is the first full review of this PR — nothing to mark as already addressed.

  • Read .prior-review.json (0 threads/reviews)
  • Fetch PR diff and metadata (gh pr diff, gh pr view)
  • Read changed files for full context
  • Review launcher.mjs / launcher.ts (install, cache, .npmrc handling, pruning)
  • Review install-lockfile.ts (build-time lockfile derivation from pnpm-lock.yaml)
  • Review init.ts / install.ts / wizard/index.ts
  • Review tests for adequacy
  • Post findings

Overall assessment

This is a large, carefully specified change (design doc, proposal, and specs all line up with the implementation), and the test suite is unusually thorough — missing/mismatched lockfile, npm failure, no-bin, npm-not-on-PATH, install race, .npmrc discovery (including the monorepo case), pruning (stale temp dirs, superseded versions, keeping the sibling package), JSON-output cleanliness, and the lockfile builder itself (two-versions-of-one-package, missing integrity, pnpm peer-suffix stripping). I traced the dependency-tree walk in scripts/install-lockfile.ts (including the optional→required upgrade path and its cycle termination) and the launcher's install/rename/prune logic in detail and didn't find a correctness break in the core flow.

Two [New] findings posted inline on packages/cli/assets/launcher/launcher.mjs, both edge cases rather than mainline breaks:

  1. Line 127 — projectNpmrc() calls homedir() with no surrounding try/catch. If $HOME is unset and the Node process has no passwd entry (a known gotcha for minimal/rootless containers — exactly the kind of environment the new ci recipe is built to run in), this throws an uncaught exception on a version's first run instead of the designed taskless: ... error message.
  2. Line 265 — The template's prune() wraps the stale-.tmp-* cleanup and the version-pruning loop in one try/catch, so a statSync failure on one temp directory silently skips pruning superseded versions too. The TS twin (pruneLauncherCache in src/install/launcher.ts) isolates stat failures per entry so the second loop still runs. Low severity given pruning is explicitly best-effort/retried next time, but worth aligning since the design calls these out as two copies of the same rule.

Nothing else rose to the level of a correctness, security, performance, or test-coverage finding. The two already-flagged, non-blocking follow-ups in the PR body (a Windows end-to-end run; no end-to-end exercise of a runtime rule through the new install path) are the author's own call, not repeated here.

Comment thread packages/cli/assets/launcher/launcher.mjs Outdated
Comment thread packages/cli/assets/launcher/launcher.mjs Outdated
@theCodeDrift

Copy link
Copy Markdown
Member Author

Re: @github-actions — "Claude finished @theCodeDrift's task in 5m 33s…"
#540 (comment)

Both inline findings are fixed in c4d0f58 (homedir guard in the .npmrc walk; per-step error containment in the launcher prune, with a test) and their threads are resolved.

— AI Coding Agent

@theCodeDrift
theCodeDrift merged commit dd99add into main Oct 20, 2026
4 checks passed
@theCodeDrift
theCodeDrift deleted the launcher-cache branch October 20, 2026 07:23
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.

Pin the CLI's tsx dependency to an exact version

1 participant