All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Move completed
Unreleaseditems into a new version section. - Update the
[Unreleased]compare link to the new tag. - Create and push a signed
vX.Y.Ztag frommaster.
- Internal: the
runalias binary now dispatches through the samedispatchentry point asrunner, building a typedClifrom the parsed alias rather than keeping a second resolver-override and command-dispatch copy indispatch_run_alias. The alias keeps its bespoke help/version forwarding, flat completions, andrunman page. One behavior delta: a barerun -k/-K(a chain-failure flag with no task and no-s/-p) now maps to the project dashboard (command: None) and drops the inert chain-failure flag before resolving overrides, so it no longer errors when the opposite polarity is supplied out-of-band viaRUNNER_KILL_ON_FAIL/RUNNER_KEEP_GOINGor a[chain]config. The old eager builder kept the flag and hit the cross-source conflict; the dashboard never consults the failure policy, so dropping it is correct. See #52.
0.16.0 - 2026-07-01
- Editor language server (
runner lsp, built with--features lsp): a stdio LSP forrunner.tomlproviding live diagnostics (the same checks asrunner config validate, plus deprecation hints), hover docs sourced from the JSON Schema, and completion of section names, field names, and value sets (package managers, the[tasks]runner/PM/source labels, policy enums, booleans). The validation, schema docs, and label vocabulary are reused from the CLI, so editor feedback never drifts fromrunneritself. [tasks]section inrunner.tomlfor a persistent, declarative preference over which source runs an ambiguous task name (one that exists under more than one source — e.g. apackage.jsonscript and aturbotask). Previously this could only be expressed per-invocation (package.json:build,--pm bun,--runner turbo).[tasks].prefer— a rank-only global order. Labels may be task runners (turbo,make, …), package managers (bun,npm, … map topackage.json;deno→deno.jsonthenpackage.json), or source names (package.json). Unlike the old[task_runner].prefer, it never hard-rejects an unlisted source — it only reorders ties.[tasks.overrides]— per-task pins, e.g.build = "turbo",dev = "bun", that beat the global order for those names.- An explicit
source:taskqualifier,--runner, or--pm/RUNNER_PMstill outranks these file settings.
[task_runner].preferis deprecated in favor of[tasks].prefer. Existing configs keep working with their original restrictive behavior and now emit a migration warning; when both sections are set,[tasks]takes over. The key is flaggeddeprecatedin the committed JSON Schema;runner config initkeeps a commented migration stub.
0.15.0 - 2026-06-29
runner installgained a two-way install-time lifecycle-script control — lifecycle/build scripts are the primary supply-chain attack surface during dependency installs, and several package managers (npm, pnpm, …) are moving to scripts-off-by-default in upcoming majors, so projects need to deny and force-on.--no-scriptsskips them, mapping to each manager's native skip mechanism:--ignore-scriptsfor npm/yarn-classic/pnpm/bun,--no-scriptsfor composer,YARN_ENABLE_SCRIPTS=falsefor yarn-berry (which dropped the flag); deno already denies by default. Managers with no skip mechanism (cargo, go, bundler, uv/poetry/pipenv) print awarn:and proceed.--scriptsforces them on where a manager can express it:--no-ignore-scriptsfor npm,YARN_ENABLE_SCRIPTS=truefor yarn-berry, and a bare--allow-scripts(allow all) for deno. Managers that already run scripts by default (composer, cargo, go, bundler, uv/poetry/pipenv, yarn-classic) are satisfied without a flag. bun and pnpm (>=10) can't be forced on by a flag — their dependency build scripts are gated by a manifest allowlist (trustedDependencies/onlyBuiltDependencies) that runner won't write — so theywarn:instead of silently no-op'ing.- The two flags are mutually exclusive. Both the dropped-deny and the
unforceable notices fire whenever their policy is active and are not
silenced by
--no-warnings/RUNNER_NO_WARNINGS(unlike the cosmetic collision/version warnings), because each is the only signal the request couldn't be honored. - Configurable via
[install].scripts = "deny" | "allow"in runner.toml andRUNNER_INSTALL_SCRIPTS=deny|allow, with the usual precedence: CLI flag overRUNNER_INSTALL_SCRIPTSover[install].scripts.
run <path>/runner run <path>now executes a local file directly instead of handing it to a package manager's package-exec primitive (bunx/npx/pnpm dlx/deno x/uvx), which used to resolve the local path as a remote package and fail with a registry 404 or agit cloneerror. A token with an explicit local prefix (./,../,/,~, or a Windows drive root), a bare filename, and a prefix-less relative path (bin/tool) are each run as the file when they match no task: a recognized source file runs via the detected runtime (.ts/.mts/.cts/.js/.mjs/.cjsvia bun,deno run, or node, while.jsx/.tsxrun only via bun or deno — Node has no JSX transform, so a node-only project reports a clearnode cannot runerror instead of building an unrunnablenode app.tsx;.pyviauv runor python;.goviago run), a#!shebang (including the#!/usr/bin/env -S <interp> <args>form, whose quoted arguments are kept intact) is parsed and invoked, and a native binary or self-executable script is spawned directly — including an execute-only binary (Unix mode 0111), whose unreadable shebang probe is treated as "no shebang" so the binary still spawns directly rather than hard-failing the run. A source file carrying the exec bit but no shebang still runs via its runtime — a rawexecveon shebang-less text failsENOEXEC— sochmod +x deploy.ts; run ./deploy.tsdispatchesbun deploy.tsrather than erroring (this also fixes whole-tree breakage on vfat/exfat/ntfs-3g mounts that report mode 0777 for every file). Only an explicit-prefix path outranks a same-named task; a prefix-lessbin/toollets a matchingmake bin/tooltarget win first and runs as a file only after task lookup misses. A missing explicit path reports a clear error rather than a 404. Path lookup is anchored on the resolved project directory (the--dir/RUNNER_DIRtarget, else the cwd) — the same directory task detection scans and the spawned child runs in — so a relative or bare token under--dirresolves there instead of silently missing and mis-routing back into the package-exec 404 path.- Chain mode now reports per-task wall-clock duration on completion. Sequential
and live (
-p) parallel runs print a concise· <task> finished in 1.2s (exit 0)line to stderr after each task; grouped parallel output folds the same summary into each task's block footer (inside the GitHub Actions::group::so it stays attached). Durations format compactly (342ms,1.2s,1m 04s); the band is chosen from the rounded value, so a duration that rounds up to a full minute (e.g.59.95s) prints1m 00s, never an out-of-band60.0s. Minute-band seconds round half-up to the nearest whole second too, so119.94sprints2m 00srather than a floored1m 59s. The synthetic install head of aninstallchain is timed the same way in both-sand-pmodes. Timing is diagnostic meta-output, so--quiet(RUNNER_QUIET) and--no-warnings(RUNNER_NO_WARNINGS) suppress it. runner install -p <TASK> <TASK>runs the post-install tasks in parallel (-sstays the default sequential). Install always runs first as the prerequisite — never as a parallel sibling — then the tasks fan out. A failed install still aborts the tasks unless-k;-K(kill siblings) now bites for the parallel post-install phase.
- Forcing a package manager with
--pm/RUNNER_PMnow biases same-name task selection toward that PM's own task source. Previously a name defined in bothpackage.jsonanddeno.json(e.g.check) always resolved to thepackage.jsonscript per the default tier and was then run through the forced PM (RUNNER_PM=deno run check→deno task check), which breaks when the script relies on npm lifecycle build artifacts deno cannot honor. Now the forced PM's own source wins the conflict, most-native first:RUNNER_PM=denopicksdeno:check,--pm bunpickspackage.json:check. The rule is general across every PM — deno is one member, not a special case — and a PM that owns no task source (Bundler, Composer) re-orders nothing. Only conflicting same-name candidates are re-ordered; runs with no--pm/RUNNER_PMare unchanged. See #70. - GitHub Actions log groups no longer nest when one
runner/runinvokes another (e.g.runner→ annpm/postinstallscript →run -p A B C). A parent that opens a group marks its descendants (RUNNER_GROUP_ACTIVE), so a nested runner detects the open group and stays silent instead of emitting a second::group::that would close the parent's fold early. Inherited through intermediate processes, so the whole chain collapses to one group. - Under GitHub Actions, a child command that emits its own
::group::/::endgroup::workflow commands (e.g. some test runners) no longer corrupts runner's grouped (-p) output: during grouped replay the group title is surfaced as plain text and the stray::endgroup::is dropped, while::warning::/::error::/::notice::annotations pass through untouched. - A leading
~/~/in--dir(orRUNNER_DIR) is now expanded to the user's home directory before the project directory is resolved. Shells only expand an unquoted tilde at the start of a word, so--dir=~/fooreachedrunnerverbatim and was treated as relative — joined onto the cwd to produce a bogus<cwd>/~/foothat never exists. Unsupported forms such as~userare left untouched, and the path passes through unchanged when no home directory is set.
0.14.3 - 2026-06-26
[install].pmsconfig +RUNNER_INSTALL_PMSenv restrict which detected package managersrunner installruns. In a polyglot repo where, e.g., bothbunanddenowould writenode_modules,pms = ["bun"]keeps install to one. A listed-but-undetected PM errors.--pm/RUNNER_PMstill takes precedence;[pm]continues to scope only script dispatch.runner installanddoctornow warn when two detected package managers would install into the same directory — todaynode_modules(a node PM plus anodeModulesDir-enabled Deno). The warning points at[install].pmsand is suppressed once the allowlist narrows install to a single writer.
- Published
@runner-run/*platform packages now carrykeywords, a descriptivedescription, and a full README (instead of a thin stub). These are the binary sub-packages npm selects viaoptionalDependencies; the richer metadata raises their Socket.dev Quality score and explains the facade-resolution mechanism to anyone landing on them directly.
runner.tomlparsing is now forward-compatible: an unrecognized section or field (a typo, or a key written by a newerrunner) is ignored with a warning instead of aborting the command. Previously an unknown key was a hard parse error, so a config written by one version could brick task dispatch — including postinstallrunhooks — under another. Genuine errors (unreadable file, malformed TOML, wrong type on a known field) still fail. The JSON Schema stays strict (additionalProperties: false) so editors keep flagging typos inline.
0.14.2 - 2026-06-25
runner configsubcommand to managerunner.toml:initscaffolds a fully-commented starter file (--forceto overwrite),showprints the effective config (--jsonfor machine output),validateparses and checks it (exit 2 on error), andpathprints the resolved file path. The scaffold's line 1 is a#:schemadirective pointing at the committed JSON Schema, so tombi/taplo give autocompletion in any project with no setup.runner.tomlis now documented in the README with a## Configurationsection covering every section and the override precedence chain.
config validaterejects a[chain]that sets bothkeep_goingandkill_on_failtrue — the resolver already errored on this combination at dispatch time; validation now catches it statically against the file alone.- JSON Schema URLs rehosted from
https://kjanat.github.io/schemas/…tohttps://kjanat.github.io/runner/schemas/…. Changes the$idof every committed schema and the$schemafield emitted bydoctor/list/why--json. The base is now sourced from[package.metadata].schema-base.
0.14.1 - 2026-06-25
- The rendered API docs (rustdoc / docs.rs) now display the project logo
and favicon, set via
#![doc(html_logo_url, html_favicon_url)]pinned tobranding/icon.svg. See #59 -q/--quiet(and a truthyRUNNER_QUIET) suppress the→dispatch line on stderr plus the dispatch-time--explaintrace, for clean output whenrunnerwraps another command. See #56- Short flags
-K(--kill-on-fail) and-f(install--frozen), plus a stable--helpflag ordering via display-order bands.
--helppolish: colorize inline flag tokens instead of rendering literal backticks, hide thebundle/go-taskaliases from the--pm/--runnerlists, terserhelp/schema/quiet descriptions, and reorder commands solistsits withrunandwhyprecedesdoctor.
cargo doc(and the docs.rs build) no longer fail under the crate'sdeny-level rustdoc lints: broken intra-doc links (cmd::run,argv[0]) are repaired. See #59- mise tasks with a whitespace-only
descriptionnow fall back to the run command instead of rendering a blank description.
0.14.0 - 2026-06-22
- Built-in verb dispatch is split between the two surfaces. The explicit
runner <verb>subcommand —install,clean,list,info,completions— is now always the built-in and is never shadowed by a same-named project task. The run path (run <verb>/runner run <verb>) runs a same-named task when one exists, and otherwise falls back to that built-in's default form instead of the package-manager exec path (sorun installwith noinstalltask installs dependencies rather than attemptingbunx install). Previously the precedence was reversed:runner installdeferred to a task namedinstall(e.g. aMakefileinstalltarget), which surprised projects whoseinstallmeans "install the built artifact" rather than "install dependencies". Reach a same-named task withrun install/runner run install; the built-in default forinfoon the run path is a plain task list (no deprecation warning, which remains specific to the explicitrunner infosubcommand). See #55 - The
runalias now forwards--help/-hand--version/-Vto the task when they follow a task name:run <task> --helpreaches the task's own help instead of printingrun's (previouslyrun <task> --was required).run --help/--versionwith no task — including after global flags likerun --pm npm --help— still print this binary's own help/version, andrun <task> -- --helpstill forwards literally. Therunner runsubcommand is unchanged. Because-h/--help/-V/--versionare no longer clap arguments on the alias, they are documented in the help footer rather than the options list.
0.13.1 - 2026-06-14
runner doctor --jsonschema v3 (now the default fordoctor): the flat detection dump becomes a structured diagnostic inventory —invocation/environment/runnerprovenance, per-ecosystemsdecisions with aconfidencegrade derived from the resolution step (override/manifest/lockfile → high, PATH probe → medium, legacy npm fallback → low, failure → none), tasksourcesas first-class objects,fqn-keyedtaskswith effectiveresolvedcommands, PATH-probedtools, duplicate-task-nameconflicts(which task wins, which are shadowed, and why), flatteneddiagnostics, and a self-describingresolutionpolicy block. Implements the formerdoctor.v3-draftschema; the real output validates against both the committeddoctor.v3.schema.jsonand the original draft. Draft shapes nothing can emit yet (rich dependency edges, workspace identity, probe errors) are deferred, not declared. v1/v2 remain available via--schema-version; human output is unchanged.runner why --jsonschema v3 (now the default forwhy): the report is restructured around{task, match}candidate pairs plus adecisionblock. Each task carries a stable identity (fqn=root:<kind>#<name>,provider,kind— cargo aliases are now labeledcargo-alias), its origin (sourcefile,source_pointerkey path), and resolution data (definition,resolvedcommand preview,cwd, siblingaliases,dependencies). Thematchhalf exposes the exact run-time selection key (source_priority,depth,display_order, alias-last), anddecision.strategynames the branch taken (single-candidate,ranked,filtered,exec-fallback). Implements the formerwhy.v3-draftexample, which the real output now reproduces verbatim; v1/v2 stay available via--schema-version.listremains at v2 — its v3 draft is still under review, and it rejects--schema-version 3rather than mislabel output.schema --allemits the committedschemas/why.v3.schema.json, and the example validates against it.- Both v3 schemas use the
<scope>:<kind>#<name>fqn form, with#separating the structured prefix from the verbatim task name so a name containing:(e.g. an npm scriptfmt:update) stays unambiguous. - Deno tasks now run without the
denobinary. Adeno.json/deno.jsonctask whose command is a leaf shell command executes in-process via the embeddeddeno_task_shell(deno's own cross-platform task shell) whendenoisn't onPATH; withdenoinstalled it still shells out todeno taskfor full fidelity. Theunstable-deno-execfeature flips the default to self-exec-first. Tasks that invokedenothemselves or declaredependenciesstill need the binary. The shell engine lives in a reusabletool::shellso other shell-string task sources can build on it later. - Deno task descriptions. The object form
(
"build": { "command": "…", "description": "…" }) is now parsed and the description surfaces inrunner list/why/doctor, alongside the existing bare-string form. runner listand the barerunnerview now print a duplicate-name conflict footer. When two sources define the same task name (e.g. ajustrunrecipe andcargo run), it names the source thatrunner run <name>actually dispatches and the ones it shadows — using the same precedence as dispatch — so a silently shadowed task no longer goes unnoticed.
- Cargo built-in aliases now fold under their canonical subcommand in
runner listand the barerunnerview.b/c/d/t/r/rmare shown as aliases of the promotedbuild/check/doc/test/run/removetasks (e.g.test (t)) instead of standing alone; both the canonical name and the short form still dispatch. Aliases that carry extra arguments (bb,cl,rq, …) keep their own rows. Promotingrun/removecan collide with a same-namedjust/other task — that collision now surfaces in the conflict footer above rather than hiding. runner doctor --json(v3) now probes package-manager and task-runner versions via<tool> --version(previously only the Node runtime carried a version), reports a per-taskself_executableflag (true for deno tasks runner can run through the embedded shell), and derives the Deno tool'srequiredfrom it. Node is included inecosystems/toolswhenever a resolver or task signal implies it, not only when a Node package manager was lockfile-detected.- The committed v3 schemas (
doctor.v3.schema.json,why.v3.schema.json) setadditionalProperties: falsethroughout, so validation catches stray or misspelled fields in real output instead of silently accepting them.
0.13.0 - 2026-06-12
runner doctor(andinfo --json) now classify PATH-probe hits that are Volta shims and resolve them to the real provisioned binary viavolta which: thePATH probeline showsnpm=<shim> -> <real bin> (volta), or(volta shim, not provisioned)when Volta fronts a tool it has no version of. JSON gains an additivesignals.node.volta_shimsmap (omitted on hosts without Volta; no schema bump). Display only — execution still spawns the shim, which performs Volta's per-project version selection.
runner installnow honors the--pm/RUNNER_PMoverride: when set, only that package manager installs (previously the override was ignored and every detected PM installed — e.g. a project with bothbun.lockanddeno.jsonalways randeno installtoo, writing an unwanteddeno.lock). An override naming a PM that detection did not find refuses the install with exit code 2. runner.toml[pm].node/[pm].pythoncontinue to scope script dispatch only.- Invalid
--pm/RUNNER_PM/--runner/RUNNER_RUNNERvalues now produce a readable error: the message names the source that carried the value, escapes control characters (no more raw ANSI codes), truncates long garbage, and — when the value contains line breaks — hints that it looks like captured command output with the correctly quoted PowerShell spelling. (An unquoted$env:RUNNER_PM=denoexecutes deno and assigns its REPL banner to the variable.)
runner doctorno longer dies when aRUNNER_*override variable holds an unparseable value — the condition it exists to diagnose. The invalid value is ignored for the report and surfaced as anenv:warning (human output and thewarningsarray ofdoctor --json, additively — no schema bump). Every other command, and an explicit bad--pm/--runnerflag even on doctor, still fails fast.- Node version constraints are now evaluated with real range semantics
(via the
semvercrate) instead of a prefix match that treated>=22.22.2as=22.22.2. Operators (>=,>,<=,<,=), caret/tilde ranges, space-separated AND comparators,||unions, hyphen ranges, andxwildcards all match per node-semver rules, soengines.node: ">=22.22.2"no longer warns on Node 22.22.3 or 25.9.0. Bare versions (.nvmrc20.11) keep the stricter prefix-at-segment-boundary behavior; unevaluable inputs (lts/*) fall back to the previous prefix match. - Task dispatch now prepends every existing
node_modules/.binbetween the project directory and the filesystem root (nearest first) to the child'sPATH, the waynpm run/pnpm run/bun rundo forpackage.jsonscripts. Tools that runner spawns directly —turboforturbo.jsontasks, and the bare-binary exec fallback — used to inherit the shell'sPATHunchanged, so a devDependency-onlyturbofailed withError: No such file or directory (os error 2)unless it was also installed globally. On Windows, bare program names are additionally re-resolved against those bin dirs withPATHEXT, sinceCreateProcessWwould never find the.cmdshims npm and pnpm install there. Local bins now shadow global installs for the spawned task and everything it launches, matching Node package-manager semantics. - The no-argument project-info banner no longer leaks the Windows
.exesuffix in its title line (e.g.run.exe 0.12.2). It now shows the samerun/runneridentity as--version,--help, and theUsage:line. The banner had its own copy of the arg0-parsing helper that skipped the.exestripping done everywhere else; it now reuses the canonicalbin_name_from_arg0. runner mannow works on Windows under--features manbuilds. The subcommand was gatednot(windows), so withexternal_subcommandin play it silently degraded to task dispatch (bun man→ "Script not found") instead of rendering. Rendering is pureclap_mangenwith no OS-specific code, so the gate bought nothing and is gone.install.shruns under any POSIXsh. It carried a#!/usr/bin/env bashshebang, butcurl … | shignores the shebang, so the bash-onlyset -o pipefailaborted on line 2 under dash/busybox — the default/bin/shon the-musltargets. Rewritten POSIX-clean. It also picks the install dir more intelligently now: reuse an already-installed runner's directory (verified by its-Vbanner, so a systemrun/runneris never clobbered), otherwise prefer~/binor~/.local/binalready onPATH(then one that exists), falling back to~/.local/bin.
0.12.2 - 2026-06-10
runner completionsnow detects PowerShell when$SHELLis unset or unrecognized by falling back to the presence of$PSModulePath, which pwsh exports on every platform (it never sets$SHELL). A recognized$SHELLstill takes precedence, so a pwsh session launched from bash keeps completing for the login shell. Previously barerunner completionsalways errored under pwsh.
0.12.1 - 2026-06-04
pyproject.toml[project.scripts]entry points (PEP 621 console scripts) are now extracted as runnable tasks for Python projects. They surface under thepyproject.tomlsource inrunner list(with the entry-point target shown as the description) and dispatch via the detected Python package manager'srunsubcommand —uv run <name>,poetry run <name>, orpipenv run <name>. Previously a uv/poetry project's declared scripts were invisible torunner, which detected the package manager but listed no tasks.- AUR distribution channel. Two packages on the Arch User Repository:
runner-run-bin(prebuilt binaries forx86_64,aarch64,armv7h) andrunner-run(source build forx86_64,aarch64).-binprovides/conflictsrunner-run, so install whichever you prefer — https://aur.archlinux.org/packages/runner-run-bin and https://aur.archlinux.org/packages/runner-run. - Shell completions shipped by both AUR packages and auto-loaded from
the canonical system dirs: bash at
/usr/share/bash-completion/completions/{runner,run}, zsh at/usr/share/zsh/site-functions/{_runner,_run}, fish at/usr/share/fish/vendor_completions.d/{runner,run}.fish. PowerShell on Linux has no autoload convention, so the pwsh script is installed at/usr/share/runner/runner.ps1for users to dot-source from their$PROFILE. Completions are clap-dynamic — the shell shells out to the binary for candidates, so tab-completing in a project picks up the current task list frompackage.json/turbo.json/Justfile/ etc., not a static snapshot. .github/workflows/aur-release.ymlpublishes both packages on everyrelease: publishedevent (with manualworkflow_dispatch+dry-runfor validation). Gated behind a dedicatedaurGitHub Environment so theAUR_SSH_PRIVATE_KEYsecret is only readable from that job. Per-pkgconcurrency:group serializes manual + automatic triggers for the same package without blocking the other matrix leg..github/scripts/publish/aur-prepare.shrewritespkgver/pkgrelin the checked-in PKGBUILDs and, for the-binpackage, injects per-archsha256sums_*read directly from the release's published.sha256companion assets (avoids theupdpkgsumshost-arch-only limitation). Strict semver regex on the version input refuses anything containing&,/,\, or newlines before anysedruns.- Man pages for
runner,run, and each subcommand. Rendered from the clap command tree by amansubcommand gated behind themanfeature (off by default — never in the shipped binary, never committed) and shipped by every channel: crates.io (in the published crate), npm (facademanfield), both AUR packages (/usr/share/man/man1/), and arunner-<tag>-man.tar.gzGitHub release asset thatinstall.shandrunner-run-binpull from.man runner/man runwork everywhere.
- All third-party
uses:incrates-release.yml,npm-release.yml, andrelease.ymlpinned to commit SHAs (with a# vNtrailing comment for readability), so an upstream tag rewrite or account-takeover cannot silently swap in a different action build. persist-credentials: falseadded to everyactions/checkoutstep inrelease.yml, soGITHUB_TOKENis not persisted into git config.- Release verification no longer saves Rust caches from pull request runs, preventing untrusted PRs from persisting cache contents.
runner list,runner run, andrunner whynow findpyproject.tomlscripts and Python package-manager signals from nested directories (bounded by the containing VCS root), so running fromsrc/inside a uv/poetry/pipenv project still surfaces and dispatches[project.scripts]tasks.runner whynow reports Python package-manager resolution forpyproject.tomltasks, including--pmand[pm].pythonoverrides, matching the actualrunner rundispatch path.runner list --sourceinvalid-label help now includespyproject.tomlin the accepted source list.- Restore the
multiple_crate_versionsClippy allow so CI accepts the current unavoidable duplicate transitive crate versions while keeping the broaderclippy::cargodeny group enabled. - Hide the feature-only
runner mangenerator from shippedrunner.1output, so installed man pages no longer document an unavailable subcommand.
0.12.0 - 2026-06-01
- GitHub Actions log grouping for task output. Sequential and single
task runs wrap each execution in a
runner: <task>section, emitted as::group::/::endgroup::workflow commands under GitHub Actions (and left untouched in a plain terminal). Toggle with[github].group_outputinrunner.toml(defaulttrue). - Grouped parallel (
-p) output. Each task's stdout/stderr is captured and printed as one contiguousrunner: <task>block when that task finishes (completion order — first done, first shown), instead of interleaving lines live. Under GitHub Actions the block is a::group::section; elsewhere it gets a plain colored header. Defaults diverge by environment so CI and local can differ:[github].group_parallel(defaulttrue, only when[github].group_outputis alsotrue) governs runs under GitHub Actions,[parallel].grouped(defaultfalse) governs runs elsewhere. Opting out on either path restores the live[<task>]-prefixed multiplexer. [github]and[parallel]sections inrunner.toml, reflected in the generated JSON schema, for the grouping toggles above.actions-rsdependency for emitting GitHub Actions workflow commands.
0.11.0 - 2026-05-19
- Task chaining for
runner runandrunner install. New-s/--sequentialand-p/--parallelflags turn the trailing positionals into a chain:runner run -s build test lintruns the three tasks in order;runner run -p test:unit test:e2efans them out concurrently.runner install build testchains install → build → test (install head is always sequential;-pis rejected oninstall). - Failure policies for chains. Default is fail-fast (sequential
stops on first non-zero; parallel lets running siblings finish,
doesn't start new ones).
-k/--keep-goingruns every task to completion regardless of failures, with the chain's final exit code reflecting the first failure.--kill-on-fail(parallel only) terminates siblings immediately when one fails.-kand--kill-on-failare mutually exclusive across CLI, env, and config — conflicting layers surfaceResolveError::ConflictingFailurePolicywith the offending source named. [chain]section inrunner.tomlplusRUNNER_KEEP_GOING/RUNNER_KILL_ON_FAILenv-var mirrors. Same resolver-chain precedence as the rest of the policy knobs: CLI > env > config. Env layer is presence-authoritative —RUNNER_KEEP_GOING=0overrides[chain].keep_going = truein config, not just the default.- Line-prefix multiplexer for parallel chain output. Each task's
piped stdout/stderr is captured by a reader thread, prefixed
with
[<task-name>](right-padded to the longest name, colored from an 8-slot deterministic palette), and forwarded to the parent's stdout/stderr. HonorsNO_COLORand non-TTY parents. - Resolver-warning deduplication across chain dispatch. Per-task
warnings collect into a shared
HashSet, then emit once at the end sorted byDisplayso output order is stable across runs.
runner install --frozen <tasks>now propagates the--frozenflag into the synthetic install head of the chain, so the install step runs lockfile-only when the flag is set. Previous behavior silently dropped the flag in chain mode.- Root single-binary Go task name now derives from the
modulepath ingo.mod(last segment, with a/vNmajor-version suffix dropped to match how Go names the built binary) instead of the project directory name, so cloning a repo into a differently-named directory no longer changes the task name. Falls back to the directory name only whengo.modis absent or has no parseablemoduleline.
- Node script discovery is no longer gated on a detected
package manager. A
package.jsonwithscriptsbut no lockfile and nopackageManager/devEnginesfield (a typical pnpm-workspace member directory) reported "No project detected" andrunner run buildfell through to a bogusbun build. Manifest presence is now the Node signal; which PM dispatches scripts is the resolver's runtime job. A manifest-less subdirectory still lists ancestor scripts, but only when it provably sits inside a JS monorepo (workspace-root-aware, VCS-bounded), so an unrelated outer project'spackage.jsonis never silently adopted. #32 - Detection now mirrors the resolver's package-manager chain
(
packageManager→devEngines.packageManager→ enclosing-workspace lockfile/manifest), sorunner info/runner installfrom a workspace member target the workspace's tool instead of resolving nothing. Corepack semantics preserved: a present-but-unparseable legacypackageManagerstill warns and is not silently superseded bydevEngines.
0.10.0 - 2026-05-14
- mise task extraction and dispatch.
misewas previously detection-only —runnerlisted it under "Task Runners" but its tasks were invisible torunner listandrunner run <task>. NewTaskSource::MiseTomlmakes mise a first-class source: tasks declared inmise.toml/.mise.toml(and the*.local.toml,mise/config.toml,.config/mise.tomlcompanions in mise's documented precedence) appear in listings, participate in the selection priority, and dispatch viamise run <task>. - Bacon-style two-tier extraction for mise. Primary path shells
out to
mise tasks --json— authoritative across mise's config layering and file-based tasks (mise-tasks/*); fallback parses the first project-local config whenmiseisn't on$PATH. Both paths exclude hidden tasks (hide = true), underscore-prefixed names, and tasks whosesourcelives outside the project root (so global /~/.config/mise/*tasks don't pollute the project's task list). Empty or whitespace-onlydescription = ""values are treated as missing so the renderer falls through to therunbody orfilereference instead of showing a blank column. Aliases come through as separate entries pointing at their target, mirroring the justfile shape.
- Resolver no longer dispatches through a Node package manager in
projects with no Node-ecosystem evidence (#23).
runner run <unknown-task>in a Go-only repo withbuninstalled used to warn "no node signals matched" and then runbun <task>anyway; theFallbackPolicy::ProbePATH probe now requires apackage.json(or equivalent manifest) somewhere upward ofctx.rootbefore it considers the canonical Node order. Without that evidence the resolver returns the existing softNoSignalsFoundsentinel andcmd::run::run_pm_exec_fallbackspawns the target directly on$PATH— no more wrong-ecosystem dispatch.
0.9.0 - 2026-05-13
- Unified package-manager resolution chain.
runner runnow follows a documented 8-step precedence — qualified syntax →--pm/--runner→RUNNER_PM/RUNNER_RUNNER→runner.toml→package.json(packageManagerthendevEngines.packageManager) → lockfile →PATHprobe → terminal error — making toolchain selection predictable across Corepack, antfu/ni, mise, and pnpm v11+ conventions. Newsrc/resolver/module owns the chain end-to-end. --pm/--runnerglobal flags withRUNNER_PM/RUNNER_RUNNERenv-var mirrors. Cross-ecosystem overrides like--pm cargoagainst a Node project fall through to detection rather than hijacking dispatch. CLI wins over env; empty env strings are treated as unset.runner.tomlproject config ([pm],[task_runner],[resolution],[sources.*]). Per-ecosystem PM overrides apply only when the named PM matches the requested ecosystem;[resolution].fallbackcontrols the no-signal behavior.devEngines.packageManagerparsing frompackage.json(OpenJS proposal, npm 10.9+). Single-object and array forms supported with per-entryonFail(ignore/warn/error).downloadcollapses towarnsince runner is not an installer. The legacypackageManagerfield still wins when both are present; manifest declarations win over lockfile signals (Corepack semantics) and emit apackage.jsonwarning when they disagree.- Semver enforcement on
devEngines.version. When the declared range doesn't match the installed PM's--version,onFail=warnemits a warning andonFail=errorbails. Unparseable ranges or missing--versionoutput skip the check silently so a partially-broken environment never blocks dispatch. --fallbackpolicy (probedefault |npmlegacy |error) withRUNNER_FALLBACKenv mirror. The default replaces the silentnpmfallback with a canonical-order PATH probe (bun > pnpm > yarn > npm); when nothing matches, the user sees an actionable error listing every source that was checked.--explainflag (RUNNER_EXPLAINenv). Emits a one-line trace describing which chain step produced the PM decision:· runner resolved: pnpm via package.json "packageManager".runner doctorsubcommand. Dumps every signal the resolver considers: detected PMs/runners, override sources in effect (with origin attribution), manifest declarations, lockfile presence, PATH probe results for each Node PM, the final decision, and any warnings.--jsonemits schema-versioned output for jq/scripts/bug reports.runner why <task>subcommand. Walks the source-selection chain for a single task: lists every candidate source with its(priority, depth, display_order, alias)tuple, names the winner, and renders the PM resolution trace when apackage.jsonscript is picked.--jsonavailable.- Passthrough-wrapper detection generalized to every supported task
runner. A
package.jsonscript like"build": "just build"is now recognized as a thin wrapper aroundjustand deduped from completion candidates when the underlying runner exposes a same- named task. Recognized runners: turbo, just, make, task (go-task), nx, bacon, mise. Ecosystemenum (Node/Deno/Python/Rust/Go/Ruby/Php) formalizing the PM-to-ecosystem mapping used by override scoping.
Task.passthrough_to_turbo: boolreplaced byTask.passthrough_to: Option<TaskRunner>so wrappers around any runner — not just turbo — can be attributed at detection time and used by completion.cmd::run::runsignature now takes a&ResolutionOverridesso the resolver-chosen PM also flows through the no-task fallback paths (bun testspecial case,npx-style exec).--pm npmagainst a Bun-detected project now correctly suppresses the bun-test fallback.- Task selection unified into a single sort key
(source_priority, source_depth, display_order, alias_last). Replaces the Deno-only depth path with a generic tiebreak that applies to every source. Every non-workspace-aware source now walks ancestors upward when computingsource_depthso the tiebreak actually distinguishes nested Makefile/Justfile/Taskfile/bacon.toml configs from workspace-root ones. Resolver::resolve_node_pmreturnsResult<ResolvedPm>so manifestonFail=errorand--fallback errorcan bubble up structured errors instead of bailing inside the resolver.
package.jsonpackageManager: "deno@…"projects no longer require adeno.jsonalongside to be recognized as a Deno project.- Stale doc comment on
detect::push_package_json_tasksupdated to reflect that passthrough detection covers every known runner, not just turbo.
0.8.1 - 2026-05-12
Error: program not foundon Windows whenrunner run <script>dispatches through npm / yarn / pnpm (issue #20). Bare-name spawns now walkPATH×PATHEXTso.cmd/.batshims (npm.cmd, yarn.cmd, pnpm.cmd) resolve the same way they do under cmd.exe / PowerShell. Same fix covers every other Windows-shimmed tool runner dispatches: turbo, deno, make, task, just, composer, poetry, pipenv, uv, bundle, go, bacon, and therunner run <bin>arbitrary-target fallback. Non-Windows targets are unchanged.
0.8.0 - 2026-05-10
bacon.tomlas arunnertask source. Jobs surface inrunner list/runner info, dispatch viarunner run <job>/run <job>, and resolve under thebacon.toml:<job>qualified syntax. Bacon is detected as a task runner alongside just / make / go-task. Jobs whose names start with_are treated as private and hidden. When thebaconCLI is onPATH, extraction shells out tobacon --list-jobsso bacon's built-in jobs (check,clippy,test, …) merge into the listing alongside whateverbacon.tomldeclares — same view bacon itself presents. Falls back to TOML parsing when bacon isn't installed. Job arguments forward through bacon's--separator (runner run test -- --ignored→bacon test -- --ignored) so they reach the underlying job intact.- Project-local
bacon.tomldefininglint,test-all, andbinsjobs for therunnercrate, mirroring thecargo l/cargo taliases. cargo binstall runner-runsupport via[package.metadata.binstall]inCargo.toml. cargo-binstall now downloads the prebuilt binary from the matching GitHub release asset (runner-v{version}-{target}.tar.gz) instead of building from source — same archivestaiki-e/upload-rust-binary-actionuploads fromrelease.yml. Bothrunnerandruninstall side by side, no toolchain required.
- Release pipeline reordered.
crates-releasenow triggers onpush: tags: ['v*']instead ofrelease: published, socargo publishfires in parallel with binary builds and no longer waits on the npm publish chain to complete first.release.ymlgains a finalpublish-releasejob that flips the draft GitHub release to published once binaries and thedistartifact land — this is now the natural pivot of the release lifecycle and drivesnpm-release.ymlviarelease: published.npm-release.ymldrops itsworkflow_runtrigger (and the draft-flip side job that was hidden in it), resolving the build run-id for cross-workflow artifact download viagh run listinstead. Net effect: tag push alone ships crates.io immediately, and the GH release auto-publishes once binaries are ready — no more manual draft-flipping. npm/facade/README.mdupdates the install fallback instructions tocargo install runner-run(crates.io) instead of the git-source form, matching the 0.7.1 README/landing-page change.npm/facade/package.jsontemplate no longer carries aversionfield. The build script (npm/scripts/build-packages.ts) injects the version fromcargo metadataat build time and the template value was always overwritten. Single source of truth is nowCargo.toml.
runner completions $SHELLno longer fails when$SHELLexpands to a full path (e.g./usr/bin/zsh). The explicit<shell>arg now accepts either a bare name (zsh) or a full path, mirroring the bare-arg fallback that already file-stems$SHELL. Previously the stock clapValueEnumparser rejected anything but the bare names, making the explicit and implicit paths inconsistent.pwshsurfaces inrunner completions --helpand the rejection error alongsidepowershell. The internal mapping has accepted both sinceshell_from_pathwas written; only the user-facing message lagged.
0.7.1 - 2026-05-10
- Turborepo configs resolve under both
turbo.jsonandturbo.jsoncfilenames, and either is parsed as JSONC (line/block comments and trailing commas), matching Turborepo v2's own parser. Detection previously hardcodedturbo.jsonand the parser was strict JSON, so.jsoncfiles were invisible to detection and any JSONC syntax surfaced parse errors. The qualified-task syntax also acceptsturbo.jsonc:task(anddeno.jsonc:task, fixed in the same line for parity). Fixes #10. - Root Tasks in
turbo.json— entries written with the//#nameprefix, invoked viaturbo run nameagainst the workspace root — now surface inrunner listunder their bare name. Workspace-scoped entries (pkg#task) remain filtered, and the result set is deduplicated when bothnameand//#nameare defined. Fixes #11.
crates-releaseCI workflow uses crates.io trusted publishing via OIDC. Replaces the long-livedCARGO_REGISTRY_TOKENsecret with a short-lived token minted per run byrust-lang/crates-io-auth-action; the secret-presence preflight is gone.- README and the landing page promote
cargo install runner-run(from crates.io) as the primary cargo install command, with the git and local-checkout forms remaining as fallbacks for unreleased commits and development work. - README adds a crates.io shields badge alongside the existing npm one; the landing page footer adds crates.io and npm registry links next to the source and changelog references.
0.7.0 - 2026-05-10
- crates.io publishing: new
crates-releaseworkflow publishes the crate to crates.io when a maintainer publishes the GitHub release thatrelease.ymlcuts as a draft. Verifies the tag matchesCargo.toml, runscargo publish --locked --dry-runfirst, then publishes viaCARGO_REGISTRY_TOKENunder thecrates-ioenvironment.workflow_dispatchis preserved for manual republishes.
- Renamed the published package from
runnertorunner-run(the barerunnername is taken on crates.io). The library crate name is pinned torunnervia[lib]so existingrunner::…imports insrc/main.rsandsrc/bin/run.rskeep working unchanged. - GitHub composite action:
uses: kjanat/[email protected]installs therunnerandrunbinaries on PATH in CI. Pinned tag refs make zero API calls (tag resolved fromgithub.action_ref);version: latesttriggers a singlereleases/latestlookup. Parallelcurlof archive + sha256 with--retry-all-errors --retry 5, sha256 verification before extract,actions/cache@v4keyed ontag + triple(cache hits ~250 ms, cold install 600–900 ms), and arunner --versionsmoke test on every install so a bad cache or missing asset surfaces here, not at the consumer's first task. - Cargo aliases as a
runnertask source. The hierarchical.cargo/config.tomlchain (cwd up to filesystem root, then$CARGO_HOME/config{,.toml}) is merged with cargo's precedence rules, recursive alias chains are expanded sorunner listshows the fully-resolved command (l → clippy --all-targets --all-features -- -D warnings,recursive_example → run --release --example recursions), and built-ins (b/c/d/t/r/rm) always surface even in projects without a user config. User attempts to redefine a built-in are silently ignored to match cargo's own rule.runner run <alias>shells out tocargo <alias> <args...>so cargo's runtime resolution stays authoritative. - Landing page at https://runner.kjanat.com, deployed to Cloudflare
Workers Assets from
site/. Single static page, dark mode viaprefers-color-scheme, click-to-copy install commands with polite ARIA live-region announcements, tab-completion section, custom 404 styled as a fakerunner <path>error line. Page weight squeezed under TCP IW10 (~14.5 KB uncompressed, ~3.6 KB brotli) so the whole first response lands in a single round-trip;_headersships strict CSP, HSTS, and edge cache. - Templated site build (
site/build.ts+site/dev.ts): Bun bundlesindex.html/404.htmlfromsrc/intodist/and substitutes{{version}},{{repo}},{{authorName}}fromCargo.toml, so the site and the crate share one source of truth for metadata.dev.tsservesdist/, watchessrc//public//Cargo.tomlwith an 80 ms debounce, and injects a WebSocket live-reload snippet into served HTML. site/build.tsreturns the emitted file list with bytes; the CLI prints a sortedraw / gzip / brtable at quality 9/11 to mirror what a CDN actually serves, surfacing budget regressions next to the build. HTML post-processing reads fromBun.buildoutputs in memory instead of re-readingdist/, andcopyTreereturns the bytes it copied sopublic/files land in the sameDistFile[]the summary walks.BuildOptions.dir("relative" | "full") toggles the size-summary path column betweendist/-relative and absolute, surfaced via theFULLenv in the local build script. Public files respect the toggle too so the rendered table is consistent across sources.- GitHub Pages-aware
publicPath: under GitHub Actions the asset prefix derives tohttps://<owner>.github.io/<repo>/fromGITHUB_REPOSITORY, so PR previews of forks load their own assets without a config flag.PUBLIC_PATHenv still overrides everything. CF_BEACON_TOKENenv overrides the inlined Cloudflare Web Analytics token; the literal stays as a fallback so production builds without the env still report correctly.- External sourcemaps emit when
SENTRY_DSNis set; otherwise the build skips them entirely so the published site stays one-round-trip-sized for end users. - README links the landing page and npm package, and adds shields.io
badges for the
runner-runnpm version and the MIT licence.
- Cloudflare Web Analytics beacon now only injects in CI / GitHub
Actions builds. Local
bun run buildand library imports leave the snippet out so dev previews don't phone home, and a missing</body>warns instead of throwing so partial HTML fragments don't fail the build. npm/scripts/build-packages.tsswaps the deprecatedcargo read-manifestforcargo metadata --no-deps --format-version 1, picking the workspace'sworkspace_default_members[0]by id (falls back to the first package for non-virtual single-member workspaces).maxBufferbumped to 64 MiB so large workspaces don't trip Node's 1 MiB default..github/scripts/publish/npm.shderivesREQUIRED_PLATFORMSandOPTIONAL_PLATFORMSfromnpm/targets.jsonat runtime viajqinstead of hardcoding two parallel lists;npm-release.ymlsparse-checkout addsnpm/targets.jsonso the script can read it. Optional ==experimental: true, matching the workflow's existingcontinue-on-errorsemantic. One source of truth for the platform matrix.- Drop the unused
NODE_AUTH_TOKENenv from the npm publish step; auth flows through the OIDC token viaid-token: writeandnpm publish --provenance, not a long-livedNPM_TOKEN. - Move the landing-page primary domain from
runner.kjanat.comtorunner.kjanat.devacross the root README, site docs, site package metadata, and the page canonical URL so published links and metadata point at the new host. - Add
runner.kjanat.devas a Cloudflare custom-domain route while keeping the existing.comroute active during the transition. - Reflow the npm facade README intro for cleaner package-page rendering.
- Set
Cargo.toml[package].homepageandnpm/facade/package.jsonhomepagetohttps://runner.kjanat.dev(was the GitHub README URL), so the published crate and npm package surface the landing page on registry pages.
site/build.tspublicPathprecedence: the originalenv["PUBLIC_PATH"] || isCI ? X : Yparsed as(... || ...) ? X : Y, so a literalPUBLIC_PATHvalue never reachedBun.build— it acted as a boolean toggle. The hardcodedrunner.kjanat.com/fallback also leaked into Cloudflare Workers preview deploys (*.workers.dev) and tripped CSP'self', blocking every asset on every PR preview. Replaced withenv["PUBLIC_PATH"] || githubPagesUrl() || "/": explicit override wins, GitHub Pages still gets its/<repo>/prefix, everything else stays same-origin.public/_headersdrops the dead/favicon.icoContent-Type override block; the icon ships as an SVG via<link rel="icon">, so nothing routes through/favicon.icoto need it..github/scripts/build/package-release-asset.shwrites checksum files as<basename>.sha256(not<basename>.tar.gz.sha256), matchingtaiki-e/upload-rust-binary-action's convention and whatverify-checksum.shenforces — the previous mismatch would have broken the npm pipeline's checksum verification on release.npm/scripts/build-packages.ts:Target.buildunion now covers all five schema enum values (previously onlycargo|cross, missing the three variants added when the BSD build paths landed) so type narrowing matches reality and a stray build-tool name fails the build instead of silently shipping.- Remove the stale
openbsd-x64entry fromnpm.sh'sOPTIONAL_PLATFORMSand the matching matrix-gen comments inrelease.yml/release-dryrun.yml; the openbsd build path was scrapped earlier and the residue would have failedoptionalDependenciesvalidation if the platform was ever re-expected. - Tab completion for turborepo monorepos no longer triple-emits
the same task. A
package.jsonscript is classified as a turbo passthrough at detection time when its command body literally invokesturbo run <name>(or the shorthandturbo <name>) for a same-named target, optionally followed by flag tokens (--filter web,--concurrency=4) or — after a bare--end-of-options separator (POSIX/getopt convention) — args forwarded to the underlying task; the full bash control set (&&,||,;,;;,;&,;;&,|,|&,&,!,{,},(,)), fd-style redirects (bare>/</>>/<<<, combined-fd&>/>&, fd-prefixed2>, composite2>&1,2>/dev/null,&>file.log), shell expansion (parameter$X/${X}/${X:-def}/${X//a/b}, special vars$@/$*/$#/$?, command substitution$(cmd)and backtick`cmd`, arithmetic$((expr)), double-quoted forms with embedded expansion"${X}") — including those positioned after a value-expecting flag or after--— and extra positional targets all reject the match so scripts that do real work beyond dispatching to turbo stay visible. Only thin passthroughs are dropped from completion when a same-namedturbo.jsontask also exists. Real scripts like"build": "vite build"keep their qualified form even when they happen to share a name with a turbo task.runner liststill surfaces both sources for transparency,runner buildalready dispatched through turbo persource_priority, and a third source (e.g. Makefile) keeps its qualified form alongsideturbo.json:buildfor disambiguation.
0.6.1 - 2026-05-08
- GitHub composite action:
uses: kjanat/[email protected]installs therunnerandrunbinaries on PATH in CI. Pinned tag refs make zero API calls (tag resolved fromgithub.action_ref);version: latesttriggers a singlereleases/latestlookup. Parallelcurlof archive + sha256 with--retry-all-errors --retry 5, sha256 verification before extract,actions/cache@v4keyed ontag + triple(cache hits ~250 ms, cold install 600–900 ms), and arunner --versionsmoke test on every install so a bad cache or missing asset surfaces here, not at the consumer's first task. - Cargo aliases as a
runnertask source. The hierarchical.cargo/config.tomlchain (cwd up to filesystem root, then$CARGO_HOME/config{,.toml}) is merged with cargo's precedence rules, recursive alias chains are expanded sorunner listshows the fully-resolved command (l → clippy --all-targets --all-features -- -D warnings,recursive_example → run --release --example recursions), and built-ins (b/c/d/t/r/rm) always surface even in projects without a user config. User attempts to redefine a built-in are silently ignored to match cargo's own rule.runner run <alias>shells out tocargo <alias> <args...>so cargo's runtime resolution stays authoritative. - Landing page at https://runner.kjanat.com, deployed to Cloudflare
Workers Assets from
site/. Single static page, dark mode viaprefers-color-scheme, click-to-copy install commands with polite ARIA live-region announcements, tab-completion section, custom 404 styled as a fakerunner <path>error line. Page weight squeezed under TCP IW10 (~14.5 KB uncompressed, ~3.6 KB brotli) so the whole first response lands in a single round-trip;_headersships strict CSP, HSTS, and edge cache. - Templated site build (
site/build.ts+site/dev.ts): Bun bundlesindex.html/404.htmlfromsrc/intodist/and substitutes{{version}},{{repo}},{{authorName}}fromCargo.toml, so the site and the crate share one source of truth for metadata.dev.tsservesdist/, watchessrc//public//Cargo.tomlwith an 80 ms debounce, and injects a WebSocket live-reload snippet into served HTML. site/build.tsreturns the emitted file list with bytes; the CLI prints a sortedraw / gzip / brtable at quality 9/11 to mirror what a CDN actually serves, surfacing budget regressions next to the build. HTML post-processing reads fromBun.buildoutputs in memory instead of re-readingdist/, andcopyTreereturns the bytes it copied sopublic/files land in the sameDistFile[]the summary walks.BuildOptions.dir("relative" | "full") toggles the size-summary path column betweendist/-relative and absolute, surfaced via theFULLenv in the local build script. Public files respect the toggle too so the rendered table is consistent across sources.- GitHub Pages-aware
publicPath: under GitHub Actions the asset prefix derives tohttps://<owner>.github.io/<repo>/fromGITHUB_REPOSITORY, so PR previews of forks load their own assets without a config flag.PUBLIC_PATHenv still overrides everything. CF_BEACON_TOKENenv overrides the inlined Cloudflare Web Analytics token; the literal stays as a fallback so production builds without the env still report correctly.- External sourcemaps emit when
SENTRY_DSNis set; otherwise the build skips them entirely so the published site stays one-round-trip-sized for end users. - README links the landing page and npm package, and adds shields.io
badges for the
runner-runnpm version and the MIT licence.
- Cloudflare Web Analytics beacon now only injects in CI / GitHub
Actions builds. Local
bun run buildand library imports leave the snippet out so dev previews don't phone home, and a missing</body>warns instead of throwing so partial HTML fragments don't fail the build. npm/scripts/build-packages.tsswaps the deprecatedcargo read-manifestforcargo metadata --no-deps --format-version 1, picking the workspace'sworkspace_default_members[0]by id (falls back to the first package for non-virtual single-member workspaces).maxBufferbumped to 64 MiB so large workspaces don't trip Node's 1 MiB default..github/scripts/publish/npm.shderivesREQUIRED_PLATFORMSandOPTIONAL_PLATFORMSfromnpm/targets.jsonat runtime viajqinstead of hardcoding two parallel lists;npm-release.ymlsparse-checkout addsnpm/targets.jsonso the script can read it. Optional ==experimental: true, matching the workflow's existingcontinue-on-errorsemantic. One source of truth for the platform matrix.- Drop the unused
NODE_AUTH_TOKENenv from the npm publish step; auth flows through the OIDC token viaid-token: writeandnpm publish --provenance, not a long-livedNPM_TOKEN.
site/build.tspublicPathprecedence: the originalenv["PUBLIC_PATH"] || isCI ? X : Yparsed as(... || ...) ? X : Y, so a literalPUBLIC_PATHvalue never reachedBun.build— it acted as a boolean toggle. The hardcodedrunner.kjanat.com/fallback also leaked into Cloudflare Workers preview deploys (*.workers.dev) and tripped CSP'self', blocking every asset on every PR preview. Replaced withenv["PUBLIC_PATH"] || githubPagesUrl() || "/": explicit override wins, GitHub Pages still gets its/<repo>/prefix, everything else stays same-origin.public/_headersdrops the dead/favicon.icoContent-Type override block; the icon ships as an SVG via<link rel="icon">, so nothing routes through/favicon.icoto need it..github/scripts/build/package-release-asset.shwrites checksum files as<basename>.sha256(not<basename>.tar.gz.sha256), matchingtaiki-e/upload-rust-binary-action's convention and whatverify-checksum.shenforces — the previous mismatch would have broken the npm pipeline's checksum verification on release.npm/scripts/build-packages.ts:Target.buildunion now covers all five schema enum values (previously onlycargo|cross, missing the three variants added when the BSD build paths landed) so type narrowing matches reality and a stray build-tool name fails the build instead of silently shipping.- Remove the stale
openbsd-x64entry fromnpm.sh'sOPTIONAL_PLATFORMSand the matching matrix-gen comments inrelease.yml/release-dryrun.yml; the openbsd build path was scrapped earlier and the residue would have failedoptionalDependenciesvalidation if the platform was ever re-expected. - Tab completion for turborepo monorepos no longer triple-emits
the same task. A
package.jsonscript is classified as a turbo passthrough at detection time when its command body literally invokesturbo run <name>(or the shorthandturbo <name>) for a same-named target, optionally followed by flag tokens (--filter web,--concurrency=4) or — after a bare--end-of-options separator (POSIX/getopt convention) — args forwarded to the underlying task; the full bash control set (&&,||,;,;;,;&,;;&,|,|&,&,!,{,},(,)), fd-style redirects (bare>/</>>/<<<, combined-fd&>/>&, fd-prefixed2>, composite2>&1,2>/dev/null,&>file.log), shell expansion (parameter$X/${X}/${X:-def}/${X//a/b}, special vars$@/$*/$#/$?, command substitution$(cmd)and backtick`cmd`, arithmetic$((expr)), double-quoted forms with embedded expansion"${X}") — including those positioned after a value-expecting flag or after--— and extra positional targets all reject the match so scripts that do real work beyond dispatching to turbo stay visible. Only thin passthroughs are dropped from completion when a same-namedturbo.jsontask also exists. Real scripts like"build": "vite build"keep their qualified form even when they happen to share a name with a turbo task.runner liststill surfaces both sources for transparency,runner buildalready dispatched through turbo persource_priority, and a third source (e.g. Makefile) keeps its qualified form alongsideturbo.json:buildfor disambiguation.
0.6.0 - 2026-05-05
- npm distribution: install prebuilt binaries via
npm install -g runner-run(orpnpm/yarn/bun). The façade package (runner-run) declares one@runner-run/<platform>-<arch>[-<libc>]package per supported target inoptionalDependencies; npm/pnpm/yarn filter at install time using each sub-package'sos/cpu/libcfields, so only the matching binary is fetched. Nopostinstallscript and no network access during install. Façade shims (bin/runner.cjs,bin/run.cjs) resolve the platform sub-package viarequire.resolve()at runtime throughlib/resolve.cjsandlib/launch.cjs, with helpful diagnostics when no matching sub-package is installed. - Release matrix expanded from Linux musl x86_64/aarch64 to 13 targets
across Linux (gnu/musl × x64/arm64 + armv7), macOS (x64, arm64),
Windows (x64, arm64, ia32), FreeBSD (x64, arm64), and NetBSD x64.
Tier-3 BSD targets are marked
experimental: trueand do not block the release. Per-target runner / build-tool selection is data-driven fromnpm/targets.json(validated bynpm/targets.schema.json). - New
npm-releaseworkflow downloads the GitHub Release tarballs, verifies SHA-256 checksums, generates per-platform packages fromnpm/targets.json, and publishes to npm with provenance, optional dry-run, and configurable dist-tag. build.rsbuild script reads[[package.metadata.authors]]fromCargo.tomland exposes the primary author as compile-time env varsRUNNER_AUTHOR_NAME(always) andRUNNER_AUTHOR_EMAIL(when set), consumed by the help byline viaenv!/option_env!.- Pin Rust toolchain via
rust-toolchain.toml(channel1.95, componentsrustfmt/clippy/rust-analyzer, profileminimal). - Add
justfilewith developer recipes for building both bins, generating per-target npm packages (build-packages), and end-to-end-testing the façade resolution against the host triple (test-release). - README documents the npm install path and the façade pattern
(per-platform sub-package via
optionalDependencies, install-time filtering, no postinstall, no network). .github/scripts/build/helpers:build-npm-packages.sh,derive-dist-dry.sh,download-release-archives.sh,verify-checksum.sh, plus.github/scripts/publish/npm.shfor the publish path.
- Move authors metadata from
package.authorsto a structured[[package.metadata.authors]]table (withname/emailfields) consumed by the new build script;src/lib.rsdrops the runtimeprimary_author/authorsregex-style parser in favour ofenv!("RUNNER_AUTHOR_NAME")/option_env!("RUNNER_AUTHOR_EMAIL").help_byline(stdout_is_terminal: bool) -> Stringreplaces the priorOption<String>-returning helper and is now part of the public API along withrequests_version. - Breaking (Cargo features): rename feature
run-alias→run; therunbinary'srequired-featuresfollows. Builds passing--features run-aliasno longer enable the alias. - Bump MSRV from
1.88to1.95(matches the pinned toolchain). - Add
build = "build.rs"and a[package.metadata.npm]block (name,subpkgscope,bugs,repository,engines) consumed by the npm build pipeline so target naming / scope live in one place. - Promote
colored,json5,serde_json,shlex, andyaml-rust2into a single[dependencies]table; addserde+tomlas[build-dependencies]for the build script. - Migrate npm build/publish scripts from
.mjsto TypeScript (npm/scripts/build-packages.ts,npm/scripts/publish.ts); addtsconfig.json. Build-script type narrowing (narrowRepository/narrowBugs/narrowAuthor) now throws on wrong-typed optional fields instead of silently dropping them, so Cargo metadata drift fails the build instead of shipping a façade with missing fields. - Release workflow archive layout updated to package per-target artifacts consumable by the npm build, and adds an experimental flag so tier-3 BSD targets do not block a release.
- Editor / formatting config:
.dprint.jsonplugin update,.gitattributesadded for line-ending consistency,.gitignoreignores generated npm artefacts,.zed/settings.jsonchecked in.
- Help-byline rendering no longer depends on parsing
clap::crate_authors!()at runtime; the email-aware OSC-8 hyperlink path is driven byoption_env!("RUNNER_AUTHOR_EMAIL")set at compile time, removing the runtime string-split fallback.
- Harden the npm release pipeline with five fail-loud input-validation
guards (commit
b404098):derive-dist-dry.shvalidatesINPUT_DIST_TAGagainst^[A-Za-z][A-Za-z0-9._-]*$so a malformed override cannot smuggle flag-like or whitespace values intonpm publish --tag, and normalisesINPUT_DRY_RUNto stricttrue/false(previouslyTrue/1/yesfell through asdry_run=false, i.e. a real publish disguised as a dry run).release.ymlsmoke-tests the packagedlinux-x64-gnubinary with--versionbefore uploading thenpm/distartefact, catching a broken bin at build time instead of asENOENTpost-publish.npm.shvalidatesoptionalDependenciesinpublish_allowed: the façade must list every required platform under the scope at exactlyEXPECTED_VERSION, and platform sub-packages must declare none — closing a vector where a tampered platform package could smuggle attacker-controlled transitive deps.npm viewandnpm publishare wrapped intimeout 120swith explicit124handling so a hung registry cannot burn the full job budget.
0.5.0 - 2026-04-21
- Detect justfile aliases (
alias b := build) and surface them as first-class tasks inrunner listandrunner <alias>. Private aliases (prefixed_, tagged[private], or pointing at a private recipe) are hidden. Works viajust --dump-format jsonwhenjustis on PATH and via the regex fallback parser otherwise. - Render justfile aliases distinctly from recipes in
runner listand shell tab completions: aliases appear under a dedicatedjustfile (aliases)group withname → targetannotations instead of duplicating the target recipe's description.
0.4.1 - 2026-04-21
- Stop zsh completion from leaking unmatched glob patterns into the
user's prompt. The completer function now scopes
NULL_GLOBviaemulate -L zsh -o NULL_GLOB, so globs evaluated by_filesinternals or user zstyles (e.g. specs taggedglobbed-files) silently drop when they match nothing — fixing both theno matches found: *:globbed-fileserror under the defaultNOMATCH, and the subsequent*(/)/*(-/)residue that would otherwise appear on the command line underNO_NOMATCHwhen completing a directory-typed flag in a directory with no subdirs. - Actually stop the
*(-/)/*(/)glob-pattern residue that--dir <TAB>in an empty directory would type into the prompt. The initial switch toNULL_GLOBwas the right idea but was defeated by a function-scopedsetopt noglobsitting on top of the_filescall: that option disabled globbing inside_path_filesas well, so its internaltmp1=( $~tmp1 )never expanded*(-/)and the literal pattern was handed tocompaddas a candidate. Replace the option with anoglobprecommand modifier on the_filescall so globs like*(*)still reach_filesunexpanded while its internals run withNULL_GLOBsemantics as intended. - Also enable
EXTENDED_GLOBin the completer'semulate -L zshscope. zsh's own_filesbuilds qualifier patterns like*(#q-/)and uses(#b)backreferences internally;emulate -L zshresets to plain-zsh defaults (extended glob off), so_files -/would emitbad pattern: *(#q-/):globbed-fileson every TAB even once the residue bug above was fixed.
0.4.0 - 2026-04-17
- Add
shfmtfor shell script formatting via dprint command integration. runner run <target>now falls back to executing<target>through the detected package manager (npx/pnpm exec/bunx/uv run/ …) when no matching task is defined, unifying task execution and ad-hoc command execution under one entrypoint.- Shorthand
runner <name>prefers a same-named task when one exists in the project, so tasks calledclean,install,list,info, orcompletionsare no longer shadowed by built-in subcommands. Passing any built-in-specific flag (--frozen,-y,--include-framework,--raw, thecompletionsshell positional) keeps the built-in path as an escape hatch;runner i/runner lsaliases always hit the built-in. runalias binary now uses a dedicated parser (no subcommands), sorun clean,run install, and friends always run the task/command even when the name matches arunnerbuilt-in.runner completions <shell>emits registration scripts for bothrunnerandrunin one invocation, so a singleeval "$(runner completions zsh)"registers completion for both CLIs.runner completionsnow accepts--output <PATH>(-o) to write the script directly to a file instead of stdout. Parent directories are not auto-created; an existing file is overwritten; a stderr confirmation line (wrote completion script to <PATH>) is printed on success.- Zsh completion honours path hints:
--dir <TAB>(and any arg carryingValueHint::DirPath/FilePath/AnyPath/ExecutablePath) delegates to zsh's native_filesso~/,~named-dir/, globs, andcdpathall work. - Declare MSRV
rust-version = "1.88"inCargo.toml(matches the let-chain syntax used throughout the crate).
- Make
install.shaccept bothX.Y.ZandvX.Y.Zversion arguments and environment overrides. - Quiet installer downloads and checksum verification, and switch install output to a more compact structured summary with the installed version.
- Streamline install docs around the installer script and custom destination override details.
runner's shorthand for a name that matches a detected task is now preferred over the built-in subcommand when no built-in flag is set. Breaking for projects that relied onrunner install/runner cleanalways hitting the built-in while also defining a same-named task.runalias binary now parses positionals unconditionally (no built-in subcommands); previously it inheritedrunner's parser, so positional names that matched a built-in would dispatch there.- Added
clap::ValueHint::DirPathto the--dirflag on both CLIs so shell completion knows to offer directories.
- Breaking:
runner exec <cmd>is gone. Userunner run <cmd>(which now falls through to the package manager when no task matches) or therunalias binary. - Remove the
tool::deno::exec_cmdandtool::cargo_pm::exec_cmdhelpers:deno run <target>treats the target as a local script, andcargo <target>dispatches to a cargo subcommand/plugin — neither runs arbitrary package binaries likenpxdoes.runner run <target>in a Deno- or Cargo-only project now spawns<target>directly viaPATH.
- Stop zsh completion from leaking caller-side
XTRACE/ alias / word-split state into the prompt: the completer function now starts withemulate -L zsh. runner run <name>in a Deno project no longer fires offdeno run <name>(which would misinterpret<name>as a script path).- Scope completion flag-hint lookup to the active subcommand chain instead
of recursing through every subcommand: a sibling subcommand's
--flagwith a differentValueHintno longer bleeds into an unrelated context, and a subcommand-local boolean--flagcorrectly shadows an ancestor's value-taking definition.
0.3.1 - 2026-04-15
- Help output now includes a quiet
by Kaj Kowalskiattribution line, with an OSC8mailto:link when rendered to a terminal.
- Enable clap's
cargo,env, andwrap_helpfeatures, and use clap cargo macros for package description/version metadata. - Shorten help copy for
runner completionsand--dir, and showRUNNER_DIRdirectly in--helpoutput. - Make clap value parsers explicit for
--dir <PATH>and the optional completions shell argument.
- Resolve Deno tasks,
deno.json(c)configs, andpackage.jsonscripts from the nearest applicable ancestor config, while stopping at VCS boundaries and ignoring workspace roots that do not list the current path as a member. - Prefer the nearest Deno task source when duplicate task names exist across ancestor configs.
0.3.0 - 2026-04-15
- Add global
--dir <PATH>andRUNNER_DIRoverrides to scan and run tasks against another project directory.
- Resolve Deno tasks and configs from the nearest applicable ancestor config, while stopping at VCS boundaries and ignoring workspace roots that do not list the current path as a member.
- Limit task source OSC8 hyperlinks to visible filename text so alignment padding is not clickable.
- Add repo and release-tag hyperlinks to
runner --versionand therunnerinfo header version display.
0.2.1 - 2026-04-15
- Bump
clap_completeto4.6.2.
- Detect Deno projects from
packageManager: "deno@..."anddeno.lockinstead of defaulting those repos tonpm. - Keep
package.jsontasks available for Deno projects during task discovery.
0.2.0 - 2026-03-29
- Add
install.shconvenience installer for Linux release assets, including latest/pinned version resolution, checksum verification, and arch selection. - Dynamic shell completions with live task candidates, source tags, and descriptions instead of static subcommand lists.
- Auto-detect shell from
$SHELLwhen no completion argument is given. descriptionfield onTask, threaded from justfile doc comments and go-taskdescfields into completion candidates.- Tag-grouped zsh completions — candidates render under section headers
(e.g.
-- justfile --,-- Commands --) via custom_describeadapter.
- Make installer destination fallback explicit as nested precedence:
RUNNER_INSTALL_DIR->XDG_BIN_HOME->~/.local/bin. - Extract zsh completion script to standalone
grouped.zshfile, embedded viainclude_str!for syntax highlighting and linting support.
- Correct checksum filename in installer.
0.1.0 - 2026-03-27
- Initial
runnerCLI release for unified project task execution. - Auto-detection for package managers and task sources across ecosystems.
runalias binary for shorter invocation.- Unified commands for task run/list, dependency install, clean, and exec.