Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic

### Changed

- 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 (so
`run install` with no `install` task installs dependencies rather than
attempting `bunx install`). Previously the precedence was reversed:
`runner install` deferred to a task named `install` (e.g. a `Makefile`
`install` target), which surprised projects whose `install` means "install
the built artifact" rather than "install dependencies". Reach a same-named
task with `run install` / `runner run install`; the built-in default for
`info` on the run path is a plain task list (no deprecation warning, which
remains specific to the explicit `runner info` subcommand). See
https://github.com/kjanat/runner/pull/55
Comment on lines +14 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Ahoy! Ye be needin' a new version section header in th' CHANGELOG, ye scallywag!

Accordin' to th' coding guidelines, when a version bump be detected (and 'tis detected, from 0.13.1 to 0.14.0 in Cargo.toml), th' CHANGELOG.md MUST contain a new section header in th' exact format: ## [X.Y.Z] - YYYY-MM-DD. Right now, all th' changes be sittin' under ## [Unreleased], which be a grievous violation of th' code of th' seas—er, guidelines.

Ye need to create a new section ## [0.14.0] - YYYY-MM-DD (with th' actual release date) and move these entries there. After that, keep th' ## [Unreleased] section for future changes.

⚓ Proposed fix to add proper version header
 ## [Unreleased]
 
+## [0.14.0] - 2026-06-22
+
 ### Changed
 
 - 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 (so
   `run install` with no `install` task installs dependencies rather than
   attempting `bunx install`). Previously the precedence was reversed:
   `runner install` deferred to a task named `install` (e.g. a `Makefile`
   `install` target), which surprised projects whose `install` means "install
   the built artifact" rather than "install dependencies". Reach a same-named
   task with `run install` / `runner run install`; the built-in default for
   `info` on the run path is a plain task list (no deprecation warning, which
   remains specific to the explicit `runner info` subcommand). See
   https://github.com/kjanat/runner/pull/55
 - The `run` alias now forwards `--help`/`-h` and `--version`/`-V` to the
   task when they follow a task name: `run <task> --help` reaches the
   task's own help instead of printing `run`'s (previously `run <task> --`
   was required). `run --help`/`--version` with no task — including after
   global flags like `run --pm npm --help` — still print this binary's own
   help/version, and `run <task> -- --help` still forwards literally. The
   `runner run` subcommand is unchanged. Because `-h`/`--help`/`-V`/
   `--version` are no longer clap arguments on the alias, they are
   documented in the help footer rather than the options list.
+
+## [Unreleased]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 14 - 28, The CHANGELOG.md file is missing a
required version section header for the 0.14.0 release. Create a new section
header `## [0.14.0] - YYYY-MM-DD` (replacing YYYY-MM-DD with the actual release
date) above the current entries, then move all the changelog entries from under
`## [Unreleased]` to be placed under this new 0.14.0 section header. Keep the
`## [Unreleased]` section after the 0.14.0 section for future unreleased
changes. This follows the standard changelog format where released versions have
their own dated sections separate from unreleased changes.

Source: Coding guidelines

- The `run` alias now forwards `--help`/`-h` and `--version`/`-V` to the
task when they follow a task name: `run <task> --help` reaches the
task's own help instead of printing `run`'s (previously `run <task> --`
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "runner-run"
version = "0.13.1"
version = "0.14.0"
edition = "2024"
rust-version = "1.95"
description = "Universal project task runner"
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,15 @@ run clean
run install
```

runs a task or command named `clean` or `install`, even when those names also
exist as built-in `runner` subcommands.
runs a project task named `clean` or `install` when one exists — even though
those names are also built-in `runner` subcommands. When no such task exists, a
bare built-in verb (`install`, `clean`, `list`, `info`, `completions`) falls
back to that built-in's default form (so `run install` installs dependencies)
rather than the package-manager exec path.

The explicit subcommand is the inverse: `runner install` (and `runner clean`,
`runner list`, …) is **always** the built-in and never runs a same-named task —
use `run install` / `runner run install` to reach a task called `install`.

## Supported Ecosystems

Expand Down
2 changes: 1 addition & 1 deletion aur/runner-run-bin/PKGBUILD
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# by CI (.github/workflows/aur-release.yml) on every release; the values below
# are only a checked-in reference snapshot.
pkgname=runner-run-bin
pkgver=0.13.1
pkgver=0.14.0
pkgrel=1
pkgdesc='Universal project task runner (prebuilt binary)'
arch=('x86_64' 'aarch64' 'armv7h')
Expand Down
2 changes: 1 addition & 1 deletion aur/runner-run/PKGBUILD
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# CI (.github/workflows/aur-release.yml) on every release; the values below
# are only a checked-in reference snapshot.
pkgname=runner-run
pkgver=0.13.1
pkgver=0.14.0
pkgrel=1
pkgdesc='Universal project task runner'
arch=('x86_64' 'aarch64')
Expand Down
2 changes: 1 addition & 1 deletion schemas/doctor.v3.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"runner": {
"binary": "/usr/local/bin/runner",
"name": "runner",
"version": "0.13.1",
"version": "0.14.0",
"schema_versions": {
"doctor": 3,
"list": 2,
Expand Down
98 changes: 65 additions & 33 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,14 @@
//! ```bash
//! runner # show detected project info
//! runner <task> # run a task (falls back to package-manager exec)
//! run <task> # alias binary: always task/exec, never a built-in
//! runner run <target> # explicit unified run: task → PM exec fallback
//! runner install # install dependencies via detected PM
//! runner clean # remove caches and build artifacts
//! runner list # list available tasks from all sources
//! run <task> # alias binary: a same-named task wins, else the
//! # built-in default (install/clean/list/info/
//! # completions), else PM exec
//! runner run <target> # explicit unified run: task → built-in → PM exec
//! runner install # ALWAYS the built-in (deps); a task named `install`
//! # is reached via `run install`
//! runner clean # remove caches and build artifacts (always built-in)
//! runner list # list available tasks from all sources (always built-in)
//! ```
// Generate docs with `cargo doc --document-private-items --open`.

Expand Down Expand Up @@ -208,9 +211,12 @@ where
/// Parse process args as the `run` alias binary, detect the current dir,
/// dispatch, and return the exit code.
///
/// Always treats positional arguments as a task or command (routed through [`cmd::run`])
/// — built-in subcommand names are never parsed specially, so
/// `run clean`, `run install`, etc. run the corresponding task/command.
/// Always treats positional arguments as a task or command (routed through
/// [`cmd::run`]) — built-in subcommand names are never parsed specially, so
/// `run clean`, `run install`, etc. run a same-named project task when one
/// exists. When no such task exists, a bare run token naming a built-in verb
/// (`install`/`clean`/`list`/`info`/`completions`) falls back to that
/// built-in's default form rather than the package-manager exec path.
///
/// When the `COMPLETE` environment variable is set, writes shell completions
/// to stdout and exits without running the normal command dispatch.
Expand Down Expand Up @@ -630,9 +636,60 @@ fn dispatch_run(
"task name required (drop -s/-p for single-task mode or supply at least one task name)"
);
};
if args.is_empty()
&& let Some(code) = run_path_builtin_fallback(ctx, overrides, task)?
{
return Ok(code);
}
cmd::run(ctx, overrides, task, &args, None)
}

/// Run-path fallback for builtin verbs.
///
/// When a bare, arg-less `run`/`runner run` token names a built-in verb and
/// no same-named task exists, run that built-in's default (no-flag) form —
/// the same behavior the explicit `runner <verb>` subcommand provides. A
/// project task of the same name takes precedence (handled by the early
/// `has_task` return → falls through to [`cmd::run`]).
///
/// Returns `Ok(Some(code))` when the fallback handled the token, `Ok(None)`
/// to fall through to [`cmd::run`] (task dispatch / PM-exec).
///
/// Qualified tokens (`source:verb`) carry the `source:` prefix, so they never
/// match a bare verb arm and fall through untouched — no qualifier parsing
/// needed here. `info` maps to a plain `list` (no deprecation warning): the
/// deprecation is specific to the explicit `runner info` subcommand, and
/// emitting it on the run path — where the user typed `run info` — would be
/// misleading and would spuriously fire the GitHub Actions annotation.
fn run_path_builtin_fallback(
ctx: &types::ProjectContext,
overrides: &resolver::ResolutionOverrides,
name: &str,
) -> Result<Option<i32>> {
if has_task(ctx, name) {
return Ok(None);
}
let code = match name {
"install" => cmd::install(ctx, overrides, false)?,
"clean" => {
cmd::clean(ctx, false, false)?;
0
}
// `info` maps to a plain `list`: the deprecation warning is specific
// to the explicit `runner info` subcommand, not the run path.
"list" | "info" => {
cmd::list(ctx, overrides, false, false, None, schema::CURRENT_VERSION)?;
0
}
"completions" => {
cmd::completions(None, None)?;
0
}
_ => return Ok(None),
};
Ok(Some(code))
}

/// Resolve the effective JSON schema version for schema-aware output:
/// explicit `--schema-version=N` wins, otherwise default to latest.
fn resolve_schema_version(requested: Option<u32>) -> Result<u32> {
Expand Down Expand Up @@ -759,11 +816,6 @@ fn dispatch(cli: cli::Cli, dir: &Path) -> Result<i32> {
let overrides = dispatch_overrides(&cli, loaded_config.as_ref(), &mut ctx)?;

match cli.command {
// A project task named `info` always shadows the deprecated
// subcommand, regardless of flags.
Some(cli::Command::Info { .. }) if has_task(&ctx, "info") => {
cmd::run(&ctx, &overrides, "info", &[], None)
}
None => {
cmd::info(&ctx, &overrides, false, schema::CURRENT_VERSION)?;
Ok(0)
Expand Down Expand Up @@ -801,33 +853,17 @@ fn dispatch(cli: cli::Cli, dir: &Path) -> Result<i32> {
cmd::run(&ctx, &overrides, &args[0], &args[1..], None)
}
}
Some(cli::Command::Install {
frozen: false,
tasks,
..
}) if tasks.is_empty() && has_task(&ctx, "install") => {
cmd::run(&ctx, &overrides, "install", &[], None)
}
Some(cli::Command::Install { frozen, tasks, .. }) if !tasks.is_empty() => {
dispatch_install_chain(&ctx, &overrides, frozen, &tasks)
}
Some(cli::Command::Install { frozen, .. }) => cmd::install(&ctx, &overrides, frozen),
Some(cli::Command::Clean {
yes: false,
include_framework: false,
}) if has_task(&ctx, "clean") => cmd::run(&ctx, &overrides, "clean", &[], None),
Some(cli::Command::Clean {
yes,
include_framework,
}) => {
cmd::clean(&ctx, yes, include_framework)?;
Ok(0)
}
Some(cli::Command::List {
raw: false,
json: false,
source: None,
}) if has_task(&ctx, "list") => cmd::run(&ctx, &overrides, "list", &[], None),
Some(cli::Command::List { raw, json, source }) => {
let schema_version = schema_version_for_json(json, cli.global.schema_version)?;
cmd::list(
Expand All @@ -840,10 +876,6 @@ fn dispatch(cli: cli::Cli, dir: &Path) -> Result<i32> {
)?;
Ok(0)
}
Some(cli::Command::Completions {
shell: None,
output: None,
}) if has_task(&ctx, "completions") => cmd::run(&ctx, &overrides, "completions", &[], None),
Some(cli::Command::Completions { shell, output }) => {
cmd::completions(shell, output.as_deref())?;
Ok(0)
Expand Down
131 changes: 131 additions & 0 deletions tests/builtin_verb_dispatch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//! Integration coverage for the builtin-verb dispatch split.
//!
//! The explicit `runner <verb>` subcommand is ALWAYS the builtin — a
//! same-named project task never shadows it. The run path (`run <verb>` /
//! `runner run <verb>`) runs a same-named task when one exists, else falls
//! back to the builtin's default (no-flag) form.
//!
//! `list` is the exercise verb here: its builtin is cheap (renders the task
//! list, spawns nothing), unlike `install`/`clean` which would shell out to
//! real package managers. The fallback helper is verb-agnostic, so proving
//! `list` exercises the same code path that serves every builtin verb.
//!
//! Fixtures use `just`; if it's not on PATH the assertions are skipped
//! rather than failing, matching `info_deprecation.rs`.

use std::path::PathBuf;
use std::process::Command;

fn runner_binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_runner"))
}

fn run_binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_run"))
}

fn fixture(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}

fn just_available() -> bool {
Command::new("just")
.arg("--version")
.output()
.is_ok_and(|o| o.status.success())
}

#[test]
fn runner_list_is_always_builtin_even_with_a_list_task() {
if !just_available() {
eprintln!("skipping: `just` not found on PATH");
return;
}
// A project task named `list` does NOT shadow the explicit subcommand.
let output = Command::new(runner_binary())
.args(["--dir", fixture("list-shadowed").to_str().unwrap(), "list"])
.output()
.expect("runner binary spawns");

assert!(output.status.success(), "`runner list` should exit 0");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("project-list-recipe-ran"),
"the project `list` recipe must NOT run for the explicit subcommand. stdout: {stdout}",
);
// Builtin list shape: the recipe names appear as listed tasks.
assert!(
stdout.contains("list") && stdout.contains("build"),
"expected the justfile recipes in the builtin list output. stdout: {stdout}",
);
}

#[test]
fn run_list_runs_a_same_named_task() {
if !just_available() {
eprintln!("skipping: `just` not found on PATH");
return;
}
let dir = fixture("list-shadowed");
let dir = dir.to_str().unwrap();
let invocations: [(&str, PathBuf, Vec<&str>); 2] = [
("run list", run_binary(), vec!["--dir", dir, "list"]),
(
"runner run list",
runner_binary(),
vec!["--dir", dir, "run", "list"],
),
];
for (label, binary, args) in invocations {
let output = Command::new(binary)
.args(&args)
.output()
.expect("binary spawns");

assert!(output.status.success(), "`{label}` should exit 0");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("project-list-recipe-ran"),
"`{label}` should run the project `list` recipe. stdout: {stdout}",
);
}
}

#[test]
fn run_list_falls_back_to_builtin_when_no_task() {
if !just_available() {
eprintln!("skipping: `just` not found on PATH");
return;
}
// `info-deprecated` defines `build`/`test` but no `list` task, so the
// run path falls back to the builtin list (rather than the PM-exec
// path, which would try to spawn a bare `list` binary and fail).
let dir = fixture("info-deprecated");
let dir = dir.to_str().unwrap();
for (label, binary, args) in [
("run list", run_binary(), vec!["--dir", dir, "list"]),
(
"runner run list",
runner_binary(),
vec!["--dir", dir, "run", "list"],
),
] {
let output = Command::new(binary)
.args(&args)
.output()
.expect("binary spawns");

assert!(
output.status.success(),
"`{label}` should fall back to the builtin list and exit 0. stderr: {}",
String::from_utf8_lossy(&output.stderr),
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("build") && stdout.contains("test"),
"`{label}` builtin-list fallback should render the justfile tasks. stdout: {stdout}",
);
}
}
5 changes: 5 additions & 0 deletions tests/fixtures/list-shadowed/justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
list:
@echo "project-list-recipe-ran"

build:
@echo "build-ran"
Loading