Skip to content

Shell completion scripts use fragile runtime-specific exec path construction (x) — should use program name instead #135

Description

@techzealot

Package

@bomb.sh/tab

Package Version

0.0.17

Node.js Version

v24.15.0

Operating System

macOS

Describe the bug

Environment

  • Shell: zsh (macOS)
  • Runtime: bun compiled binary (bun build --compile)
  • Also tested with: tsx (Node.js)

Problem

All three framework adapters (commander, cac, citty) construct an exec path string x at module load time:

const execPath = process.execPath;
const processArgs = process.argv.slice(1);
const x = `${quotedExecPath} ${quotedProcessExecArgs.join(' ')} ${quotedProcessArgs[0]}`;

This x is baked into generated completion scripts as the command to invoke for completions:

requestComp="/path/to/node --import tsx /path/to/script.ts complete -- ${quoted_args[*]}"

This breaks when the CLI is compiled to a binary with bun build --compile, because bun injects a virtual filesystem path into process.argv[1] (/$bunfs/root/binary-name), producing an invalid command:

requestComp="/path/to/my-cli  /$bunfs/root/my-cli complete -- ..."

Root Cause

The x construction assumes Node's process.argv structure where argv[0] is the interpreter and argv[1] is the script path. This assumption doesn't hold for compiled runtimes (bun, deno compile, pkg, etc.) where argv[1] is already a user argument or an internal virtual path.

How Shell Completion Triggering Works (zsh)

In zsh, completion scripts use compdef to bind a completion function to a command name:

compdef _my-cli my-cli    # bind _my-cli function to "my-cli" command

When the user types my-cli <Tab>, zsh checks:

  1. Is my-cli a recognized command? (in PATH, alias, or shell function)
  2. If yes → look up compdef binding → call _my-cli
  3. If no → fall back to default completion (file paths, etc.)

The requestComp variable inside _my-cli is only executed after the function is triggered. It doesn't affect whether completion is triggered in the first place.

This means: as long as the command name is recognized by the shell, using the program name in requestComp is sufficient — the shell resolves it via PATH, alias, or function. This is how mature implementations work (Go's cobra, Rust's clap, Python's argcomplete).

Why x Has No Practical Benefit

Given the zsh completion trigger mechanism above, the auto-constructed x provides no value in any real scenario:

Scenario Completion triggers? Program name sufficient?
Global install / binary in PATH Yes (my-cli recognized) Yes
Via package manager delegation (pnpm my-cli) Yes (pnpm in PATH) Yes (pnpm resolves local deps)
Binary not in PATH, with shell function Yes (function recognized) Yes (function resolves to binary)
Binary not in PATH, no alias/function No (command not recognized) Nothing works
Development via tsx, no alias/function No (command not recognized) Nothing works

In every scenario where completion can trigger, the program name works. In scenarios where it can't trigger, no x construction can help.

Proposed Fix

Replace the runtime-specific x construction with the program name in all adapters:

// Before (commander adapter)
t.setup(programName, x, shell);

// After
t.setup(programName, programName, shell);

This:

  • Eliminates runtime-specific bugs (bun, deno, pkg, etc.)
  • Removes ~15 lines of fragile code per adapter
  • Aligns with industry standards (cobra, clap, argcomplete)
  • Works with the package manager delegation feature (pnpm my-cli)

For development testing without PATH installation, users can create a shell function:

my-cli() { ./my-cli "$@"; }
source <(my-cli complete zsh)
my-cli greet <TAB>   # completion works

Affected Files

  • src/commander.ts (lines 9-18)
  • src/cac.ts (lines 10-16)
  • src/citty.ts (lines 22-27)

To Reproduce

1.install bun
2.bun build examples/demo.commander-async.ts --compile --target=bun-darwin-arm64 --outfile my-cli
3.source <(./my-cli complete zsh)
4.complete not work

Expected behavior

bun binary version also works

Additional Information

No response

AI assistance disclosure

  • This bug report was drafted with the assistance of an AI tool.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions