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

Skip to content

fix(run): run <path> mis-routes local files to package-exec instead of executing them #69

Description

@kjanat

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.

  1. Executable file → spawn directly with forwarded args (→ script <path>).
  2. Non-executable file with a #! shebang → parse the interpreter and spawn <interp> [shebang-arg] <file> <args> (#!/usr/bin/env -S deno rundeno run <file>).
  3. Recognized source file by extension → run with the project runtime, NOT package-exec: .ts/.tsx/.js/.mjs/.cjsbun <file> / deno run <file> / node <file> (per detected runtime); .pypython/uv run; etc.
  4. 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.)

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions