diff --git a/Cargo.lock b/Cargo.lock
index 510eb88ee6..0ecbde48eb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7209,6 +7209,7 @@ dependencies = [
"base64-simd",
"chrono",
"clap",
+ "crossterm",
"flate2",
"junction",
"node-semver",
diff --git a/crates/vite_global_cli/Cargo.toml b/crates/vite_global_cli/Cargo.toml
index 80b2f46a7f..82ab505e84 100644
--- a/crates/vite_global_cli/Cargo.toml
+++ b/crates/vite_global_cli/Cargo.toml
@@ -26,6 +26,7 @@ tokio = { workspace = true, features = ["full"] }
tracing = { workspace = true }
owo-colors = { workspace = true }
oxc_resolver = { workspace = true }
+crossterm = { workspace = true }
vite_error = { workspace = true }
vite_install = { workspace = true }
vite_js_runtime = { workspace = true }
diff --git a/crates/vite_global_cli/src/command_picker.rs b/crates/vite_global_cli/src/command_picker.rs
new file mode 100644
index 0000000000..a2ab2b082f
--- /dev/null
+++ b/crates/vite_global_cli/src/command_picker.rs
@@ -0,0 +1,520 @@
+//! Interactive top-level command picker for `vp`.
+
+use std::{
+ io::{self, IsTerminal, Write},
+ ops::ControlFlow,
+};
+
+use crossterm::{
+ cursor,
+ event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
+ execute,
+ style::{Attribute, Print, ResetColor, SetAttribute, SetForegroundColor},
+ terminal::{self, ClearType},
+};
+
+const NEWLINE: &str = "\r\n";
+const SELECTED_COLOR: crossterm::style::Color = crossterm::style::Color::Blue;
+const SELECTED_MARKER: &str = "›";
+const UNSELECTED_MARKER: &str = " ";
+const HELP_LABEL_NOTE: &str = " (view all commands)";
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct PickedCommand {
+ pub command: &'static str,
+ pub append_help: bool,
+}
+
+#[derive(Clone, Copy)]
+struct CommandEntry {
+ label: &'static str,
+ command: &'static str,
+ summary: &'static str,
+ append_help: bool,
+}
+
+const COMMANDS: &[CommandEntry] = &[
+ CommandEntry {
+ label: "create",
+ command: "create",
+ summary: "Create a new project from a template.",
+ append_help: false,
+ },
+ CommandEntry {
+ label: "migrate",
+ command: "migrate",
+ summary: "Migrate an existing project to Vite+.",
+ append_help: false,
+ },
+ CommandEntry {
+ label: "dev",
+ command: "dev",
+ summary: "Run the development server.",
+ append_help: false,
+ },
+ CommandEntry {
+ label: "check",
+ command: "check",
+ summary: "Run format, lint, and type checks.",
+ append_help: false,
+ },
+ CommandEntry { label: "test", command: "test", summary: "Run tests.", append_help: false },
+ CommandEntry {
+ label: "install",
+ command: "install",
+ summary: "Install dependencies, or add packages when names are provided.",
+ append_help: false,
+ },
+ CommandEntry { label: "run", command: "run", summary: "Run tasks.", append_help: false },
+ CommandEntry {
+ label: "build",
+ command: "build",
+ summary: "Build for production.",
+ append_help: false,
+ },
+ CommandEntry { label: "pack", command: "pack", summary: "Build library.", append_help: false },
+ CommandEntry {
+ label: "preview",
+ command: "preview",
+ summary: "Preview production build.",
+ append_help: false,
+ },
+ CommandEntry {
+ label: "outdated",
+ command: "outdated",
+ summary: "Check for outdated packages.",
+ append_help: false,
+ },
+ CommandEntry {
+ label: "env",
+ command: "env",
+ summary: "Manage Node.js versions.",
+ append_help: false,
+ },
+ CommandEntry {
+ label: "help (view all commands)",
+ command: "help",
+ summary: "Show the full command list and help details.",
+ append_help: false,
+ },
+];
+
+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() -> io::Result