diff --git a/.clippy.toml b/.clippy.toml
index fd0b1c45c2..40215aa466 100644
--- a/.clippy.toml
+++ b/.clippy.toml
@@ -12,6 +12,7 @@ disallowed-methods = [
{ path = "str::replace", reason = "To avoid memory allocation, use `cow_utils::CowUtils::cow_replace` instead." },
{ path = "str::replacen", reason = "To avoid memory allocation, use `cow_utils::CowUtils::cow_replacen` instead." },
{ path = "std::env::current_dir", reason = "To get an `AbsolutePathBuf`, Use `vite_path::current_dir` instead." },
+ { path = "std::io::IsTerminal::is_terminal", reason = "Use `vite_powershell::is_stdin_terminal` for stdin, or `vite_shared::is_stdout_terminal` / `vite_shared::is_stderr_terminal`, which memoize the lookup." },
]
disallowed-types = [
diff --git a/Cargo.lock b/Cargo.lock
index 756f413507..f05c4894e7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8818,6 +8818,7 @@ dependencies = [
"tracing",
"tracing-subscriber",
"vite_path",
+ "vite_powershell",
"vite_str",
"which",
]
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/apps/web/index.html b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/apps/web/index.html
new file mode 100644
index 0000000000..ce6d6280c2
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/apps/web/index.html
@@ -0,0 +1,6 @@
+
+
+
+ web
+
+
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/apps/web/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/apps/web/package.json
new file mode 100644
index 0000000000..64630869c1
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/apps/web/package.json
@@ -0,0 +1 @@
+{ "name": "web", "private": true }
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/package.json
new file mode 100644
index 0000000000..05c1c2ef02
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/package.json
@@ -0,0 +1 @@
+{ "name": "app-root-auto-select", "private": true, "workspaces": ["apps/*", "packages/*"] }
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/packages/ui/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/packages/ui/package.json
new file mode 100644
index 0000000000..70bb6aab17
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/packages/ui/package.json
@@ -0,0 +1 @@
+{ "name": "ui", "private": true, "type": "module", "main": "src/index.ts" }
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/packages/ui/src/index.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/packages/ui/src/index.ts
new file mode 100644
index 0000000000..905c7a9bbb
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/packages/ui/src/index.ts
@@ -0,0 +1,3 @@
+export function hello(name: string): string {
+ return `hello ${name}`;
+}
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots.toml
new file mode 100644
index 0000000000..0d74f9f251
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots.toml
@@ -0,0 +1,10 @@
+[[case]]
+name = "auto_select"
+vp = ["local", "global"]
+comment = """
+With exactly one likely-runnable package, a bare app command in an interactive
+terminal auto-selects it, prints the Selected/Tip teaching lines, and runs
+there (rfcs/cwd-flag.md). This TTY-only branch was untestable in the old
+harness.
+"""
+steps = [["vp", "build"]]
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots/auto_select.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots/auto_select.global.md
new file mode 100644
index 0000000000..edc6dc5023
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots/auto_select.global.md
@@ -0,0 +1,21 @@
+# auto_select
+
+With exactly one likely-runnable package, a bare app command in an interactive
+terminal auto-selects it, prints the Selected/Tip teaching lines, and runs
+there (rfcs/cwd-flag.md). This TTY-only branch was untestable in the old
+harness.
+
+## `vp build`
+
+```
+VITE+ - The Unified Toolchain for the Web
+
+Selected package: web (apps/web)
+Tip: run this directly with `vp -C apps/web build`
+vite building client environment for production...
+✓ 2 modules transformed.
+computing gzip size...
+dist/index.html 0.06 kB │ gzip: 0.06 kB
+
+✓ built in
+```
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots/auto_select.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots/auto_select.local.md
new file mode 100644
index 0000000000..a33f993f6c
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_auto_select/snapshots/auto_select.local.md
@@ -0,0 +1,19 @@
+# auto_select
+
+With exactly one likely-runnable package, a bare app command in an interactive
+terminal auto-selects it, prints the Selected/Tip teaching lines, and runs
+there (rfcs/cwd-flag.md). This TTY-only branch was untestable in the old
+harness.
+
+## `vp build`
+
+```
+Selected package: web (apps/web)
+Tip: run this directly with `vp -C apps/web build`
+vite building client environment for production...
+✓ 2 modules transformed.
+computing gzip size...
+dist/index.html 0.06 kB │ gzip: 0.06 kB
+
+✓ built in
+```
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/package.json
new file mode 100644
index 0000000000..b9fabc032d
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/package.json
@@ -0,0 +1 @@
+{ "name": "app-root-default-package", "private": true }
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/packages/ui/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/packages/ui/package.json
new file mode 100644
index 0000000000..70bb6aab17
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/packages/ui/package.json
@@ -0,0 +1 @@
+{ "name": "ui", "private": true, "type": "module", "main": "src/index.ts" }
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/packages/ui/src/index.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/packages/ui/src/index.ts
new file mode 100644
index 0000000000..905c7a9bbb
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/packages/ui/src/index.ts
@@ -0,0 +1,3 @@
+export function hello(name: string): string {
+ return `hello ${name}`;
+}
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots.toml
new file mode 100644
index 0000000000..9b923956c1
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots.toml
@@ -0,0 +1,9 @@
+[[case]]
+name = "default_package"
+vp = ["local", "global"]
+comment = """
+defaultPackage in the root config acts as an implicit -C for bare app
+commands, including at a root that is not a JS workspace; vp prints a note
+line and runs in the configured directory (rfcs/cwd-flag.md).
+"""
+steps = [["vp", "pack"]]
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package.global.md
new file mode 100644
index 0000000000..6e964447e2
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package.global.md
@@ -0,0 +1,18 @@
+# default_package
+
+defaultPackage in the root config acts as an implicit -C for bare app
+commands, including at a root that is not a JS workspace; vp prints a note
+line and runs in the configured directory (rfcs/cwd-flag.md).
+
+## `vp pack`
+
+```
+VITE+ - The Unified Toolchain for the Web
+
+note: vp pack: using ./packages/ui (defaultPackage)
+ℹ entry: src/index.ts
+ℹ Build start
+ℹ dist/index.mjs 0.10 kB │ gzip: 0.11 kB
+ℹ 1 files, total: 0.10 kB
+✔ Build complete in
+```
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package.local.md
new file mode 100644
index 0000000000..19638ef47d
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/snapshots/default_package.local.md
@@ -0,0 +1,16 @@
+# default_package
+
+defaultPackage in the root config acts as an implicit -C for bare app
+commands, including at a root that is not a JS workspace; vp prints a note
+line and runs in the configured directory (rfcs/cwd-flag.md).
+
+## `vp pack`
+
+```
+note: vp pack: using ./packages/ui (defaultPackage)
+ℹ entry: src/index.ts
+ℹ Build start
+ℹ dist/index.mjs 0.10 kB │ gzip: 0.11 kB
+ℹ 1 files, total: 0.10 kB
+✔ Build complete in
+```
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/vite.config.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/vite.config.ts
new file mode 100644
index 0000000000..36dab4eb9e
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_default_package/vite.config.ts
@@ -0,0 +1,3 @@
+export default {
+ defaultPackage: './packages/ui',
+};
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/admin/index.html b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/admin/index.html
new file mode 100644
index 0000000000..9a194fe836
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/admin/index.html
@@ -0,0 +1,6 @@
+
+
+
+ admin
+
+
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/admin/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/admin/package.json
new file mode 100644
index 0000000000..bebad673a4
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/admin/package.json
@@ -0,0 +1 @@
+{ "name": "admin", "private": true }
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/web/index.html b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/web/index.html
new file mode 100644
index 0000000000..ce6d6280c2
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/web/index.html
@@ -0,0 +1,6 @@
+
+
+
+ web
+
+
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/web/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/web/package.json
new file mode 100644
index 0000000000..64630869c1
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/apps/web/package.json
@@ -0,0 +1 @@
+{ "name": "web", "private": true }
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/package.json
new file mode 100644
index 0000000000..0807f1400d
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/package.json
@@ -0,0 +1 @@
+{ "name": "app-root-listing", "private": true, "workspaces": ["apps/*", "packages/*"] }
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/packages/ui/package.json b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/packages/ui/package.json
new file mode 100644
index 0000000000..70bb6aab17
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/packages/ui/package.json
@@ -0,0 +1 @@
+{ "name": "ui", "private": true, "type": "module", "main": "src/index.ts" }
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/packages/ui/src/index.ts b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/packages/ui/src/index.ts
new file mode 100644
index 0000000000..905c7a9bbb
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/packages/ui/src/index.ts
@@ -0,0 +1,3 @@
+export function hello(name: string): string {
+ return `hello ${name}`;
+}
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots.toml b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots.toml
new file mode 100644
index 0000000000..8063e4df48
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots.toml
@@ -0,0 +1,9 @@
+[[case]]
+name = "listing"
+vp = ["local", "global"]
+comment = """
+A bare app command at a workspace root with several candidate packages prints
+the ranked package listing with -C hints and exits 1 instead of building the
+root, even in an interactive terminal (rfcs/cwd-flag.md).
+"""
+steps = [["vp", "build"]]
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/listing.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/listing.global.md
new file mode 100644
index 0000000000..aa2352460b
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/listing.global.md
@@ -0,0 +1,23 @@
+# listing
+
+A bare app command at a workspace root with several candidate packages prints
+the ranked package listing with -C hints and exits 1 instead of building the
+root, even in an interactive terminal (rfcs/cwd-flag.md).
+
+## `vp build`
+
+**Exit code:** 1
+
+```
+VITE+ - The Unified Toolchain for the Web
+
+error: `vp build` at the workspace root needs a target package.
+
+ Packages in this workspace:
+ admin apps/admin
+ web apps/web
+ ui packages/ui
+
+ Pass a directory: vp -C apps/admin build
+ Or run every package's build script: vp run -r build
+```
diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/listing.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/listing.local.md
new file mode 100644
index 0000000000..f8aa7200cd
--- /dev/null
+++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/app_root_listing/snapshots/listing.local.md
@@ -0,0 +1,21 @@
+# listing
+
+A bare app command at a workspace root with several candidate packages prints
+the ranked package listing with -C hints and exits 1 instead of building the
+root, even in an interactive terminal (rfcs/cwd-flag.md).
+
+## `vp build`
+
+**Exit code:** 1
+
+```
+error: `vp build` at the workspace root needs a target package.
+
+ Packages in this workspace:
+ admin apps/admin
+ web apps/web
+ ui packages/ui
+
+ Pass a directory: vp -C apps/admin build
+ Or run every package's build script: vp run -r build
+```
diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs
index 2d91427f40..2b7d50c5a9 100644
--- a/crates/vite_global_cli/src/cli.rs
+++ b/crates/vite_global_cli/src/cli.rs
@@ -3,7 +3,7 @@
//! This module defines the CLI structure using clap and routes commands
//! to their appropriate handlers.
-use std::{collections::HashSet, ffi::OsStr, io::IsTerminal, process::ExitStatus};
+use std::{collections::HashSet, ffi::OsStr, process::ExitStatus};
use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
use clap_complete::ArgValueCompleter;
@@ -53,6 +53,10 @@ pub struct Args {
#[arg(short = 'V', long = "version")]
pub version: bool,
+ /// Run as if vp was started in instead of the current working directory
+ #[arg(short = 'C', value_name = "DIR")]
+ pub chdir: Option,
+
#[clap(subcommand)]
pub command: Option,
}
@@ -805,7 +809,7 @@ fn should_reinstall_node_mismatches(
return true;
}
- if !std::io::stdin().is_terminal() || std::env::var_os("CI").is_some() {
+ if !vite_shared::is_stdin_terminal() || std::env::var_os("CI").is_some() {
let package_names =
packages.iter().map(|package| package.name.as_str()).collect::>().join(", ");
output::warn(&format!(
@@ -848,10 +852,20 @@ pub async fn run_command(cwd: AbsolutePathBuf, args: Args) -> Result Result {
+ // Apply the global `-C ` flag before anything reads cwd, so local CLI
+ // resolution and command execution behave as if vp was started in .
+ // `clean()` normalizes `.`/`..` so upward workspace walks never see them.
+ if let Some(dir) = &args.chdir {
+ cwd = cwd.join(dir).clean();
+ if !cwd.as_path().is_dir() {
+ return Err(Error::UserMessage(format!("directory not found: {dir}").into()));
+ }
+ }
+
// Handle --version flag (Category B: delegates to JS)
if args.version {
return commands::version::execute(cwd).await;
diff --git a/crates/vite_global_cli/src/command_picker.rs b/crates/vite_global_cli/src/command_picker.rs
index dc31289b9c..540faef750 100644
--- a/crates/vite_global_cli/src/command_picker.rs
+++ b/crates/vite_global_cli/src/command_picker.rs
@@ -1,7 +1,7 @@
//! Interactive top-level command picker for `vp`.
use std::{
- io::{self, IsTerminal, Write},
+ io::{self, Write},
ops::ControlFlow,
};
@@ -114,20 +114,6 @@ const COMMANDS: &[CommandEntry] = &[
},
];
-const CI_ENV_VARS: &[&str] = &[
- "CI",
- "CONTINUOUS_INTEGRATION",
- "GITHUB_ACTIONS",
- "GITLAB_CI",
- "CIRCLECI",
- "TRAVIS",
- "JENKINS_URL",
- "BUILDKITE",
- "DRONE",
- "CODEBUILD_BUILD_ID",
- "TF_BUILD",
-];
-
pub fn pick_top_level_command_if_interactive(
cwd: &AbsolutePath,
) -> io::Result {
@@ -144,14 +130,7 @@ pub fn pick_top_level_command_if_interactive(
}
fn should_enable_picker() -> bool {
- std::io::stdin().is_terminal()
- && std::io::stdout().is_terminal()
- && std::env::var("TERM").map_or(true, |term| term != "dumb")
- && !is_ci_environment()
-}
-
-fn is_ci_environment() -> bool {
- CI_ENV_VARS.iter().any(|key| std::env::var_os(key).is_some())
+ vite_shared::is_interactive_terminal()
}
fn run_picker(command_order: &[usize]) -> io::Result