Summary
run <path> / runner [run] <path> does not recognize local files at all. A token that points at a local file should be run as that file — executable/shebang scripts executed, source files (.ts/.js/.py/…) run via the project runtime — and must never be handed to a package manager's package-exec primitive (bunx/npx/deno x) as a remote package spec. No ./ should be required.
Actual current behavior (broken)
runner has no local-file / script detection. A path token is looked up as a task name; on no match it hits the PM-exec fallback (build_pm_exec_command, src/cmd/run/dispatch.rs:264). What happens then depends entirely on whether a node PM was detected:
A node PM is detected (bun/npm/pnpm/yarn) → <pm> x <token>, which resolves the local path as a remote package:
$ run bin/recipe-tmlang.ts → bun bin/recipe-tmlang.ts
error: GET https://api.github.com/repos/bin/recipe-tmlang.ts/tarball/ - 404
$ run ./bin/recipe-tmlang.ts → bun ./bin/recipe-tmlang.ts
error: "git clone" for "@./bin/recipe-tmlang.ts" failed
error: @./bin/recipe-tmlang.ts failed to resolve
This file is executable and has a shebang — none of that is consulted. Adding ./ changes nothing; both forms go to bun x. This is the core breakage.
No node PM is detected → the fallback's None arm spawns the token directly from PATH/cwd, which happens to run executables but is brittle:
$ run ./build.sh foo → exec ./build.sh foo ✓ (only because no PM was detected)
$ run ./noexec.sh → exec ./noexec.sh
Error: Permission denied (os error 13) ✗ (shebang ignored)
So today the exec bit and shebang are never used; whether a local script runs is an accident of PM detection.
Proposed behavior
Detect a path-like token — contains a path separator (/, or \ on Windows), starts with .//..////~, or names an existing file — and short-circuit to local-file execution before PM resolution and the PM-exec fallback. No ./ required.
- Executable file → spawn directly with forwarded args (
→ script <path>).
- Non-executable file with a
#! shebang → parse the interpreter and spawn <interp> [shebang-arg] <file> <args> (#!/usr/bin/env -S deno run → deno run <file>).
- Recognized source file by extension → run with the project runtime, NOT package-exec:
.ts/.tsx/.js/.mjs/.cjs → bun <file> / deno run <file> / node <file> (per detected runtime); .py → python/uv run; etc.
- Otherwise → a clear, actionable error instead of a 404 or "Permission denied".
Hard rule
A token that resolves to an existing local file must never reach bunx/npx/pnpm dlx/deno x. Those are for remote/registry packages; a local path there is always a bug (the GitHub 404 / git clone failures above).
Windows
No +x bit — rely on the shebang (parse + invoke interpreter), known executable extensions (.ps1/.cmd/.bat/.exe), and the extension→runtime map.
Implementation pointers
- Add a path-token branch at the top of
resolve_dispatch (src/cmd/run/dispatch.rs), before task-name lookup, the PM resolve, and build_pm_exec_command. A token with a separator (or an existing-file match) is unambiguously not a bare task name.
- Classify: executable bit (unix) → direct spawn;
#! first line → interpreter; extension → runtime map (reuse tool::bun/tool::deno/tool::node/tool::uv); else error.
- Reuse
configure_command so the file inherits cwd + node_modules/.bin PATH like any dispatched task.
- Note the existing Go special-case in
build_pm_exec_command already treats /-containing tokens as go run <path> — generalize that instinct to all runtimes instead of leaving it Go-only.
Open questions
- Trigger on any separator (
bin/x.ts) or only .//..//abs/~? (Lean: any separator AND any existing-file match — the user should never need ./.)
- Path token that also matches a task name → file or task? (Lean: explicit path wins; collisions are unlikely since paths have separators.)
- Extension→runtime mapping driven by detected project runtime, then a default table? (
.ts in a Deno project → deno run; in a Bun project → bun.)
- For an executable file, honor its shebang directly, or spawn-direct and only parse the shebang on
EACCES/ENOEXEC? (Lean: spawn direct, retry via shebang on failure.)
Summary
run <path>/runner [run] <path>does not recognize local files at all. A token that points at a local file should be run as that file — executable/shebang scripts executed, source files (.ts/.js/.py/…) run via the project runtime — and must never be handed to a package manager's package-exec primitive (bunx/npx/deno x) as a remote package spec. No./should be required.Actual current behavior (broken)
runner has no local-file / script detection. A path token is looked up as a task name; on no match it hits the PM-exec fallback (
build_pm_exec_command,src/cmd/run/dispatch.rs:264). What happens then depends entirely on whether a node PM was detected:A node PM is detected (bun/npm/pnpm/yarn) →
<pm> x <token>, which resolves the local path as a remote package:This file is executable and has a shebang — none of that is consulted. Adding
./changes nothing; both forms go tobun x. This is the core breakage.No node PM is detected → the fallback's
Nonearm spawns the token directly from PATH/cwd, which happens to run executables but is brittle:So today the exec bit and shebang are never used; whether a local script runs is an accident of PM detection.
Proposed behavior
Detect a path-like token — contains a path separator (
/, or\on Windows), starts with.//..////~, or names an existing file — and short-circuit to local-file execution before PM resolution and the PM-exec fallback. No./required.→ script <path>).#!shebang → parse the interpreter and spawn<interp> [shebang-arg] <file> <args>(#!/usr/bin/env -S deno run→deno run <file>)..ts/.tsx/.js/.mjs/.cjs→bun <file>/deno run <file>/node <file>(per detected runtime);.py→python/uv run; etc.Hard rule
A token that resolves to an existing local file must never reach
bunx/npx/pnpm dlx/deno x. Those are for remote/registry packages; a local path there is always a bug (the GitHub 404 /git clonefailures above).Windows
No
+xbit — rely on the shebang (parse + invoke interpreter), known executable extensions (.ps1/.cmd/.bat/.exe), and the extension→runtime map.Implementation pointers
resolve_dispatch(src/cmd/run/dispatch.rs), before task-name lookup, the PM resolve, andbuild_pm_exec_command. A token with a separator (or an existing-file match) is unambiguously not a bare task name.#!first line → interpreter; extension → runtime map (reusetool::bun/tool::deno/tool::node/tool::uv); else error.configure_commandso the file inherits cwd +node_modules/.binPATH like any dispatched task.build_pm_exec_commandalready treats/-containing tokens asgo run <path>— generalize that instinct to all runtimes instead of leaving it Go-only.Open questions
bin/x.ts) or only.//..//abs/~? (Lean: any separator AND any existing-file match — the user should never need./.).tsin a Deno project →deno run; in a Bun project →bun.)EACCES/ENOEXEC? (Lean: spawn direct, retry via shebang on failure.)