Skip to content

feat: Add wt cd command and wt init for native shell integration#41

Open
beastyrabbit wants to merge 6 commits intojohnlindquist:mainfrom
beastyrabbit:feature/cd-command
Open

feat: Add wt cd command and wt init for native shell integration#41
beastyrabbit wants to merge 6 commits intojohnlindquist:mainfrom
beastyrabbit:feature/cd-command

Conversation

@beastyrabbit
Copy link

@beastyrabbit beastyrabbit commented Jan 30, 2026

Summary

Adds two new commands that work together to let users navigate into worktree directories with a native cd — no subshell required.

wt cd [pathOrBranch]

Navigate into a worktree directory. Resolves by branch name, filesystem path, or interactive picker.

  • Default: spawns a subshell in the target directory (works without any setup)
  • --print: outputs the resolved path to stdout and exits — used by the shell wrapper below

Errors go to stderr, stdout is reserved for machine-readable output. Propagates shell exit codes and signals correctly.

wt init [shell]

Outputs a shell wrapper function (zsh, bash, or fish) that intercepts wt cd and performs a native builtin cd using --print.

# Auto-detects shell from $SHELL
eval "$(wt init)"

# Or specify explicitly
eval "$(wt init zsh)"
  • Auto-detects shell from $SHELL when no argument given
  • Detects if already installed in the user's rc file
  • Shows a copyable command to append the init line and reload the shell
  • If --print fails, falls back to the normal subshell wt cd

Other improvements

  • wt open now routes all errors to process.stderr.write consistently
  • Narrowed catch blocks to ENOENT where appropriate (cd.ts, open.ts)
  • Added reject: false to git rev-parse calls for graceful error handling
  • wt open outputs raw path when editor is none

Files changed

File What
src/commands/cd.ts New cd command handler
src/commands/init.ts New init command with shell functions
src/index.ts Register cd and init commands
src/utils/tui.ts Add stdout stream option to selectWorktree
src/commands/open.ts Consistent stderr, narrowed catches
test/cd.test.ts 23 tests for cd
test/init.test.ts 23 tests for init
README.md Document shell integration

Somewhere a GPU is overheating so I don't have to think.

Summary by CodeRabbit

  • New Features

    • Added wt cd [pathOrBranch] (with --print) to print or open a worktree, wt init [shell] to emit shell integration for zsh/bash/fish, and CLI wiring for both.
    • TUI/select now supports forwarding stdout for scripted integration.
  • Bug Fixes

    • Clearer stderr messaging, consistent exit codes, ENOENT-aware resolution, interactive-cancel handling, and editor-skip reliably prints path to stdout.
  • Documentation

    • README updated with examples for open, cd, init, shell setup, and interactive usage.
  • Tests

    • Added comprehensive tests for cd and init.

✏️ Tip: You can customize this high-level summary in your review settings.

Add `wt cd` to navigate into worktree directories via subshell, and
`wt init` to output a shell wrapper function that enables native `cd`
without spawning a subshell.

wt cd:
- Resolve worktree by branch name, filesystem path, or interactive picker
- Spawn a subshell in the target directory (fallback for users without shell integration)
- --print flag outputs the resolved path to stdout for scripting/shell wrappers
- Proper signal forwarding and exit code propagation
- All user-facing output goes to stderr; stdout reserved for machine-readable output

wt init:
- Outputs a shell wrapper function for zsh, bash, or fish
- Auto-detects shell from $SHELL when no argument given
- Wrapper intercepts `wt cd`, uses --print + builtin cd for native directory change
- Falls back to subshell-based `wt cd` if --print fails
- Detects if already installed in rc file
- Shows copyable commands to add to rc file and reload shell

Also improves wt open:
- Route all errors to stderr consistently (process.stderr.write)
- Narrow catch blocks to ENOENT only
- Output raw path when editor is set to "none"
- Add reject:false to git rev-parse for graceful error handling

Powered by human calories and mass GPU cycles.
@coderabbitai
Copy link

coderabbitai bot commented Jan 30, 2026

📝 Walkthrough

Walkthrough

Adds two CLI subcommands (cd, init), implements path-first, ENOENT-aware resolution for open/cd with a --print mode, provides shell integration wrappers and installer guidance, and threads an optional stdout through the TUI to control interactive output routing.

Changes

Cohort / File(s) Summary
Docs
README.md
Adds user docs for wt open editor behavior, wt cd (including --print) and wt init shell integration examples for zsh/bash/fish.
CD Command
src/commands/cd.ts, build/commands/cd.js, test/cd.test.ts
New cdWorktreeHandler: interactive selection, path-first resolution with ENOENT-aware stat fallbacks, --print mode vs spawning cross-platform subshells, detailed errors, and comprehensive tests.
Init Command
src/commands/init.ts, build/commands/init.js, test/init.test.ts
New initHandler, detectShell, getShellFunction: emits shell wrapper functions, autodetects/validates shells, checks RC install lines, and prints installation guidance; tested.
Open Command Updates
src/commands/open.ts, build/commands/open.js
Path-first resolution, ENOENT-preserving stat handling, clearer stdout/stderr separation, interactive cancellation handling, editor-skip behavior (print path), and local isEnoent helper.
TUI Output Propagation
src/utils/tui.ts, build/utils/tui.js
selectWorktree accepts optional stdout and forwards it to prompts so interactive UI can be directed to a chosen stream.
CLI Wiring
src/index.ts, build/index.js
Registers new cd and init subcommands and imports their handlers.
Tests
test/*.ts
Adds extensive tests for cd and init, covering interactive flows, filesystem ENOENT cases, shell detection, RC install checks, and stdout/stderr behavior.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI
    participant TUI
    participant Filesystem
    participant Git
    participant Shell

    User->>CLI: wt cd [pathOrBranch]
    alt no argument
        CLI->>TUI: prompt selection (forwards stdout if provided)
        TUI-->>CLI: selected WorktreeInfo or null
        alt null
            CLI-->>User: write cancellation to stderr
            CLI->>CLI: exit 0
        end
    end
    CLI->>Filesystem: stat(path) (capture ENOENT)
    alt path exists & is directory
        CLI->>Git: findWorktreeByPath(path)
        alt found
            Git-->>CLI: WorktreeInfo
        else .git present
            Filesystem-->>CLI: construct minimal WorktreeInfo
        end
    else path missing or not dir
        CLI->>Git: findWorktreeByBranch(branch)
        alt found
            Git-->>CLI: WorktreeInfo
        else not found
            CLI-->>User: write error to stderr (suggest interactive)
            CLI->>CLI: exit 1
        end
    end
    CLI->>Filesystem: verify target path exists (handle ENOENT)
    alt options.print
        CLI-->>User: write path to stdout
    else spawn subshell
        CLI-->>User: "Entering ..." to stderr
        CLI->>Shell: spawn subshell in target dir
        Shell-->>User: interactive session
        Shell-->>CLI: exit code/signal
        CLI-->>User: propagate exit code
    end
Loading
sequenceDiagram
    actor User
    participant CLI
    participant ShellDetect
    participant Filesystem
    participant UserRC

    User->>CLI: wt init [shell]
    alt shell arg provided
        CLI->>ShellDetect: validate shell name
        ShellDetect-->>CLI: supported or unsupported
    else autodetect
        CLI->>ShellDetect: detect from $SHELL
        ShellDetect-->>CLI: shell name or null
    end
    CLI->>ShellDetect: getShellFunction(shell)
    ShellDetect-->>CLI: function text (stdout)
    CLI->>UserRC: read RC file (~/.rc)
    alt already installed
        CLI-->>User: write installed notice to stderr
    else not installed
        CLI-->>User: write append command and reload instruction to stderr
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hopping through branches, shells, and paths so bright,

I print the burrow's way or spawn a subshell at night.
Init stitches my burrow in zsh, bash, and fish,
TUI guides my hops, and cd brings the wish. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Add wt cd command and wt init for native shell integration' accurately and concisely describes the main changes: two new commands (cd and init) enabling native shell integration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@build/commands/open.js`:
- Around line 13-14: The git validation currently calls execa("git",
["rev-parse", "--is-inside-work-tree"], { reject: false }) but ignores the
returned result, so failures are not detected; change this to capture the result
(e.g., const result = await execa(...)) and check result.exitCode (or
result.failed) and, if non-zero/failed, handle it (throw an error or log and
process.exit(1)) so the command aborts when not inside a git repo; alternatively
remove reject: false to let execa throw and catch that exception—update the call
site where execa("git", ["rev-parse", "--is-inside-work-tree"], ...) is invoked.

In `@src/commands/open.ts`:
- Around line 18-19: The git repo validation using execa("git", ["rev-parse",
"--is-inside-work-tree"], { reject: false }) in open.ts currently ignores the
command result; update the validation to inspect the returned result (e.g.,
check result.exitCode === 0 or result.stdout === "true") and handle
non-zero/false by throwing or returning a clear error, or switch to { reject:
true } and catch the thrown error; modify the code around the execa call so that
the function (the surrounding command handler in open.ts) fails fast when not
inside a git work tree.

Powered by human calories and mass GPU cycles.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/commands/open.ts (1)

54-69: ⚠️ Potential issue | 🟡 Minor

Inconsistent ENOENT handling in .git check.

The inner catch block at line 66 catches all errors, unlike other catch blocks in this file that use isEnoent(). If stat(.git) fails with a permission error, the message incorrectly states the path "is not a git worktree."

🔧 Proposed fix for consistency
                         try {
                             await stat(resolve(pathOrBranch, ".git"));
                             targetWorktree = {
                                 path: resolve(pathOrBranch),
                                 head: '',
                                 branch: null,
                                 detached: false,
                                 locked: false,
                                 prunable: false,
                                 isMain: false,
                                 bare: false,
                             };
-                        } catch {
+                        } catch (gitErr: unknown) {
+                            if (!isEnoent(gitErr)) throw gitErr;
                             process.stderr.write(chalk.red(`The path "${pathOrBranch}" exists but is not a git worktree.`) + "\n");
                             process.exit(1);
                         }
🤖 Fix all issues with AI agents
In `@build/commands/open.js`:
- Around line 60-63: The bare catch in the open command swallow errors; update
the catch in the open logic (the try/catch that checks whether pathOrBranch is a
git worktree in open.js and, more importantly, in src/commands/open.ts) to
detect ENOENT using isEnoent(err) and only treat missing-path as the expected
case; rethrow or log other errors. Specifically, change the catch to accept the
error parameter (e.g., catch (err)) and call isEnoent(err) to decide whether to
print the "not a git worktree" / exit or rethrow (or log) the unexpected error,
then rebuild the project so the generated build/commands/open.js matches
src/commands/open.ts.

Powered by human calories and mass GPU cycles.
When `eval "$(wt init zsh)"` runs from .zshrc on every terminal open,
the "Already installed" message and setup hints were printed to stderr
each time. Now `initHandler` returns silently after emitting the shell
function when called with an explicit shell arg. Guidance output is
reserved for the interactive `wt init` (auto-detect) path.

Powered by human calories and mass GPU cycles.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/commands/init.ts`:
- Around line 24-35: Update the embedded FISH_FUNCTION so the resolved directory
is passed safely: change the builtin cd invocation to use an argument separator
and quoted variable (replace builtin cd $dir with builtin cd -- "$dir") and also
ensure the fallback call uses -- before positional args (replace command wt cd
$argv[2..-1] with command wt cd -- $argv[2..-1]) so directories that start with
dashes or contain spaces are handled correctly.

fix for fish

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/commands/init.ts`:
- Around line 6-22: The bash/zsh wrapper BASH_ZSH_FUNCTION should protect
against dash-prefixed directory names by terminating option parsing when calling
cd; update the branch that currently does builtin cd "$dir" inside the wt()
function to use builtin cd -- "$dir" so leading "-" is treated as a literal
directory name (note that a directory literally named "-" still requires a user
to refer to it as ./-). Ensure the change is only applied to the builtin cd
invocation in the wt() function.

also zsh fix

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant