ahel is live on Product Hunt today. Upvote

Package Scripts

SkillDocs & knowledge

Esposter pnpm script reference, apps/web scripts (lint, typecheck, test, format, dev, build), the root scripts (test, coverage, bench, graph:gen, outdated:dependencies, release), a Settled list (no root `build:<app>` script per app, the release stays one local script rather than a CI publish, a renamed export of a published package is never a major), `nuxt typecheck` and the root lint being the only checks that match CI, the wrapper exit code a backgrounded run reports, `oxfmt` never touching markdown so a table is realigned by hand, the `scriptsComments` key that holds only a script's `@TODO:`, the check suite run once per chunk with tests scoped to the paths touched, and the pnpm traps (a `--filter` matching nothing exits 0, `pnpm <script> -- <args>` drops the args, a workflow runs the script not the binary), plus deep dives on how each pnpm trap fails, on running a `.ts` script under `node` where it can and `tsx` where it cannot without ever bending the code to fit `node`, and why a pre-install CI check is shell, and on the `ai:sweep:*` / `ai:coderabbit:*` script catalogue an agent runs. Apply whenever running or recommending package scripts.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Package Scripts skill

What this skill tells your AI

The instructions your AI receives, as published by esposter/esposter in .agents/skills/package-scripts/SKILL.md and read by ahel’s review.

apps/web scripts run from apps/web/; root scripts run from the repo root. Always pnpm — never npm or npx.

Settled — do not re-propose

  • A root build:<app> script per app. build:web, build:functions and build:infra existed, one pnpm -C apps/<app> run build delegation each, and build:web carried a virrun -- besides. One app is that -C line at the call site: a root script that only delegates there is a second definition of the same line, one per app, that says nothing the flag does not. The sandbox prefix went with it — the app build is native for the reason apps/web/content/docs/virrun/adoption.md gives. A root script earns its line only when it adds what a call site cannot say in a flag: a selector (build:packages), a chain (build, release), or a virrun -- on a command a win32 developer runs (typecheck, test).
  • Majoring the published packages because an export was renamed. lerna.json is conventionalCommits: true in fixed mode, so a BREAKING CHANGE: footer moves all seven public packages to the next whole number — including the ones that changed nothing. A renamed export is not a breaking change for these packages and a rename ships as the refactor it is; the reasoning, and the condition that would end it, are apps/web/content/docs/architecture/no-compatibility-debt.md.
  • Splitting the release into a local lerna version and a CI publish — a tag-triggered job publishing through npm's trusted publishing, which lerna-lite supports out of the box (id-token: write, a per-package token exchange, provenance attached for a public package). It buys an attestation that the published tarball is the one CI built. Nobody here is asking for that attestation, and the price is a release path that lives in two places and a per-package trusted-publisher registration on npmjs.com that fails closed the day a new package is added. One script, run locally, is the whole release: pnpm release gates the tree and hands lerna publish a version, a tag and a dist it just built, and 🚀 Release turns the pushed tag into a GitHub release. Publishing from a developer's machine is the deliberate simplification, not an oversight.

apps/web

CommandRunsWhen to use
pnpm lintTIMING=1 eslint .CI/check-only lint verification
pnpm lint:fixTIMING=1 eslint --fix .ESLint only, this package only — never the last lint a change runs
pnpm typechecknuxt typecheckTypeScript type checking — never vue-tsc directly, see below
pnpm testvitest (watch mode)Run this package's tests in watch mode
pnpm formatoxfmtFormat code
pnpm format:checkoxfmt --checkCheck formatting without writing
pnpm devnuxt devStart dev server
pnpm benchvitest bench --runRun this package's benchmarks
pnpm buildnuxt buildBuild for production

nuxt typecheck is the only typecheck, and pnpm lint from the repo root is the only lint. Reaching past either for the underlying binary — vue-tsc -p tsconfig.json in apps/web, oxlint over a path — checks strictly less than CI does and reports success while CI fails: the app's real project is the generated .nuxt tsconfig rather than the one in the package, and a package's lint is ESLint alone. Which rules only the root pass carries, and when a targeted oxlint is still worth running, is the oxlint skill's.

A backgrounded run of either reports the wrapper's exit code, which is 0 even when the run inside it failed. Read the output for exited 1 or a problem/error line rather than trusting the status.

oxfmt formats code, not markdown, and no prettier binary is installed — reaching for npx prettier or pnpm exec prettier fails. A .md file's own layout is therefore hand-maintained: a table whose cells changed width is realigned in the edit that changed them, because nothing downstream will do it and nothing fails when it drifts.

Root Scripts

CommandRunsNotes
pnpm iRefresh deps/lockfile after manifest changes.
pnpm testvirrun -- vitest runWhole suite once via the root vitest projects config — apps/*, packages/*, scripts. Never run bare (testing skill): CI shards vitest directly rather than calling this, so a bare local run only buys a slower answer. Takes paths and vitest flags — --project "apps/web" is the app suite alone.
pnpm test:packagesvirrun -- vitest run --project "packages/*"Every library suite, no Nuxt — a release gate, and local shorthand for the same filter. Takes paths like pnpm test does: pass them.
pnpm build--filter "@esposter/web..." run buildThe app and everything it imports, one derived selector, topological so the app builds last. No virrun -- and no build:<app> — see Settled. The bare name meaning the app rather than the workspace is deliberate — Railway runs it as its default build command.
pnpm build:packagespnpm -r --filter "./packages/*" run buildThe libraries as a set: what CI caches and hands to every check. No app is in it, since nothing imports an app's dist — the coverage shards build the two whose bundles the suite asserts against.
pnpm coveragevitest run --coverage (no virrun)Root-only (packages have no coverage script). Both CI test jobs call it with trailing flags rather than reaching for vitest themselves — a shard is pnpm coverage --reporter=default --reporter=blob --shard=i/n, the merge is pnpm coverage --merge-reports.
pnpm benchpnpm -r --workspace-concurrency=1 --if-present run benchEvery member owning a bench, one at a time — the local gate that rewrites the committed *.bench.md. Why the concurrency flag is load-bearing is the bench skill's.
pnpm bench:civitest bench --runOne process over every project's bench files at once — a smoke signal that they all still execute, not a measurement. What the 🏎️ Bench job runs, and the only caller. Not a shorter pnpm bench; see the bench skill.
pnpm outdated:dependenciespnpm -C scripts run outdated:dependenciesChecks manifests use catalog:/workspace:, and catalog/configDependency/engines specifiers against the lockfile + npm latest.
pnpm graph:genpnpm -C scripts run graph:genRegenerate dependency-graph.svg from the workspace manifests. Run it after changing one.
pnpm releasechecks, then lerna publishThe whole release, run locally — see Settled above. Lerna versions EVERY workspace member (lerna.json's packages repeats the pnpm globs, or it silently defaults to packages/*), while the gates in front of it stay packages/*-scoped and publish skips the private ones — apps/web/content/docs/architecture/monorepo-tooling.md.

The ai:<domain>:<verb> entries are the scripts no human types, named by audience — the rule is the skill-authoring skill's (references/embedded-recipes.md). Which ones exist and what each prints, read when a sweep or a review needs its script: references/ai-scripts.md.

A .ts script runs under node where it can, tsx where it cannot — references/typescript-scripts.md

Node strips types natively; an enum, a tsconfig alias or an extensionless import moves the script to tsx, and the code is never bent to fit node. Adding a script, choosing its runner, or writing a check CI runs before an install is that page.

scriptsComments

JSON has no comments, so a script that records something to undo later carries it in a sibling top-level scriptsComments object keyed by the script name — never a "// …" key inside scripts, which pnpm lists as a runnable script. The value is one @TODO:-prefixed string naming the condition that ends it, and that is all the object holds: why a script is shaped as it is lives in this skill's table and the docs page that owns it, where the reasoning already sits, so a copy in the manifest is a second one that drifts.

{
  "scriptsComments": {
    "build": "@TODO: restore `pnpm build:docs` to the chain when …"
  }
}

Check Suite (after edits)

The suite runs once per coherent chunk, on develop, before that chunk is pushed — not per commit — see the git skill's "Verify On develop". Run before declaring work done:

  1. pnpm typecheck
  2. pnpm lint:fix from the repo root — CI runs root pnpm lint, and root lint:fix is that same scope (oxlint, ESLint, every package's lint) with autofix on, so what it leaves unfixed is what CI would report; a package's own lint:fix is ESLint over that package alone (oxlint skill). Reach for the package-local one only to iterate inside one package mid-change; the last lint a chunk runs is the root one.
  3. Tests for what the change touched, passed as package-relative paths from apps/web/ — root pnpm test is the whole suite under virrun and resolves a path against the repo root, so app/... matches nothing there: pnpm test app/services/message/emoji app/components/Styled/EmojiPicker -u --run. -u refreshes snapshots, --run forces a single non-watch run. Never the whole suite — the ban, and how the paths are scoped, are the testing skill's ("Never run the full suite locally"). A test-only edit runs the test file(s) it touched. Only a doc-only edit skips the step, and not one under apps/web/content/docs, whose index.test.ts parses every page's diagram.

Key Rules

  • A --filter that matches nothing exits 0 — prefer pnpm -C <dir>, and treat a filtered check's empty output as "it did not run" until a real compiler banner or test count proves otherwise.
  • Never pnpm <script> -- <args> — pnpm forwards the literal -- and the flags are dropped; pass them as direct args (pnpm test -u).
  • A caller runs the script, not the binary under itpnpm exec <binary> in a workflow is a second definition that drifts; where no script has the shape, add one (bench:ci).
  • How each of the three fails, and the @esposter/virrun typo that passed clean while CI failed: references/pnpm-traps.md.

Signals

GitHub stars
23
Forks
3
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
package-scripts
Source
github.com/esposter/esposter