Commit Graph

100 Commits

Author SHA1 Message Date
Worktrunk Bot 96c6c846f7 fix(shell): register completions under the --cmd name, not clap's (#3817)
## Problem

`wt config shell init <shell> --cmd <name>` renames the shell wrapper
and its lazy completion loader, but the registration that loader evals
comes from clap, which derives every identifier in it from its own
compile-time `Command` name (`wt`) — not from `argv[0]` and not from
`--cmd`. The two halves never agreed:

```console
$ wt config shell init zsh --cmd wot | grep _clap
        if ! (( $+functions[_clap_dynamic_completer_wot] )); then
        _clap_dynamic_completer_wot "$@"

$ COMPLETE=zsh wt | grep -oE '_clap_dynamic_completer_[a-z_]*' | sort -u
_clap_dynamic_completer_wt
```

Nothing completed, and because the guard never became true the
completion script was regenerated and re-evaluated on *every* TAB. Same
shape in bash (`_clap_complete_*`); PowerShell emitted
`Register-ArgumentCompleter -Native -CommandName wt`, so the `--cmd`
name was never registered at all. The documented `--cmd=git-wt` case
(the Windows Terminal conflict) was broken too — including for a binary
genuinely installed under that name, since clap's name comes from the
declaration rather than `argv[0]`.

There is a second, sharper edge: zsh's registration ends with `compdef
<completer> <cmd>`, so the first TAB on `wot` also bound worktrunk's
completer to plain `wt` — handing completions to the *other* `wt` that
`--cmd` exists to step around.

fish and nushell were unaffected. Both register a completer that shells
out to the binary rather than depending on a clap-emitted identifier, so
the reporter's "unverified" row for fish is a pass.

## Solution

The bash, zsh, and PowerShell loaders now pass the name they bind in
`WORKTRUNK_COMPLETE_NAME`, and `registration_name()` in
`src/completion.rs` emits the registration under that name (validated
through the same `validate_shell_command_name` guard `--cmd` uses, since
the value lands verbatim in generated shell code). The fallback is
`binary_name()`, which covers a binary installed as `git-wt` and invoked
directly. The templates apply clap's own `-` → `_` escaping to the
function they call, so `--cmd git-wt` guards on `_clap_complete_git_wt`
rather than the invalid `_clap_complete_git-wt`.

That fixes all four shells and the stray `compdef` in one place, rather
than pinning the templates to clap's internal naming:

```console
$ WORKTRUNK_COMPLETE_NAME=wot COMPLETE=zsh wt | grep -oE '_clap_dynamic_completer_[a-z_]*|compdef .*' | sort -u
_clap_dynamic_completer_wot
compdef _clap_dynamic_completer_wot wot
```

## Testing

Two reproduction tests in `tests/integration_tests/completion.rs`, both
failing before the change:

- `test_init_custom_cmd_defines_clap_completer_in_bash` drives the whole
chain through a real bash — generate the init script with `--cmd`, call
the loader it defines, then assert clap's completer function exists
afterwards. Printed `MISSING` before, `DEFINED` after. Cases for `wot`
and `git-wt`.
- `test_completion_registration_uses_shell_integration_cmd_name` covers
zsh and PowerShell, which CI can't drive: the identifier the init script
references must be the one the registration defines, and the `compdef` /
`-CommandName` target must be the `--cmd` name.

`cargo test --lib --bins` and `cargo test --test integration` are
otherwise green (one unrelated failure locally,
`test_copy_ignored_preserves_file_executable_permissions`, from this
sandbox's `umask 0002`), and `cargo clippy --all-targets --all-features`
/ `cargo fmt --check` are clean.

---
Closes #3816 — automated triage

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-08-13 16:44:02 -07:00
Worktrunk Bot bdd7113c95 fix(shell): catch a failing --execute body in the nushell wrapper (#3734) 2026-08-05 13:38:58 -07:00
Worktrunk Bot 78010166bd fix(shell): bypass rm aliases in the nushell wrapper's cleanup (#3732)
## Problem

The nushell wrapper's temp-file cleanup called a bare `rm -f`. Nushell
resolves aliases at parse time, and `config.nu` runs before the
`$nu.vendor-autoload-dirs` file the wrapper installs into — so a user's
`alias rm = ...` is already in scope when the wrapper's `def` is parsed,
and intercepts the cleanup. #3714 fixed the same shadowing in the bash
and zsh wrappers with `command rm`; nushell has no `command` builtin, so
it was left out.

Two failures, both verified end-to-end against nushell 0.114.1 (the
version CI pins) driving the real rendered wrapper:

| `alias rm =` | before | after |
|---|---|---|
| *(none)* | exit 0, stdout returned, 0 temp files left | unchanged |
| `^false` | exit 1, **stdout empty**, **3 temp files leaked** | exit 0,
stdout returned, 0 left |
| `^echo TRASHED` | `TRASHED -f /tmp/...` lines **injected into
stdout**, **3 temp files leaked** | exit 0, stdout returned, 0 left |

The `^false` row is the serious one. Cleanup sits between `let output =
(open $stdout_file --raw)` and the function's return, and nushell 0.98+
raises `ShellError` on a non-zero external exit — so an `rm` alias that
fails, prompts, or isn't installed on that machine aborts the wrapper
before it returns, and the command's stdout is silently discarded. Only
stdout is affected; stderr has already streamed to the terminal, which
is why the symptom hides on `wt switch` (its output is on stderr) and
shows on `wt config show`.

## Solution

Consolidate the two cleanup calls into one, after the last read of any
temp file, and branch at runtime on `$nu.os-info.family`:

```nu
if $nu.os-info.family == "windows" {
    try { rm -f $cd_file $exec_file $stdout_file }
} else {
    try { ^rm -f $cd_file $exec_file $stdout_file }
}
```

`^rm` bypasses alias expansion entirely — the Unix fix is complete, not
partial. Windows keeps the builtin because `^rm` is external-only and
Windows has no external `rm`; `try` covers both branches so cleanup can
never abort the wrapper again.

Branching at *runtime* rather than forking the template by build
platform is what keeps this cheap: `wt config shell init nu` renders one
text on every platform, so there's a single `init_nu` snapshot and no
platform-specific rendering to maintain.

<details><summary>Options considered and rejected</summary>

- **`` `rm` `` (backtick-quoted)** — bypasses the alias, but `` `rm` ``
with `PATH` emptied reports ``Command `rm` not found``, and `` `rm` ``
with no args prints `/usr/bin/rm: missing operand`. It resolves to the
*external*, so it's just `^rm` spelled differently — same Windows gap,
no benefit.
- **`hide rm`** — works inside the `def`, but `hide` is a parse-time
keyword that leaks into the enclosing scope. For an autoloaded file that
scope is the user's session, so installing shell integration would
silently unbind their own `rm` alias. Same leak inside a `do {}`
closure. Worse than the bug.
- **`try` alone, no `^rm`** — cross-platform and kills the abort, but
leaves the alias running: a `trash` alias still trashes worktrunk's temp
files on every invocation, and an `rm -i`-style alias still prompts.
- **Forking the template by build platform** — same end state on each
platform, at the cost of platform-specific `init_nu` snapshots. The
runtime branch gets there without them.

</details>

## Testing

`test_nu_wrapper_cleanup_survives_rm_alias` in
`tests/integration_tests/shell_wrapper.rs` runs the real wrapper through
a PTY with `alias rm = ^false` declared ahead of it, against a dedicated
`TMPDIR`, and asserts both symptoms: the wrapper's stdout comes back
non-empty, and the temp dir is empty afterwards. It fails on the current
template (marker absent — the wrapper aborted) and passes with the fix.

Also run locally: `cargo test --lib --bins`, and the full
`shell_wrapper` + `config_show` + `test_docs_are_in_sync` integration
set with `--features shell-integration-tests` (201 passed).

## Not fixed here

A user alias to a *custom command or builtin* whose signature rejects
`-f` — e.g. `alias rm = print "…"` — makes the whole wrapper file fail
to parse, leaving `wt` undefined rather than merely broken. That's
pre-existing (today's template has two bare `rm -f` calls) and survives
this change, because the Windows branch is still parsed on Unix even
though it never runs. Eliminating it means emitting no bare `rm` at all
on Unix, which is the template fork above. Flagged on #3730 rather than
bundled in here.

---
Closes #3730 — automated triage

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-08-05 12:24:31 -07:00
Aral e949ed5fda fix(shell): use command rm in wrapper cleanup to bypass rm aliases (#3714) 2026-08-03 07:53:14 -07:00
Maximilian Roos 1eded27943 Simplify forge, shell integration, and removal internals (#3662)
This consolidates cross-cutting models that had accumulated parallel
representations, while preserving the CLI and config interfaces.

## What changed

- Forge identity now flows through one `ForgeKind`, with boundary-safe
network-host classification shared by CI, remote references, and
structured repository metadata. Branded SSH aliases such as
`github-personal` remain outside provider dispatch and receive an
actionable `forge.platform` diagnostic when forge data is requested.
- Worktree removal now uses one owned target type and rechecks uncached
worktree topology immediately before compare-and-swap branch deletion,
retaining branches that gained a live or locked checkout.
- Shell integration no longer implements the retired single-file
directive writer. Stale wrappers receive repair guidance, execution
fails closed, and child processes cannot inherit the retired
sourceable-file capability.
- Zsh and Git-version probes are shared, while dead mocks, redundant
dependency edges, serializer detours, pass-through types, and duplicate
tests are removed.

The removal guard deliberately distinguishes live, stale-prunable, and
locked registrations. The detached cleanup path performs the same
record-aware check before deleting a branch ref.

## Testing

`cargo run -- hook pre-merge --yes` passed after merging current
`origin/main`: 4,489 tests, Clippy, formatting, lockfile checks, docs,
doctests, and snapshot review.

> _This was written by Claude Code on behalf of max_.
2026-07-30 02:10:07 -07:00
Worktrunk Bot eba35d039c fix(shell): stop PowerShell wrapper leaking a stray exit-code line to stdout (#3425) 2026-07-12 01:54:55 -07:00
Worktrunk Bot c2b7ab683e fix(shell): guard fish lazy-load wrapper against completion-mode recursion (#3250)
Follow-up to #3241, requested in #3240.

#3241 made the completion files **wt itself writes** resolve the binary,
so they no longer re-enter the fish lazy-load stub. But the stub
(`functions/wt.fish`) is still unguarded, leaving one narrow residual
case: a **stale third-party completion** that calls the bare `wt`
command with `COMPLETE` set — e.g. an old Homebrew
`vendor_completions.d/wt.fish` left on disk from before an upgrade —
re-enters the stub and recurses to fish's call-stack limit until the
package is rebuilt.

This adds the same `COMPLETE` short-circuit that bash and zsh already
use, so the stub delegates straight to the binary in completion mode
regardless of which completion file is active:

```fish
function wt
    if set -q COMPLETE
        command wt $argv   # bypass the stub, let the binary emit completions
        return
    end
    command wt config shell init fish | source
    ...
end
```

This makes fish immune to stale third-party completions, matching
bash/zsh.

## Changes
- `templates/fish_wrapper.fish` — add the `COMPLETE` guard to the
lazy-load stub.
- `src/shell/mod.rs` — `test_fish_wrapper_guards_completion_mode`
regression test (#3240).
- Regenerated the `configure_shell_fish_dry_run` snapshot.

Closes #3240.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: worktrunk-bot <worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:21:39 -07:00
Worktrunk Bot 03dbf5f70e fix: use builtin cd in fish shell integration (#3160)
## Problem

`wt switch <branch>` reported a successful switch but then failed to
change directory under fish when the
[`kidonng/zoxide.fish`](https://github.com/kidonng/zoxide.fish) plugin
is active, printing `zoxide: no match found` / `Error: Directory not
found` even though the worktree exists (#3159).

The fish integration changed directory with a bare `cd -- "$target"`.
`zoxide.fish` replaces `cd` with a zoxide query function. With `cd --
"$target"` it receives two arguments (`--` and the path), so its
single-directory fast path (`argc -eq 1`) is skipped and it falls
through to fuzzy-query mode, which finds no match for `--
/path/to/worktree`.

The bash and zsh wrappers were already immune because they use `builtin
cd`; only fish used a bare `cd`.

## Solution

Use `builtin cd -- "$target"` in `templates/fish.fish`, bypassing any
user `cd` override (verified: in fish `cd` is a regular function that
`builtin cd` skips). The `--` is retained for consistency with the
bash/zsh wrappers; the target is an absolute path written by the binary,
so it can never be flag-injected.

## Testing

- Added `test_fish_init_uses_builtin_cd` in `src/shell/mod.rs`, which
fails on the old template (`cd -- "$target"`) and passes after the fix.
- Updated the fish init snapshots
(`src/shell/snapshots/...init_fish.snap`,
`tests/snapshots/...init_fish.snap`).
- Verified fish `3.7.0` behavior directly: `cd` is a function, `builtin
cd` and `builtin cd --` both succeed, and `builtin cd` bypasses a
user-defined `cd` function (the simulated override was not invoked).
- `cargo test --test integration test_docs_are_in_sync` passes.

---
Closes #3159 — automated triage

Co-authored-by: worktrunk-bot <worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Maximilian Roos <5635139+max-sixty@users.noreply.github.com>
2026-06-23 16:50:17 +00:00
Ben Drucker ad43df7656 feat(completions): ship dynamic registration for package installs (#3105) 2026-06-18 01:36:59 -07:00
Maximilian Roos 62a7a6c0a5 fix(shell): shell-correct (PowerShell + fish) escaping for the --execute payload (#2843)
## What

`wt switch -x <cmd> -- <args>` builds the `--execute` payload as a
single shell-escaped string. When shell integration is active that
string is written to the EXEC directive file and evaluated by the user's
shell wrapper. wt escaped it with POSIX single-quote escaping
(`shell_escape::unix::escape`) unconditionally — but two wrappers do not
have POSIX single-quote semantics:

- **PowerShell** evaluates the EXEC file with `Invoke-Expression`; the
POSIX `'\''` quoting idiom is invalid PowerShell, which wants embedded
quotes doubled (`''`). This completes the PowerShell-escaping follow-up
that #2815 explicitly deferred.
- **fish** evaluates it with `eval`; inside `'…'` fish treats `\` as an
escape character (`\\`→`\`, `\'`→`'`), unlike POSIX where `\` is
literal. A POSIX-escaped payload containing a backslash is mis-evaluated
under fish — `a\\b` silently loses a backslash, and a trailing backslash
makes fish abort the whole `eval` with a quote-balance parse error.

## Approach

Escaping now keys on the active directive shell. A `ShellEscapeMode`
enum (`Literal` / `Posix` / `PowerShell` / `Fish`) replaces the old
`shell_escape: bool` parameter on `expand_template`;
`directive_shell_escape_mode()` reads the `WORKTRUNK_SHELL` env var (the
PowerShell wrapper already exported it; the fish wrapper now does too).
Only the `--execute` payload is shell-aware — hooks and aliases always
run through `Cmd::shell` (`sh` / Git Bash) and stay POSIX;
filesystem-path templates stay `Literal`.

## For reviewers

- `src/shell_exec.rs` — the new escaping section: `ShellEscapeMode`,
`directive_shell_escape_mode()`, `powershell_escape()`, `fish_escape()`,
`shell_escape_for()`.
- `src/output/global.rs` — `escape_legacy_cd` consolidated onto the same
shell decision.
- `src/commands/worktree/switch.rs` — the `--execute` block escapes via
the active-shell escaper.
- `templates/fish.fish` — exports `WORKTRUNK_SHELL=fish` so wt can
identify the wrapper.
- The diff is large (~+1250/−210) but the substantive change is ~150
lines; the rest is the mechanical `bool`→enum migration across
`expand_template` call sites plus `cargo fmt` reflow of the now-longer
calls.

## Non-breaking

For every non-PowerShell, non-fish user, behavior is byte-identical:
`WORKTRUNK_SHELL` absent ⇒ `Posix` ⇒ the previous
`shell_escape::unix::escape`. The pre-existing POSIX snapshots are
unchanged.

## Testing

`cargo run -- hook pre-merge --yes` clean (3790 tests). The fish escaper
was round-tripped through the actual fish binary (4.7.1) — adversarial
inputs (consecutive, leading, and trailing backslashes; embedded quotes;
`$(…)`; backticks; newlines; empty) all deliver byte-exact, and the
original bug reproduces without the fix. New integration tests cover
both the new-protocol EXEC path and the legacy `cd` directive under
`WORKTRUNK_SHELL=powershell` and `=fish`; unit tests cover
`powershell_escape` and `fish_escape` directly.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:07:45 -07:00
Worktrunk Bot 3ddfc84f58 fix(shell): use builtin cd to bypass cd aliases (e.g. zoxide) (#2644) 2026-05-08 08:15:41 -07:00
Maximilian Roos fdc6df61b9 fix(nushell): switch wrapper to @complete attribute on untyped rest (#2458)
Per fdncred's recommendation in nushell/nushell#18128, replaces the
parameter-level `[...args: string@\"nu-complete wt\"]` with a
function-level `@complete` attribute on an untyped `[...args]` rest. The
completer signature moves from `[context: string]` (with manual `split
row \" \"` reconstruction) to `[spans: list<string>]`, which nushell
pre-tokenizes — net deletion of seven lines of fragile token reassembly.

Untyped rest is the form nushell routes through its normal
external-argument handling, so once nushell/nushell#18131 ships in
stable nu the wrapper will automatically benefit from `--flag=\"value\"`
quote stripping and `~` expansion without further changes. Until then
`--flag=\"value\"` still arrives at the binary with literal quotes — a
pre-existing bug, not a regression introduced here. Supersedes #2437
(the body-level regex strip workaround).

## Test plan

- [x] `cargo test --test integration --features shell-integration-tests`
— 1717 passed (including all four nu wrapper cases)
- [x] `pre-commit run --all-files` — clean
- [x] Smoke-tested the rendered template parses cleanly under nu 0.112.1
- [ ] Hand-verify subcommand completion at trailing space (e.g. `wt
switch <TAB>`) in an interactive nu session — couldn't drive reedline
via PTY in test, but fdncred's example in nushell/nushell#18128 implies
`spans` is populated at the cursor position

Ref nushell/nushell#18128, nushell/nushell#18131

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-29 09:28:42 -07:00
Maximilian Roos 60f571d279 Remove global backslash snapshot filter + fix nushell multi-line exec (#2134)
The test snapshot framework had a global `\\` → `/` filter
(`tests/common/mod.rs`) intended to normalize Windows paths before
path-specific filters ran. It silently corrupted intentional backslashes
in output:

- **JSON-encoded ANSI escapes** (`\u001b` → `/u001b`) in `wt list
--format=json` and diff snapshots. The snapshots had been capturing
corrupted JSON for a long time.
- **Shell line continuations** (`command \` → `command /`) in `wt config
shell init` snapshots, masking template regressions.

Worktrunk emits forward-slash paths via `path_slash` everywhere, so the
blanket normalization wasn't carrying its weight. Dropped it; a
follow-up comment points future contributors at
`add_repo_and_worktree_path_filters` if any test ever produces a raw
Windows path.

14 JSON/diff snapshots and 3 init snapshots regenerated with the real
output (visible in the diff: `/u001b` → `\u001b`, `cmd /` → `cmd \`).

While here: nushell wrapper executed the exec directive file
line-by-line (`^sh -c $directive` in a loop), so multi-line `--execute`
payloads ran as separate shell sessions — variables didn't persist, `cd`
didn't affect later lines, etc. Switched to a single `^sh -c $script`
invocation matching bash/zsh/fish `source` semantics.

> _This was written by Claude Code on behalf of Maximilian_

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:38:32 -07:00
Maximilian Roos b174658e29 Split directive file into CD (raw path) and EXEC (shell) files (#2118)
The shell wrapper previously used a single `WORKTRUNK_DIRECTIVE_FILE`
where wt wrote shell commands (`cd '/path'`, arbitrary `--execute`
payloads). This meant the cd path went through shell parsing — any
content wt wrote was sourced as shell.

This splits the protocol into two files with different trust levels:

- **`WORKTRUNK_DIRECTIVE_CD_FILE`** — raw path, read with `cd -- "$(<
file)"`. No shell parsing, no escaping, no injection surface. Safe to
pass through to alias/hook child processes.
- **`WORKTRUNK_DIRECTIVE_EXEC_FILE`** — arbitrary shell (from
`--execute`), sourced by the wrapper. Scrubbed from alias/hook child
environments so hook bodies cannot inject shell into the parent session.

When a nested `wt` inside an alias body tries `--execute` without the
EXEC file, the command is dropped with a warning linking to #2101 for
user feedback.

The old `WORKTRUNK_DIRECTIVE_FILE` is silently honored for one release
(users who upgrade wt without restarting their shell). Bash, zsh, fish,
and PowerShell self-update on restart; nushell requires `wt config shell
install`.

Closes #2101

> _This was written by Claude Code on behalf of @max-sixty_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-12 13:52:04 -07:00
Maximilian Roos fd00dbd8bf fix(fish): prevent getcwd error after worktree removal in Zellij (#1787)
Replace `cat` (external subprocess) with `string collect` (fish builtin)
when reading the directive file in the fish shell wrapper.

After worktree removal via the fast path, the shell's CWD inode follows
the `rename()` into `.git/wt/trash/`. Spawning any subprocess from this
CWD can trigger `shell-init: error retrieving current directory` — on
macOS, `/bin/sh` calls `getcwd()` during initialization, and in terminal
multiplexers like Zellij the error is visible to the user. Using a fish
builtin avoids subprocess spawning entirely.

The `sleep 1` delay in the background removal command remains as defense
in depth, but the primary fix is eliminating the subprocess. bash/zsh
wrappers already use `source` (a builtin) and are unaffected.

> _This was written by Claude Code on behalf of @max-sixty_

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-28 12:57:59 -07:00
worktrunk-bot 6093b61241 fix(nushell): use source in Home Manager module and export template defs (#1476) 2026-03-12 23:25:39 -07:00
Maximilian Roos cbd5e4da50 feat: add nushell tab completions (#1220)
## Summary

- Add `nu-complete wt` completer function to the nushell init template,
wiring it to the wrapper's rest parameter via `@"nu-complete wt"`
- The completer calls the binary with `COMPLETE=nu` and parses
tab-separated output into nushell's `{value, description}` record format
- Add `"nu"` to multi-shell completion test loops (help, version,
single-dash, deprecated flags) and a dedicated subcommand snapshot test

Thanks to @omerxx for reporting in #1215.

Closes #1215

## Limitations

Nushell's completion engine bypasses custom completers when the current
token starts with `-`, so flag completions (e.g. `wt switch --<TAB>`)
don't appear. Subcommand and value completions work correctly. This is a
nushell engine limitation (nushell/nushell#14504), not something we can
fix in our template.

## Test plan

- [x] Unit tests pass (`cargo test --lib --bins`) — snapshot updated
- [x] Integration tests pass (`cargo test --test integration`) — 1121
tests
- [x] Shell integration test (`cargo test --test integration --features
shell-integration-tests -- test_nushell_completion_subcommands`)
- [x] Lints pass (`pre-commit run --all-files`)
- [x] Manual verification in nushell 0.110.0: `wt <TAB>` shows
subcommands, `wt switch <TAB>` shows branches with descriptions

> _This was written by Claude Code on behalf of @max-sixty_

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 19:12:15 -08:00
Maximilian Roos eecb5c7e1e fix(nushell): enable stdout piping for list while preserving directives (#1081)
## Summary

- Split the nushell wrapper into two code paths: `list` gets direct
passthrough (binary as last expression, stdout flows through pipes),
everything else captures stdout to a temp file and processes directives
before returning output
- Fixes `wt list --format json | from json` which previously failed
because nushell's typed pipeline model requires the function's last
expression to be the return value
- No tradeoffs — the two command sets (streaming vs directives) are
disjoint, so each path is optimal for its commands

## Background

Nushell's pipeline model differs from POSIX shells: a function's
"output" is its return value (last expression), not bytes written to
fd1. When an external command isn't the last expression, stdout goes to
the terminal as a side effect but doesn't flow through `|`. This means
post-processing (directive handling, exit code capture) after the binary
call prevents stdout from reaching the pipeline.

The wrapper comment documents the nushell limitation with references to
nushell/nushell#12643.

## Test plan

- [x] All 530 unit tests pass
- [x] All 1052 integration tests pass
- [x] All lints pass (`pre-commit run --all-files`)
- [ ] Manual: `wt list --format json | from json` works in nushell
- [ ] Manual: `wt list` shows progressive table rendering in nushell
- [ ] Manual: `wt switch` still changes directory in nushell

Closes #1062

> _This was written by Claude Code on behalf of @max-sixty_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-17 13:35:17 -08:00
Maximilian Roos 516f90bac4 fix(shell): PowerShell wrapper swallows -D flag as -Debug (#1057)
* fix(shell): PowerShell wrapper swallows -D flag as -Debug (#885)

The `[Parameter(ValueFromRemainingArguments)]` attribute promoted the
wrapper to an "advanced function", which adds common parameters like
-Debug and -Verbose. PowerShell then consumed `-D` as `-Debug` instead
of passing it to wt.exe — so `wt remove -D` silently lost the flag.

Replace with `$args` (automatic variable for simple functions) which
passes all arguments through unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(test): use .ps1 mock for cross-platform PowerShell test

The shell script mock (#!/bin/sh) doesn't work on Windows. Use a .ps1
script instead — pwsh can invoke it directly with &, and pwsh is already
required for this test.

Co-Authored-By: Claude <noreply@anthropic.com>

* style: apply cargo fmt to PowerShell test

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-15 23:36:32 -08:00
Maximilian Roos 5a10dcc788 fix(shell): harden nushell wrapper and improve diagnostics (#1059)
* fix(shell): harden nushell wrapper and improve diagnostics

- Move LAST_EXIT_CODE capture inside do{} block so it reflects the
  actual command exit code, not a subsequent operation
- Wrap directive processing in try/catch to ensure temp file cleanup
  on error
- Include nushell vendor autoload paths in scan_for_detection_details
  so `wt config show` reports nushell integration status
- Add "nu" to the supported shells hint shown on unsupported shells
- Fix detection tests to use actual nushell config line patterns
- Document why non-cd directives delegate to sh -c

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(shell): remove try/catch from nushell directive cleanup

Drop error-path cleanup for the temp directive file. On error, the file
persists in /tmp as a useful debugging artifact (the OS cleans it up).
This matches bash and fish which already use a single rm on the happy
path with no error wrapping.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-15 23:23:34 -08:00
Arnaud Limbourg c7128ce11b Add nushell support (#964) 2026-02-14 08:57:30 -08:00
Maximilian Roos 9394261ad4 fix(fish): use env for inline variable assignment in shell wrapper (#1000)
* fix(fish): use `env` for inline variable assignment in shell wrapper

The fish shell wrapper used `VAR=value command` syntax which requires
fish 3.1+. Use `env VAR=value command` which works on all fish versions.

Closes #999

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: drop `command` from env invocation in fish wrapper

`env` can't invoke fish builtins like `command`. Since $WORKTRUNK_BIN
is already an absolute path (from `type -P`), we can invoke it directly.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-12 17:08:15 -08:00
Maximilian Roos 3d69cec1a7 fix(docs): correct broken worktrunk.dev URLs in templates (#882)
- fish_wrapper.fish: /docs/shell-integration → /config/#shell-integration
- config.example.toml: /config.md#project-configuration/ → /config/#project-configuration
- Remove worktrunk.dev from lychee exclusions so future broken links are caught

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-26 18:17:05 -08:00
Maximilian Roos a081f29972 fix(powershell): use WORKTRUNK_BIN for test isolation + more Windows tests (#674)
* test: add cross-platform PTY infrastructure for Windows shell tests

Make shell integration tests cross-platform using portable_pty's ConPTY
support on Windows.

Changes:
- tests/common/mod.rs: Remove Unix-only gate from shell module
- tests/common/shell.rs: Add Windows env vars, PowerShell -Command flag,
  fix PATH separator for PowerShell
- tests/integration_tests/shell_wrapper.rs: Add PowerShell support to
  build_shell_script and exec_in_pty_interactive, add Windows-only
  PowerShell test cases

The Windows tests are gated with #[cfg(windows)] and will run when CI
has Windows runners with PowerShell available.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: gate remaining Unix-only shell tests with #[cfg(unix)]

Three more tests use `shell_command` which is Unix-only:
- test_zsh_completion_produces_correct_output
- test_wrapper_help_redirect_captures_all_output
- test_wrapper_help_interactive_uses_pager

These tests require zsh/bash/fish which aren't available on Windows.
The Windows PowerShell tests at the end of the file remain ungated.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: gate all Unix shell tests with #[cfg(unix)]

Comprehensive update to gate all tests that use bash/zsh/fish shells
with #[cfg(unix)] to prevent them from running on Windows CI.

Tests gated:
- All parameterized shell tests (bash/zsh/fish cases)
- Bash-specific tests (completions, job control, shell integration)
- Zsh-specific tests (wrapper function, job control)
- Fish-specific tests (completions, multiline commands)
- README example tests that use Unix shells

Windows PowerShell tests at the end of the file remain active on
Windows via their existing #[cfg(windows)] attributes.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): gate Unix-only imports and statics for Windows CI

When all Unix shell tests are gated with #[cfg(unix)], the imports and
static variables they use become unused on Windows, causing clippy errors.

Add #[cfg(unix)] to:
- Imports: canonicalize, wait_for_file_content, assert_snapshot, fs,
  PathBuf, LazyLock, shell
- Statics: TMPDIR_REGEX, TMPDIR_PLACEHOLDER_COLLAPSE_REGEX, WORKSPACE_REGEX,
  COMMIT_HASH_REGEX, JOB_CONTROL_REGEX
- Methods: assert_no_job_control_messages, normalized
- Function: generate_completions

Co-Authored-By: Claude <noreply@anthropic.com>

* test(windows): temporarily ignore PowerShell PTY tests

PowerShell PTY tests timeout in CI (60+ seconds). The issue is likely
that Get-Command finds Windows Terminal's wt.exe instead of the test
binary, causing the completion setup to hang.

Mark tests as ignored pending investigation. The test infrastructure
and cross-platform PTY support remain in place.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): gate assert_success method for Windows CI

The assert_success() method (added in #668) is only used by Unix shell
tests. Gate it with #[cfg(unix)] to fix Windows clippy dead_code error.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(tests): organize shell tests by platform

- Group Unix-only imports in single #[cfg(unix)] block
- Move Windows tests into `mod windows_tests` with single #[cfg(windows)]
- Add documentation explaining test organization by platform
- Remove individual #[cfg(windows)] from each Windows test

This makes it clearer which tests run on which platform and reduces
the number of scattered cfg attributes.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(tests): organize shell_wrapper tests by platform with module-level gating

- Replace 46 individual #[cfg(unix)] annotations with single module-level gate
- Restructure from nested `mod tests { mod windows_tests }` to sibling modules:
  - `#[cfg(unix)] mod unix_tests` - 47 bash/zsh/fish tests
  - `#[cfg(windows)] mod windows_tests` - 4 PowerShell tests
- Rename 30 snapshot files to match new module path (__tests__ → __unix_tests__)

This makes platform organization explicit at the module level rather than
scattered across individual test functions.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(powershell): use WORKTRUNK_BIN env var for test isolation

The PowerShell template was ignoring WORKTRUNK_BIN and always using
Get-Command to find wt. This caused test timeouts on Windows CI when
Windows Terminal's wt.exe was found first.

Now checks $env:WORKTRUNK_BIN first (like bash/zsh/fish templates do),
falling back to Get-Command only when not set.

Also re-enables the 4 PowerShell PTY tests that were marked #[ignore].

Co-Authored-By: Claude <noreply@anthropic.com>

* test(windows): add 8 more PowerShell integration tests

New tests:
- test_powershell_execute_exit_code_propagation - verifies exit codes
- test_powershell_branch_with_slashes - Windows path handling
- test_powershell_branch_with_dashes_underscores - branch name variants
- test_powershell_wrapper_function_registered - wrapper function check
- test_powershell_completion_registered - completion setup
- test_powershell_step_for_each - multi-worktree operations
- test_powershell_help_output - help text rendering
- test_powershell_worktrunk_bin_env - env var preservation

Total Windows tests: 12 (up from 4)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(windows): add 6 more PowerShell tests

New tests:
- test_powershell_merge - merge operations
- test_powershell_switch_with_execute - execute flag with PowerShell command
- test_powershell_switch_existing - switch without --create
- test_powershell_list_json - JSON output format
- test_powershell_config_show - config diagnostics
- test_powershell_version - version output

Total Windows tests: 18 (42% of Unix test count)

Co-Authored-By: Claude <noreply@anthropic.com>

* style: cargo fmt

* test(windows): add 12 more PowerShell tests to reach 70% coverage

New tests:
- test_powershell_shell_integration_hint_suppressed
- test_powershell_select_basic
- test_powershell_switch_between_worktrees
- test_powershell_long_branch_name
- test_powershell_remove_by_name
- test_powershell_list_verbose
- test_powershell_config_shell_init
- test_powershell_switch_nonexistent_branch
- test_powershell_step_next
- test_powershell_step_prev
- test_powershell_special_branch_name
- test_powershell_hook_show

Total Windows tests: 30 (70% of 43 Unix tests)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: update PowerShell init snapshot to match template changes

Co-Authored-By: Claude <noreply@anthropic.com>

* test(windows): mark PowerShell tests as ignored pending PTY investigation

All 30 PowerShell PTY tests timeout in Windows CI (~60s each). Investigation:

What was fixed:
- PowerShell template now uses WORKTRUNK_BIN env var (like bash/zsh/fish)
- This was needed because Windows Terminal's wt.exe was being found first

What still doesn't work:
- Tests still timeout, suggesting a deeper PTY + PowerShell issue
- Likely causes: ConPTY implementation, profile loading, env isolation

The test infrastructure and 30 tests are in place for when the issue is resolved.
Enable by removing #[ignore] attributes in windows_tests module.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(windows): add diagnostic tests to debug PowerShell PTY timeouts

Added 6 diagnostic tests that are NOT ignored to help identify where
PowerShell PTY interactions fail:

1. test_diag_01_pwsh_spawn_basic - Can we spawn pwsh via ConPTY?
2. test_diag_02_pwsh_with_env - Do env vars work?
3. test_diag_03_pwsh_env_clear - Does env_clear() work?
4. test_diag_04_pwsh_multiline_script - Do multi-line scripts work?
5. test_diag_05_wt_binary_direct - Can we run wt directly via PTY?
6. test_diag_06_pwsh_invokes_wt - Can pwsh invoke the wt binary?

These will print diagnostic output in CI to help pinpoint where hangs occur.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(windows): add more diagnostic tests for PTY debugging

Added 3 more diagnostic tests:
- test_diag_07_drop_writer_before_read - Tests if explicitly dropping writer helps
- test_diag_08_cmd_exe_basic - Tests cmd.exe (simpler than PowerShell)
- test_diag_09_wt_with_writer_drop - Tests wt binary with writer dropped

Previous diagnostics showed the read blocking forever. Testing if
dropping the master writer before reading helps on ConPTY.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(windows): add more PTY diagnostics (std::process, wait-first, nonblocking)

Added 3 more diagnostic tests:
- test_diag_10_no_pty_cmd_works - Tests std::process::Command (no PTY)
- test_diag_11_wait_then_read - Tests waiting for child exit first
- test_diag_12_nonblocking_read - Tests non-blocking read with polling

These help isolate if the issue is PTY-specific or something else.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): add Windows env vars to configure_pty_command()

The PTY tests were timing out on Windows because configure_pty_command()
called env_clear() but didn't restore critical Windows environment
variables needed for processes to run:

- SystemRoot / windir - Critical for DLL loading
- SystemDrive - Drive letter (usually C:)
- USERPROFILE - Windows equivalent of HOME
- TEMP / TMP - Temp directory paths
- COMSPEC - Path to cmd.exe
- PSModulePath - PowerShell module paths

The exit code 0xC0000138 (STATUS_DLL_NOT_FOUND) in diagnostic tests
confirmed processes were crashing due to missing env vars.

Also added DIAG13 and DIAG14 tests to verify the fix.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): make pty module cross-platform

Changed pty module gate from #[cfg(unix)] to #[cfg(feature = "shell-integration-tests")]
since portable_pty supports Windows via ConPTY.

Also removed unused PathBuf import in DIAG14.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): handle Windows ConPTY read blocking in PTY helpers

On Windows ConPTY, read_to_string() blocks forever because the pipe
doesn't close properly even after the child process exits.

Fix by:
1. Starting the read in a background thread
2. Waiting for child to exit
3. Dropping the master PTY to signal EOF
4. Joining the read thread with a 5-second timeout

This is encapsulated in a new `read_pty_output()` helper that handles
platform-specific reading. Unix continues to use the simpler direct read.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): fix Child trait bounds for Windows PTY

The spawn_command() returns Box<dyn Child + Send + Sync>, so the
read_pty_output helper must accept the same trait bounds.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(windows): remove PTY diagnostic tests, document ConPTY limitations

The diagnostic tests served their purpose - they identified that ConPTY
(Windows Console Pseudo Terminal) has fundamental limitations:

- ConPTY does not properly close the read pipe when child process exits
- This causes read_to_string() to block forever waiting for EOF
- Even cmd.exe /C "echo hello" hangs when reading via ConPTY
- Dropping the PTY master sends CTRL+C to the child (exit code 0xC000013A)

The actual PowerShell tests remain #[ignore] with a detailed TODO
explaining the ConPTY limitations and potential future solutions.

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(tests): implement proper ConPTY handling with cursor response

Based on research into ConPTY behavior, implement proper handling:

1. Keep writer alive during reading to respond to cursor queries
2. Read in chunks instead of read_to_string() (blocks forever on ConPTY)
3. Detect ESC[6n cursor position requests and respond with ESC[1;1R
4. Close master on separate thread while continuing to drain output

Key insight: ConPTY doesn't close the output pipe when child exits.
The pipe is owned by the pseudoconsole, not the child. We must:
- Drain output continuously
- Answer cursor position queries (PSEUDOCONSOLE_INHERIT_CURSOR)
- Call ClosePseudoConsole on a different thread than the reader

References:
- https://learn.microsoft.com/en-us/windows/console/closepseudoconsole
- https://github.com/microsoft/terminal/discussions/17716

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): drop writer before reading on Unix to signal EOF

On Unix PTYs, dropping the writer signals EOF to the child's stdin.
The previous change moved the writer into read_pty_output but used
`let _ = writer;` which doesn't drop immediately (it happens at end
of scope). This caused snapshot tests to fail because the child
wasn't seeing EOF at the right time.

Fix: Use `drop(writer)` explicitly on Unix.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(windows): enable PowerShell PTY tests now that ConPTY works

Enable test_powershell_switch_create and test_powershell_command_failure
to verify the ConPTY cursor response handling works with the actual
PowerShell shell wrapper.

The remaining PowerShell tests can be enabled once these pass.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): use ConPTY-aware reader in exec_in_pty_interactive

The exec_in_pty_interactive function in shell_wrapper.rs had its own
PTY reading logic that didn't include the ConPTY handling required
for Windows. This caused the PowerShell tests to timeout because:

1. It called read_to_string() which blocks waiting for EOF
2. ConPTY doesn't close the pipe when child exits - it stays open
   until the pseudoconsole is torn down

Fixed by:
1. Making read_pty_output() public in common/pty.rs
2. Using read_pty_output() in exec_in_pty_interactive instead of
   direct read_to_string()

This ensures all PTY-based tests use the same ConPTY handling code.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(powershell): make completion registration more robust

The PowerShell completion registration was failing with:
"Cannot run a document in the middle of a pipeline"

This happened because piping executable output directly in PowerShell
can fail in certain configurations/terminals (like ConPTY).

Fixed by:
1. Capturing output to a variable first, then piping
2. Adding a catch block so completion errors don't break the wrapper
3. Redirecting stderr to null during completion generation

The wrapper function still works even if completion registration fails.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): add Windows env vars to exec_in_pty_interactive

The PowerShell tests were failing with "The specified module could not
be found" because exec_in_pty_interactive was missing critical Windows
environment variables after env_clear().

Added the same Windows env vars that configure_pty_command() sets:
- SystemRoot/windir - needed for system DLL loading
- SystemDrive - needed for many programs
- TEMP/TMP - for temporary files
- COMSPEC - cmd.exe path
- PSModulePath - for PowerShell modules

Without SystemRoot, child processes can't find system DLLs and fail
to load even though the executable path is correct.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(windows): add basic PowerShell diagnostic test

Adding a simpler PowerShell test to debug why the shell integration
tests are failing. This test runs a minimal PowerShell command
directly without the shell wrapper to isolate the issue.

If this test passes but the wrapper tests fail, the issue is with
how we build/execute the wrapper script. If this test fails too,
it's a more fundamental PowerShell/ConPTY issue.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): remove unused import

Co-Authored-By: Claude <noreply@anthropic.com>

* test(windows): add debug output to PowerShell test

Adding detailed debug output to see exactly what script is being
generated and what output/exit code we're getting. This will help
identify why the PowerShell wrapper tests are failing while simple
PowerShell commands work fine.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: remove PowerShell script block wrapper that suppresses output

The `& { } 2>&1` wrapper was causing ConPTY to lose the script output.
Run the script directly instead - stderr naturally appears in the PTY.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: use -File instead of -Command for PowerShell in PTY tests

Write the PowerShell script to a temp file and execute via -File.
Using -Command with long scripts may cause issues with ConPTY output capture.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add debug Write-Host statements to trace PowerShell execution

Adding debug markers at key points:
- Script starting
- Env vars set
- Loading wrapper
- Wrapper loaded
- About to call wt
- wt returned

This will show where script execution fails or output is lost.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add debug output inside PowerShell wt wrapper function

Trace execution inside the wt() function to see:
- Arguments received
- Resolved binary path
- File existence check
- Before/after binary execution

This will show where the output is lost.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add stderr redirect and exception handling to debug binary execution

Wrap binary execution in try/catch and redirect stderr to stdout.
This should capture any errors from the wt.exe binary.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: capture wt binary output into variable and write explicitly

Capture output into variable to check:
- Output length
- Output type
- Then pipe to Write-Host

This will show if the binary produces any output at all.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add working directory debug output to diagnose no-output issue

Check current directory and whether .git exists:
- At script start
- Inside wt function

Binary produces 0 output - likely working directory issue.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: simplify binary execution - don't capture output to variable

Run binary directly with `& $wtBin @Arguments` without capturing
to variable. Let output go directly to console.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: use System.Diagnostics.Process for explicit process execution

Bypass PowerShell's `&` operator and use .NET Process class directly.
This gives explicit control over stdin/stdout/stderr and working directory.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): propagate PowerShell exit code via explicit exit

The wt wrapper function sets $global:LASTEXITCODE but when running
via -File, the script process doesn't return that exit code unless
there's an explicit 'exit $LASTEXITCODE' at the end.

This fixes test_powershell_command_failure which expects exit code 1.

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: remove PowerShell debug output

The ConPTY fix is working. Remove the debug statements that were added
during investigation.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(windows): enable all PowerShell PTY tests

Now that ConPTY pipe closure is handled correctly, all PowerShell tests
should work. The System.Diagnostics.Process approach reliably captures
output and exit codes in ConPTY environments.

Removes #[ignore] from 19 PowerShell tests.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(powershell): properly escape arguments for Windows command line

When using System.Diagnostics.Process, arguments must be properly
escaped for the Windows CommandLineToArgvW parsing rules:
- Arguments with spaces/quotes/backslashes need quoting
- Internal double quotes must be escaped as \"
- Backslashes before quotes must be doubled

Add _wt_escape_arg helper function to handle this correctly.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): remove double-quoting in PowerShell test scripts

The test scripts were using format strings like "$env:WORKTRUNK_BIN = '{}'"
but powershell_quote() already adds single quotes. This resulted in
double-quoted paths like "''path''" which caused PowerShell ParserError.

Fixed by removing the quotes from the format strings.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): fix PowerShell select and step for-each tests

- Fix test_powershell_select_basic: wt select is not available on
  Windows, so test the error behavior instead of non-existent --list
- Fix argument escaping: quote `--` in PowerShell since it's a
  stop-parsing token that PowerShell consumes instead of passing through

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): prevent PTY read thread join from blocking forever

On Windows ConPTY, the reader thread's blocking read() may not return
even after ClosePseudoConsole is called. The previous code would then
hang forever on read_thread.join().

Fix by dropping the read_thread instead of joining it. The thread will
be cleaned up when the test process exits. We already have the output
from the channel (or timed out trying to get it), so joining serves
no purpose except causing hangs.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): handle ConPTY thread deadlocks and skip flaky verbose test

Two fixes for Windows ConPTY test reliability:

1. Don't join either close_thread or read_thread in read_pty_output.
   These can form a deadlock: ClosePseudoConsole waits for reader to
   drain, reader waits for ClosePseudoConsole to close the pipe.
   "Leaking" threads is acceptable for test code.

2. Skip test_powershell_list_verbose - it triggers a ConPTY race
   condition where the output pipe doesn't properly close when the
   child exits. The --verbose flag produces enough output to trigger
   this timing issue. Other PowerShell tests pass because they produce
   less output.

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: remove ISSUE.md research document

Co-Authored-By: Claude <noreply@anthropic.com>

* simplify(shell): remove Process workaround from PowerShell template

The System.Diagnostics.Process approach was added to work around ConPTY
output issues in our test harness (portable_pty). Real PowerShell terminals
work fine with the standard `& $wtBin @Arguments` call.

Removed:
- _wt_escape_arg helper function
- System.Diagnostics.Process for output redirection
- Manual stdout/stderr handling

Now uses: `& $wtBin @Arguments` which is PowerShell's standard splatting
operator, matching how bash/zsh/fish templates work.

Co-Authored-By: Claude <noreply@anthropic.com>

* Revert "simplify(shell): remove Process workaround from PowerShell template"

This reverts commit 4ef2c44576.

The System.Diagnostics.Process approach is required for ConPTY environments
(including our test harness with portable_pty). When using the simpler
`& $wtBin @Arguments` in ConPTY, stdout/stderr don't appear in the output -
only ANSI escape sequences for cursor positioning and terminal titles.

This is a ConPTY limitation, not a real user issue - normal PowerShell
terminals work fine with `&`. However, since we need tests to pass, we
keep the Process workaround.

Co-Authored-By: Claude <noreply@anthropic.com>

* simplify(shell): use simple & operator for PowerShell template

The production template now uses `& $wtBin @Arguments` like bash/zsh/fish,
instead of the complex System.Diagnostics.Process workaround.

ConPTY has a known issue where the `&` operator's output doesn't appear
when the host process has stdout redirected (Microsoft Terminal #11276).
This affects our PTY-based tests (portable_pty) but not real users in
normal PowerShell terminals.

For tests, we inject a workaround in generate_wrapper() that captures
output explicitly and pipes through Out-Host. This keeps production code
clean while maintaining test coverage.

Removed:
- _wt_escape_arg helper function
- System.Diagnostics.Process approach with manual output redirection

Co-Authored-By: Claude <noreply@anthropic.com>

* test: ignore PowerShell wrapper tests due to ConPTY stdout redirect limitation

When cargo test redirects stdout to capture test output, ConPTY's output
bypasses the PTY pipe and goes to the original stdout instead. This is
a known Windows limitation documented in Microsoft Terminal #11276.

The simplified PowerShell template (& $wtBin @Arguments) works correctly
in normal terminal usage - only the test harness is affected.

- Mark all test_powershell_* wrapper tests with #[ignore]
- Keep test_conpty_* diagnostic tests active (they test direct command
  execution without the shell wrapper)
- Add explanatory comment in windows_tests module

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: remove unnecessary let binding (clippy let_and_return)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: add manual verification notes for PowerShell wrapper tests

Document that the PowerShell wrapper was hand-tested on macOS using
PowerShell Core (pwsh) and works correctly. The tests are only disabled
due to ConPTY output capture issues in the test harness, not because
the wrapper doesn't work.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-16 13:59:11 -08:00
Maximilian Roos 1f55823c3d fix(powershell): handle multiple binaries with same name (#652)
On Windows, `Get-Command wt -CommandType Application` can return multiple
results when both Windows Terminal's wt.exe and worktrunk's wt.exe exist.
This caused two errors:

1. `Cannot convert 'System.Object[]' to the type 'System.String'`
2. `The term 'wt.exe wt.exe' is not recognized...`

Fix by using `Select-Object -First 1` to get the first match from PATH,
which is the standard PowerShell pattern for this situation.

Relates to #648

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-15 00:54:17 -08:00
Maximilian Roos f649a65061 Replace possessive pronouns with articles in user-facing text 2026-01-14 11:21:11 -08:00
Maximilian Roos 62dfad1562 fix: move fish shell integration to functions/ for Homebrew compatibility (#580)
* fix: move fish shell integration to functions/ for Homebrew compatibility

Fish shell integration now installs to ~/.config/fish/functions/wt.fish
instead of ~/.config/fish/conf.d/wt.fish. The functions/ directory is
autoloaded on first command invocation, ensuring PATH is fully configured
(including Homebrew) before the wt function loads.

Changes:
- Fish template now writes to functions/ (autoloaded on first use)
- `wt config show` detects legacy conf.d installations and shows migration hint
- `wt config shell install` removes legacy conf.d file during migration
- `wt config shell uninstall` cleans up both locations
- Fish template now suppresses `type -P` stderr when command not found
- Shell count in uninstall output counts unique shells (not files)

Fixes #566

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: simplify fish wrapper to match bash/zsh pattern

- Reduce fish wrapper from ~30 lines to ~10 lines
- Wrapper now just calls `wt config shell init fish | source`
- WORKTRUNK_BIN handling moves to the full function (sourced at runtime)
- Remove incorrect WORKTRUNK_DIRECTIVE_FILE= checks (never written to files)
- Update docs link in wrapper comment
- Fix tests to use realistic wrapper content

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: prevent infinite recursion in fish wrapper when wt not in PATH

When `wt` isn't in PATH, `command wt` fails but `source` with empty input
succeeds (exit 0). The `or return` wouldn't trigger, causing the wrapper
to recurse into itself infinitely.

Fix by capturing $pipestatus[1] (the exit status of the first command in
the pipeline) and returning that status if non-zero.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add test for fish wrapper behavior when wt not in PATH

This tests the WRAPPER file (fish_wrapper.fish) specifically, not the
full function. The wrapper tries to source the full function at runtime.
When wt isn't in PATH, it should return 127 (not infinite loop).

The existing test (test_fish_binary_not_found_clear_error) tests the
FULL function which has its own WORKTRUNK_BIN check.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: show deprecated message when uninstalling legacy fish conf.d

When uninstalling fish shell integration, the deprecated conf.d location
now shows a clearer message:

  ○ Removed ~/.config/fish/conf.d/wt.fish (deprecated; now using ~/.config/fish/functions/wt.fish)

Instead of the generic:

  ✓ Removed shell extension for fish @ ~/.config/fish/conf.d/wt.fish

This helps users understand that conf.d was a legacy location.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: mock claude in config_show test for CI consistency

The test was failing in CI because `claude` isn't installed there,
causing a different message format. Now uses mock CI tools to ensure
consistent output across all environments.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: trim both sides when comparing fish wrapper content

The fish wrapper template has a trailing newline. When comparing existing
content with expected content, we trim the existing file content but not the
generated content. This causes the comparison to fail on Windows where line
ending handling differs.

Fixes Windows CI failure in test_configure_shell_fish_all_already_configured.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add coverage for fish legacy detection with functions/ dir

This test covers the code path where fish functions/ directory exists but
wt.fish doesn't, while legacy conf.d/wt.fish does exist. This exercises
a different code path in render_shell_status() than the existing
test_config_show_detects_fish_legacy_conf_d test:
- Existing test: functions/ doesn't exist → fish is "skipped"
- New test: functions/ exists but empty → fish is "configured" with WouldCreate

Both should show the migration hint for the legacy conf.d location.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add dry-run install test for fish shell

This test exercises show_install_preview() which displays the fish wrapper
content and completions with gutter formatting when running:
  wt config shell install fish --dry-run

The test verifies:
- "Will create" messages are shown (not "Created")
- Fish wrapper content is displayed in gutter
- Fish completions content is displayed in gutter
- No files are actually created

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: update fish dry-run snapshot for syntax highlighting change

The snapshot had a minor syntax highlighting difference where the fish
`or` keyword was highlighted differently. Accept the updated rendering.

Co-Authored-By: Claude <noreply@anthropic.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* refactor: make Windows .exe suffix handling Windows-only

Use #[cfg(windows)] to compile the .exe suffix check only on Windows,
where it's meaningful. This removes dead code from non-Windows builds.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: update fish dry-run snapshot for or syntax highlighting

Co-Authored-By: Claude <noreply@anthropic.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* config: exclude snapshot files from typos checking

ANSI escape codes in snapshots create false positives (e.g., the fish
shell 'or' keyword wrapped in [34m...[0m gets flagged as a typo for 'ore').

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: simplify error handling in configure_shell

- Use .ok() for file reads after exists() check - treat read failures
  as 'not configured' rather than hard errors (race conditions are rare)
- Simplify error messages for write operations - just show path and error
- Remove verbose 'Failed to...' prefixes from error messages

These error paths are unlikely (permission errors, race conditions) and
don't warrant dedicated test coverage.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: refactor Windows cfg block to avoid clippy blocks_in_conditions

Move the let bindings into a block scope instead of using them in an
if-condition block, which clippy on Windows flags as a lint violation.

Co-Authored-By: Claude <noreply@anthropic.com>

* style: use 'To X, run Y' pattern for migration hints

Follow CLI output formatting guidelines - command at end for easy copying.

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix fish wrapper error handling and legacy cleanup

- Add source_status check to fish wrapper to catch sourcing failures
- Refactor cleanup_legacy_fish_conf_d with early returns and error handling
- Move dry-run check before legacy cleanup to prevent file deletion
- Add regression test for dry-run with legacy fish conf.d file

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-13 07:57:30 +00:00
Maximilian Roos 4b8d3b4899 Inline shell helper function to fix Claude Code compatibility (#292)
Claude Code's shell snapshots filter out underscore-prefixed functions,
which caused `_wt_exec` to be missing while `wt` was captured. This broke
the `wt` command in Claude Code sessions with "command not found: _wt_exec".

The fix inlines the directive file handling directly into the `wt` function
for bash, zsh, and fish (PowerShell already used this pattern). This makes
the shell wrapper self-contained with no external dependencies.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-25 19:11:11 -08:00
Maximilian Roos 5656ed4e1b Remove unnecessary piped stdout bypass from shell wrappers (#278)
The shell wrappers had a check to bypass shell integration when stdout
was piped (e.g., `wt list | jq`). This was needed for an older output
system that didn't cleanly separate stdout from stderr.

With the current output system:
- `table()` → stdout (data)
- `print()` → stderr (status messages)
- Directives → file (cd commands)

This separation means piping works correctly with shell integration
active. Removing the bypass actually improves behavior for cases like
`wt switch feature | tee log.txt` where you want both piped output AND
shell effects (cd).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-21 01:36:44 -08:00
Maximilian Roos 7146ab0d3d Refactor shell function names to avoid conflicts
Add `safe_fn` Askama filter to convert command names with hyphens to
underscores in shell function identifiers. Rename generic `wt_exec`
functions to `_<cmd>_exec` format (e.g., `_wt_exec`, `_git_wt_exec`)
to prevent naming conflicts when multiple commands are loaded.

Update all shell templates (bash, zsh, fish, posix) and add test
coverage for hyphenated command names.
2025-12-20 22:59:56 -08:00
Maximilian Roos 87321281d4 Replace --internal flag with WORKTRUNK_DIRECTIVE_FILE environment variable (#269)
* Replace --internal flag with WT_DIRECTIVE_FILE environment variable

* Rename directive file environment variable to WORKTRUNK_DIRECTIVE_FILE

* Update stale comments referencing directive scripts

Codex review identified comments that still referenced "directive scripts"
after migrating to file-based directive passing. Updated to reference
"data output" which is the current terminology.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Address documentation review feedback

- Add WORKTRUNK_DIRECTIVE_FILE to env var tables (cli.rs, config.md)
- Update stale "directive mode" terminology in test comments to use
  "shell integration" or "data output" as appropriate
- Regenerate help snapshot

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Refactor progressive table output to use output module

Remove finalize_non_tty method and simplify non-TTY output path to use
output::table() directly, consistent with buffered mode. Add WORKTRUNK_SHELL
environment variable support for PowerShell-specific path escaping. Update
documentation and templates accordingly.

* Preserve directive script exit code in PowerShell

Add logic to use the directive script's exit code when wt succeeds, and
add redaction for WORKTRUNK_DIRECTIVE_FILE in test snapshots.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-20 21:33:25 -08:00
Maximilian Roos b4e2f2e4ed Fix shell wrapper piping and simplify process detachment (#258)
Detached processes now use process_group(0) instead of nohup, allowing
them to survive PTY closure while remaining responsive. Shell wrappers
now detect piping via `[[ ! -t 1 ]]` and bypass the wrapper when stdout
isn't a terminal, enabling commands like `wt list --format=json | jq`.

Updated shell templates (bash, zsh, fish, PowerShell) to check for
redirected output and run the binary directly in those cases. Removed
dev-detach dependency in favor of portable PTY for shell integration
tests. Simplified output routing by removing data_raw() function and
consolidating data output handling. Added FAQ entry explaining JSON
output routing and piping behavior.
2025-12-20 16:59:03 -08:00
Maximilian Roos d3c9bf650b Add git-wt binary for Windows (#177)
* Add git-wt binary for Windows (wt conflicts with Windows Terminal)

- Add `git-wt` binary behind `--features git-wt` feature flag
- Add `--cmd` option to `config shell init/install` (defaults to argv[0])
- Shell templates now use `{{ cmd }}` for binary name throughout
- Fix `--source` flag to use correct binary with `cargo run --bin`
- Extract `add_standard_env_redactions()` for DRY snapshot settings
- Document Windows workaround in FAQ

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix rustdoc link in argv[0] doc comment

* Fix Windows binary name detection stripping .exe extension

Use `file_stem()` instead of `file_name()` to strip the `.exe` extension
on Windows. This fixes `test_configure_shell_already_exists` which was
failing because the config line detection compared `wt` (in file) with
`wt.exe` (from argv[0]).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-11 23:18:42 -08:00
Maximilian Roos 6922780d5e Add Windows support via Git Bash with PowerShell fallback (#122)
* Add Windows support via Git Bash with PowerShell fallback

Implements cross-platform shell execution for Windows:

- New `shell_exec` module providing unified shell abstraction
  - Unix: uses `sh -c`
  - Windows: prefers Git Bash (checks $MSYSTEM, standard paths,
    derives from git.exe), falls back to PowerShell
  - Cached detection via OnceLock for performance

- Updated shell execution points to use ShellConfig:
  - execute_streaming() in output/handlers.rs
  - spawn_detached_windows() in commands/process.rs
  - InteractiveOutput::execute() in output/interactive.rs
  - help_pager.rs (skips less default without POSIX shell)

- Added PowerShell shell integration:
  - New Shell::PowerShell variant with config paths
  - templates/powershell.ps1 with documented limitations
  - Basic wt wrapper and tab completion support

Design: Git Bash enables same bash hook syntax on all platforms.
PowerShell fallback works for basic commands but has limitations
(no directory change, hooks using bash syntax won't work).

Closes #121

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add which crate and Windows-specific tests

- Use `which` crate for cleaner executable path detection
- Add Windows-specific unit tests for CI:
  - Shell detection (Git Bash vs PowerShell)
  - Command execution validation
  - POSIX syntax support verification
- Add documentation about Windows limitations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix clippy collapsible_if and improve error message

- Collapse nested if statement in find_git_bash() to satisfy clippy
- Update home directory error message to mention Windows environment variable

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix Windows clippy errors and add PowerShell integration detection

- Gate Command import with #[cfg(unix)] in process.rs
- Gate Stdio and ShellConfig imports with #[cfg(unix)] in interactive.rs
- Add PowerShell profile detection to is_integration_configured()
- Fix collapsible_if warnings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix PowerShell tab completion to use clap's generated script

Previously we had a custom Register-ArgumentCompleter that didn't
properly interface with clap's completion system. Now we let clap
generate its completion script and eval it, which handles:
- Proper cursor position tracking
- Tab-separated output format with help text
- Correct argument passing with -- separator

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add warnings for Windows users without Git Bash

When installing PowerShell shell integration on Windows without Git Bash:
- Show warning about limited functionality
- List specific limitations (no directory change, bash hooks won't work)
- Suggest installing Git for Windows

Also:
- Include PowerShell in auto-detected shells on Windows
- Add is_windows_without_git_bash() helper to ShellConfig

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Improve Windows test assertions

- Check stdout contains expected output instead of just "not empty"
- Remove fallback assertions that would pass even if wrong stream used
- Simplify POSIX redirection test command

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Document PowerShell stdout redirection limitation

PowerShell doesn't support redirecting stdout to stderr (*>&2 fails).
Document this known limitation with link to PowerShell issue #7620.

The practical impact is limited since:
- Users are warned at shell install time about PowerShell limitations
- Hooks using bash syntax need Git Bash anyway
- Most hook output is informational

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add PowerShell directory change support via --internal=powershell

PowerShell can now change directories after `wt switch` using the same
pattern as zoxide/fnm/starship:

1. `--internal` now takes an optional shell type: `--internal=posix` (default)
   or `--internal=powershell`
2. PowerShell wrapper passes `--internal=powershell` and captures stdout
3. Binary outputs `Set-Location 'path'` for PowerShell, `cd 'path'` for POSIX
4. Wrapper executes the directive via Invoke-Expression

Changes:
- Add DirectiveShell enum (Posix, Powershell) to cli.rs
- Update OutputMode::Directive to carry shell type
- Add shell-specific path escaping in directive.rs
- Rewrite PowerShell template to use directive mode
- Update install warning (only hooks limitation now, not cd)
- Add tests for PowerShell path formatting

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix Windows implementation weaknesses

- Fix PowerShell backtick escaping (backticks are literal in single quotes)
- Sanitize Windows-illegal characters in log filenames (< > : " \ | ? *)
- Use dirs crate for non-English Windows Documents folder
- Support both PowerShell Core and Windows PowerShell 5.1 profiles
- Add require_equals to --internal flag to fix CLI parsing
- Fix PowerShell wrapper stderr handling (don't merge with stdout)
- Redirect hook stdout to stderr on non-POSIX shells via Stdio::from(io::stderr())

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Address Codex review findings

- Handle Windows reserved device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
  in filename sanitization by prefixing with underscore
- Propagate exit code in PowerShell wrapper so $? and $LASTEXITCODE are
  consistent for scripts and CI

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Enable shell integration tests on Windows with runtime shell detection

- Remove Unix-only restriction from test common module
- Add shell_available() function to detect available shells at runtime
- Add skip_if_shell_unavailable! macro to skip tests for unavailable shells
- Enable shell-integration-tests feature on Windows CI
- Tests automatically skip unavailable shells (zsh, fish on Windows)

This allows bash tests to run on Windows via Git Bash while gracefully
skipping zsh/fish tests that require those shells.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix Windows reserved device name detection

Code review found that COM0 and LPT0 were incorrectly being treated as
reserved device names. Only COM1-9 and LPT1-9 are reserved on Windows,
not COM0/LPT0.

Changes:
- Change `is_ascii_digit()` to `matches!(c, '1'..='9')` in sanitize_for_filename
- Add comprehensive test coverage for reserved device names
- Add explanatory comment for Windows process detachment

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Gate Unix-only test modules to fix Windows CI

The shell integration tests and progressive output tests use Unix-specific
features (PTY, dev-detach binary with setsid). Gate these modules with
#[cfg(unix)] so they don't compile on Windows, fixing dead code warnings.

Changes:
- Gate shell module with #[cfg(all(unix, feature = "shell-integration-tests"))]
- Gate progressive_output module with #[cfg(unix)]
- Gate e2e_shell.rs, e2e_shell_post_start.rs, shell_wrapper.rs with #[cfg(all(unix, ...))]
- Gate list_progressive.rs with #[cfg(unix)]

Windows shell integration support is planned for a future PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Gate test utilities module to Unix-only to fix Windows CI

The tests/common/mod.rs module is only used by integration tests which
are gated to Unix-only. On Windows, the common module was being compiled
but had no consumers, causing dead code warnings that failed CI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-10 11:35:45 +00:00
Maximilian Roos 334f6d9927 fix: reset ANSI state before returning to shell
Colored output on stderr left terminal in non-default state. Reset ANSI
formatting before emitting shell script to stdout to ensure clean terminal
state for subsequent commands.

Also remove color forcing logic from shell templates as it's now handled
consistently by the binary.
2025-12-09 23:01:50 -08:00
Maximilian Roos f01cf931a4 Prevent zsh completion grouping of branches with identical descriptions
Add `list-grouped false` zstyle to prevent zsh from grouping completions with
identical descriptions onto a single line. When multiple branches share the same
timestamp, zsh's `_describe` function normally groups them together (e.g.,
"release  main  -- + 12m") to save space. This zstyle forces each branch to
display on its own line while preserving the value/description association.

Also document the issue in ISSUE.md with technical analysis, attempted solutions,
and open questions about zsh completion behavior.
2025-12-08 20:48:21 -08:00
Maximilian Roos 6ae9d0f939 Fix zsh _describe syntax and add validation tests
The sed command was incorrectly passing arguments to the -V flag, which
does not accept arguments in zsh's _describe function. The -V flag only
sets a flag for unsorted groups; the actual nosort behavior is handled
internally by _describe when passed to compadd.

Add validation tests to catch invalid _describe syntax patterns in
future changes, including checks for improper flag arguments and invalid
option combinations.
2025-12-08 14:20:16 -08:00
Maximilian Roos 0301d4bf1b Fix stderr redirection in fish shell wrapper
Fish's command substitution `(...)` runs in an isolated pipeline where stderr
doesn't inherit caller redirects. Replace with pipeline capture using `| read`
so stderr flows through normally and respects redirects like `wt --help &>file`.

Add `--internal` flag handling to skip pager in directive mode, ensuring help
output goes to stderr without paging when called from the shell wrapper.

Add comprehensive integration tests verifying stderr/stdout redirection works
correctly across bash, zsh, and fish shells.
2025-12-08 13:18:33 -08:00
Maximilian Roos a80a5eaa96 Fix shell completion to bypass shell function (#105)
* Add test for zsh lazy completion function definition

Test that _wt_lazy_complete correctly defines the clap completer
function when triggered. This catches a bug where the shell function
shadows the binary, causing the completion script to be eval'd in
the wrong context.

The test simulates the real user scenario where WORKTRUNK_BIN is not
set and wt is found in PATH, which is when the bug manifests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix zsh lazy completion to bypass shell function

The _wt_lazy_complete function was calling ${WORKTRUNK_BIN:-wt} to
generate completions, but when WORKTRUNK_BIN is unset, wt resolves
to the shell function (not the binary). The shell function runs
wt_exec which evals the completion script internally but doesn't
re-emit it, causing the outer eval to receive nothing.

The fix uses command to bypass the shell function and call the
binary directly:

  eval "$(COMPLETE=zsh command "${WORKTRUNK_BIN:-wt}" ...)"

This error manifested as:
  _wt_lazy_complete:5: command not found: _clap_dynamic_completer_wt

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add test for bash lazy completion function definition

Test that _wt_lazy_complete correctly defines the clap completer
function when triggered. This catches the same bug as zsh where the
shell function shadows the binary.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix bash lazy completion to bypass shell function

Same fix as zsh: use `command` builtin to ensure the binary is called
instead of the shell function when generating completions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix completion to bypass wt_exec when COMPLETE is set

The shell function wt() was passing completion output through wt_exec,
which evals stdout as shell script. But with COMPLETE set, the binary
outputs completion candidates (not shell script), causing bash to try
executing them as commands.

Fix: Detect COMPLETE env var in shell function and call binary directly,
bypassing --internal and wt_exec. This check must be in the shell layer
because clap's completion handler runs before argument parsing.

Also rewrites completion tests to be black-box:
- Use `complete -p wt` / `$_comps[wt]` to verify registration
- Dynamically discover completion function names
- Don't reference internal function names like _clap_complete_wt

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Preserve recency ordering in zsh completions

Zsh's _describe function sorts completions alphabetically by default.
This patch post-processes clap's generated completion script to add:
- `-V wt` creates an unsorted group
- `-o nosort` explicitly disables sorting

This preserves our recency-based ordering (most recently used worktrees
first) instead of alphabetical sort, matching fish's behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-08 11:21:48 -08:00
Maximilian Roos 50e5cf171d fix(fish): Handle empty WORKTRUNK_BIN in completions (#83)
The fish completion script was failing with "The expanded command was
empty" when WORKTRUNK_BIN was set but empty. The `set -q` check only
verifies the variable exists, not that it has a value.

Fix by using `test -n "$WORKTRUNK_BIN"` which correctly handles both
unset and empty variables, eliminating the need for separate existence
checks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 20:20:23 -08:00
Maximilian Roos fa512406e4 Fix: Correctly load wt binary in Fish shell integration 2025-11-30 11:10:17 -08:00
Maximilian Roos c24f5bfecb feat(shell): Stream stderr to terminal directly
Initializes the shell to redirect stderr directly to the terminal, improving real-time error visibility.
2025-11-25 11:52:21 -08:00
Maximilian Roos 42de37e9b4 Streamline --source execution in shell wrappers
The `--source` flag now directly uses `cargo run` instead of `cargo build`
and then setting `WORKTRUNK_BIN`. This simplifies the logic and removes the
need to restore `WORKTRUNK_BIN` after execution.
2025-11-24 22:12:09 -08:00
Maximilian Roos 72a00e4629 Remove config shell completions subcommand
This removes the `wt config shell completions` subcommand, as Bash and Zsh completions are inline in their respective init scripts, and Fish completions are now written directly to `~/.config/fish/completions/wt.fish` during `wt config shell install` (which uses `$WORKTRUNK_BIN` to bypass the shell wrapper).
2025-11-24 21:43:40 -08:00
Maximilian Roos c14a20c9a7 refactor: Simplify shell integration protocol for robustness
Updates the `--internal` flag's output format from NUL-terminated directives
to a shell script emitted on stdout at the end of execution. User-facing
messages now stream exclusively to stderr in real-time.

This change:
- Eliminates the need for FIFOs and background processes in shell wrappers, simplifying implementation.
- Ensures all user messages stream directly to the terminal, improving real-time feedback.
- Uses standard POSIX single-quote escaping for paths, making it more robust against injection.
- Aligns with proven patterns like direnv (stderr for logs, stdout for script).

Removes support for Elvish, Nushell, PowerShell, and Xonsh shells due to their
complexities in implementing the previous streaming NUL-byte directive protocol.
Bash, Zsh, and Fish are now simpler and more reliably supported.

Updates documentation, code comments, and integration tests to reflect the new protocol.
2025-11-24 19:31:35 -08:00
Maximilian Roos 0fe75642ed feat(zsh): Advise on compinit if missing during install
Adds a warning to `wt config shell install` for zsh users if their `~/.zshrc` does not
enable `compinit`. Zsh completions require `compinit` to be sourced, but it's not
enabled by default.

The advisory message is only shown during `install` (not `init`) and only when:
- The user explicitly runs `wt install zsh`.
- The user runs `wt install` (all shells) and their current shell is zsh.

The `wt config shell init zsh` output no longer warns about missing compinit,
as that output is sourced on every shell startup and shouldn't produce warnings.
Instead, it now includes an inline comment in the generated shell script.

This commit also adds a new `shell::detect_zsh_compinit()` function to probe
for `compinit`'s presence and `shell::is_current_shell_zsh()` to check the
current shell.
2025-11-24 17:19:17 -08:00
Maximilian Roos ca931796f2 Fix: Suppress bash job control notifications in wt_exec
This changes the `wt_exec` function to disable both `START` and `DONE` job
notifications in bash.

Previously, only `START` notifications (`[1] 12345`) were suppressed via
stderr redirection. This update adds logic to also suppress `DONE`
notifications (`[1]+ Done ...`) by temporarily disabling `monitor` mode
(`set +m`).

A detailed comment block is added to explain the two distinct bash job
notification mechanisms and how each is suppressed, along with references
to bash documentation and community discussions.
2025-11-24 16:20:15 -08:00
Maximilian Roos 35766787c5 Improve Bash/Zsh lazy completion for config shell init
Refactors `config shell init` for Bash and Zsh to:
- Use a simpler lazy loading mechanism. Completions are generated on the first TAB press and then delegated to the `clap` completer.
- Output directly into the init script, removing the need for separate completion files (`~/.zfunc/_wt` and `~/.local/share/bash-completion/completions/wt`).
- Update `config shell install` and `config shell uninstall` to reflect these changes in messages and behavior.
- Add warnings for Zsh users if `compinit` is not loaded before the `worktrunk` init script.
2025-11-24 15:09:13 -08:00
Maximilian Roos 3806b65d9b feat: Enhance shell completion management and installation
This commit introduces a comprehensive set of improvements for shell completion management. It includes:

- Writing completions to native shell directories for better integration.
- Implementing lazy Zsh completions for easier setup.
- Configuring shell completions during the installation process.
- Removing completion files during shell uninstallation.
- Grouping shell configuration with completions in the install output.
- Using XDG base directories for completion paths.
- Using `home_dir()` for `completion_path` to properly handle missing `HOME`.

This also includes various style improvements, updated test snapshots, and documentation regarding zsh completion installation issues.
2025-11-24 09:44:54 -08:00
Maximilian Roos 1407051e35 fix: Make bash completion nosort option conditional for macOS (#37)
The `nosort` option for bash's `complete` command is only available
in Bash 4.4+. macOS ships with Bash 3.2 due to licensing, which causes
CI failures with `bash: complete: nosort: invalid option name`.

Check the bash version before using nosort and fall back to not using
it on older versions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-23 22:34:04 -08:00