feat(cli): global -C flag for working-directory switching#2031
feat(cli): global -C flag for working-directory switching#2031fengmk2 wants to merge 36 commits into
Conversation
✅ Deploy Preview for viteplus-preview canceled.
|
|
✅ Staging deployment successful! Preview: https://viteplus-staging.void.app/ |
Manual verification stepsSetupgit fetch origin && git checkout feat/cwd-flag
pnpm install && pnpm bootstrap-cli # installs this branch as the global vp
vp --help | grep -- '-C' # the new global flag is listed( Create a scratch monorepo with no local vite-plus install, so the branch's bundled CLI runs end to end: mkdir -p /tmp/vp-c-demo/apps/web /tmp/vp-c-demo/apps/admin /tmp/vp-c-demo/packages/ui/src && cd /tmp/vp-c-demo
echo '{ "name": "demo", "private": true, "workspaces": ["apps/*", "packages/*"] }' > package.json
for a in web admin; do
echo "{ \"name\": \"$a\", \"private\": true }" > apps/$a/package.json
echo "<!doctype html><h1>$a</h1>" > apps/$a/index.html
done
echo '{ "name": "ui", "private": true, "type": "module", "main": "src/index.ts" }' > packages/ui/package.json
echo 'export const ui = 1' > packages/ui/src/index.ts1.
|
How to use the Graphite Merge QueueAdd the label auto-merge to this PR to add it to the merge queue. You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
fefbe9d to
e6ed290
Compare
Uses the interactive snapshot harness to cover the TTY-gated branches the old snap tests cannot reach: single-app auto-select (Selected/Tip lines), the multi-candidate listing under a real terminal, and the defaultPackage note, each in both local and global vp flavors.
…rkspace root Replaces the interim TTY listing with the fuzzy vite_select picker (the vp run selector component): typing filters packages, Enter runs the selection as an implicit -C, Ctrl+C cancels with exit 130. Single-runnable auto-select and the non-interactive listing are unchanged. Every picker render emits a package-select:<query>:<index> milestone so PTY snapshot tests synchronize on real keystrokes (picker_select / picker_cancel / non-TTY listing cases, both flavors). Requires the configurable prompt from voidzero-dev/vite-task#510; the vite-task pin points at that PR's commit and needs a re-bump to vite-task main after it merges.
…arness Moves the four old-harness fixtures added by this PR (command-cwd-flag, command-app-root-listing, command-default-package, command-default-package-missing) into crates/vite_cli_snapshots: - cwd_flag: -C vs cd byte-identical for pack and run, missing-dir error, positional entry-semantics parity, with vpt list-dir/rm assertions - app_root_listing: non-TTY dev listing joins the build step - app_root_default_package: dist assertion plus a missing-directory case via a nested non-workspace root Real pass/fail assertions and recorded exit codes replace the regenerate-and-diff model for these cases.
…efault entry A Vite config without a pack block does not make bare vp pack succeed, so it no longer counts as pack-runnable; document the per-command runnable rules normatively in the RFC and docs.
Locks in the corrected pack-runnable rule: an app's vite.config.ts without a pack block does not count, so bare vp pack at the root auto-selects the library whose only signal is tsdown's default src/index.ts entry and packs it with no pack config at all.
Bare vp build and vp pack in a standalone repo (no workspaces field) run in place even in an interactive terminal: no picker, no auto-select line, no listing. Guards resolution-order step 6 against regressions.
- exempt -v from elicitation: Vite and tsdown are cac-based and use -v, --version, so vp build -v now reaches the tool instead of the picker; the help/version predicate moves next to the other foreign-flag knowledge in help.rs - add FieldMap::contains for presence checks without cloning the field value; use it in the pack-runnable rule - store PackageRow name/path as Str, deleting the Str->String->Str round-trip; flatten the picker branch and the AppTarget dispatch into one exhaustive match; inline the one-line should_enable_picker wrapper; test and comment tidying
- defaultPackage: only an explicitly declared key changes behavior. New FieldMap::get_declared keeps unanalyzable/spread configs from failing every bare app command (they fall through to picker/current-dir); a declared non-static value still errors - consume -C first thing in main, before alias normalization, the no-command picker, and clap: vp -C dir node --version now normalizes like the cd form, bare vp -C dir opens the command picker for dir, and setting the process cwd makes in-process helpers (global add -g file: specs, node version inference) resolve from dir - PTY regression case for the spread-config fall-through; unit tests for parse_leading_chdir and get_declared
- known value-taking flags (--port 4000, --mode production) keep an invocation bare so root elicitation still applies; unknown/optional-value flags keep the conservative fallback - refresh POSIX PWD wherever -C or an elicited target applies (global main, local bin, binding env for retargeted tools) so process.env.PWD readers match the cd form - a runnable workspace root stays selectable as a '.' row (picker, listing, auto-select), with a PTY regression fixture - open field maps now record explicit NonStatic declarations, so a spread followed by defaultPackage: process.env.X errors instead of being silently ignored - apply -C before shell completion so cwd-sensitive completers suggest from the target directory
- vpr -C <dir> <task> works on both entry points: the global vpr dispatch consumes the flag before delegating to run, and the local bin/vpr shim inserts 'run' after the -C tokens - the workspace root needs a stronger runnable signal than members: a shared root vite.config.ts (lint/fmt/tasks) no longer makes the root an app target for dev/build/preview; root index.html still does - pack ranking requires an explicitly declared pack block (get_declared), so a spread-only config cannot be auto-selected into a tsdown run; FieldMap::contains is gone with its last caller - add --assetsDir/--assetsInlineLimit/--filter to the required-value flag list per vite's CLI docs Regression fixtures: a shared config at the listing fixture's root and a spread-only package in pack_default_entry, both pinned by unchanged snapshots.
- vp/vpr script commands with a leading -C run verbatim instead of being parsed as CLIArgs (which has no global flags): the spawned binary applies the directory change exactly like a direct invocation - bare app commands in scripts get the same workspace-root target elicitation as direct invocations via a pure needs_elicitation predicate: the handler spawns the real binary (which elicits identically) instead of synthesizing a silent root run - add --configLoader/--config-loader/--tsconfig/--log-level/ --deps.never-bundle to the required-value flag list - PTY regression cases: a root script 'build: vp build' fails with the listing through vp run, and a 'vp -C' script runs in the target package Shell-integration template gaps (env-use wrapper, vpr completers) are tracked separately in #2056.
- split the required-value flag table per forwarded tool: Vite commands and pack have different flags (--env-file/--on-success/--copy/... for pack, and Vite's -d/--debug is optional-value so it no longer consumes), plus pack's --env.NAME <value> pattern flags - bin/vpr: a bare 'vpr -C' with no directory no longer inserts run into the -C value position; bin.ts reports the missing-argument error
- defaultPackage is consulted only at the invocation root: a member package's own config never redirects (or fails) a command already running in that member; regression case pins a member config pointing at a missing directory - inline --env.NAME=value pack flags no longer consume the next token, so an explicit entry after them stays a positional - complete the pack value-flag table from pack-bin.ts: --root, -F/--filter, -c/--config; add cac kebab-case twins to the Vite table - mark the two mid-case expected-failure steps continue-on-failure per the harness's new shell-like && semantics
…rkspace In a non-workspace layout the member IS its own invocation root (standalone package), where honoring defaultPackage is by design; the finding is about workspace members, so the case moves to a fixture with a workspaces field and asserts the pack runs in place.
- rewrite is_bare to mirror cac/mri parsing: a non-flag token after any non-boolean flag is that flag's value (required and optional alike, so vp dev --host 0.0.0.0 and --open /foo elicit again), and only tokens no flag consumes are positionals; the per-tool required-value tables are replaced by smaller boolean tables derived from the shipped --help - pack workspace selectors (-W/--workspace, -F/--filter) define their own target set and always bypass elicitation - document in the RFC that implicit -C runs inside the already-chosen CLI (single-version workspace model); full re-delegation for defaultPackage targets with their own install/runtime pin is tracked in #2057
The test module imports items explicitly; a restack conflict resolution kept the tests but dropped the import, which only all-targets compilation catches (workspace clippy now runs clean with CI's exact invocation).
- pack workspace selectors also bypass elicitation in inline form (--filter=ui, --workspace=packages/ui) - honor the -- option terminator: a token after it is an explicit positional, and bare -- stays bare - global vpr rejects a bare -C with no value instead of delegating it as a run argument (mirrors the local wrapper fix) - local bin.ts catches process.chdir failures (e.g. permission denied on an existing directory) as a normal CLI error instead of an uncaught stack trace - render the spread-config build case through the PTY: non-TTY steps record raw bytes and the Windows byte stream of a vite build differs invisibly from the recorded macOS one
vp pack --root packages/lib at a workspace root was a valid targeted invocation before this PR; like -W/--workspace and -F/--filter it defines pack's own target, so it bypasses elicitation (spaced and inline forms) instead of hitting the picker or the non-TTY listing.
Match the documented convention in packages/prompts/src/milestone.ts: only the PTY snapshot harness sets the env, so real terminals and piped output never receive the OSC 8 marker bytes. The picker fixtures keep passing because the harness sets the gate for spawned processes.
fs.statSync can itself throw (EACCES on a parent, ELOOP) before the chdir try/catch; chdir reports every failure mode through one catchable path, with ENOENT/ENOTDIR keeping the directory-not-found message.
- one apply_chdir helper (join/clean/validate/chdir/PWD) replaces the four drifting copies in vite_global_cli: main's normal path, the completion peek, vpr, and the clap-path fallback in cli.rs — which previously skipped set_current_dir and the PWD sync, silently weakening the cd equivalence on orderings like a second -C - one classify() core (bare -> defaultPackage-at-root -> workspace root) now feeds both resolve_app_target and needs_elicitation, so the RFC's resolution order is written once and the two entry points cannot drift; the extracted defaultPackage value rides the enum, removing a duplicate config parse - bin/vpr is a grammar-free shim: bin.ts inserts 'run' itself via argv0 after its existing -C consumption, deleting the hand-synchronized second JS copy of the -C token grammar - pack runnable check stats src/index.ts before parsing the config (runs per workspace package); PWD in the direct-subcommand env is only overridden when elicitation retargeted (plain runs keep the caller's possibly-symlinked PWD verbatim); vpr parses -C once; identical FieldMapInner arms collapse via or-patterns; handler's -C predicate simplifies to starts_with
The helper was inserted between normalize_args's bullet-list doc comment and the function, merging the docs; clippy reads the helper's prose as unindented list continuations under --deny warnings.
The vp-config e2e broke: a single package with a settings-only pnpm-workspace.yaml (catalogs, minimumReleaseAge) is a workspace root, and the runnable-root picker row made the candidate list non-empty, so non-TTY CI got the target listing and exit 1 for a bare vp pack that packed fine before this branch. Deeper rule instead of the row: when the workspace root itself looks runnable for the command, classify() never elicits and the command runs in place, exactly the pre-elicitation behavior. The root is never a picker/listing row; elicitation only engages when the root is not a plausible target, which is what keeps the feature purely additive. New settings_only_workspace fixture pins the vp-config shape non-TTY; app_root_runnable_root now asserts in-place runs for both TTY modes.
The tty=false vite-build capture differs invisibly on Windows (same class as the round-9 unanalyzable_config_ignored fix); settings_only_workspace already covers the non-TTY in-place guarantee with Windows-stable tsdown output.

Implements the approved RFC, now included in this PR as
rfcs/cwd-flag.md(moved from #2022).vp -C <dir> <cmd>behaves exactly likecd <dir> && vp <cmd>for every command, parsed by the global Rust binary (crates/vite_global_cli) and by the local bin (packages/cli/src/bin.ts); the tool child process is spawned with<dir>as cwd, noprocess.chdirin the Rust layer.vp dev/build/preview/packat a workspace root now resolve a target instead of silently running against the root:defaultPackagefrom the root config (statically extracted, so it works at roots without a vite-plus install), single-runnable auto-select in interactive terminals, otherwise a ranked package listing with-Chints and exit 1.[root], tsdown entries); a parity regression snap test locks that in.Tests live on the PTY snapshot harness (stacked on #2052), with real pass/fail assertions in both local and global vp flavors:
cwd_flag(-C vs cd byte-identical for pack and run, missing-dir error, positional entry-semantics parity),app_root_listing(picker select via real keystrokes, Ctrl+C cancel, non-TTY listing for build and dev),app_root_auto_select, andapp_root_default_package(note line, dist assertion, missing-directory error). The full oldsnap-test-localsuite still passes unchanged; clippy, unit tests, and a manual global-binary run verified.The interactive package picker is now implemented on
vite_select(fuzzy filter, Enter = implicit-C, Ctrl+C = exit 130), with PTY snapshot coverage driving real keystrokes in both flavors. Requires voidzero-dev/vite-task#510 (configurable selector prompt); the vite-task pin points at that PR's commit and needs a re-bump to vite-task main after it merges.