feat: add top-level -C <path> flag (git -C semantics)#523
Conversation
Implements the foundation for `daft -C <path> <subcommand>` — a global flag that chdirs before any path-dependent state is resolved (repo discovery, layout, hooks, daft.yml, multicall dispatch). Semantics match git's `-C`: - Compose, not last-wins: `daft -C /a -C b ...` → /a/b - Empty `-C ""` is a no-op (matches `git -C ""`) - Stops scanning at first non-option token, so subcommand-local `-C` flags (e.g. `daft exec -C ... script.sh`) are preserved - Missing/non-directory path → terse error, exit 2 (clap usage convention) Architecture (per ARCHITECTURE.md): seeds the future `daft-cli` crate as a small functional-core / imperative-shell split: - `src/cli/argv.rs` — pure `parse_top_level_cwd()`, 10 unit tests - `src/cli/mod.rs` — `install_and_apply()` (chdir + OnceLock install) and `argv()` accessor All callers that previously read `std::env::args()` directly are migrated to `cli::argv()` so the strip is universally visible. Without this, subcommands re-reading raw argv via clap would reject the now-consumed `-C` pair as unknown flags. The install runs at the very top of `main()` — before `skip_startup_tasks_for(argv)` so the fork-bomb gate keeps seeing the real subcommand name in argv[1]. Tests, completions, docs, skill in follow-up commits per the plan. Refs #519 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
YAML scenarios (tests/manual/scenarios/global-flags/): - c-basic: -C chdirs before subcommand dispatch - c-missing: non-existent path → exit 2 + terse error - c-relative: first -C resolves against invocation cwd - c-compose: multiple -C compose (git semantic), not last-win - c-empty-noop: `-C ""` is a no-op (matches `git -C ""`) - c-cd-redirect: binary writes DAFT_CD_FILE relative to post-`-C` cwd - c-hooks-from-new-cwd: worktree-post-create hook in the -C target fires when invoking from another repo Bash integration tests (tests/integration/test_shell_init.sh): - c_flag_cd_redirect_through_wrapper: full shell-wrapper round-trip - c_flag_symlink_entry: `git-worktree-list -C` via the symlink entry Wrapper fix (src/commands/shell_init.rs): Adding the integration test surfaced a wrapper-level bug: when invoked as `daft -C /path verb args`, the `case "$1"` in the bash/zsh and fish daft() wrappers matched `-C`, not the verb. The default arm runs `command daft "$@"` WITHOUT setting DAFT_CD_FILE — so the shell never followed the binary into a newly-created worktree, defeating the cd-redirect for the agent use case the feature was built for. Fix: strip leading `-C <path>` pairs into a local before dispatching, and re-attach them as arguments to the wrapped binary so cli::install_and_apply applies the chdir. Applied symmetrically to bash/zsh and fish; the existing `layout|repo` cd-redirect path also benefits (e.g., `daft -C /a repo remove` now keeps its cd-out behavior). Refs #519 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surface the -C feature in every channel users discover it through:
- **Shell completions** (bash/zsh/fish/fig): top-level `-C <path>` flag with
directory-value completion. Bash/zsh additionally normalize `${words[*]}`
by stripping leading `-C <path>` pairs so subsequent subcommand-specific
completion logic keeps working when `-C` is the first arg. Fish's positional
short-letter matching makes the strip-and-restore unnecessary there.
- **Man pages** (`man/daft.1`): top-level `-C` arg declared on the clap
Command in `xtask::build_top_level_command()`. Strictly for man-gen and
--help rendering — actual parsing still happens in cli::install_and_apply
pre-clap.
- **`daft --help` / `git daft --help`**: new "Global options" section in
both renderers.
- **Docs site**: new `docs/cli/daft.md` reference page with full semantics
(compose, empty-noop, error behavior, interactions) and new how-to
`docs/worktrees/from-anywhere.md` framing the agent-driven use case.
Sidebar registered in `docs/.vitepress/config.ts`.
- **Agent skill** (`SKILL.md`): "Global Flags" section calling out -C as the
recommended pattern for agents operating across multiple worktrees in one
session. This is the primary motivating audience for the feature.
The fig spec gained a `template` field on `FigOptionArg` so `-C` can use Fig's
built-in `folders` completion source; existing instantiations updated
mechanically.
Refs #519
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_tests Two cleanups surfaced by post-implementation review: **Exit code consistency.** `daft -C` (trailing flag with no path) was propagating MissingPathAfterC through anyhow Err → main's `?` → exit code 1, while `daft -C /missing list` exits 2 via the apply_chdir branch. Both are usage errors and should match clap's exit-2 convention. Switch to direct process::exit(2) in install_and_apply for the parse-error branch. Extended c-missing.yml with a regression step for the trailing-`-C` case so this can't drift again. **Dead code.** install_for_tests() in cli/mod.rs was left behind when I simplified the test module — never called. Removed; cfg(test) pub fn doesn't trip default clippy dead-code lints, which is why it slipped through. Refs #519 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/claude review |
|
Claude finished @avihut's task in 5m 9s —— View job PR Review: feat: add top-level -C <path> flag (git -C semantics)Todo List
SummarySolid, well-scoped implementation. The functional-core / imperative-shell split is clean, the OnceLock global-argv pattern is appropriate, and the test suite is thorough. One stale test grep pattern to fix, a minor UX gap in the shell wrapper, and a few observations. IssuesMedium — stale grep in
|
27180f0 to
b8462a0
Compare
…al grep Two PR review follow-ups (#523): **bash/zsh wrapper: silent exit on `daft -C` (no path).** Previously `shift 2 || return 2` short-circuited silently when `$#` was 1, so the user saw exit code 2 with no message — the binary's `eprintln!("daft: -C: option requires an argument")` never ran. Now the wrapper prints the same message to stderr before returning. Added `test_c_flag_no_arg_through_wrapper_errors_cleanly` to lock in the behavior. Fish wrapper unaffected: its strip-loop falls through to `command daft -C` when argv is exhausted, so the binary's error path does fire. **Drop tautological grep in `test_daft_wrapper_function_exists`.** The grep `'command daft "\$@"'` was originally a smoke check for the wrapper's passthrough arm. After PR #523 the wrapper emits `command daft "${__daft_pre[@]}" "$@"` and the literal pattern no longer matches the code — but it *does* match an explanatory comment in the wrapper that includes the literal `*) command daft "$@"`, so the test passes by accident. Removed; `test_daft_wrapper_passthrough` already exercises the behavior end-to-end by sourcing the wrapper and invoking daft. Refs #519, addresses /claude-review feedback on #523. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Adds a top-level
daft -C <path> <subcommand>global flag that chdirs beforeany path-dependent state is resolved (repo discovery, layout, hooks,
daft.yml, multicall dispatch). Semantics matchgit -C/make -C/pnpm -C. Works for both thedaftmulticall arm and everygit-worktree-*symlinked entry.
Motivating use case (per the issue): coding agents operating across multiple
daft worktrees in one session can't reliably
cd, so each invocation has tobe self-contained.
-C <path>turns every call into "do X in path Y".Fixes #519.
Two corrections to the issue (raised in this comment)
-Ccomposes, doesn't last-win. The issue claimed "last-Cwinsmatches git" — that's empirically wrong about git, which composes
(verified:
git -C /tmp/x -C a -C b ...lands in/tmp/x/a/b).Implementation matches git: compose. Reasons in the issue comment.
main().Subcommands re-read
std::env::args()directly viacrate::get_clap_args()and a dozen other callers. Implemented as a
OnceLock<Vec<String>>in anew
src/cli/module that owns the strip-and-install lifecycle; all argvreaders now route through
cli::argv().Architecture (per ARCHITECTURE.md)
The new
src/cli/module is the seed for the futuredaft-clicrate, appliedas a small functional-core / imperative-shell split:
src/cli/argv.rs— pureparse_top_level_cwd(), 10 unit tests, no I/O.src/cli/mod.rs—install_and_apply()(chdir + OnceLock install), tinyimperative shell.
No ports/adapters ceremony for a CLI-layer concern — explicitly out of scope
per the doc's "vertical slice at the CLI command layer" rule. Just lifts argv
reading from "scattered
env::args()calls" into one accessor.Wrapper bug surfaced during integration testing
Adding the bash integration test caught a real wrapper-level bug: the
bash/zsh and fish
daft()wrappers dispatched oncase "\$1", sodaft -C /path verb argsmatched-C(not the verb) and fell through to theno-
DAFT_CD_FILEdefault arm. The shell would never follow the binary intothe new worktree — defeating the cd-redirect for the primary use case.
Fixed by stripping leading
-C <path>pairs into a local before dispatching,reattaching as args to the wrapped binary. Same pattern in both bash/zsh and
fish wrappers. The existing
layout|repocd-redirect branch also benefits.Commits (4, logically separated for review)
feat(cli): add top-level -C <path> flag with git semantics— corecli::install_and_apply+cli::argv(), wired intomain.rs, all argvreaders migrated.
test(cli): cover -C end-to-end across YAML, integration, wrapper—7 YAML scenarios in
tests/manual/scenarios/global-flags/(basic,missing, relative, compose, empty-noop, cd-redirect, hooks-from-new-cwd),
2 bash integration tests in
test_shell_init.sh, plus the wrapper fix.docs(cli): document -C across completions, man, help, skill, docs site— bash/zsh/fish/fig completions, man-page declaration in xtask(man-gen only; still parsed pre-clap), new
Global optionssection indaft --help/git daft --help, newdocs/cli/daft.mdreference page,new
docs/worktrees/from-anywhere.mdhow-to,SKILL.md"Global Flags"section for agents.
fix(cli): -C trailing-no-arg exits 2 (was 1)— post-review fix.daft -C(no path) was exiting via anyhow Err → 1; now matches thedirectory-not-found branch with exit 2 (clap usage-error convention).
Regression covered.
Acceptance criteria (from the issue)
daft -C <path> <subcommand>runs as if invoked from<path>daftand allgit-worktree-*/ shortcut-alias symlinks-C) or priorapplied cwd (subsequent
-Cs) — git's actual semanticdaft: -C: '<path>': not a directory. Trailing-C(no arg): exit 2 withoption requires an argument-C-Ccwd;daft.ymlresolves from new repo root-Cwith path completiondaft --helpdocuments-Cas global flagtests/manual/scenarios/global-flags/Test plan
cargo test --lib— 1813/1813cargo clippy --all-targets -D warnings— cleanmise run fmt— cleanmise run man:verify— up-to-datemise run test:integration(full matrix: default+gitoxide × bash+yaml)— 2211/2211 across 579 scenarios
mise run test:manual -- --ci global-flags:*— 8 scenarios passtests/integration/test_completions.sh— 27/27tests/integration/test_shell_init.sh— 19/21 (see Known issues below)-Cand non--Cinvocations.Known issues, not from this PR
test_shell_init_bash_aliasesandtest_shell_init_fish_aliasesfailbecause they expect a
gwcobalias that was renamed togwcbupstream. Thefailures pre-exist this PR; do not chase them as part of review.
🤖 Generated with Claude Code