Skip to content

feat(cli): global -C flag for working-directory switching#2031

Draft
fengmk2 wants to merge 31 commits into
graphite-base/2031from
feat/cwd-flag
Draft

feat(cli): global -C flag for working-directory switching#2031
fengmk2 wants to merge 31 commits into
graphite-base/2031from
feat/cwd-flag

Conversation

@fengmk2

@fengmk2 fengmk2 commented Jul 3, 2026

Copy link
Copy Markdown
Member

Implements the approved RFC, now included in this PR as rfcs/cwd-flag.md (moved from #2022).

  • vp -C <dir> <cmd> behaves exactly like cd <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, no process.chdir in the Rust layer.
  • Bare vp dev/build/preview/pack at a workspace root now resolve a target instead of silently running against the root: defaultPackage from 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 -C hints and exit 1.
  • Positionals keep upstream semantics (Vite [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, and app_root_default_package (note line, dist assertion, missing-directory error). The full old snap-test-local suite 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.

@fengmk2 fengmk2 self-assigned this Jul 3, 2026
@netlify

netlify Bot commented Jul 3, 2026

Copy link
Copy Markdown

Deploy Preview for viteplus-preview canceled.

Name Link
🔨 Latest commit 8f73602
🔍 Latest deploy log https://app.netlify.com/projects/viteplus-preview/deploys/6a49b84f4df08c0008bb1077

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

✅ Staging deployment successful!

Preview: https://viteplus-staging.void.app/
Commit: be91316

@fengmk2

fengmk2 commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

Manual verification steps

Setup

git 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

(pnpm bootstrap-cli replaces the installed ~/.vite-plus CLI; re-run it from main afterwards to restore.)

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.ts

1. -C runs any command as if started in the directory

vp -C packages/ui pack        # packs ui; dist lands in packages/ui/dist
vp -C apps/web dev            # dev server for web, from the root
vp -C apps/web build
vp -C nope build              # error: directory not found: nope (exit 1)

Compare with cd packages/ui && vp pack: identical output. The positional form is unchanged: vp pack packages/ui still treats the path as a tsdown entry resolved from the invocation directory (upstream semantics).

2. Bare app commands at the workspace root

vp build         # error + package listing (apps ranked first) + -C hints, exit 1
vp dev           # same; no server starts against the root anymore
vp dev --help    # still prints vite's dev help, never the listing
vp build | cat   # non-TTY: same listing, deterministic

3. Single-app auto-select (interactive terminal only)

rm -rf apps/admin
vp dev    # prints "Selected package: web (apps/web)" + a Tip line, then starts web

4. defaultPackage

echo 'export default { defaultPackage: "./apps/web" }' > vite.config.ts
vp build            # note: vp build: using ./apps/web (defaultPackage)
vp -C packages/ui pack   # explicit -C still wins over the config
echo 'export default { defaultPackage: "./missing" }' > vite.config.ts
vp build            # error: defaultPackage points to a missing directory (exit 1)

5. The original cwd-divergence bug

The reproduction repo demonstrates the process.cwd() config read that motivated -C:

git clone https://github.com/why-reproductions-are-required/vite-plus-monorepo-app-commands-repro
cd vite-plus-monorepo-app-commands-repro   # do NOT pnpm install: keep the branch CLI in charge
vp dev apps/admin      # still fails with ENOENT (positional = root only, unchanged)
vp -C apps/admin dev   # works: cert loads, server starts

@fengmk2 fengmk2 changed the base branch from main to rfc/interactive-snapshot-tests July 5, 2026 01:50

fengmk2 commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more


How to use the Graphite Merge Queue

Add 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.

@fengmk2 fengmk2 changed the base branch from rfc/interactive-snapshot-tests to graphite-base/2031 July 5, 2026 02:20
@fengmk2 fengmk2 changed the base branch from graphite-base/2031 to rfc/interactive-snapshot-tests July 5, 2026 04:36
@fengmk2 fengmk2 changed the base branch from rfc/interactive-snapshot-tests to graphite-base/2031 July 5, 2026 04:41
@fengmk2 fengmk2 changed the base branch from graphite-base/2031 to rfc/interactive-snapshot-tests July 5, 2026 04:56
@fengmk2 fengmk2 changed the base branch from rfc/interactive-snapshot-tests to graphite-base/2031 July 5, 2026 05:17
@fengmk2 fengmk2 changed the base branch from graphite-base/2031 to rfc/interactive-snapshot-tests July 5, 2026 05:37
@fengmk2 fengmk2 changed the base branch from rfc/interactive-snapshot-tests to graphite-base/2031 July 5, 2026 05:46
@fengmk2 fengmk2 changed the base branch from graphite-base/2031 to rfc/interactive-snapshot-tests July 5, 2026 05:50
@fengmk2 fengmk2 force-pushed the rfc/interactive-snapshot-tests branch from fefbe9d to e6ed290 Compare July 5, 2026 06:10
@fengmk2 fengmk2 changed the base branch from rfc/interactive-snapshot-tests to graphite-base/2031 July 5, 2026 06:44
@fengmk2 fengmk2 changed the base branch from graphite-base/2031 to rfc/interactive-snapshot-tests July 5, 2026 06:52
@fengmk2 fengmk2 changed the base branch from rfc/interactive-snapshot-tests to graphite-base/2031 July 5, 2026 07:03
fengmk2 added 22 commits July 6, 2026 10:09
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.
@fengmk2 fengmk2 changed the base branch from graphite-base/2031 to rfc/interactive-snapshot-tests July 6, 2026 02:10
@fengmk2

fengmk2 commented Jul 6, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 386e3d1602

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/vite_global_cli/src/commands/vpr.rs
Comment thread .clippy.toml
Comment thread packages/cli/src/bin.ts Outdated
Comment thread crates/vite_global_cli/src/cli.rs
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.
@fengmk2

fengmk2 commented Jul 6, 2026

Copy link
Copy Markdown
Member Author

@codex review

@fengmk2 fengmk2 changed the base branch from rfc/interactive-snapshot-tests to graphite-base/2031 July 6, 2026 02:35
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: be91316b7e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant