Commit Graph

105 Commits

Author SHA1 Message Date
Worktrunk Bot 13bc27e70a fix(shell): preview and confirm legacy-file removal on shell install (#3648)
## Problem

`wt config shell install` reclaims its own legacy wrapper paths after
installing to the canonical location — the fish `conf.d/{cmd}.fish`
(#566) and stranded nushell autoload copies (#2878). That reclaim is
deliberate and documented, but the deletion never appeared in anything
the user saw *before* it happened (#3644):

- `--dry-run` returned an empty `legacy_cleanups`, so a preview run
never mentioned the file it would remove.
- The confirmation prompt was passed only the files to *add* — the
legacy removals weren't in the `?` preview.
- When everything was already configured, the cleanup ran and returned
**without prompting at all**, deleting a hand-written `conf.d/wt.fish`
as a silent side effect.

Removals were reported after the fact (`Removed <old> (deprecated; now
using <new>)`), so nothing was silent — but it was after the fact, and
repo policy is that destructive operations get explicit consent.

## Solution

The two cleanup helpers now take a `dry_run` flag: with it set, the same
detection runs but nothing is removed, so the paths a real install
*would* delete can be computed up front. `handle_configure_shell`
collects that list once and threads it into `show_install_preview` and
`prompt_for_install`, so both `--dry-run` and the `?` preview render a
`Will remove <old> (deprecated; now using <new>)` line — the
after-the-fact message in the future tense.

The already-configured branch now prompts (`Remove deprecated shell
integration files?`) when there is anything to remove, and preserves the
file on decline. `--yes` still skips the prompt and removes as before,
so no existing non-interactive flow changes.

## Testing

- New `test_configure_shell_fish_dry_run_previews_legacy_removal` —
`--dry-run` now names the legacy `conf.d` removal on stdout and leaves
the file in place.
- New
`test_configure_shell_fish_legacy_removal_declined_when_already_configured`
— declining the prompt in the already-configured branch exits non-zero
and preserves the file (the core consent gap).
- The existing
`test_configure_shell_fish_dry_run_does_not_delete_legacy` snapshot was
updated to include the new `Will remove …` preview line; its "must NOT
delete" file assertion still holds.
- Verified manually end-to-end (dry-run preview + declined removal both
preserve the file), and ran the full `configure_shell`,
`shell_integration_prompt`, and nushell test suites plus clippy and the
doc-sync test.

---
Closes #3644 — automated triage

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-07-28 20:35:13 -07:00
Maximilian Roos 9032308400 refactor(tests): give the PTY test environment one home (#3618)
Follow-up to #3616, which fixed a PTY snapshot flake by adding one env
knob — and to add it I had to touch three separate env builders, none of
which knew about the others. This consolidates that surface.

## What was there

The environment a test subprocess runs in was assembled at nine sites:
five copies of the PTY prologue (`env_clear`, HOME, PATH, the Windows
block, coverage passthrough) and four partial restatements of the
determinism knobs. There was no rule for which belonged where, so adding
a knob meant finding every copy, and a missed copy surfaced later as a
flake somewhere unrelated.

## What's there now

Three named layers, each with one home in `src/testing/mod.rs`:

| Layer | Home | Contents |
|---|---|---|
| Baseline | `STATIC_TEST_ENV_VARS` | knobs every child needs, whatever
it's attached to |
| Terminal | `PTY_TEST_ENV_VARS` (new) | knobs only a TTY triggers —
`WORKTRUNK_TEST_SPINNERS=0` |
| Fixture | `pty_env_vars(TestEnvPaths { … })` (new) | the paths that
vary per fixture |

`configure_cli_command` and `configure_pty_command` apply them by
transport. That turns the standing `// NOTE: TERM is intentionally NOT
in STATIC_TEST_ENV_VARS` comment into a consequence rather than an
exception: `TERM` is transport-level, so it can't sit in a baseline both
transports share.

`WORKTRUNK_TEST_SPINNERS` stays out of the shared baseline deliberately.
It's inert on a pipe, and insta-cmd records the whole environment into
every snapshot it writes (`Info::from_std_command` builds it
unconditionally from `cmd.get_envs()` — there's no hook to suppress it),
so putting it there would add a no-op line to 1043 snapshot files.

`configure_pty_command` is now the only place a PTY child's isolation is
set up. `shell_command`, `execute_shell_script`,
`configure_pty_environment`, `exec_in_pty_shell`,
`exec_bash_truly_interactive` and two `wt switch` spawns all delegate to
it. `shell_wrapper`'s `STANDARD_TEST_ENV` and `bare_repository`'s
hand-rolled `test_env_vars` are gone, as are `configure_shell`'s
hand-copied knobs and four redundant `CLICOLOR_FORCE` lines in
`switch_picker`. Net −149 lines.

One spawn stays outside: the Windows ConPTY smoke test, which runs
PowerShell against a deliberately bare environment and isn't a wt child
at all.

## Reviewing

Start at `src/testing/mod.rs` — the three layers and their doc comments
are the whole design. Everything under `tests/` is deletion plus a
delegation call.

Two snapshots change: `install_preview_with_gutter` and
`install_preview_declined` now carry ANSI, because those two tests
previously ran without `CLICOLOR_FORCE`. Text is identical. Arguably a
fix — the test named "with_gutter" couldn't see the gutter (a
background-color block), while its own prompt line was already colored,
so the file was internally inconsistent.

## Testing

Full `wt hook pre-merge --yes` green: 4601 tests, `--features
shell-integration-tests`, `RUSTFLAGS='-D warnings'`, `insta --check`.

The knob's delivery path was verified by probe rather than by
inspection. With `sleep 6` in the mock `llm`,
`test_readme_example_hooks_pre_merge` passes; flipping
`PTY_TEST_ENV_VARS` to `"1"` reproduces the original failure byte for
byte:

```
+␛[1G␛[J␛[2m↳␛[22m ␛[2mWaiting for the commit generation command (4s)␛[22m␛[1G␛[J␛[2m↳␛[22m ␛[2mWaiting for the commit generation command (5s)␛[22m
```

So the knob reaches the shell-wrapper PTY child through the shared
setup, not through a surviving copy. Both probe edits are reverted.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:59:48 -07:00
Maximilian Roos 9645e3e132 fix(shell): reclaim the legacy wrapper paths instead of inspecting them (#3602)
`wt config shell install` decided whether the file at a legacy location
was worktrunk's by reading it — a substring test for the wrapper header,
falling back to "every code line looks like an integration line".
Anything that didn't match was left in place. Fish sources `conf.d` at
startup, so whatever `conf.d/{cmd}.fish` defines is already loaded by
the time fish would autoload the `functions/{cmd}.fish` the install just
wrote — the stale definition wins and the new wrapper may never load.

Ownership is now the path. `conf.d/{cmd}.fish` and the stranded nushell
`{cmd}.nu` candidates are paths worktrunk itself computes for the
command name being installed, so install takes them back whole without
reading them. Only that exact filename is touched: a neighbour under
another name is not worktrunk's, and each removal is still reported.

`wt config shell uninstall` still reads the header, because it takes no
`--cmd` and so genuinely doesn't know the name — it lists the
shell-owned directories and has to tell our `{cmd}.fish` from the user's
own files beside it. That is the one place ownership can't come from the
path, and it prompts and previews every file before removing it, which
install does neither of. `is_worktrunk_managed_content`'s docstring now
says so; `is_worktrunk_managed_nushell` existed only to gate the
install-time deletion and is gone.

Audited the rest of the surface for the same principle: the OpenCode
plugin (`~/.config/opencode/plugins/worktrunk.ts`) is already removed by
path with no inspection, the Claude plugin delegates to `claude plugin
uninstall`, Gemini installs through `gemini extensions install` and
leaves nothing of ours on disk, and fish is the only shell with a
separate completion file — which uninstall already covers. No other
gaps.

## Data safety

This deliberately widens what install deletes, so it's worth naming: two
tests asserted the old conservatism and now assert the new boundary —
`test_configure_shell_fish_reclaims_conf_d_path` (was
`..._preserves_user_conf_d_file`, from #3589) and
`test_nushell_install_reclaims_only_the_command_name` (was
`..._keeps_unmanaged_legacy_file`, from #2992). Both put a neighbour
file in the same directory to pin that only `{cmd}.{ext}` is taken. The
FAQ's "What can Worktrunk delete?" inventory is updated to describe
path-based ownership rather than the old content-marker rule.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:10:00 -07:00
Maximilian Roos b623d7f951 fix(shell): don't delete user data that only mentions the init command
Three places decided whether text was worktrunk's by testing a blob for
substrings. Two of them deleted the user's data when they guessed wrong.

`wt config shell uninstall` asked two unlinked questions of an rc line —
is an exec keyword anywhere in it, and is `<name> config shell init`
anywhere in it — so `alias setup='echo "run: wt config shell init fish |
source"'` classified as worktrunk's and was removed. One question
replaces both: does the line invoke the init command, with the name in
command position? The exec-keyword scan is gone rather than kept
alongside; it read English words out of the whole line and rejected
nothing the positional rule doesn't.

`wt config shell install` had the same defect one file over and one step
worse: `is_worktrunk_managed_content` tested the whole file for
`config shell init` and `| source`, and `cleanup_legacy_fish_conf_d`
deleted a user's own `conf.d/wt.fish` outright — during an install, which
never offered to remove anything. The cmd-agnostic twin beside it already
answered correctly, so the substring version is gone.

Uninstall now prints every line it takes, in the confirmation and again
after removal, since shell quoting stays undecidable without running the
line.

Sweeping for the same shape found it in `az` failure classification,
where `stderr.contains("login")` alone meant "not authenticated" and
`bail!` discarded the real error; `az` states the remedy itself, so its
text now reaches the user. Provider host matching moved from
`contains("github")` to label-boundary comparison, so
`dev.azure.com.attacker.example` and `mygithubmirror.com` no longer
resolve to Azure DevOps and GitHub.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 19:25:31 -07:00
Maximilian Roos f9cd38eb88 fix(shell): validate command names and tighten install/uninstall detection (#2864)
Two shell-integration correctness fixes plus an uninstall-scope
redesign.

**Command-name validation.** The shell-integration command name comes
from the user-typed `--cmd` flag, or `argv[0]` when `--cmd` is omitted.
A malformed value (an empty string, a leading `-`, or characters like
`;` or whitespace) renders into a broken shell rc line that the user
then has to find and fix. `validate_shell_command_name()` rejects empty,
leading-`-`, and non-`[A-Za-z0-9._-]` names at the CLI entry points, so
a bad value produces a clear error up front instead of a silently broken
config file. This is input hygiene on a value the user already controls,
not a security boundary.

**Noncanonical-line detection on install.** Install recognized only its
exact current canonical line, so a manually-added or older-form
integration line (e.g. `eval "$(wt config shell init zsh)"`) was not
seen as already-present and a duplicate was appended on the next
`install`. It now detects those forms and reports already-configured.

**Uninstall scans every worktrunk-managed file, regardless of cmd.**
Previously `uninstall` located Fish/Nushell wrapper files by name
(`{cmd}.fish` / `{cmd}.nu`), so removing integration installed under an
alternate binary name needed a matching `--cmd`. That symmetry leaked an
implementation detail: uninstall already has a content marker to
recognize worktrunk wrappers regardless of binary name. It now scans the
wrapper directories (`~/.config/fish/functions`, `conf.d`; the nushell
candidates' `vendor/autoload`) and admits every file whose content
matches the cmd-agnostic marker `# worktrunk shell integration for
<shell>` (always emitted by the wrapper templates). A headerless file
(the legacy `conf.d/{cmd}.fish`, which held nothing but the init line)
qualifies only when every non-blank, non-comment line is an integration
line, so a user's own file that runs or merely mentions `wt config shell
init` amid other code survives the scan. Fish completion files carry
their own `# worktrunk completions for` header and are scanned the same
way, so a completion whose wrapper is already gone is still cleaned up.

For Bash/Zsh/PowerShell (line-based),
`is_shell_integration_line_for_uninstall_any_cmd` reads the binary name
off the line rather than pattern-matching it. `config shell init` is a
fixed literal, so the name is the run of command-name characters ending
at it, held to the same `validate_shell_command_name` rule that governs
what worktrunk writes into shell code. The execution-context check that
pairs with it is a line-level property, so it lifts out of the
per-position loop it used to sit in, which is what let both cmd-agnostic
detectors drop their regexes. Since the result asks only whether a line
is worktrunk's, not which install owns it, uninstall also removes
hand-written `git wt config shell init` lines, and it cleans every
matching profile file rather than the first (both PowerShell profiles on
Windows). All detection reads only the code portion of a line: a
trailing `#` comment can mention an integration line without running it,
so uninstall does not delete an unrelated `eval`/`source` line over a
comment, and install does not treat such a mention as
already-configured. The cmd-specific permissive detector lost its last
caller in this cutover and is deleted.

The `--cmd` flag is removed from `wt config shell uninstall`; `install`
keeps it (installing under an alternate name is still a deliberate
per-cmd act). The FAQ's "What can Worktrunk delete?" inventory documents
the widened uninstall surface.

Covered by new integration tests: custom-cmd install + cmd-less
uninstall round-trips for zsh/fish/nushell (verifying scan-all removes
the custom-cmd integration, a hand-written `git wt` line, a stale second
wrapper, and an orphaned completion), a PowerShell uninstall round-trip
on the old pre-`Out-String` line, malformed-name rejection at the CLI
edge (`--cmd` and `argv[0]`), and the older-line dedup case. Unit tests
pin the deletion criterion: worktrunk's own wrappers and the headerless
legacy init file match; a file that mentions the integration, or runs it
amid other code, does not.

> _This was written by Claude Code on behalf of Maximilian Roos_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Worktrunk Bot <w@worktrunk.dev>
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-07-24 09:32:17 -07:00
Maximilian Roos a6972e4be0 fix(shell): detect the current shell from the process tree, not $SHELL (#3455)
`wt switch` could tell a fish user "shell requires restart" because
`$SHELL` (the login shell, say zsh) had the integration line while the
shell actually in use didn't. `current_shell()` keyed everything on
`$SHELL`, which names the login shell rather than the shell wt was
invoked from.

**Process-tree detection.** `shell::ancestor_shell()`
(src/shell/utils.rs) walks up from wt's parent to the nearest enclosing
shell: Linux reads `/proc/<pid>/stat`; macOS parses one `ps -Ao
pid=,ppid=,comm=` snapshot. macOS cannot use per-pid `p_comm`
(libproc/sysctl): `/bin/sh` re-execs bash via `/var/select/sh`, so an sh
script's `p_comm` reads "bash", while `ps`'s comm preserves argv[0].
Verified live and pinned by `test_probe_reports_invoked_name_for_sh`.
The walk passes through non-shell wrappers (`git`, `sudo`) and plain
script interpreters (`sh`, `dash`); a known-but-unsupported interactive
shell (tcsh, ksh) stops the walk so a shell further up can't claim the
session. `current_shell()`/`current_shell_name()` consult the walk first
and fall back to `$SHELL` / `PSModulePath` (Windows behavior unchanged;
Git Bash sets `$SHELL` itself).

**Hedged messaging.** The reason string is now "shell integration
installed but not active", and the hint presents restart as the likely
fix with an escape hatch: "A shell restart usually activates shell
integration; if it doesn't, ask an agent to debug with the docs @
https://worktrunk.dev/llms.txt" (the LLM docs index works for any agent;
the `/worktrunk` skill exists only for plugin users). Post-install hints
are unchanged (restart advice there is unconditional by construction).
`wt config show` now prints `Detected shell: <name> (process tree)` next
to `$SHELL` in the diagnostic gutter.

**Also fixed along the way:** `shell_from_name` requires a word
boundary, so `fishd`/`bashtop`/`numactl` no longer classify as shells
(`numactl` previously matched Nushell via the bare `nu` prefix); the
post-install/uninstall restart hints compare `Shell` values instead of
display strings, which never matched process-tree names like `pwsh` or
`zsh-5.9`.

**Testing.** Integration tests spawn wt under a test harness whose real
ancestry (nextest, cargo, the developer's shell) would leak in, so
`WORKTRUNK_TEST_PARENT_SHELL` is pinned empty in `STATIC_TEST_ENV_VARS`
(and in the two PTY helpers that mirror it by hand, which CI round 1
caught: locally the walk found the dev zsh, on CI the runner's bash) and
set per-test to simulate ancestors. New integration tests cover the
three detection outcomes (mismatch suppresses restart advice, configured
ancestor shows the hedged hint, unsupported ancestor overrides
`$SHELL`); unit tests cover the classifier, the walk itself (injectable
process-table lookup: wrapper pass-through, transparency,
cycle/depth/init terminations), and both OS probes against live
processes; `wt config show` is snapshot-tested with an override ancestor
and run once unpinned so the production walk executes end-to-end.

Known limits, accepted: the walk stops at the nearest supported shell
even when it's a non-interactive script interpreter (`bash -c` wrapping
wt reports bash), hence the hedged wording; the walk is bounded at 16
hops and any unreadable hop falls back to `$SHELL`.

> _This was written by Claude Code on behalf of max_

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 17:33:55 -07:00
Maximilian Roos 3ea7392a65 fix: bound config-show's zsh probe and isolate tests from host env (#3165)
Bounds the `wt config show` zsh compinit probe with a 2s kill-on-timeout (the
only un-bounded interactive shell spawn in production), matching its twin
`shell::detect_zsh_compinit`. Also isolates several tests from host environment:
`TZ=UTC` in the statusline helper, `--no-config-file` on the nu probes, and the
`wt --help` / `git log` spawns routed through the isolated `wt_command()` /
`repo.git_command()` helpers.

The macOS PTY-test hang that prompted the audit is fixed structurally in #3161.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:54:55 -07:00
Maximilian Roos 9a58ad27e8 refactor(output): route --dry-run previews to stdout (#3085)
Adopt one stdout/stderr rule across the CLI: stdout carries the command's
answer in whatever format the user selected (human or `--format=json`),
including `--dry-run` previews; stderr carries narration (progress,
success/warning/error, hints, interactive prompts, `-v`/`-vv`). A `--dry-run`
is the answer when nothing mutates, so the preview goes to stdout, matching
`wt list` / `wt config show`, the wider ecosystem (`git clean -n`, `rsync -n`,
`terraform plan`), and keeping paging working.

Five commands had their `--dry-run` human preview moved from stderr to stdout:
`wt hook <type>`, `wt step relocate`, `wt step prune`, `wt step copy-ignored`,
and `wt config shell install/uninstall`. For split previews the human output
mirrors exactly what `--format=json` already puts on stdout; narration the
json omits (prune's skipped-young caveat, "nothing to do" no-ops) stays on
stderr. The shell preview renderers return a `String` so the caller picks the
sink: `--dry-run` to stdout, the interactive `?` re-preview to stderr.

Documented in the `writing-user-outputs` skill and `src/commands/CLAUDE.md`.
2026-06-17 10:49:15 -07:00
Maximilian Roos 5fba0bdc9b refactor(tests): remove byte-normalization snapshot filters (#3061) 2026-06-12 08:58:50 -07:00
Maximilian Roos f2a8cb696d refactor(styling): end bash-gutter lines at their content, not a reopened dim (#3056)
Dispatched to fix a reported ANSI dim-bleed after bash-gutter blocks: in
committed snapshots, multi-command hook announcements appear to inherit
an unclosed `[2m` from the preceding gutter line (e.g.
`post_start_named_commands.snap`, where the gutter line ends
`'Installing deps'[0m[2m`).

Diagnosis: the bleed doesn't exist in real output. Fresh `cat -v`
captures of `wt hook pre-merge --yes` and `wt switch --create` with
two-key hook tables show every gutter line closing with a full `[0m`,
and later `◎ Running …` lines rendering un-dimmed. The snapshots are
misleading by construction: the cross-platform filter in
`tests/common/mod.rs` deletes every line-final `[0m` before
snapshotting, and the formatter reopens dim after the last highlight
token before its per-line reset, so filtered snapshots end `[0m[2m` and
read exactly like a dangling dim.

The change removes that misleading byte pattern at the source: phase 2
of `format_bash_with_gutter_impl` strips the no-op reopened dim (and the
lone dim on blank lines) before appending each line's closing reset.
Rendering is identical; lines now end at their content plus one reset.

Also bundled:

- A CAUTION comment on the reset-stripping snapshot filter, so future
readers don't diagnose SGR bleed from `.snap` bytes.
- `config_show_theme` now binds the standard env redactions;
regenerating its snapshot leaked a host `LLVM_PROFILE_FILE` path and
tripped `test_no_host_specific_paths_in_snapshots` (pre-existing gap,
invisible until regeneration since insta never compares `info:` blocks).
- 108 regenerated snapshots. Verified mechanically: ANSI-stripped bodies
are byte-identical; raw diffs are confined to line-end SGR sequences
plus stale `env:` header refreshes (`RUST_LOG: warn` from an older
harness).

Possible follow-up, not done here: the line-final-reset filter itself
may be vestigial (anstream pass-through suggests piped output is
identical across platforms now); removing it would make snapshots
byte-truthful but churns nearly every snapshot and needs Windows CI to
confirm.

The first Windows CI run caught a real latent bug the strip exposed:
askama strips a template's final newline, so on a CRLF checkout (Windows
autocrlf) the fish wrapper render ends with a lone `\r` that the
formatter's pair-wise CRLF normalization missed. The `\r` reached
tree-sitter and came back as a trailing token after the highlight
closed, defeating the end-of-line cleanup (and historically invisible
because insta trims trailing whitespace when comparing). Fixed by
trimming trailing `\r` in the formatter's normalization, plus
`templates/* text eol=lf` in `.gitattributes` since CRLF templates
embedded at compile time would leak `\r` into the shell code
Windows-built binaries emit at runtime.

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

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:38:55 -07:00
Worktrunk Bot b75b3cf1a7 fix(shell): install nushell wrapper to vendor-autoload dir (#2878) (#2992)
Fixes #2878.

Implements the approach agreed in the issue: query
`$nu.vendor-autoload-dirs` directly and install to its last
(user-writable) entry, with cross-platform tests and stranded-file
cleanup on install/uninstall.

## The bug

The nushell integration wrote `wt.nu` to
`$nu.default-config-dir/vendor/autoload`, which Nushell **never
autoloads**. It happened to work on macOS/Windows because there
`$nu.default-config-dir == $nu.data-dir`, so the wrong subpath resolved
to the real vendor dir; on Linux the wrapper was written but silently
never loaded — so `wt` was never wrapped.

Verified against `nu` 0.113.1: `$nu.vendor-autoload-dirs | last` is
`<data-dir>/vendor/autoload`, and
`$nu.default-config-dir/vendor/autoload` is in neither
`$nu.vendor-autoload-dirs` nor `$nu.user-autoload-dirs`.

## The fix

- **Resolve the install target from `$nu.vendor-autoload-dirs | last`**
(one memoised `nu` spawn that also reads `$nu.default-config-dir` for
legacy cleanup).
- **Fallback when `nu` isn't on PATH** mirrors nushell's own
`nu_path::data_dir`: `XDG_DATA_HOME` (when absolute) wins on every
platform, otherwise `dirs::data_dir()` — matching nushell on
Linux/macOS/Windows.
- **Wrapper-based shells now always write to the canonical location**
(`paths.first()`), so a wrapper stranded at a legacy path can't become
the write target. (No-op for fish, which has a single canonical path.)
- **Stranded-file cleanup**: install writes the correct path and removes
any worktrunk-managed wrapper left under the legacy
`<config-dir>/vendor/autoload` locations; uninstall already iterates
every candidate, which now includes those legacy paths. Legacy files are
only removed when they carry the `# worktrunk shell integration for
nushell` header, so a user's own `wt.nu` is never touched.
- `config_line`, the `wt config shell init` manual-setup help, and the
FAQ/shell-integration docs now use `$nu.vendor-autoload-dirs | last |
path join wt.nu`.

## Tests

- New `WORKTRUNK_TEST_NU_VENDOR_AUTOLOAD_DIR` override pins the install
dir so the install/uninstall/cleanup tests are **deterministic on Linux,
macOS, and Windows** and don't depend on `nu` being installed.
- New `test_nushell_install_target_is_a_vendor_autoload_dir` (gated on
`shell-integration-tests`, where CI has `nu`) runs the **real `nu`
flow** and asserts the wrapper worktrunk wrote is a member of
`$nu.vendor-autoload-dirs`. This fails against `main` (the old
config-dir path is in neither autoload list) and passes with the fix.
- New `test_nushell_install_cleans_stranded_legacy` and a reworked
`test_uninstall_nushell_cleans_all_candidate_locations` cover the
migration cleanup.
- Existing nushell tests updated to the data-dir path; full suite green
(`cargo test --lib` + `cargo test --test integration`, and the nushell
subset with real `nu` under `--features shell-integration-tests`).

## Not in scope

"Not best practice" cleanup flagged in the issue (the overlap between
the candidate-building helpers, and the dubious Windows branch) — the
helpers here are restructured around the autoload model, but a broader
tidy can be a follow-up.

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

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 15:43:38 -07:00
Maximilian Roos 76754ca076 fix(output): correct user-output consistency defects (#2867)
An audit of `wt`'s user-visible strings against the repo's
`writing-user-outputs` skill. Six consistency corrections, each matching
a rule the skill states — and, in most cases, a sibling message that
already follows it:

- `RefCreateConflict` hint: dropped the cross-message pronoun ("it") and
switched to the skill's `"To X, run Y"` command-hint form.
- `"All shells already configured"` and the version-check `"Up to
date"`: `success ✓` → `info ○` — both acknowledge existing state without
changing anything.
- `relocate` summary: keyed the message type on whether anything was
*relocated*, not on whether anything was *skipped* (the same outcome was
rendered two ways).
- `"Diagnostic saved"`: → `success` (a file was created) and the
`@`-before-path convention.
- Removed a stray trailing period — the only single-line message in the
codebase that had one.

`--no-verify` is deliberately untouched (handled in the
`canonical-paths` PR). Snapshot tests updated.

> _This was written by Claude Code on behalf of Maximilian Roos_

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:42:25 -07:00
Worktrunk Bot 73f7c48500 fix(shell): label nu/pwsh as "shell extension & completions" (#2705) 2026-05-11 09:21:18 -07:00
Worktrunk Bot 2b0c0f3f99 fix(shell): correct shell-extension label inversion for nu/pwsh (#2699) 2026-05-11 02:49:21 -07:00
Maximilian Roos 5144b1fbdc style(output): bold paths in warning messages (#2677)
Two follow-ups from the #2674 review session, plus a coverage fix.

`style(output)`: with the cross-platform bold-around-placeholder trap
closed in #2674, paths in warning messages can be styled per the
`writing-user-outputs` convention. Bolds the path in four warning sites
— config-file-not-found (user config load), failed-to-remove-deprecated
(legacy fish migration), completions-not-configured and
outdated-shell-extension (both in `wt config show`).

`refactor(test)`: inlines `setup_snapshot_settings_impl` into its two
callers. The wrapper only allocated an empty worktrees map; both callers
now call `setup_snapshot_settings_for_paths_with_home` directly.

`test(configure-shell)`: covers the legacy fish cleanup remove-failure
branch that the styling commit lit up under `codecov/patch`. Unix-only —
chmods the conf.d dir read-only so `fs::remove_file` returns EACCES,
then asserts the install succeeds and emits the warning. Skips when
running as root.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-10 18:17:59 -07:00
Maximilian Roos 302575894a fix(output): bold identifiers in skipped/squash/path messages (#2661)
## Summary

Several user-facing messages interpolated branch, worktree, file, and
path names as plain text. Per the project's `writing-user-outputs`
skill, branches and status values should be bolded; in hints, paths use
underline.

## Changes

- `wt step prune` "Skipped … (younger than X)" lines now bold each
branch name. The joined-list variant bolds names individually so commas
stay unbold.
- `wt step squash`'s "Nothing to squash; no commits ahead of `<branch>`"
bolds the branch.
- `wt config update`'s "Copied approved commands to `<file>`" bolds the
filename.
- `ConflictingChanges` hint underlines the path (hint convention).
- Config-load warning: `at PATH` → `@ <bold>PATH</>` (matches the
codebase `@`-not-`at` convention and bolds the identifier).
- Explicit-shell uninstall warnings bold the path so they match the hint
counterpart.

## Test plan

- [x] `cargo test --test integration` (1634 passing)
- [x] `pre-commit run --all-files` (lints + fmt clean)
- [x] Snapshots updated via `cargo insta accept` — all diffs are exactly
the formatting changes plus cosmetic env-var drift

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-10 00:10:14 -07:00
Maximilian Roos bd11961030 test(config): exercise WouldCreate arm in fish legacy migration test (#2607)
## Summary

Follow-up to #2591. The codecov/patch miss on the
`ConfigAction::WouldAdd | WouldCreate` call site of the new
`render_fish_legacy_migration` helper traced to the test that was
supposed to cover it
(`test_config_show_fish_legacy_with_functions_dir`).

The test sets up an empty `functions/` directory plus a legacy
`conf.d/wt.fish`, intending fish to be detected as
configured-with-`WouldCreate`. But without
`WORKTRUNK_TEST_FISH_INSTALLED=1`, fish was treated as not installed and
routed into `scan_result.skipped` instead — the migration hint was
emitted from the skipped loop, and the configured-WouldCreate call site
stayed uncovered.

Setting the env var (matching the convention in the adjacent
`test_configure_shell_skipped_lists_only_installed_shells`) routes fish
into the configured arm, where `ConfigAction::WouldCreate` fires because
`functions/wt.fish` doesn't exist. No production code changed; output is
identical because the helper produces the same string from either call
site, so the only snapshot change is the captured env var flipping from
`"0"` to `"1"`.

## Test plan

- [x] `cargo run -- hook pre-merge --yes` (all 3492 tests pass, lints
pass)
- [x] Snapshot diff is just the env-var line; stdout unchanged
- [ ] codecov/patch on the WouldCreate arm flips to covered

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 16:02:28 -07:00
Maximilian Roos 61046fb108 fix(config): iterate PowerShell uniformly with other shells (#2581)
## Summary

- Adds PowerShell to `default_shells` unconditionally; the
`Shell::is_installed()` filter (added in #2574) now hides `Skipped pwsh`
on hosts without pwsh, the same way `Skipped fish` is hidden on a stock
zsh-only Mac
- `should_auto_configure_powershell()` still gates `allow_create`, so
users who have pwsh installed but never opened it don't get an
unsolicited profile written (issue #885 behavior unchanged)
- Removes the iteration asymmetry that left PowerShell invisible in `wt
config show` for users who configured a profile but aren't currently in
a PS session

## Visible changes

| Host state | Before | After |
|---|---|---|
| pwsh on PATH, profile exists, not in PS session | invisible | shown as
`configured` |
| pwsh on PATH, no profile, not in PS session | invisible | `Skipped
pwsh; profile not found` |
| pwsh absent | invisible (iteration gate) | invisible (`is_installed()`
filter) |
| in PS session, no profile | auto-creates | auto-creates (unchanged) |

## Test plan

- [x] `cargo test --test integration` (1622 passed)
- [x] `pre-commit run --all-files`
- [x] Extended
`test_configure_shell_skipped_lists_only_installed_shells` to flag pwsh
installed and assert it appears as `Skipped`

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-04 01:01:48 -07:00
Maximilian Roos c2e38f6347 fix(config): clean up wt config show shell-integration section (#2574)
On a stock zsh-only macOS, `wt config show` listed `bash`, `fish`, and
`nu` as `Skipped; ~/.foorc not found` — clutter for shells the user
doesn't have installed. The block was the loudest thing in the SHELL
INTEGRATION section for users who'd never touch those shells.

After this PR, that fresh-user case shows just the warning + install
hint with no per-shell clutter. Together with #2572 (which renamed the
warning to "not configured" in this state), the section reads:

```
SHELL INTEGRATION
▲ Shell integration not configured
↳ To configure, run wt config shell install
  Invoked as: …
  $SHELL: /bin/zsh
```

## What changed

Adds `Shell::is_installed()` (PATH lookup with
`WORKTRUNK_TEST_<SHELL>_INSTALLED` env override) and gates the
`skipped.push` in `scan_shell_configs`. Shells with rc files still
render normally — the filter only affects the "binary absent AND no rc
file" case, which is exactly when the entry has no useful information.
`nu` and `pwsh` are filtered cleanly because neither ships with
macOS/Linux. `bash` and `zsh` on macOS still report (`/bin/bash` and
`/bin/zsh` are preinstalled), but that's the honest answer.

Side fixes:
- Removed a redundant `Err("No shell config files found")` from
`scan_shell_configs`. The install command's main.rs handler already
covered this case.
- Made the blank line between header status and the per-shell list
conditional, so the trailing `↳ To configure…` hint stays attached to
its subject when no shells render.
- Replaced the standalone `is_nushell_available()` helper with
`Shell::Nushell.is_installed()` — same semantics, one fewer function.
The historical `WORKTRUNK_TEST_NUSHELL_ENV` env-var name is preserved to
avoid churning hundreds of recorded snapshots.

## Tests

- New `test_configure_shell_skipped_lists_only_installed_shells` pairs
with `test_configure_shell_no_files` to cover both arms of the install
guard (binary on PATH + rc absent → Skipped; binary absent + rc absent →
drop entirely).
- Test defaults moved into `STATIC_TEST_ENV_VARS` so PTY tests inherit
them through `TestRepo::test_env_vars`.
- 50+ snapshot files updated to reflect the merged behavior.

Coverage caveat: the `which::which` fallback in `is_installed()` is not
exercised by tests (every test sets the env override). Same shape as the
prior `is_nushell_available()` helper, documented inline.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-03 23:36:54 -07:00
Maximilian Roos 5279d1f671 Route --help output to stdout (#2155)
Worktrunk sent all help output to stderr. This is an outlier versus most
CLIs (cargo, curl, python, `git <cmd> -h` — all stdout) and versus
`--version`, which moved to stdout in #2072 for the same reasons. Move
help to stdout and canonicalize the shared pager function so its other
callers (`wt config show`, `wt hook show`, `wt config state`) emit on
stdout too.

The prior rationale — "stdout is reserved for data output" so `wt config
show | jq` works — was already self-contradictory: `config show` went
through the same pager helper and was landing on stderr.

## What changed

- `src/help_pager.rs` — removed the `>&2` pager redirect and switched
fallback prints from `std::print!` to the anstream-aware
`worktrunk::styling::print`, so piped/redirected output is plain text
rather than raw ANSI. Simplified the TTY guard to check stdout only (the
`stdout OR stderr` check was a leftover from the redirect era).
- `src/help.rs` — DisplayHelp fallback prints via `styling::println`.
- `src/commands/config/{show,state}.rs`, `src/commands/hook_commands.rs`
— pager fallbacks print to stdout.
- Added `test_help_goes_to_stdout` and
`test_help_strips_ansi_when_piped` as regression anchors (parallel to
existing `test_version_goes_to_stdout`). Verified the ANSI test catches
the bug by reverting temporarily.
- Updated ~90 snapshots where content moved from the `----- stderr
-----` section to `----- stdout -----`.

## User-visible behavior change

Scripts that did `wt --help 2>/dev/null | x` previously discarded help
and piped nothing; they now pipe the help text. `wt --help > file`
captures help instead of leaving the file blank. Both changes align
Worktrunk with POSIX conventions. Terminal usage (pager on TTY) is
unchanged.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:30:26 -07:00
Maximilian Roos dc7f5bca5c Replace bright-black with underline for inline references in hints (#1285) 2026-03-05 02:14:38 -08:00
worktrunk-bot f3ae9a8ac6 fix: unify config path resolution so config create matches config load (#1135) 2026-02-22 06:09:13 -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
Maximilian Roos f4ce6d1804 fix(shell): nushell uninstall cleans all candidate locations, dedup candidates (#1050)
* fix(shell): nushell uninstall cleans all candidate locations, dedup candidates

Follow-up fixes to #1038 where config_paths() was changed to return
multiple Nushell candidates but not all consumers were updated:

- Uninstall now iterates all candidate paths instead of only the first,
  so config files installed at a different location (e.g., nu was in PATH
  during install but not during uninstall) get cleaned up
- has_config_location checks any candidate's parent directory for
  auto-detection, not just the first
- Removed redundant etcetera entry from nushell_config_candidates() (always
  duplicated either $XDG_CONFIG_HOME/nushell or ~/.config/nushell) and added
  HashSet-based dedup for remaining edge cases

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

* refactor(shell): extract remove_config_file helper for uninstall

Deduplicate 4 identical remove_file + map_err + format! patterns into
a shared remove_config_file() helper. Improves patch coverage by making
the nushell uninstall block a simple function call.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-16 05:15:39 +00:00
Maximilian Roos 8408c020ff fix(test): eliminate spurious blank lines in PTY snapshots (#1040)
* Remove blank lines after prompts; add prompt-aware PTY tests

Remove redundant eprintln!() after read_line() in prompt functions —
the Enter key echo already provides the line break, so the extra blank
line violated "blank before prompts, not after."

Add prompt-aware PTY test helpers that wait for the prompt marker before
sending input, so captured output matches real terminal behavior. The
PTY module now exposes 3 composable functions (build_pty_command +
exec_cmd_in_pty / exec_cmd_in_pty_prompted) instead of 8 wrappers.

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

* fix(test): eliminate PTY echo artifact causing blank lines in snapshots

portable_pty's UnixMasterWriter::drop() sends \n + EOT to the PTY master.
When drop(writer) happened before child.wait(), the terminal echoed this
\n as \r\n — producing a spurious blank line in captured output. Fix by
reordering: wait for the child first, then drop the writer (echo goes to
a dead PTY). Also remove the "collapse consecutive newlines" filter in
configure_shell tests that was masking this bug.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-15 14:11:01 -08:00
Arnaud Limbourg dba80e362d fix(shell): auto-detect nushell for install even without vendor/autoload (#1032)
* fix(shell): auto-detect nushell for install even without vendor/autoload

When `nu` is in PATH, `wt config shell install` now creates the
vendor/autoload directory automatically instead of skipping nushell.
The directory was introduced in nushell v0.96.0 and isn't always
pre-created, causing install to silently skip nushell users.

Follows the existing PowerShell pattern: detect the shell binary,
then set should_configure and allow_create so the directory gets
created on the user's behalf.

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

* refactor(tests): move nushell env var to configure_cli_command, add assertion

Move WORKTRUNK_TEST_NUSHELL_ENV alongside WORKTRUNK_TEST_POWERSHELL_ENV in
configure_cli_command for consistency — both are shell detection overrides.

Add negative assertion (!contains("Skipped nu")) to directly test the
failure mode this fix addresses.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
2026-02-15 11:19:17 -08:00
Arnaud Limbourg c7128ce11b Add nushell support (#964) 2026-02-14 08:57:30 -08:00
Maximilian Roos dc753d0f85 fix(shell): auto-configure PowerShell on Windows when SHELL is not set (#898)
* Refactor PowerShell environment detection for auto-config

Introduce is_powershell_environment() function to detect PowerShell sessions via PSModulePath env var. Use this detection to auto-configure PowerShell when running `wt config shell install` from a PowerShell session, even if the profile doesn't exist yet (issue #885). On non-Windows, add PowerShell to the default shells list when detected. Rename explicit_shell parameter to allow_create for clarity, representing whether config files can be created for a shell.

* fix(shell): restrict PowerShell auto-detect to non-Windows

On Windows, PSModulePath is set system-wide (even in Git Bash, cmd),
making it unreliable for detecting PowerShell sessions. This caused
false positives where `wt config shell install` would create PowerShell
profiles when run from Git Bash.

Changes:
- Restrict is_powershell_environment() to non-Windows only
- On Windows, require explicit `install powershell` to create profile
- Add Windows-specific hint when PowerShell is skipped
- Use var_os() instead of var() for robust env var checking

On macOS/Linux, PowerShell Core must be explicitly installed, so
PSModulePath being set is a reliable signal for auto-detection.

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

* fix(shell): auto-detect PowerShell on Windows via SHELL absence

On Windows, detect Windows-native shells (cmd/PowerShell) by checking
if the SHELL env var is NOT set. Git Bash, MSYS2, and Cygwin set SHELL,
but cmd.exe and PowerShell don't.

When detected, create both PowerShell profile files:
- Documents/PowerShell/Microsoft.PowerShell_profile.ps1 (pwsh 7+)
- Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1 (5.1)

This solves issue #885 where PowerShell users couldn't auto-configure
shell integration because PSModulePath detection had false positives.

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

* refactor(shell): rename is_powershell_environment to should_auto_configure_powershell

The new name better reflects the function's purpose: it determines whether
to auto-configure PowerShell profiles, not whether we're "in" PowerShell.

Also adds an integration test for the WORKTRUNK_TEST_POWERSHELL_ENV override
that verifies PowerShell profile creation works correctly.

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

* fix(shell): show "shell extension" not "& completions" for PowerShell preview

The preview output for `--dry-run` and `?` help was incorrectly showing
"shell extension & completions" for PowerShell. Only Bash/Zsh have inline
completions; Fish has separate files and PowerShell has no tab completion.

This makes the preview consistent with the actual install output.

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

* fix(tests): isolate PowerShell detection in test environment

On CI environments:
- Linux: PowerShell Core is installed, setting PSModulePath
- Windows: Removing SHELL triggers the "SHELL not set" detection

Both cause should_auto_configure_powershell() to return true in tests,
adding PowerShell to the shell list and triggering the config hint.

Fix by:
1. Removing PSModulePath to prevent Linux false positive
2. Setting WORKTRUNK_TEST_POWERSHELL_ENV=0 to disable detection entirely
3. Skip the PowerShell detection test on Windows (Documents folder can't
   be easily overridden in tests)

Tests that need PowerShell detection should set the env var to "1".

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-28 22:28:42 -08:00
worktrunk-bot 4bfe85403c fix(ci): use compile-time binary path for nextest compatibility (#884)
* fix(ci): build wt binary before running nextest

## Problem

The test `test_switch_with_execute_through_wrapper` was failing on macOS CI with:
```
bash: /Users/runner/work/worktrunk/worktrunk/target/debug/wt: No such file or directory
```

Root cause: `cargo nextest` only builds test binaries, not the main binary. The
shell_wrapper tests use `insta_cmd::get_cargo_bin("wt")` which expects the main
binary to exist at `target/debug/wt`. When running via nextest without building
the binary first, this path doesn't exist, causing the test to fail.

The issue surfaced after commit 0679e6d8 but wasn't caused by that change - it
exposed a pre-existing race condition where tests sometimes ran before the binary
was built.

## Solution

Add `cargo build --bin wt` before the nextest command in the pre-merge hook.
This ensures the main binary is built before tests that depend on it run.

The fix is at the right level because:
- Tests legitimately need the binary to exist (they're testing CLI behavior)
- Building the binary once before all tests is more efficient than per-test builds
- The pre-merge hook is the natural place to ensure test prerequisites are met

## Testing

Verified locally:
```
cargo build --bin wt && NEXTEST_NO_INPUT_HANDLER=1 cargo nextest run \
  --test integration --features shell-integration-tests \
  test_switch_with_execute_through_wrapper
```

Test passes with the binary built first.

---
🤖 Automated fix for [failed run](https://github.com/max-sixty/worktrunk/actions/runs/21382473224)

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

* fix(ci): use compile-time binary path instead of runtime workaround

Replace `get_cargo_bin("wt")` with `env!("CARGO_BIN_EXE_wt")` via a new
`wt_bin()` helper. The compile-time macro tells Cargo to build the binary
when compiling tests, fixing nextest compatibility without needing an
explicit `cargo build` step.

This supersedes the previous commit's workaround (adding `cargo build --bin wt`
to the pre-merge hook).

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

* fix: move wt_bin imports into cfg(unix) blocks

Fix unused import warnings on Windows where some tests are Unix-only.

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

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
2026-01-26 21:24:48 -08:00
Maximilian Roos b1ca0d22f3 fix: use format_path_for_display() consistently for user-facing paths (#856)
- Add "Path Formatting" documentation to writing-user-outputs skill
- Fix error messages in configure_shell.rs, resolve.rs, user.rs, worktrees.rs
  to use format_path_for_display() instead of .display()
- Add canonicalization in test helpers for macOS /var -> /private/var symlinks
- Update snapshot filter to preserve directory names (_PARENT_/$1)

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-25 15:39:16 -08:00
worktrunk-bot ce3d44c4b0 fix(test): update PTY filter to handle optional ANSI codes (#844)
After commit 020b790c switched to anstream for TTY-aware output, the
shell install PTY tests started failing on macOS CI because the filter
regex expected ANSI escape codes that are no longer present in non-TTY
environments.

The filter was:
  (\[y/N/\?\]\x1b\[22m) [yn]

This expected the ANSI reset code \x1b[22m after [y/N/?], but anstream
strips ANSI codes when TERM isn't set (common in test environments).

Update the regex to make the ANSI code optional:
  (\[y/N/\?\](?:\x1b\[22m)?) [yn]

This handles both cases:
- With ANSI codes: [y/N/?]\x1b[22m y
- Without ANSI codes: [y/N/?] y

Fixes macOS CI failure in test_install_preview_with_gutter.

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-24 21:17:14 -08:00
Maximilian Roos 2293dee043 refactor(tests): unify PTY execution into common/pty module (#675)
* refactor(tests): unify PTY execution into common/pty module

Consolidates 6 duplicate PTY execution functions from test files into a
shared module at tests/common/pty.rs:

- exec_in_pty() - simple PTY execution
- exec_in_pty_with_home() - with HOME override for shell config tests
- exec_cmd_in_pty() - for pre-configured CommandBuilder
- exec_in_pty_multi_input() - for tests requiring multiple inputs

Also:
- Migrates shell_integration_prompt.rs from manual normalize_output() to
  insta filters, consistent with other PTY tests
- Removes duplicate get_shell_binary() from shell_wrapper.rs

Net reduction of ~183 lines of duplicated code.

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

* style: apply cargo fmt formatting

* fix: move get_shell_binary import to shared section for Windows

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-15 17:58:47 -08:00
Maximilian Roos ffe2d41b42 refactor: simplify test normalization by using insta filters (#672)
* refactor: simplify test normalization by using insta filters

- Remove redundant `normalize_newlines()` from shell_wrapper.rs
  (already handled by `add_pty_filters()`)
- Remove dead `add_pty_tmpdir_filters()` from common/mod.rs
  (no snapshots used [TMPDIR] placeholder)
- Convert config_state.rs `normalize_log_path()` to insta filter
- Convert configure_shell.rs `normalize_output()` to insta filter
  via `install_pty_settings()` function

Analyzed but kept as-is (appropriate for their use cases):
- shell_integration_prompt.rs: Uses `contains()` assertions, not snapshots
- select.rs: Requires line-specific manipulation for TUI timing
- diagnostic.rs: Complex domain-specific normalization with ordering

Net reduction: 46 lines of test code.

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

* fix: restore CRLF normalization in PTY exec functions

The insta filter for \r\n wasn't fully equivalent to the
String::replace() call. Restore eager normalization to avoid
subtle edge cases with ANSI code matching.

Also removes redundant set_snapshot_path() since it's inherited
from TestRepo's bound settings.

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

* refactor: consolidate CRLF normalization to PTY source

Move CRLF normalization from insta filter to PTY exec functions.
This follows the principle: normalize once, at the source.

Changes:
- Add CRLF normalization to all PTY exec functions
- Remove CRLF filter from add_pty_filters()
- Update snapshots (removes redundant trailing [0m codes)

This eliminates subtle ordering issues between filters and ensures
consistent data for all downstream processing.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-15 17:06:58 -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 f0f3e18629 test: add GitLab MR status and shell config creation tests (#546)
Add coverage for:
- GitLab MR detection via mock glab (passed, failed, running, pending,
  conflicts, stale, no CI, project ID filtering)
- Shell config file creation when targeting a shell explicitly

288 lines of tests for 47 lines of coverage gain.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-11 18:34:40 -08:00
Maximilian Roos 9425c78c59 test: add coverage tests for --dry-run flag (#545)
* feat: add --dry-run flag to shell install/uninstall commands

Add --dry-run flag to `wt config shell install` and `wt config shell uninstall`
commands. When passed, shows what would be changed without prompting or making
any modifications.

Useful for scripting, CI pipelines, and non-interactive contexts where users
want to preview changes without being prompted.

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

* test: add coverage tests for --dry-run flag

Add integration tests covering the dry-run code paths for shell
install/uninstall commands:

- test_configure_shell_dry_run: single shell install preview
- test_configure_shell_dry_run_multiple: multiple shells install preview
- test_configure_shell_dry_run_already_configured: already configured case
- test_uninstall_shell_dry_run: single shell uninstall preview
- test_uninstall_shell_dry_run_fish: fish with completions (covers completion_results loop)
- test_uninstall_shell_dry_run_multiple: multiple shells uninstall preview

All tests verify that --dry-run shows the preview without modifying files.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-11 18:14:17 -08:00
Maximilian Roos b6f88689b2 fix(test): normalize PTY blank lines in shell install tests (#499)
* fix(test): apply mtime sleep to all platforms, not just Windows

The `test_list_full_working_tree_conflicts` test was failing on Linux CI
because git's mtime-based change detection wasn't reliably seeing file
modifications. The sleep was previously Windows-only, but CI environments
on all platforms can have this issue.

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

* fix(ci): use bot token so Claude commits trigger CI workflows (#491)

Add WORKTRUNK_BOT_TOKEN to both checkout and claude-code-action steps
in claude-mention.yaml and claude-ci-fix.yaml. This ensures pushed
commits trigger CI workflows, since GITHUB_TOKEN is intentionally
blocked from triggering workflows to prevent infinite loops.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(windows): remove hardcoded fallback, rely on PATH only (#489)

* refactor(windows): simplify Git Bash detection with single fallback

Simplify Git Bash detection:
1. Primary: Find git.exe in PATH and derive bash.exe location
2. Fallback: Check standard Git for Windows path (C:\Program Files\Git)

Removed the other 5 hardcoded paths (Program Files (x86), C:\Git, MSYS2
locations) since they're rare edge cases. The standard path covers
GitHub Actions and most installations.

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

* refactor(windows): simplify Git Bash detection with single fallback

Simplify Git Bash detection from 6 hardcoded paths to just 1:
1. Primary: find git.exe in PATH and derive bash.exe location
2. Fallback: C:\Program Files\Git\bin\bash.exe

The fallback is needed because `which::which("git")` fails in some CI
environments even when git is installed (Windows PATH handling quirk).

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

* ci: retry flaky windows test

* ci: retry (flaky working_tree_conflicts test)

---------

Co-authored-by: Claude <noreply@anthropic.com>

* fix(test): replace sleep with polling for racy git detection (#492)

Replace the fixed 100ms sleep in test_list_full_working_tree_conflicts
with a polling-based approach using the new general `wait_for` helper.

The "racy git" problem occurs when file modifications happen within the
same filesystem timestamp granularity - git's mtime-based detection may
not see the change. Polling with exponential backoff (10ms -> 500ms cap,
15s timeout) is both faster on normal systems and more reliable on slow CI.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(security): add prompt instruction to prevent secret exposure (#493)

Add security instruction to both Claude workflows prohibiting commands
that could expose secrets (env, printenv, etc.) and forbidding inclusion
of credentials in responses.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(ci): use OAuth token for Claude Code subscription auth (#494)

Switch from ANTHROPIC_API_KEY to CLAUDE_CODE_OAUTH_TOKEN to use
subscription-based authentication instead of per-token API pricing.

Co-authored-by: Claude <noreply@anthropic.com>

* test: verify OAuth token authentication (#495)

* feat(ci): use OAuth token for Claude Code subscription auth

Switch from ANTHROPIC_API_KEY to CLAUDE_CODE_OAUTH_TOKEN to use
subscription-based authentication instead of per-token API pricing.

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

* test: verify OAuth token authentication

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

* fix(ci): use input parameter instead of env var for OAuth token

The claude-code-action expects claude_code_oauth_token as an input
parameter, not an environment variable.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>

* fix(test): normalize PTY blank lines in shell install tests

PTY echo timing varies between systems, causing blank lines to appear
at different positions in the output. On CI macOS, the newline from
user input appears after the prompt text rather than before it.

Normalize by collapsing consecutive newlines and stripping leading
newlines, making the snapshots consistent across all environments.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-09 12:29:48 -08:00
Maximilian Roos 928b3017cd test: remove verbose assertion messages and doc comments (#458)
* Simplify assert messages by removing redundant descriptions

Remove verbose assertion messages that repeat the condition being tested.
Messages like "Should contain indicator" when asserting `contains("●")` are
redundant since the code is self-documenting. This reduces noise in tests
while maintaining clarity through meaningful assertion failures.

* test: remove verbose assertion messages and doc comments

Removed facile test verbosity across three categories:

1. **Doc comments** (~236 lines): Removed `/// Test X` comments that just
   restated test function names. Kept multi-line explanations that add context.

2. **Test names** (9 functions): Shortened overly verbose names by removing
   filler words while maintaining clarity:
   - test_merge_when_primary_not_on_default_but_default_has_worktree →
     test_merge_primary_not_on_default_with_default_worktree
   - test_complete_switch_option_prefix_shows_options_not_branches →
     test_complete_switch_option_prefix_shows_options

3. **Assertion messages** (19 instances): Removed messages that just restate
   the check:
   - assert!(path.exists(), "Path should exist") → assert!(path.exists())
   - assert!(x.is_file(), "Should be a file") → assert!(x.is_file())

Kept messages that add semantic meaning (e.g., "hook should have run") or
include debugging variables.

Total: 279 lines removed across 31 files. All 723 integration + 401 unit tests
passing.

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

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

* test: remove unreferenced snapshot files from renamed tests

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

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

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 16:51:44 -08:00
Maximilian Roos d2051f3ef9 Fix CI: PTY echo timing normalization and workflow permissions (#456)
* fix(test): normalize PTY echo timing in shell install snapshot tests

PTY tests capturing interactive install prompt had flaky output due to
timing differences between local and CI environments. The user's input
character ('y' or 'n') would appear either before the prompt or at the
end of the prompt line depending on timing.

Added normalization in `normalize_output()` to:
- Remove echoed input character from end of prompt line
- Remove standalone input lines that appear before the prompt

This ensures consistent snapshots regardless of PTY echo timing.

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

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

* fix(ci): add id-token:write permission to claude-ci-fix workflow

The claude-code-action requires `id-token: write` permission to fetch
an OIDC token for GitHub authentication. Without this, the action fails
with "Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable".

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-06 16:29:35 -08:00
Maximilian Roos 49c15097ff refactor(tests): simplify snapshot testing with auto-bound settings (#396)
* refactor(tests): simplify snapshot testing with auto-bound settings

- Auto-bind snapshot settings in TestRepo::new() via _snapshot_guard field
- Remove manual setup_snapshot_settings().bind() wrappers from tests
- Inline make_snapshot_cmd calls directly in assert_cmd_snapshot! macros
- Remove unused helper functions (json_settings, bind, bind_json)
- Use auto-naming from test function names (explicit names only for multi-snapshot tests)
- Fix directive file guard lifetime in test_remove_internal_mode
- Clean up orphaned snapshot files and let git detect renames

Net reduction: -183 lines in test files

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

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

* fix: update statusline tests for fish-abbreviated paths

The auto-bound snapshot settings from TestRepo use `_REPO_` filters
for full paths, but the statusline command uses fish-style path
abbreviation (e.g., `/p/v/f/.../repo`). These abbreviated paths
weren't matched by the existing filters.

Updated `claude_code_snapshot_settings()` to:
- Filter fish-abbreviated paths ending in `/repo` to `[PATH]`
- Strip leading ANSI reset codes from output
- Remove unused `repo` parameter

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

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

* fix: normalize statusline paths across platforms

The statusline output varies by platform:
- Linux: Raw path filtered by auto-bound settings to `_REPO_`
- macOS: Fish-style abbreviation bypasses auto-bound filters

Updated claude_code_snapshot_settings() to normalize both cases
to a consistent `[PATH]` placeholder for cross-platform tests.

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-03 01:00:10 -08:00
Maximilian Roos 8a9db5f8f0 refactor: unify shell restart messages (#381)
* Add shell integration prompt after wt switch

When shell integration is not active after `wt switch`, show a one-time
interactive prompt asking users if they want to install it. This helps
new users discover shell integration without requiring them to read docs.

## Features

- Interactive prompt with preview option (?) to see what will be installed
- Remembers user choice via `skip-shell-integration-prompt` config flag
- Falls back to hint message for non-TTY or after declining

## Decision flow

1. Unsupported shell → hint with supported shells
2. $SHELL not set → hint to run install command
3. Already installed → hint to restart shell
4. Already prompted → hint to run install command
5. TTY available → show interactive prompt
6. Non-TTY → hint to run install command

## Code improvements

- Consolidated shell install output into `print_shell_install_result()`
- Extracted `print_skipped_shells()` helper to eliminate duplication
- Improved hint styling: commands/paths use `<bright-black>` per guidelines

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

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

* fix: install for all shells on prompt accept, skip flag only on decline

- Prompt now installs for ALL shells with config files, matching
  `wt config shell install` behavior
- Skip flag only set on explicit decline, not before prompt (Ctrl+C
  doesn't permanently disable prompts)
- Shell detection uses starts_with for versioned binaries (zsh-5.9)
- If no config files exist anywhere, show install hint

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

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

* fix: update prompt_accept snapshot for all-shells install

The prompt now installs for all shells (same as `wt config shell install`),
so the snapshot includes skip messages for shells without config files.

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

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

* feat: show shell integration warnings for all switch operations

Previously, `wt switch` only warned about missing shell integration when
switching to existing worktrees without `--execute`. Now warnings are shown
consistently:

- `SwitchResult::Existing`: warns when shell integration not active
- `SwitchResult::Created`: shows success, then warns if shell won't cd
- `--execute`: custom message noting command runs but shell stays put

Warning reasons are now proper reasons, not suggestions:
- "shell requires restart" (configured but not active)
- "shell integration not installed" (not configured)

For created worktrees, the warning is simplified to avoid repetition:
  ✓ Created new worktree for feature from main @ ~/repo.feature
  ▲ Cannot change directory — shell integration not installed

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

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

* refactor: unify shell restart messages

- All restart hints now use "Restart shell to activate" (removed
  fish/bash/zsh branching that showed source command for bash/zsh)
- Warning reasons use statement form: "shell requires restart" and
  "shell integration not installed" (per em-dash guidelines)
- Combined three conditions that showed same hint into single if
- Removed "run:" colon per CLI output formatting guidelines

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

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

* Replace repo_root with repo_path in wt.toml

Update template variable references from repo_root to repo_path
to match the current configuration convention.

* fix: restore full "shell integration" text in restart hint

The abbreviated "Restart shell to activate" was unclear about what was
being activated. Restored to "Restart shell to activate shell integration".

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-02 14:50:27 -08:00
Maximilian Roos d21a463430 Add shell integration prompt after wt switch (#370)
* Add shell integration prompt after wt switch

When shell integration is not active after `wt switch`, show a one-time
interactive prompt asking users if they want to install it. This helps
new users discover shell integration without requiring them to read docs.

## Features

- Interactive prompt with preview option (?) to see what will be installed
- Remembers user choice via `skip-shell-integration-prompt` config flag
- Falls back to hint message for non-TTY or after declining

## Decision flow

1. Unsupported shell → hint with supported shells
2. $SHELL not set → hint to run install command
3. Already installed → hint to restart shell
4. Already prompted → hint to run install command
5. TTY available → show interactive prompt
6. Non-TTY → hint to run install command

## Code improvements

- Consolidated shell install output into `print_shell_install_result()`
- Extracted `print_skipped_shells()` helper to eliminate duplication
- Improved hint styling: commands/paths use `<bright-black>` per guidelines

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

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

* fix: install for all shells on prompt accept, skip flag only on decline

- Prompt now installs for ALL shells with config files, matching
  `wt config shell install` behavior
- Skip flag only set on explicit decline, not before prompt (Ctrl+C
  doesn't permanently disable prompts)
- Shell detection uses starts_with for versioned binaries (zsh-5.9)
- If no config files exist anywhere, show install hint

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

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

* fix: update prompt_accept snapshot for all-shells install

The prompt now installs for all shells (same as `wt config shell install`),
so the snapshot includes skip messages for shells without config files.

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-02 12:31:07 -08:00
Maximilian Roos 7cff9afcaa Investigate: Remove compinit warning suppression (#358)
* Remove ZSH_DISABLE_COMPFIX suppression to investigate root cause

Remove all compinit warning suppressions to understand:
1. When exactly the "insecure directories" warning appears
2. Whether it's caused by worktrunk or pre-existing in the CI environment
3. Whether the warning appears even when wt hasn't modified shell configs

Changes:
- Remove ZSH_DISABLE_COMPFIX=true from shell.rs and config.rs
- Remove WORKTRUNK_TEST_COMPINIT_CONFIGURED bypass from PTY test
- Let the compinit probe run naturally to see actual behavior

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

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

* Add diagnostic test to prove compinit warning source

This test runs `zsh -ic` directly in a PTY without any worktrunk
involvement - no custom HOME, no environment changes. If the compinit
warning appears here too, it proves the warning is a pre-existing CI
environment issue, not something worktrunk causes.

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

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

* Force diagnostic test to panic to show output in CI

* Fix diagnostic test to match worktrunk test environment

* Add dynamic compinit suppression in PTY tests

Restore ZSH_DISABLE_COMPFIX suppression with clear documentation:
- shell.rs: Full rationale explaining why suppression is safe
- config.rs: Brief reference to shell.rs rationale

Implement dynamic suppression in PTY tests:
- Add env_needs_compinit_suppression() helper that detects if plain
  zsh -ic produces the "insecure directories" warning
- Only add ZSH_DISABLE_COMPFIX when the environment needs it
- Tests work on both clean environments and problematic CI

This approach was validated in PR #358 which proved the warning
comes from the CI environment's global zsh config, not from worktrunk.

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

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

* Simplify compinit suppression: clarify rationale, remove unneeded test code

Update documentation to explain clearly:
- Worktrunk does NOT cause the "insecure directories" warning
- Our shell init doesn't modify fpath or call compinit
- The warning comes from global zsh configs (e.g., /etc/zsh/zshrc)
- We only register completions with compdef if user already set up compinit

Remove unnecessary dynamic detection from PTY tests - the library code
already handles suppression, so the test just needs a simple env var.

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

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

* Skip compinit probe in PTY tests to avoid env-specific warnings

The compinit probe spawns `zsh -ic` which sources global zshrc configs.
On some CI environments (Ubuntu), these configs call compinit which
produces "insecure directories" warnings that go to /dev/tty and leak
into PTY output despite ZSH_DISABLE_COMPFIX suppression.

Use WORKTRUNK_TEST_COMPINIT_MISSING=1 to skip the probe while still
testing the compinit advisory output.

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-01 14:30:08 -08:00
Maximilian Roos fc425957de Fix zsh compinit insecure directories warning on CI (#353)
* Ignore flaky shell install preview test on CI

The test_install_preview_with_gutter test fails on GitHub Actions
because zsh shows "compinit: insecure directories" warnings that
leak into the PTY output. This is an environment-specific issue
that doesn't affect the actual functionality being tested.

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

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

* Fix zsh compinit insecure directories warning on CI

The test_install_preview_with_gutter test was failing on GitHub Actions
because zsh's compinit security check would prompt about insecure
directories. This warning goes to the TTY (not stderr) so it wasn't
suppressed by our stderr(Stdio::null()).

Root cause fix:
- Set ZSH_DISABLE_COMPFIX=true when spawning zsh to probe compinit
- Add stdin(Stdio::null()) to prevent compinit from prompting interactively
- Applied to both detect_zsh_compinit() in shell.rs and
  check_zsh_compinit_missing() in config.rs

This properly fixes the flaky test that was temporarily ignored in #352.

Also fixes Windows compilation error where <branch> was parsed as a color tag.

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-01 10:49:36 -08:00
Maximilian Roos 3f74563188 fix(list): dim URL for current worktree when health check fails (#350)
* fix(list): dim URL for current worktree when health check fails

Update snapshots to show dimmed URLs when dev servers are not running.
The health check correctly reports ports as inactive, so URLs should
display with dim styling.

Previous snapshots incorrectly showed undimmed URLs for the main worktree
because CI and test environments don't have servers running on those ports.

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

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

* test: isolate URL health checks from local environment

Add WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK env var that, when set, makes
the URL health check always return inactive. This prevents flaky tests
caused by random local processes (like dev servers) listening on ports
that happen to match the hash_port values.

The env var is set in configure_cli_command() so all test commands
get consistent behavior.

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

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

* test: disable zsh compinit security warning in PTY tests

Set ZSH_DISABLE_COMPFIX=true to prevent the "insecure directories"
warning from interfering with test output on CI runners.

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

* fix: escape branch placeholder in Windows wt select error

The `<branch>` was being interpreted as a cformat! color tag, causing
a compilation error on Windows. Use `BRANCH` instead.

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

* fix: use let _ = instead of ? for output::print in Windows wt select error

The main() function doesn't return Result, so the ? operator cannot be used.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-01 10:36:28 -08:00
Maximilian Roos 471d33188b Ignore flaky shell install preview test on CI (#352)
The test_install_preview_with_gutter test fails on GitHub Actions
because zsh shows "compinit: insecure directories" warnings that
leak into the PTY output. This is an environment-specific issue
that doesn't affect the actual functionality being tested.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-01 10:08:15 -08:00
Maximilian Roos 14ac1bd9d5 Move zsh compinit advisory to after success message
Refactor the zsh compinit detection logic to return a flag in ScanResult
instead of printing directly. This moves the advisory message from the
configure_shell module to main.rs, allowing it to be displayed after the
success summary rather than before the configuration results.

Also updates the advisory message text from "Completions won't work" to
"Completions require compinit" for clarity.
2026-01-01 10:00:18 -08:00
Maximilian Roos 81973cffc2 Replace shell tests with snapshot-based assertions 2026-01-01 09:42:34 -08:00
Maximilian Roos bceedcade7 Fix test isolation for wt commands and improve test API (#314)
Tests that spawn `wt` must be isolated from the host environment to
prevent directive leakage (test commands writing to user's shell
directive file), config pollution, and git interference.

Changes:
- Add `TestRepo::wt_command()` returning pre-configured Command
- Rename `clean_cli_env` → `configure_wt_cmd` (parallel to git)
- Make `git_command()` consistent (no args, chain `.args()`)
- Fix approval_ui.rs and readme_sync.rs missing isolation
- Add `#[must_use]` to command builders
- Document pattern in tests/CLAUDE.md

The public API is now 3 symmetric methods:
- `repo.wt_command()` - for wt commands with TestRepo
- `repo.git_command()` - for git commands
- `wt_command()` - for wt without TestRepo (help tests)

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-30 20:29:27 -08:00
Maximilian Roos c1ea17d1b4 Replace --force flag with --yes throughout codebase
Update documentation, tests, and snapshots to use the new `--yes` flag
instead of `--force`. The `--yes` flag is used to bypass approval prompts
and confirmations, while `--force` is now reserved for destructive
operations like forced file removal.
2025-12-30 14:22:38 -08:00