diff --git a/packages/cli/snap-tests-global/migration-already-vite-plus-with-husky-hookspath/snap.txt b/packages/cli/snap-tests-global/migration-already-vite-plus-with-husky-hookspath/snap.txt
index 822c940421..a497792e14 100644
--- a/packages/cli/snap-tests-global/migration-already-vite-plus-with-husky-hookspath/snap.txt
+++ b/packages/cli/snap-tests-global/migration-already-vite-plus-with-husky-hookspath/snap.txt
@@ -1,10 +1,11 @@
 > git init
 > git config core.hooksPath .husky/_
 > vp migrate --no-interactive # should override husky's core.hooksPath and migrate hooks
-◇ Updated .
-• Node <semver>  unknown latest
+◇ Migrated . to Vite+
+• Node <semver>  pnpm <semver>
 • 2 config updates applied
 • Git hooks configured
+• Package manager settings configured
 
 > cat package.json # husky/lint-staged should be removed, prepare should be vp config
 {
@@ -15,6 +16,13 @@
   "devDependencies": {
     "vite": "^7.0.0",
     "vite-plus": "latest"
+  },
+  "devEngines": {
+    "packageManager": {
+      "name": "pnpm",
+      "version": "<semver>",
+      "onFail": "download"
+    }
   }
 }
 
diff --git a/packages/cli/snap-tests-global/migration-already-vite-plus-with-husky-lint-staged/snap.txt b/packages/cli/snap-tests-global/migration-already-vite-plus-with-husky-lint-staged/snap.txt
index 459167693f..81dfa7d245 100644
--- a/packages/cli/snap-tests-global/migration-already-vite-plus-with-husky-lint-staged/snap.txt
+++ b/packages/cli/snap-tests-global/migration-already-vite-plus-with-husky-lint-staged/snap.txt
@@ -1,9 +1,10 @@
 > git init
 > vp migrate --no-interactive # should still migrate husky/lint-staged even though vite-plus exists
-◇ Updated .
-• Node <semver>  unknown latest
+◇ Migrated . to Vite+
+• Node <semver>  pnpm <semver>
 • 2 config updates applied
 • Git hooks configured
+• Package manager settings configured
 
 > cat package.json # husky/lint-staged should be removed, prepare should be vp config
 {
@@ -14,6 +15,13 @@
   "devDependencies": {
     "vite": "^7.0.0",
     "vite-plus": "latest"
+  },
+  "devEngines": {
+    "packageManager": {
+      "name": "pnpm",
+      "version": "<semver>",
+      "onFail": "download"
+    }
   }
 }
 
diff --git a/packages/cli/snap-tests-global/migration-already-vite-plus/package.json b/packages/cli/snap-tests-global/migration-already-vite-plus/package.json
index 2239d0ec8f..89b32b5b69 100644
--- a/packages/cli/snap-tests-global/migration-already-vite-plus/package.json
+++ b/packages/cli/snap-tests-global/migration-already-vite-plus/package.json
@@ -2,5 +2,16 @@
   "name": "migration-already-vite-plus",
   "devDependencies": {
     "vite-plus": "latest"
+  },
+  "overrides": {
+    "vite": "npm:@voidzero-dev/vite-plus-core@latest",
+    "vitest": "npm:@voidzero-dev/vite-plus-test@latest"
+  },
+  "devEngines": {
+    "packageManager": {
+      "name": "npm",
+      "version": "11.16.0",
+      "onFail": "download"
+    }
   }
 }
diff --git a/packages/cli/snap-tests-global/migration-already-vite-plus/snap.txt b/packages/cli/snap-tests-global/migration-already-vite-plus/snap.txt
index 36981c77e3..868ba69b18 100644
--- a/packages/cli/snap-tests-global/migration-already-vite-plus/snap.txt
+++ b/packages/cli/snap-tests-global/migration-already-vite-plus/snap.txt
@@ -1,3 +1,33 @@
-> vp migrate --no-interactive # should detect existing vite-plus and exit
+> vp migrate --no-interactive # should detect existing vite-plus and exit without setup defaults
 This project is already using Vite+! Happy coding!
 
+
+> vp migrate --no-interactive --hooks --agent agents # explicit setup should still update existing vite-plus project
+◇ Updated .
+• Node <semver>  npm <semver>
+• 2 config updates applied
+
+> cat package.json # prepare script should be configured for vp config
+{
+  "name": "migration-already-vite-plus",
+  "devDependencies": {
+    "vite-plus": "latest"
+  },
+  "overrides": {
+    "vite": "npm:@voidzero-dev/vite-plus-core@latest",
+    "vitest": "npm:@voidzero-dev/vite-plus-test@latest"
+  },
+  "devEngines": {
+    "packageManager": {
+      "name": "npm",
+      "version": "<semver>",
+      "onFail": "download"
+    }
+  },
+  "scripts": {
+    "prepare": "vp config"
+  }
+}
+
+> test -f AGENTS.md # explicit agent instructions should be written
+> test -f .vite-hooks/pre-commit # explicit pre-commit hook should be written
\ No newline at end of file
diff --git a/packages/cli/snap-tests-global/migration-already-vite-plus/steps.json b/packages/cli/snap-tests-global/migration-already-vite-plus/steps.json
index de62b61d76..4321aadfe0 100644
--- a/packages/cli/snap-tests-global/migration-already-vite-plus/steps.json
+++ b/packages/cli/snap-tests-global/migration-already-vite-plus/steps.json
@@ -1,5 +1,11 @@
 {
-  "commands": ["vp migrate --no-interactive # should detect existing vite-plus and exit"],
+  "commands": [
+    "vp migrate --no-interactive # should detect existing vite-plus and exit without setup defaults",
+    "vp migrate --no-interactive --hooks --agent agents # explicit setup should still update existing vite-plus project",
+    "cat package.json # prepare script should be configured for vp config",
+    "test -f AGENTS.md # explicit agent instructions should be written",
+    "test -f .vite-hooks/pre-commit # explicit pre-commit hook should be written"
+  ],
   "ignoredPlatforms": [
     {
       "os": "linux",
diff --git a/packages/cli/snap-tests-global/migration-eslint-legacy-already-vite-plus/snap.txt b/packages/cli/snap-tests-global/migration-eslint-legacy-already-vite-plus/snap.txt
index 71376d24c8..8ddfefbbfd 100644
--- a/packages/cli/snap-tests-global/migration-eslint-legacy-already-vite-plus/snap.txt
+++ b/packages/cli/snap-tests-global/migration-eslint-legacy-already-vite-plus/snap.txt
@@ -1,5 +1,6 @@
 > vp migrate --no-interactive # should show legacy config warning and already using Vite+
 
 Legacy ESLint configuration detected (.eslintrc). Automatic migration to Oxlint requires ESLint v9+ with flat config format (eslint.config.*). Please upgrade to ESLint v9 first: https://eslint.org/docs/latest/use/migrate-to-9.0.0
-This project is already using Vite+! Happy coding!
-
+◇ Migrated . to Vite+
+• Node <semver>  pnpm <semver>
+• Package manager settings configured
diff --git a/packages/cli/snap-tests-global/migration-eslint-lint-staged-mjs/snap.txt b/packages/cli/snap-tests-global/migration-eslint-lint-staged-mjs/snap.txt
index 1dbfebd730..5e6122aa90 100644
--- a/packages/cli/snap-tests-global/migration-eslint-lint-staged-mjs/snap.txt
+++ b/packages/cli/snap-tests-global/migration-eslint-lint-staged-mjs/snap.txt
@@ -1,20 +1,11 @@
-> vp migrate --no-interactive # migration should warn about non-JSON lint-staged config
-
-Migrating ESLint config to Oxlint...
-
-ESLint config migrated to .oxlintrc.json
-
-Replacing ESLint comments with Oxlint equivalents...
-
-ESLint comments replaced
-
-✔ Removed eslint.config.mjs
-
-lint-staged.config.mjs — please update eslint references manually
-◇ Updated .
-• Node <semver>  unknown latest
-• 2 config updates applied
+> vp migrate --no-interactive # migration should preserve non-JSON lint-staged config when hooks are not requested
+◇ Migrated . to Vite+
+• Node <semver>  pnpm <semver>
+• 3 config updates applied
 • ESLint rules migrated to Oxlint
+• Package manager settings configured
+! Warnings:
+  - lint-staged.config.mjs — please update eslint references manually
 
 > cat lint-staged.config.mjs # verify non-JSON lint-staged config is preserved unchanged
 export default {
diff --git a/packages/cli/snap-tests-global/migration-eslint-lint-staged-mjs/steps.json b/packages/cli/snap-tests-global/migration-eslint-lint-staged-mjs/steps.json
index 650a464554..d8e9e1aeff 100644
--- a/packages/cli/snap-tests-global/migration-eslint-lint-staged-mjs/steps.json
+++ b/packages/cli/snap-tests-global/migration-eslint-lint-staged-mjs/steps.json
@@ -1,6 +1,6 @@
 {
   "commands": [
-    "vp migrate --no-interactive # migration should warn about non-JSON lint-staged config",
+    "vp migrate --no-interactive # migration should preserve non-JSON lint-staged config when hooks are not requested",
     "cat lint-staged.config.mjs # verify non-JSON lint-staged config is preserved unchanged"
   ],
   "ignoredPlatforms": [
diff --git a/packages/cli/snap-tests-global/migration-eslint-rerun-dual-config/snap.txt b/packages/cli/snap-tests-global/migration-eslint-rerun-dual-config/snap.txt
index 9dea8eb1f3..0771255168 100644
--- a/packages/cli/snap-tests-global/migration-eslint-rerun-dual-config/snap.txt
+++ b/packages/cli/snap-tests-global/migration-eslint-rerun-dual-config/snap.txt
@@ -1,20 +1,9 @@
 > vp migrate --no-interactive # should detect vite-plus + eslint and auto-migrate eslint
-
-Migrating ESLint config to Oxlint...
-
-ESLint config migrated to .oxlintrc.json
-
-Replacing ESLint comments with Oxlint equivalents...
-
-ESLint comments replaced
-
-✔ Removed eslint.config.mjs
-
-✔ Removed .eslintrc
-◇ Updated .
-• Node <semver>  unknown latest
-• 2 config updates applied
+◇ Migrated . to Vite+
+• Node <semver>  pnpm <semver>
+• 4 config updates applied
 • ESLint rules migrated to Oxlint
+• Package manager settings configured
 
 > cat package.json # check eslint removed and scripts rewritten
 {
diff --git a/packages/cli/snap-tests-global/migration-eslint-rerun-mjs/snap.txt b/packages/cli/snap-tests-global/migration-eslint-rerun-mjs/snap.txt
index 976477a0a6..dc0441dd50 100644
--- a/packages/cli/snap-tests-global/migration-eslint-rerun-mjs/snap.txt
+++ b/packages/cli/snap-tests-global/migration-eslint-rerun-mjs/snap.txt
@@ -1,18 +1,9 @@
 > vp migrate --no-interactive # should detect vite-plus + eslint and auto-migrate eslint
-
-Migrating ESLint config to Oxlint...
-
-ESLint config migrated to .oxlintrc.json
-
-Replacing ESLint comments with Oxlint equivalents...
-
-ESLint comments replaced
-
-✔ Removed eslint.config.mjs
-◇ Updated .
-• Node <semver>  unknown latest
-• 1 config update applied
+◇ Migrated . to Vite+
+• Node <semver>  pnpm <semver>
+• 2 config updates applied
 • ESLint rules migrated to Oxlint
+• Package manager settings configured
 
 > cat package.json # check eslint removed from devDependencies and scripts rewritten
 {
diff --git a/packages/cli/snap-tests-global/migration-eslint-rerun/snap.txt b/packages/cli/snap-tests-global/migration-eslint-rerun/snap.txt
index bec40844e3..fa4bf5b15c 100644
--- a/packages/cli/snap-tests-global/migration-eslint-rerun/snap.txt
+++ b/packages/cli/snap-tests-global/migration-eslint-rerun/snap.txt
@@ -1,18 +1,9 @@
 > vp migrate --no-interactive # should detect vite-plus + eslint and auto-migrate eslint
-
-Migrating ESLint config to Oxlint...
-
-ESLint config migrated to .oxlintrc.json
-
-Replacing ESLint comments with Oxlint equivalents...
-
-ESLint comments replaced
-
-✔ Removed eslint.config.mjs
-◇ Updated .
-• Node <semver>  unknown latest
-• 2 config updates applied
+◇ Migrated . to Vite+
+• Node <semver>  pnpm <semver>
+• 3 config updates applied
 • ESLint rules migrated to Oxlint
+• Package manager settings configured
 
 > cat package.json # check eslint removed from devDependencies and scripts rewritten
 {
diff --git a/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/package.json b/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/package.json
new file mode 100644
index 0000000000..c041e7bed6
--- /dev/null
+++ b/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/package.json
@@ -0,0 +1,26 @@
+{
+  "name": "manual-vp-migrate",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "tsc -b && vite build",
+    "lint": "vp lint .",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "react": "^19.2.6",
+    "react-dom": "^19.2.6"
+  },
+  "devDependencies": {
+    "@types/node": "^24.12.3",
+    "@types/react": "^19.2.14",
+    "@types/react-dom": "^19.2.3",
+    "@vitejs/plugin-react": "^6.0.1",
+    "globals": "^17.6.0",
+    "typescript": "~6.0.2",
+    "vite": "^8.0.12",
+    "vite-plus": "^0.1.24"
+  }
+}
diff --git a/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/pnpm-lock.yaml b/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/pnpm-lock.yaml
new file mode 100644
index 0000000000..b0a073a9c1
--- /dev/null
+++ b/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/pnpm-lock.yaml
@@ -0,0 +1 @@
+lockfileVersion: '9.0'
diff --git a/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/snap.txt b/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/snap.txt
new file mode 100644
index 0000000000..84b3cac9b4
--- /dev/null
+++ b/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/snap.txt
@@ -0,0 +1,76 @@
+> vp migrate --no-interactive --no-hooks --no-agent --no-editor # should finish core rewrites even when vite-plus is already installed in a pnpm project
+◇ Migrated . to Vite+
+• Node <semver>  pnpm <semver>
+• 1 config update applied, 1 file had imports rewritten
+• Package manager settings configured
+
+> cat package.json # scripts should be rewritten without adding package.json overrides
+{
+  "name": "manual-vp-migrate",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vp dev",
+    "build": "tsc -b && vp build",
+    "lint": "vp lint .",
+    "preview": "vp preview"
+  },
+  "dependencies": {
+    "react": "^19.2.6",
+    "react-dom": "^19.2.6"
+  },
+  "devDependencies": {
+    "@types/node": "^24.12.3",
+    "@types/react": "^19.2.14",
+    "@types/react-dom": "^19.2.3",
+    "@vitejs/plugin-react": "^6.0.1",
+    "globals": "^17.6.0",
+    "typescript": "~6.0.2",
+    "vite": "^8.0.12",
+    "vite-plus": "^0.1.24"
+  },
+  "devEngines": {
+    "packageManager": {
+      "name": "pnpm",
+      "version": "<semver>",
+      "onFail": "download"
+    }
+  }
+}
+
+> cat pnpm-workspace.yaml # pnpm overrides and peerDependencyRules should be configured
+catalog:
+  vite: npm:@voidzero-dev/vite-plus-core@latest
+  vitest: npm:@voidzero-dev/vite-plus-test@latest
+  vite-plus: latest
+overrides:
+  vite: 'catalog:'
+  vitest: 'catalog:'
+peerDependencyRules:
+  allowAny:
+    - vite
+    - vitest
+  allowedVersions:
+    vite: '*'
+    vitest: '*'
+
+> cat vite.config.ts # vite imports should be rewritten
+import { defineConfig } from 'vite-plus'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+  plugins: [react()],
+})
+
+> cat tsconfig.app.json # vite/client types should be rewritten
+{
+  "compilerOptions": {
+    "types": ["vite-plus/client"]
+  }
+}
+
+> test ! -f AGENTS.md # disabled agent setup should not write instructions
+> test ! -d .vite-hooks # disabled hook setup should not write hooks
+> test ! -d .vscode # disabled editor setup should not write editor config
\ No newline at end of file
diff --git a/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/steps.json b/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/steps.json
new file mode 100644
index 0000000000..b84b754938
--- /dev/null
+++ b/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/steps.json
@@ -0,0 +1,12 @@
+{
+  "commands": [
+    "vp migrate --no-interactive --no-hooks --no-agent --no-editor # should finish core rewrites even when vite-plus is already installed in a pnpm project",
+    "cat package.json # scripts should be rewritten without adding package.json overrides",
+    "cat pnpm-workspace.yaml # pnpm overrides and peerDependencyRules should be configured",
+    "cat vite.config.ts # vite imports should be rewritten",
+    "cat tsconfig.app.json # vite/client types should be rewritten",
+    "test ! -f AGENTS.md # disabled agent setup should not write instructions",
+    "test ! -d .vite-hooks # disabled hook setup should not write hooks",
+    "test ! -d .vscode # disabled editor setup should not write editor config"
+  ]
+}
diff --git a/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/tsconfig.app.json b/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/tsconfig.app.json
new file mode 100644
index 0000000000..295dc6a36f
--- /dev/null
+++ b/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/tsconfig.app.json
@@ -0,0 +1,5 @@
+{
+  "compilerOptions": {
+    "types": ["vite/client"]
+  }
+}
diff --git a/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/vite.config.ts b/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/vite.config.ts
new file mode 100644
index 0000000000..8b0f57b91a
--- /dev/null
+++ b/packages/cli/snap-tests-global/migration-partially-installed-vite-plus/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+  plugins: [react()],
+})
diff --git a/packages/cli/snap-tests-global/migration-prettier-rerun/snap.txt b/packages/cli/snap-tests-global/migration-prettier-rerun/snap.txt
index a661cbeee2..9bc156a317 100644
--- a/packages/cli/snap-tests-global/migration-prettier-rerun/snap.txt
+++ b/packages/cli/snap-tests-global/migration-prettier-rerun/snap.txt
@@ -1,15 +1,11 @@
 > vp migrate --no-interactive # should detect vite-plus + prettier and auto-migrate prettier
 
 Prettier configuration detected. Auto-migrating to Oxfmt...
-
-Migrating Prettier config to Oxfmt...
-
-Prettier config migrated to .oxfmtrc.json
-
-✔ Removed .prettierrc.json
-◇ Updated .
-• Node <semver>  unknown latest
+◇ Migrated . to Vite+
+• Node <semver>  pnpm <semver>
+• 1 config update applied
 • Prettier migrated to Oxfmt
+• Package manager settings configured
 
 > cat package.json # check prettier removed from devDependencies and scripts rewritten
 {
@@ -19,6 +15,13 @@ Prettier config migrated to .oxfmtrc.json
   },
   "devDependencies": {
     "vite-plus": "latest"
+  },
+  "devEngines": {
+    "packageManager": {
+      "name": "pnpm",
+      "version": "<semver>",
+      "onFail": "download"
+    }
   }
 }
 
diff --git a/packages/cli/src/migration/__tests__/migrator.spec.ts b/packages/cli/src/migration/__tests__/migrator.spec.ts
index f0971b1570..3963fc91bc 100644
--- a/packages/cli/src/migration/__tests__/migrator.spec.ts
+++ b/packages/cli/src/migration/__tests__/migrator.spec.ts
@@ -21,6 +21,10 @@ const {
   rewriteStandaloneProject,
   rewriteMonorepo,
   rewriteMonorepoProject,
+  detectPendingCoreMigration,
+  detectVitePlusBootstrapPending,
+  ensureVitePlusBootstrap,
+  finalizeCoreMigrationForExistingVitePlus,
   parseNvmrcVersion,
   detectNodeVersionManagerFile,
   migrateNodeVersionManagerFile,
@@ -31,6 +35,7 @@ const {
   rewriteEslintPackageJson,
   detectIncompatibleEslintIntegration,
   preflightGitHooksSetup,
+  detectLegacyGitHooksMigrationCandidate,
   setPackageManager,
 } = await import('../migrator.js');
 
@@ -989,10 +994,10 @@ function makeWorkspaceInfo(
     packageManager,
     packageManagerVersion: '10.33.0',
     downloadPackageManager: {
-      name: 'pnpm',
+      name: packageManager,
       installDir: '/tmp',
       binPrefix: '/tmp/bin',
-      packageName: 'pnpm',
+      packageName: packageManager,
       version: '10.33.0',
     },
     packages: [],
@@ -1011,6 +1016,397 @@ function readYamlObject(filePath: string): Record<string, unknown> {
   return parseYaml(readYaml(filePath)) as Record<string, unknown>;
 }
 
+describe('ensureVitePlusBootstrap', () => {
+  let tmpDir: string;
+
+  beforeEach(() => {
+    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-test-bootstrap-'));
+  });
+
+  afterEach(() => {
+    fs.rmSync(tmpDir, { recursive: true, force: true });
+  });
+
+  it('adds missing npm overrides and package manager pin for existing Vite+ projects', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({ name: 'test', devDependencies: { 'vite-plus': 'latest' } }),
+    );
+
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.npm)).toBe(true);
+
+    const report = createMigrationReport();
+    const result = ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir, PackageManager.npm), report);
+
+    expect(result.changed).toBe(true);
+    expect(report.packageManagerBootstrapConfigured).toBe(true);
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.npm)).toBe(false);
+
+    const pkg = readJson(path.join(tmpDir, 'package.json')) as {
+      overrides: Record<string, string>;
+      devEngines: { packageManager: { name: string } };
+    };
+    expect(pkg.overrides.vite).toContain('@voidzero-dev/vite-plus-core');
+    expect(pkg.overrides.vitest).toContain('@voidzero-dev/vite-plus-test');
+    expect(pkg.devEngines.packageManager.name).toBe(PackageManager.npm);
+  });
+
+  it('does not rewrite semantic npm overrides that already point at Vite+ packages', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({
+        name: 'test',
+        devDependencies: { 'vite-plus': 'latest' },
+        overrides: {
+          vite: 'npm:@voidzero-dev/vite-plus-core@0.1.0',
+          vitest: 'npm:@voidzero-dev/vite-plus-test@0.1.0',
+        },
+        devEngines: {
+          packageManager: { name: 'npm', version: '10.33.0', onFail: 'download' },
+        },
+      }),
+    );
+    const before = fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf-8');
+
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.npm)).toBe(false);
+    const result = ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir, PackageManager.npm));
+
+    expect(result.changed).toBe(false);
+    expect(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf-8')).toBe(before);
+  });
+
+  it('rewrites direct npm Vite dependencies before adding overrides', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({
+        name: 'test',
+        devDependencies: { 'vite-plus': 'latest', vite: '^7.0.0' },
+        dependencies: { vitest: '^3.0.0' },
+        overrides: {
+          vite: 'npm:@voidzero-dev/vite-plus-core@latest',
+          vitest: 'npm:@voidzero-dev/vite-plus-test@latest',
+        },
+        devEngines: {
+          packageManager: { name: 'npm', version: '10.33.0', onFail: 'download' },
+        },
+      }),
+    );
+
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.npm)).toBe(true);
+    const result = ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir, PackageManager.npm));
+
+    expect(result.changed).toBe(true);
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.npm)).toBe(false);
+    const pkg = readJson(path.join(tmpDir, 'package.json')) as {
+      devDependencies: Record<string, string>;
+      dependencies: Record<string, string>;
+    };
+    expect(pkg.devDependencies.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest');
+    expect(pkg.dependencies.vitest).toBe('npm:@voidzero-dev/vite-plus-test@latest');
+  });
+
+  it('normalizes catalog vite-plus pins for npm projects', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({
+        name: 'test',
+        devDependencies: { 'vite-plus': 'catalog:' },
+        overrides: {
+          vite: 'npm:@voidzero-dev/vite-plus-core@latest',
+          vitest: 'npm:@voidzero-dev/vite-plus-test@latest',
+        },
+        devEngines: {
+          packageManager: { name: 'npm', version: '10.33.0', onFail: 'download' },
+        },
+      }),
+    );
+
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.npm)).toBe(true);
+    const result = ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir, PackageManager.npm));
+
+    expect(result.changed).toBe(true);
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.npm)).toBe(false);
+    const pkg = readJson(path.join(tmpDir, 'package.json')) as {
+      devDependencies: Record<string, string>;
+    };
+    expect(pkg.devDependencies['vite-plus']).toBe('latest');
+  });
+
+  it('adds missing pnpm workspace overrides without writing optional setup files', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({ name: 'test', devDependencies: { 'vite-plus': 'catalog:' } }),
+    );
+
+    const result = ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir, PackageManager.pnpm));
+
+    expect(result.changed).toBe(true);
+    expect(fs.existsSync(path.join(tmpDir, 'pnpm-workspace.yaml'))).toBe(true);
+    expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(false);
+    expect(fs.existsSync(path.join(tmpDir, '.vite-hooks'))).toBe(false);
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(false);
+  });
+
+  it('detects missing pnpm workspace catalog entry for vite-plus', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({
+        name: 'test',
+        devDependencies: { 'vite-plus': 'catalog:' },
+        devEngines: {
+          packageManager: { name: 'pnpm', version: '10.33.0', onFail: 'download' },
+        },
+      }),
+    );
+    fs.writeFileSync(
+      path.join(tmpDir, 'pnpm-workspace.yaml'),
+      [
+        'catalog:',
+        '  vite: npm:@voidzero-dev/vite-plus-core@latest',
+        '  vitest: npm:@voidzero-dev/vite-plus-test@latest',
+        'overrides:',
+        "  vite: 'catalog:'",
+        "  vitest: 'catalog:'",
+        'peerDependencyRules:',
+        '  allowAny:',
+        '    - vite',
+        '    - vitest',
+        '  allowedVersions:',
+        "    vite: '*'",
+        "    vitest: '*'",
+      ].join('\n'),
+    );
+
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(true);
+    const result = ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir, PackageManager.pnpm));
+
+    expect(result.changed).toBe(true);
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(false);
+    const workspace = readYamlObject(path.join(tmpDir, 'pnpm-workspace.yaml')) as {
+      catalog: Record<string, string>;
+    };
+    expect(workspace.catalog['vite-plus']).toBe('latest');
+  });
+
+  it('uses a concrete vite-plus version when pnpm config stays in package.json', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({
+        name: 'test',
+        dependencies: { 'vite-plus': 'latest' },
+        pnpm: {},
+      }),
+    );
+
+    const result = ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir, PackageManager.pnpm));
+
+    expect(result.changed).toBe(true);
+    expect(fs.existsSync(path.join(tmpDir, 'pnpm-workspace.yaml'))).toBe(false);
+    const pkg = readJson(path.join(tmpDir, 'package.json')) as {
+      devDependencies: Record<string, string>;
+      pnpm: { overrides: Record<string, string> };
+    };
+    expect(pkg.devDependencies['vite-plus']).toBe('latest');
+    expect(pkg.pnpm.overrides.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest');
+  });
+
+  it('normalizes an existing catalog vite-plus pin when pnpm config stays in package.json', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({
+        name: 'test',
+        devDependencies: { 'vite-plus': 'catalog:' },
+        devEngines: {
+          packageManager: { name: 'pnpm', version: '10.33.0', onFail: 'download' },
+        },
+        pnpm: {
+          overrides: {
+            vite: 'npm:@voidzero-dev/vite-plus-core@latest',
+            vitest: 'npm:@voidzero-dev/vite-plus-test@latest',
+          },
+          peerDependencyRules: {
+            allowAny: ['vite', 'vitest'],
+            allowedVersions: { vite: '*', vitest: '*' },
+          },
+        },
+      }),
+    );
+
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(true);
+    const result = ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir, PackageManager.pnpm));
+
+    expect(result.changed).toBe(true);
+    expect(fs.existsSync(path.join(tmpDir, 'pnpm-workspace.yaml'))).toBe(false);
+    const pkg = readJson(path.join(tmpDir, 'package.json')) as {
+      devDependencies: Record<string, string>;
+    };
+    expect(pkg.devDependencies['vite-plus']).toBe('latest');
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(false);
+  });
+
+  it('normalizes catalog vite-plus pins outside devDependencies when pnpm config stays in package.json', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({
+        name: 'test',
+        dependencies: { 'vite-plus': 'catalog:' },
+        optionalDependencies: { 'vite-plus': 'catalog:' },
+        devEngines: {
+          packageManager: { name: 'pnpm', version: '10.33.0', onFail: 'download' },
+        },
+        pnpm: {
+          overrides: {
+            vite: 'npm:@voidzero-dev/vite-plus-core@latest',
+            vitest: 'npm:@voidzero-dev/vite-plus-test@latest',
+          },
+          peerDependencyRules: {
+            allowAny: ['vite', 'vitest'],
+            allowedVersions: { vite: '*', vitest: '*' },
+          },
+        },
+      }),
+    );
+
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(true);
+    const result = ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir, PackageManager.pnpm));
+
+    expect(result.changed).toBe(true);
+    expect(fs.existsSync(path.join(tmpDir, 'pnpm-workspace.yaml'))).toBe(false);
+    const pkg = readJson(path.join(tmpDir, 'package.json')) as {
+      devDependencies: Record<string, string>;
+      dependencies: Record<string, string>;
+      optionalDependencies: Record<string, string>;
+    };
+    expect(pkg.devDependencies['vite-plus']).toBe('latest');
+    expect(pkg.dependencies['vite-plus']).toBe('latest');
+    expect(pkg.optionalDependencies['vite-plus']).toBe('latest');
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(false);
+  });
+
+  it('uses a concrete vite-plus version for pnpm monorepos that keep pnpm config in package.json', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({
+        name: 'test',
+        dependencies: { 'vite-plus': 'latest' },
+        pnpm: {},
+      }),
+    );
+
+    const result = ensureVitePlusBootstrap({
+      ...makeWorkspaceInfo(tmpDir, PackageManager.pnpm),
+      isMonorepo: true,
+      workspacePatterns: ['packages/*'],
+    });
+
+    expect(result.changed).toBe(true);
+    expect(fs.existsSync(path.join(tmpDir, 'pnpm-workspace.yaml'))).toBe(false);
+    const pkg = readJson(path.join(tmpDir, 'package.json')) as {
+      devDependencies: Record<string, string>;
+    };
+    expect(pkg.devDependencies['vite-plus']).toBe('latest');
+  });
+
+  it('keeps yarn monorepo bootstrap rewrites out of package dependency specs', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({
+        name: 'test',
+        devDependencies: { 'vite-plus': 'latest', vite: '^7.0.0' },
+        devEngines: {
+          packageManager: { name: 'yarn', version: '4.0.0', onFail: 'download' },
+        },
+      }),
+    );
+
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.yarn)).toBe(true);
+    const result = ensureVitePlusBootstrap({
+      ...makeWorkspaceInfo(tmpDir, PackageManager.yarn),
+      isMonorepo: true,
+      workspacePatterns: ['packages/*'],
+    });
+
+    expect(result.changed).toBe(true);
+    expect(result.packageManagerConfig).toBe(true);
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.yarn)).toBe(false);
+    const pkg = readJson(path.join(tmpDir, 'package.json')) as {
+      devDependencies: Record<string, string>;
+      resolutions: Record<string, string>;
+    };
+    expect(pkg.devDependencies.vite).toBe('^7.0.0');
+    expect(pkg.devDependencies['vite-plus']).toBe('latest');
+    expect(pkg.resolutions.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest');
+    const yarnrc = readYamlObject(path.join(tmpDir, '.yarnrc.yml')) as {
+      nodeLinker: string;
+      catalog: Record<string, string>;
+    };
+    expect(yarnrc.nodeLinker).toBe('node-modules');
+    expect(yarnrc.catalog.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest');
+    expect(yarnrc.catalog.vitest).toBe('npm:@voidzero-dev/vite-plus-test@latest');
+    expect(yarnrc.catalog['vite-plus']).toBe('latest');
+  });
+
+  it('completes missing pnpm workspace peer dependency rules', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({
+        name: 'test',
+        devDependencies: { 'vite-plus': 'catalog:' },
+        devEngines: {
+          packageManager: { name: 'pnpm', version: '10.33.0', onFail: 'download' },
+        },
+      }),
+    );
+    fs.writeFileSync(
+      path.join(tmpDir, 'pnpm-workspace.yaml'),
+      [
+        'catalog:',
+        '  vite: npm:@voidzero-dev/vite-plus-core@latest',
+        '  vitest: npm:@voidzero-dev/vite-plus-test@latest',
+        '  vite-plus: latest',
+        'overrides:',
+        "  vite: 'catalog:'",
+        "  vitest: 'catalog:'",
+        '',
+      ].join('\n'),
+    );
+
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(true);
+    const result = ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir, PackageManager.pnpm));
+
+    expect(result.changed).toBe(true);
+    expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(false);
+    const workspace = readYamlObject(path.join(tmpDir, 'pnpm-workspace.yaml')) as {
+      peerDependencyRules: { allowAny: string[]; allowedVersions: Record<string, string> };
+    };
+    expect(workspace.peerDependencyRules.allowAny).toEqual(['vite', 'vitest']);
+    expect(workspace.peerDependencyRules.allowedVersions).toEqual({ vite: '*', vitest: '*' });
+  });
+
+  it('preserves package.json workspace patterns when creating pnpm-workspace.yaml', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({
+        name: 'test',
+        workspaces: ['packages/*'],
+        devDependencies: { 'vite-plus': 'catalog:' },
+      }),
+    );
+
+    const result = ensureVitePlusBootstrap({
+      ...makeWorkspaceInfo(tmpDir, PackageManager.pnpm),
+      isMonorepo: true,
+      workspacePatterns: ['packages/*'],
+    });
+
+    expect(result.changed).toBe(true);
+    const workspace = readYamlObject(path.join(tmpDir, 'pnpm-workspace.yaml')) as {
+      packages: string[];
+    };
+    expect(workspace.packages).toEqual(['packages/*']);
+  });
+});
+
 describe('rewriteStandaloneProject pnpm workspace yaml', () => {
   let tmpDir: string;
 
@@ -1988,6 +2384,110 @@ describe('rewriteStandaloneProject — tsconfig types rewriting', () => {
   });
 });
 
+describe('existing Vite+ core migration finalization', () => {
+  let tmpDir: string;
+
+  beforeEach(() => {
+    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-test-existing-vite-plus-'));
+  });
+
+  afterEach(() => {
+    fs.rmSync(tmpDir, { recursive: true, force: true });
+  });
+
+  it('detects and finalizes legacy scripts, imports, and tsconfig types without dependency rewrites', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify(
+        {
+          name: 'test',
+          scripts: {
+            dev: 'vite',
+            build: 'tsc -b && vite build',
+            preview: 'vite preview',
+          },
+          devDependencies: {
+            'vite-plus': 'latest',
+            '@voidzero-dev/vite-plus-core': 'latest',
+          },
+        },
+        null,
+        2,
+      ),
+    );
+    fs.writeFileSync(
+      path.join(tmpDir, 'vite.config.ts'),
+      "import { defineConfig } from 'vite';\nexport default defineConfig({});\n",
+    );
+    fs.writeFileSync(
+      path.join(tmpDir, 'tsconfig.app.json'),
+      JSON.stringify({ compilerOptions: { types: ['vite/client'] } }, null, 2),
+    );
+
+    const workspaceInfo = makeWorkspaceInfo(tmpDir, PackageManager.npm);
+    expect(detectPendingCoreMigration(workspaceInfo)).toEqual({
+      scripts: true,
+      tsconfigTypes: true,
+    });
+
+    expect(finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true)).toEqual({
+      scripts: true,
+      tsconfigTypes: true,
+      imports: true,
+    });
+
+    const pkg = readJson(path.join(tmpDir, 'package.json')) as {
+      scripts: Record<string, string>;
+      devDependencies: Record<string, string>;
+      overrides?: Record<string, string>;
+    };
+    expect(pkg.scripts).toMatchObject({
+      dev: 'vp dev',
+      build: 'tsc -b && vp build',
+      preview: 'vp preview',
+    });
+    expect(pkg.devDependencies).toEqual({
+      'vite-plus': 'latest',
+      '@voidzero-dev/vite-plus-core': 'latest',
+    });
+    expect(pkg.overrides).toBeUndefined();
+    expect(fs.readFileSync(path.join(tmpDir, 'vite.config.ts'), 'utf8')).toContain(
+      "from 'vite-plus'",
+    );
+    const tsconfig = readJson(path.join(tmpDir, 'tsconfig.app.json'));
+    expect((tsconfig.compilerOptions as { types: string[] }).types).toEqual(['vite-plus/client']);
+    expect(detectPendingCoreMigration(workspaceInfo)).toEqual({
+      scripts: false,
+      tsconfigTypes: false,
+    });
+  });
+
+  it('detects package-level legacy signals in workspaces', () => {
+    const appDir = path.join(tmpDir, 'packages', 'app');
+    fs.mkdirSync(appDir, { recursive: true });
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({ name: 'root', devDependencies: { 'vite-plus': 'latest' } }, null, 2),
+    );
+    fs.writeFileSync(
+      path.join(appDir, 'package.json'),
+      JSON.stringify({ name: 'app', scripts: { dev: 'vite' } }, null, 2),
+    );
+    const workspaceInfo = {
+      ...makeWorkspaceInfo(tmpDir, PackageManager.pnpm),
+      isMonorepo: true,
+      packages: [{ name: 'app', path: 'packages/app' }],
+    };
+
+    expect(detectPendingCoreMigration(workspaceInfo).scripts).toBe(true);
+    expect(finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true).scripts).toBe(true);
+    const appPkg = readJson(path.join(appDir, 'package.json')) as {
+      scripts: Record<string, string>;
+    };
+    expect(appPkg.scripts.dev).toBe('vp dev');
+  });
+});
+
 // Regression: templates such as `create-fate` ship a populated vite.config.ts
 // alongside a standalone `.oxfmtrc.jsonc` / `.oxlintrc.json`. The merge step
 // must not insert a second `fmt:` / `lint:` block when one is already present.
@@ -2035,6 +2535,83 @@ export default defineConfig({
   });
 });
 
+describe('detectLegacyGitHooksMigrationCandidate', () => {
+  let tmpDir: string;
+
+  beforeEach(() => {
+    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-test-legacy-hooks-'));
+  });
+
+  afterEach(() => {
+    fs.rmSync(tmpDir, { recursive: true, force: true });
+  });
+
+  it('detects leftover husky and lint-staged in an existing Vite+ project', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({
+        scripts: { prepare: 'husky' },
+        devDependencies: { husky: '^9.1.7', 'lint-staged': '^16.2.7', 'vite-plus': 'latest' },
+        'lint-staged': { '*': 'vp check --fix' },
+      }),
+    );
+
+    expect(detectLegacyGitHooksMigrationCandidate(tmpDir)).toBe(true);
+  });
+
+  it('does not treat a completed Vite+ project as needing hook migration', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({
+        scripts: { prepare: 'vp config' },
+        devDependencies: { 'vite-plus': 'latest' },
+      }),
+    );
+    fs.mkdirSync(path.join(tmpDir, '.vite-hooks'));
+
+    expect(detectLegacyGitHooksMigrationCandidate(tmpDir)).toBe(false);
+  });
+
+  it('does not treat standalone lint-staged config as active hook migration', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({
+        devDependencies: { 'vite-plus': 'latest' },
+      }),
+    );
+    fs.writeFileSync(path.join(tmpDir, 'lint-staged.config.mjs'), 'export default {};\n');
+
+    expect(detectLegacyGitHooksMigrationCandidate(tmpDir)).toBe(false);
+  });
+
+  it('does not treat a passive .husky directory as active hook migration', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({
+        devDependencies: { 'vite-plus': 'latest' },
+      }),
+    );
+    fs.mkdirSync(path.join(tmpDir, '.husky'));
+
+    expect(detectLegacyGitHooksMigrationCandidate(tmpDir)).toBe(false);
+  });
+
+  it('does not treat passive husky or lint-staged dependencies as active hook migration', () => {
+    fs.writeFileSync(
+      path.join(tmpDir, 'package.json'),
+      JSON.stringify({
+        devDependencies: {
+          husky: '^9.1.7',
+          'lint-staged': '^16.2.7',
+          'vite-plus': 'latest',
+        },
+      }),
+    );
+
+    expect(detectLegacyGitHooksMigrationCandidate(tmpDir)).toBe(false);
+  });
+});
+
 describe('preflightGitHooksSetup husky catalog resolution', () => {
   let tmpDir: string;
 
diff --git a/packages/cli/src/migration/bin.ts b/packages/cli/src/migration/bin.ts
index 39be9ccd00..eed35af4bf 100644
--- a/packages/cli/src/migration/bin.ts
+++ b/packages/cli/src/migration/bin.ts
@@ -54,8 +54,13 @@ import {
   detectFramework,
   detectIncompatibleEslintIntegration,
   detectNodeVersionManagerFile,
+  detectPendingCoreMigration,
   detectPrettierProject,
+  detectVitePlusBootstrapPending,
+  ensureVitePlusBootstrap,
+  finalizeCoreMigrationForExistingVitePlus,
   hasFrameworkShim,
+  detectLegacyGitHooksMigrationCandidate,
   injectLintTypeCheckDefaults,
   installGitHooks,
   mergeViteConfigFiles,
@@ -63,8 +68,6 @@ import {
   migrateNodeVersionManagerFile,
   migratePrettierToOxfmt,
   preflightGitHooksSetup,
-  promptEslintMigration,
-  promptPrettierMigration,
   rewriteMonorepo,
   rewriteStandaloneProject,
   warnIncompatibleEslintIntegration,
@@ -324,8 +327,7 @@ function parseArgs() {
   };
 }
 
-interface MigrationPlan {
-  packageManager: PackageManager;
+interface MigrationSetupPlan {
   shouldSetupHooks: boolean;
   selectedAgentTargetPaths?: string[];
   agentConflictDecisions: Map<string, 'append' | 'skip'>;
@@ -333,6 +335,10 @@ interface MigrationPlan {
   editorConflictDecisions: Map<string, 'merge' | 'skip'>;
   migrateEslint: boolean;
   eslintConfigFile?: string;
+}
+
+interface MigrationPlan extends MigrationSetupPlan {
+  packageManager: PackageManager;
   migratePrettier: boolean;
   prettierConfigFile?: string;
   fixBaseUrl: boolean;
@@ -341,17 +347,104 @@ interface MigrationPlan {
   frameworkShimFrameworks?: Framework[];
 }
 
-async function collectMigrationPlan(
+function getFrameworkShimCandidates(rootDir: string, packages?: WorkspacePackage[]): Framework[] {
+  const allDetectedFrameworks = new Set<Framework>(detectFramework(rootDir));
+  for (const pkg of packages ?? []) {
+    for (const framework of detectFramework(path.join(rootDir, pkg.path))) {
+      allDetectedFrameworks.add(framework);
+    }
+  }
+
+  return [...allDetectedFrameworks].filter((framework) => {
+    if (detectFramework(rootDir).includes(framework) && !hasFrameworkShim(rootDir, framework)) {
+      return true;
+    }
+    return (packages ?? []).some((pkg) => {
+      const pkgPath = path.join(rootDir, pkg.path);
+      return detectFramework(pkgPath).includes(framework) && !hasFrameworkShim(pkgPath, framework);
+    });
+  });
+}
+
+async function collectFrameworkShimFrameworks(
   rootDir: string,
-  detectedPackageManager: PackageManager | undefined,
   options: MigrationOptions,
   packages?: WorkspacePackage[],
-): Promise<MigrationPlan> {
-  // 1. Package manager selection
-  const packageManager =
-    detectedPackageManager ?? (await selectPackageManager(options.interactive, true));
+): Promise<Framework[] | undefined> {
+  const frameworkShimFrameworks: Framework[] = [];
+  for (const framework of getFrameworkShimCandidates(rootDir, packages)) {
+    const addShim = await confirmFrameworkShim(framework, options.interactive);
+    if (addShim) {
+      frameworkShimFrameworks.push(framework);
+    }
+  }
+  return frameworkShimFrameworks.length > 0 ? frameworkShimFrameworks : undefined;
+}
+
+function addFrameworkShimsForWorkspace(
+  rootDir: string,
+  frameworks: Framework[] | undefined,
+  packages: WorkspacePackage[] | undefined,
+  report: MigrationReport,
+  updateMigrationProgress: (message: string) => void,
+): boolean {
+  if (!frameworks) {
+    return false;
+  }
+
+  let changed = false;
+  updateMigrationProgress('Adding TypeScript shim');
+  for (const framework of frameworks) {
+    if (detectFramework(rootDir).includes(framework) && !hasFrameworkShim(rootDir, framework)) {
+      addFrameworkShim(rootDir, framework, report);
+      changed = true;
+    }
+    for (const pkg of packages ?? []) {
+      const pkgPath = path.join(rootDir, pkg.path);
+      if (detectFramework(pkgPath).includes(framework) && !hasFrameworkShim(pkgPath, framework)) {
+        addFrameworkShim(pkgPath, framework, report);
+        changed = true;
+      }
+    }
+  }
+  return changed;
+}
+
+function hasEnabledOption(value: string | string[] | false | undefined): boolean {
+  if (Array.isArray(value)) {
+    return value.some((item) => Boolean(item));
+  }
+  return value !== undefined && value !== false && value !== '';
+}
+
+function hasExplicitExistingVitePlusSetupRequest(options: MigrationOptions): boolean {
+  return (
+    options.hooks === true || hasEnabledOption(options.agent) || hasEnabledOption(options.editor)
+  );
+}
+
+function hasExistingVitePlusMigrationCandidates(
+  workspaceInfo: WorkspaceInfoOptional,
+  options: MigrationOptions,
+): boolean {
+  const eslintProject = detectEslintProject(workspaceInfo.rootDir, workspaceInfo.packages);
+  const prettierProject = detectPrettierProject(workspaceInfo.rootDir, workspaceInfo.packages);
+  return (
+    hasExplicitExistingVitePlusSetupRequest(options) ||
+    detectLegacyGitHooksMigrationCandidate(workspaceInfo.rootDir) ||
+    hasBaseUrlInWorkspace(workspaceInfo) ||
+    eslintProject.hasDependency ||
+    prettierProject.hasDependency ||
+    detectNodeVersionManagerFile(workspaceInfo.rootDir) !== undefined ||
+    getFrameworkShimCandidates(workspaceInfo.rootDir, workspaceInfo.packages).length > 0
+  );
+}
 
-  // 2. Git hooks (including preflight check)
+async function collectGitHooksDecision(
+  rootDir: string,
+  packageManager: PackageManager | undefined,
+  options: MigrationOptions,
+): Promise<boolean> {
   let shouldSetupHooks = await promptGitHooks(options);
   if (shouldSetupHooks) {
     const reason = preflightGitHooksSetup(rootDir, packageManager);
@@ -360,8 +453,16 @@ async function collectMigrationPlan(
       shouldSetupHooks = false;
     }
   }
+  return shouldSetupHooks;
+}
 
-  // 3. Agent selection (auto-detect existing agent files to skip the selector prompt)
+async function collectAgentInstructionPlan(
+  rootDir: string,
+  options: MigrationOptions,
+): Promise<{
+  selectedAgentTargetPaths?: string[];
+  agentConflictDecisions: Map<string, 'append' | 'skip'>;
+}> {
   const existingAgentTargetPaths =
     options.agent !== undefined || !options.interactive
       ? undefined
@@ -375,7 +476,6 @@ async function collectMigrationPlan(
           onCancel: () => cancelAndExit(),
         });
 
-  // 4. Agent conflict detection + prompting
   const agentConflicts = await detectAgentConflicts({
     projectRoot: rootDir,
     targetPaths: selectedAgentTargetPaths,
@@ -405,14 +505,22 @@ async function collectMigrationPlan(
     }
   }
 
-  // 5. Editor selection
+  return { selectedAgentTargetPaths, agentConflictDecisions };
+}
+
+async function collectEditorConfigPlan(
+  rootDir: string,
+  options: MigrationOptions,
+): Promise<{
+  selectedEditor?: EditorId;
+  editorConflictDecisions: Map<string, 'merge' | 'skip'>;
+}> {
   const selectedEditor = await selectEditor({
     interactive: options.interactive,
     editor: options.editor,
     onCancel: () => cancelAndExit(),
   });
 
-  // 6. Editor conflict detection + prompting
   const editorConflicts = detectEditorConflicts({
     projectRoot: rootDir,
     editorId: selectedEditor,
@@ -446,7 +554,14 @@ async function collectMigrationPlan(
     }
   }
 
-  // 7. ESLint detection + prompt
+  return { selectedEditor, editorConflictDecisions };
+}
+
+async function collectEslintMigrationDecision(
+  rootDir: string,
+  options: MigrationOptions,
+  packages?: WorkspacePackage[],
+): Promise<{ migrateEslint: boolean; eslintConfigFile?: string }> {
   const eslintProject = detectEslintProject(rootDir, packages);
   const incompatibleEslintIntegration = detectIncompatibleEslintIntegration(rootDir, packages);
   let migrateEslint = false;
@@ -466,7 +581,63 @@ async function collectMigrationPlan(
     warnPackageLevelEslint();
   }
 
-  // 8. Prettier detection + prompt
+  return { migrateEslint, eslintConfigFile: eslintProject.configFile };
+}
+
+async function collectMigrationSetupPlan(
+  rootDir: string,
+  packageManager: PackageManager | undefined,
+  options: MigrationOptions,
+  packages?: WorkspacePackage[],
+  includeEslint = true,
+): Promise<MigrationSetupPlan> {
+  const shouldSetupHooks = await collectGitHooksDecision(rootDir, packageManager, options);
+  const agentPlan = await collectAgentInstructionPlan(rootDir, options);
+  const editorPlan = await collectEditorConfigPlan(rootDir, options);
+  const eslintPlan = includeEslint
+    ? await collectEslintMigrationDecision(rootDir, options, packages)
+    : { migrateEslint: false };
+
+  return {
+    shouldSetupHooks,
+    ...agentPlan,
+    ...editorPlan,
+    ...eslintPlan,
+  };
+}
+
+function getExistingVitePlusSetupOptions(
+  options: MigrationOptions,
+  legacyGitHooksMigrationCandidate: boolean,
+  useFullMigrationDefaults = false,
+): MigrationOptions {
+  if (useFullMigrationDefaults) {
+    return options;
+  }
+  return {
+    ...options,
+    hooks:
+      options.hooks ??
+      (legacyGitHooksMigrationCandidate ? (options.interactive ? undefined : true) : false),
+    agent: options.agent ?? false,
+    editor: options.editor ?? false,
+  };
+}
+
+async function collectMigrationPlan(
+  rootDir: string,
+  detectedPackageManager: PackageManager | undefined,
+  options: MigrationOptions,
+  packages?: WorkspacePackage[],
+): Promise<MigrationPlan> {
+  // 1. Package manager selection
+  const packageManager =
+    detectedPackageManager ?? (await selectPackageManager(options.interactive, true));
+
+  // 2. Shared setup/tooling decisions
+  const setupPlan = await collectMigrationSetupPlan(rootDir, packageManager, options, packages);
+
+  // 3. Prettier detection + prompt
   const prettierProject = detectPrettierProject(rootDir, packages);
   let migratePrettier = false;
   if (prettierProject.hasDependency && prettierProject.configFile) {
@@ -491,47 +662,17 @@ async function collectMigrationPlan(
   }
 
   // 11. Framework shim detection + prompt
-  // Collect unique frameworks from root and all workspace packages
-  const allDetectedFrameworks = new Set<Framework>(detectFramework(rootDir));
-  for (const pkg of packages ?? []) {
-    for (const framework of detectFramework(path.join(rootDir, pkg.path))) {
-      allDetectedFrameworks.add(framework);
-    }
-  }
-  const frameworkShimFrameworks: Framework[] = [];
-  for (const framework of allDetectedFrameworks) {
-    const anyMissingShim =
-      (detectFramework(rootDir).includes(framework) && !hasFrameworkShim(rootDir, framework)) ||
-      (packages ?? []).some((pkg) => {
-        const pkgPath = path.join(rootDir, pkg.path);
-        return (
-          detectFramework(pkgPath).includes(framework) && !hasFrameworkShim(pkgPath, framework)
-        );
-      });
-    if (anyMissingShim) {
-      const addShim = await confirmFrameworkShim(framework, options.interactive);
-      if (addShim) {
-        frameworkShimFrameworks.push(framework);
-      }
-    }
-  }
+  const frameworkShimFrameworks = await collectFrameworkShimFrameworks(rootDir, options, packages);
 
   const plan: MigrationPlan = {
     packageManager,
-    shouldSetupHooks,
-    selectedAgentTargetPaths,
-    agentConflictDecisions,
-    selectedEditor,
-    editorConflictDecisions,
-    migrateEslint,
-    eslintConfigFile: eslintProject.configFile,
+    ...setupPlan,
     migratePrettier,
     prettierConfigFile: prettierProject.configFile,
     fixBaseUrl,
     migrateNodeVersionFile,
     nodeVersionDetection,
-    frameworkShimFrameworks:
-      frameworkShimFrameworks.length > 0 ? frameworkShimFrameworks : undefined,
+    frameworkShimFrameworks,
   };
 
   return plan;
@@ -623,6 +764,9 @@ function showMigrationSummary(options: {
   if (report.frameworkShimAdded) {
     log(`${styleText('gray', '•')} TypeScript shim added for framework component files`);
   }
+  if (report.packageManagerBootstrapConfigured) {
+    log(`${styleText('gray', '•')} Package manager settings configured`);
+  }
   if (report.warnings.length > 0) {
     log(`${styleText('yellow', '!')} Warnings:`);
     for (const warning of report.warnings) {
@@ -653,6 +797,60 @@ async function checkRolldownCompatibility(rootDir: string, report: MigrationRepo
   }
 }
 
+async function downloadSupportedPackageManager(options: {
+  rootDir: string;
+  packageManager: PackageManager;
+  packageManagerVersion: string;
+  interactive: boolean;
+  updateMigrationProgress: (message: string) => void;
+  failMigrationProgress: (message: string) => void;
+}): Promise<Awaited<ReturnType<typeof downloadPackageManager>>> {
+  const {
+    rootDir,
+    packageManager,
+    packageManagerVersion,
+    interactive,
+    updateMigrationProgress,
+    failMigrationProgress,
+  } = options;
+
+  updateMigrationProgress('Preparing migration');
+  const downloadResult = await downloadPackageManager(
+    packageManager,
+    packageManagerVersion,
+    interactive,
+    true,
+  );
+
+  if (
+    packageManager === PackageManager.yarn &&
+    semver.satisfies(downloadResult.version, '>=4.0.0 <4.10.0')
+  ) {
+    updateMigrationProgress('Upgrading Yarn');
+    await upgradeYarn(rootDir, interactive, true);
+  } else if (
+    packageManager === PackageManager.pnpm &&
+    semver.satisfies(downloadResult.version, '< 9.5.0')
+  ) {
+    failMigrationProgress('Migration failed');
+    prompts.log.error(
+      `✘ pnpm@${downloadResult.version} is not supported by auto migration, please upgrade pnpm to >=9.5.0 first`,
+    );
+    cancelAndExit('Vite+ cannot automatically migrate this project yet.', 1);
+  } else if (
+    packageManager === PackageManager.npm &&
+    semver.satisfies(downloadResult.version, '< 8.3.0')
+  ) {
+    failMigrationProgress('Migration failed');
+    prompts.log.error(
+      `✘ npm@${downloadResult.version} is not supported by auto migration, please upgrade npm to >=8.3.0 first`,
+    );
+    cancelAndExit('Vite+ cannot automatically migrate this project yet.', 1);
+  }
+
+  return downloadResult;
+}
+
 async function executeMigrationPlan(
   workspaceInfoOptional: WorkspaceInfoOptional,
   plan: MigrationPlan,
@@ -690,46 +888,20 @@ async function executeMigrationPlan(
   };
 
   // 1. Download package manager + version validation
-  updateMigrationProgress('Preparing migration');
-  const downloadResult = await downloadPackageManager(
-    plan.packageManager,
-    workspaceInfoOptional.packageManagerVersion,
+  const downloadResult = await downloadSupportedPackageManager({
+    rootDir: workspaceInfoOptional.rootDir,
+    packageManager: plan.packageManager,
+    packageManagerVersion: workspaceInfoOptional.packageManagerVersion,
     interactive,
-    true,
-  );
+    updateMigrationProgress,
+    failMigrationProgress,
+  });
   const workspaceInfo: WorkspaceInfo = {
     ...workspaceInfoOptional,
     packageManager: plan.packageManager,
     downloadPackageManager: downloadResult,
   };
 
-  // 2. Upgrade yarn if needed, or validate PM version
-  if (
-    plan.packageManager === PackageManager.yarn &&
-    semver.satisfies(downloadResult.version, '>=4.0.0 <4.10.0')
-  ) {
-    updateMigrationProgress('Upgrading Yarn');
-    await upgradeYarn(workspaceInfo.rootDir, interactive, true);
-  } else if (
-    plan.packageManager === PackageManager.pnpm &&
-    semver.satisfies(downloadResult.version, '< 9.5.0')
-  ) {
-    failMigrationProgress('Migration failed');
-    prompts.log.error(
-      `✘ pnpm@${downloadResult.version} is not supported by auto migration, please upgrade pnpm to >=9.5.0 first`,
-    );
-    cancelAndExit('Vite+ cannot automatically migrate this project yet.', 1);
-  } else if (
-    plan.packageManager === PackageManager.npm &&
-    semver.satisfies(downloadResult.version, '< 8.3.0')
-  ) {
-    failMigrationProgress('Migration failed');
-    prompts.log.error(
-      `✘ npm@${downloadResult.version} is not supported by auto migration, please upgrade npm to >=8.3.0 first`,
-    );
-    cancelAndExit('Vite+ cannot automatically migrate this project yet.', 1);
-  }
-
   // 3. Migrate node version manager file → .node-version (independent of vite version)
   if (plan.migrateNodeVersionFile && plan.nodeVersionDetection) {
     updateMigrationProgress('Migrating node version file');
@@ -847,23 +1019,13 @@ async function executeMigrationPlan(
   });
 
   // 11. Add framework shims if requested
-  if (plan.frameworkShimFrameworks) {
-    updateMigrationProgress('Adding TypeScript shim');
-    for (const framework of plan.frameworkShimFrameworks) {
-      if (
-        detectFramework(workspaceInfo.rootDir).includes(framework) &&
-        !hasFrameworkShim(workspaceInfo.rootDir, framework)
-      ) {
-        addFrameworkShim(workspaceInfo.rootDir, framework, report);
-      }
-      for (const pkg of workspaceInfo.packages) {
-        const pkgPath = path.join(workspaceInfo.rootDir, pkg.path);
-        if (detectFramework(pkgPath).includes(framework) && !hasFrameworkShim(pkgPath, framework)) {
-          addFrameworkShim(pkgPath, framework, report);
-        }
-      }
-    }
-  }
+  addFrameworkShimsForWorkspace(
+    workspaceInfo.rootDir,
+    plan.frameworkShimFrameworks,
+    workspaceInfo.packages,
+    report,
+    updateMigrationProgress,
+  );
 
   // 12. Reinstall after migration
   // npm needs --force to re-resolve packages with newly added overrides,
@@ -906,7 +1068,7 @@ async function main() {
   const workspaceInfoOptional = await detectWorkspace(projectPath);
   const resolvedPackageManager = workspaceInfoOptional.packageManager ?? 'unknown';
 
-  // Early return if already using Vite+ (only ESLint/hooks migration may be needed)
+  // Early return if already using Vite+ (only finalization/setup migrations may be needed)
   // In force-override mode (file: tgz overrides), skip this check and run full migration
   const rootPkg = readNearestPackageJson(
     workspaceInfoOptional.rootDir,
@@ -936,6 +1098,119 @@ async function main() {
         migrationProgressStarted = false;
       }
     };
+    const failMigrationProgress = (message: string) => {
+      if (migrationProgress && migrationProgressStarted) {
+        migrationProgress.error(message);
+        migrationProgressStarted = false;
+      }
+    };
+
+    const pendingCoreMigration = detectPendingCoreMigration(workspaceInfoOptional);
+    const legacyGitHooksMigrationCandidate = detectLegacyGitHooksMigrationCandidate(
+      workspaceInfoOptional.rootDir,
+    );
+    const vitePlusBootstrapPending = detectVitePlusBootstrapPending(
+      workspaceInfoOptional.rootDir,
+      workspaceInfoOptional.packageManager,
+    );
+    let packageManager: PackageManager | undefined = vitePlusBootstrapPending
+      ? (workspaceInfoOptional.packageManager ??
+        (await selectPackageManager(options.interactive, true)))
+      : workspaceInfoOptional.packageManager;
+    let downloadedPackageManager: Awaited<ReturnType<typeof downloadPackageManager>> | undefined;
+    let packageManagerVersion = workspaceInfoOptional.packageManagerVersion;
+    const downloadExistingPackageManager = async () => {
+      if (!packageManager) {
+        return undefined;
+      }
+      downloadedPackageManager ??= await downloadSupportedPackageManager({
+        rootDir: workspaceInfoOptional.rootDir,
+        packageManager,
+        packageManagerVersion,
+        interactive: options.interactive,
+        updateMigrationProgress,
+        failMigrationProgress,
+      });
+      packageManagerVersion = downloadedPackageManager.version;
+      return downloadedPackageManager;
+    };
+    const ensureExistingPackageManager = async () => {
+      packageManager ??= await selectPackageManager(options.interactive, true);
+      return downloadExistingPackageManager();
+    };
+
+    if (vitePlusBootstrapPending) {
+      await ensureExistingPackageManager();
+    }
+
+    const coreMigrationResult = finalizeCoreMigrationForExistingVitePlus(
+      workspaceInfoOptional,
+      true,
+      report,
+      pendingCoreMigration,
+    );
+    if (
+      coreMigrationResult.scripts ||
+      coreMigrationResult.tsconfigTypes ||
+      coreMigrationResult.imports
+    ) {
+      didMigrate = true;
+    }
+
+    if (
+      !didMigrate &&
+      report.warnings.length === 0 &&
+      !vitePlusBootstrapPending &&
+      !hasExistingVitePlusMigrationCandidates(workspaceInfoOptional, options)
+    ) {
+      prompts.outro(`This project is already using Vite+! ${accent('Happy coding!')}`);
+      return;
+    }
+
+    const fullMigrationSummary =
+      vitePlusBootstrapPending ||
+      coreMigrationResult.scripts ||
+      coreMigrationResult.tsconfigTypes ||
+      coreMigrationResult.imports;
+    const useFullMigrationDefaults = options.interactive && fullMigrationSummary;
+    const setupOptions = getExistingVitePlusSetupOptions(
+      options,
+      legacyGitHooksMigrationCandidate,
+      useFullMigrationDefaults,
+    );
+    const plan = await collectMigrationSetupPlan(
+      workspaceInfoOptional.rootDir,
+      packageManager,
+      setupOptions,
+      workspaceInfoOptional.packages,
+    );
+    const frameworkShimFrameworks = await collectFrameworkShimFrameworks(
+      workspaceInfoOptional.rootDir,
+      options,
+      workspaceInfoOptional.packages,
+    );
+
+    let needsInstall = false;
+    let forceInstall = false;
+    if (vitePlusBootstrapPending) {
+      const downloadResult = await ensureExistingPackageManager();
+      if (downloadResult && packageManager) {
+        updateMigrationProgress('Configuring package manager');
+        const bootstrapResult = ensureVitePlusBootstrap(
+          {
+            ...workspaceInfoOptional,
+            packageManager,
+            downloadPackageManager: downloadResult,
+          },
+          report,
+        );
+        didMigrate = bootstrapResult.changed || didMigrate;
+        needsInstall = bootstrapResult.changed || needsInstall;
+        forceInstall =
+          bootstrapResult.changed &&
+          (packageManager === PackageManager.npm || packageManager === PackageManager.bun);
+      }
+    }
 
     const fixBaseUrl = hasBaseUrlInWorkspace(workspaceInfoOptional)
       ? await confirmBaseUrlFix(options.interactive)
@@ -957,18 +1232,50 @@ async function main() {
     }
     clearMigrationProgress();
 
-    const eslintMigrated = await promptEslintMigration(
-      workspaceInfoOptional.rootDir,
-      options.interactive,
-      workspaceInfoOptional.packages,
-    );
+    let eslintMigrated = false;
+    if (plan.migrateEslint) {
+      await ensureExistingPackageManager();
+      updateMigrationProgress('Migrating ESLint');
+      const eslintOk = await migrateEslintToOxlint(
+        workspaceInfoOptional.rootDir,
+        options.interactive,
+        plan.eslintConfigFile,
+        workspaceInfoOptional.packages,
+        { silent: true, report },
+      );
+      if (!eslintOk) {
+        clearMigrationProgress();
+        cancelAndExit('ESLint migration failed. Fix the issue and re-run `vp migrate`.', 1);
+      }
+      eslintMigrated = true;
+    }
 
-    // Check if Prettier migration is needed
-    const prettierMigrated = await promptPrettierMigration(
+    const prettierProject = detectPrettierProject(
       workspaceInfoOptional.rootDir,
-      options.interactive,
       workspaceInfoOptional.packages,
     );
+    let prettierMigrated = false;
+    if (prettierProject.hasDependency && prettierProject.configFile) {
+      const migratePrettier = await confirmPrettierMigration(options.interactive);
+      if (migratePrettier) {
+        await ensureExistingPackageManager();
+        updateMigrationProgress('Migrating Prettier');
+        const prettierOk = await migratePrettierToOxfmt(
+          workspaceInfoOptional.rootDir,
+          options.interactive,
+          prettierProject.configFile,
+          workspaceInfoOptional.packages,
+          { silent: true, report },
+        );
+        if (!prettierOk) {
+          clearMigrationProgress();
+          cancelAndExit('Prettier migration failed. Fix the issue and re-run `vp migrate`.', 1);
+        }
+        prettierMigrated = true;
+      }
+    } else if (prettierProject.hasDependency) {
+      warnPackageLevelPrettier();
+    }
 
     // Check if node version manager file migration is needed
     const nodeVersionDetection = detectNodeVersionManagerFile(workspaceInfoOptional.rootDir);
@@ -985,7 +1292,19 @@ async function main() {
       }
     }
 
-    // Merge configs and reinstall once if any tool migration happened
+    if (
+      addFrameworkShimsForWorkspace(
+        workspaceInfoOptional.rootDir,
+        frameworkShimFrameworks,
+        workspaceInfoOptional.packages,
+        report,
+        updateMigrationProgress,
+      )
+    ) {
+      didMigrate = true;
+    }
+
+    // Merge configs and reinstall once if any tool or bootstrap migration happened
     if (eslintMigrated || prettierMigrated) {
       updateMigrationProgress('Rewriting configs');
       mergeViteConfigFiles(
@@ -994,61 +1313,67 @@ async function main() {
         report,
         workspaceInfoOptional.packages,
       );
-      updateMigrationProgress('Installing dependencies');
-      // Resolve the actual pnpm version that `vp install` will use so the
-      // auto-install can opt into `--ignore-scripts` on pnpm v11 (which fails
-      // unapproved build scripts with `ERR_PNPM_IGNORED_BUILDS`).
-      let resolvedVersion = workspaceInfoOptional.packageManagerVersion;
-      if (
-        workspaceInfoOptional.packageManager &&
-        !semver.valid(semver.coerce(resolvedVersion) ?? '')
-      ) {
-        const resolved = await downloadPackageManager(
-          workspaceInfoOptional.packageManager,
-          resolvedVersion,
-          options.interactive,
-          true,
-        );
-        resolvedVersion = resolved.version;
+      needsInstall = true;
+      didMigrate = true;
+      report.eslintMigrated = eslintMigrated;
+      report.prettierMigrated = prettierMigrated;
+    }
+
+    if (plan.shouldSetupHooks) {
+      await ensureExistingPackageManager();
+      updateMigrationProgress('Configuring git hooks');
+      if (installGitHooks(workspaceInfoOptional.rootDir, true, report, packageManager)) {
+        didMigrate = true;
+        needsInstall = true;
       }
+    }
+
+    if (needsInstall) {
+      const resolved = await ensureExistingPackageManager();
+      updateMigrationProgress('Installing dependencies');
+      const resolvedVersion = resolved?.version ?? packageManagerVersion;
       const installSummary = await runViteInstall(
         workspaceInfoOptional.rootDir,
         options.interactive,
-        undefined,
+        forceInstall ? ['--force'] : undefined,
         {
           silent: true,
-          packageManager: workspaceInfoOptional.packageManager,
+          packageManager,
           packageManagerVersion: resolvedVersion,
         },
       );
       installDurationMs += installSummary.durationMs;
+      if (installSummary.status === 'failed') {
+        clearMigrationProgress();
+        cancelAndExit(
+          'Dependency installation failed. Run `vp install` manually and re-run `vp migrate`.',
+          1,
+        );
+      }
+    }
+
+    if (plan.selectedAgentTargetPaths && plan.selectedAgentTargetPaths.length > 0) {
+      updateMigrationProgress('Writing agent instructions');
+      await writeAgentInstructions({
+        projectRoot: workspaceInfoOptional.rootDir,
+        targetPaths: plan.selectedAgentTargetPaths,
+        interactive: options.interactive,
+        conflictDecisions: plan.agentConflictDecisions,
+        silent: true,
+      });
       didMigrate = true;
-      report.eslintMigrated = eslintMigrated;
-      report.prettierMigrated = prettierMigrated;
     }
 
-    // Check if husky/lint-staged migration is needed
-    const hasHooksToMigrate =
-      rootPkg?.devDependencies?.husky ||
-      rootPkg?.dependencies?.husky ||
-      rootPkg?.devDependencies?.['lint-staged'] ||
-      rootPkg?.dependencies?.['lint-staged'];
-    if (hasHooksToMigrate) {
-      const shouldSetupHooks = await promptGitHooks(options);
-      if (shouldSetupHooks) {
-        updateMigrationProgress('Configuring git hooks');
-      }
-      if (
-        shouldSetupHooks &&
-        installGitHooks(
-          workspaceInfoOptional.rootDir,
-          true,
-          report,
-          workspaceInfoOptional.packageManager,
-        )
-      ) {
-        didMigrate = true;
-      }
+    if (plan.selectedEditor) {
+      updateMigrationProgress('Writing editor configs');
+      await writeEditorConfigs({
+        projectRoot: workspaceInfoOptional.rootDir,
+        editorId: plan.selectedEditor,
+        interactive: options.interactive,
+        conflictDecisions: plan.editorConflictDecisions,
+        silent: true,
+      });
+      didMigrate = true;
     }
 
     // Check for Rolldown-incompatible config patterns (root + workspace packages)
@@ -1066,14 +1391,14 @@ async function main() {
       clearMigrationProgress();
       showMigrationSummary({
         projectRoot: workspaceInfoOptional.rootDir,
-        packageManager: resolvedPackageManager,
-        packageManagerVersion: workspaceInfoOptional.packageManagerVersion,
+        packageManager: packageManager ?? resolvedPackageManager,
+        packageManagerVersion,
         installDurationMs,
         report,
-        updatedExistingVitePlus: true,
+        updatedExistingVitePlus: !fullMigrationSummary,
       });
     } else {
-      prompts.outro(`This project is already using Vite+! ${accent(`Happy coding!`)}`);
+      prompts.outro(`This project is already using Vite+! ${accent('Happy coding!')}`);
     }
     return;
   }
diff --git a/packages/cli/src/migration/migrator.ts b/packages/cli/src/migration/migrator.ts
index ce24565ff3..9b98ae615b 100644
--- a/packages/cli/src/migration/migrator.ts
+++ b/packages/cli/src/migration/migrator.ts
@@ -39,6 +39,7 @@ import { cancelAndExit, getSpinner } from '../utils/prompts.ts';
 import {
   findTsconfigFiles,
   hasBaseUrlInTsconfig,
+  hasTypesToRewriteInTsconfig,
   removeDeprecatedTsconfigFalseOption,
   rewriteTypesInTsconfig,
 } from '../utils/tsconfig.ts';
@@ -891,10 +892,16 @@ function cleanupDeprecatedTsconfigOptions(
   }
 }
 
-function rewriteTsconfigTypes(projectPath: string, silent = false, report?: MigrationReport): void {
+function rewriteTsconfigTypes(
+  projectPath: string,
+  silent = false,
+  report?: MigrationReport,
+): boolean {
+  let changed = false;
   const files = findTsconfigFiles(projectPath);
   for (const filePath of files) {
     if (rewriteTypesInTsconfig(filePath)) {
+      changed = true;
       if (report) {
         report.removedConfigCount++;
       }
@@ -903,6 +910,11 @@ function rewriteTsconfigTypes(projectPath: string, silent = false, report?: Migr
       }
     }
   }
+  return changed;
+}
+
+function hasTsconfigTypesToRewrite(projectPath: string): boolean {
+  return findTsconfigFiles(projectPath).some((filePath) => hasTypesToRewriteInTsconfig(filePath));
 }
 
 // .svelte files are handled by @sveltejs/vite-plugin-svelte (transpilation)
@@ -1913,6 +1925,565 @@ function readPrepareRulesYaml(): string {
   return cachedPrepareRulesYaml;
 }
 
+type CoreMigrationWorkspace = {
+  rootDir: string;
+  packages?: WorkspacePackage[];
+};
+
+export type PendingCoreMigration = {
+  scripts: boolean;
+  tsconfigTypes: boolean;
+};
+
+export type CoreMigrationFinalizationResult = {
+  scripts: boolean;
+  tsconfigTypes: boolean;
+  imports: boolean;
+};
+
+function getCoreMigrationProjectPaths(workspaceInfo: CoreMigrationWorkspace): string[] {
+  return [
+    workspaceInfo.rootDir,
+    ...(workspaceInfo.packages ?? []).map((pkg) => path.join(workspaceInfo.rootDir, pkg.path)),
+  ];
+}
+
+function hasCorePackageScriptRewrites(projectPath: string): boolean {
+  const packageJsonPath = path.join(projectPath, 'package.json');
+  if (!fs.existsSync(packageJsonPath)) {
+    return false;
+  }
+  const pkg = readJsonFile(packageJsonPath) as { scripts?: Record<string, string> };
+  if (!pkg.scripts) {
+    return false;
+  }
+  return !!rewriteScripts(JSON.stringify(pkg.scripts), getScriptRulesYaml(true));
+}
+
+function rewriteCorePackageScripts(projectPath: string): boolean {
+  const packageJsonPath = path.join(projectPath, 'package.json');
+  if (!fs.existsSync(packageJsonPath)) {
+    return false;
+  }
+
+  let changed = false;
+  editJsonFile<{ scripts?: Record<string, string> }>(packageJsonPath, (pkg) => {
+    if (!pkg.scripts) {
+      return undefined;
+    }
+    const updated = rewriteScripts(JSON.stringify(pkg.scripts), getScriptRulesYaml(true));
+    if (!updated) {
+      return undefined;
+    }
+    pkg.scripts = JSON.parse(updated);
+    changed = true;
+    return pkg;
+  });
+  return changed;
+}
+
+export function detectPendingCoreMigration(
+  workspaceInfo: CoreMigrationWorkspace,
+): PendingCoreMigration {
+  const projectPaths = getCoreMigrationProjectPaths(workspaceInfo);
+  return {
+    scripts: projectPaths.some((projectPath) => hasCorePackageScriptRewrites(projectPath)),
+    tsconfigTypes: projectPaths.some((projectPath) => hasTsconfigTypesToRewrite(projectPath)),
+  };
+}
+
+export function finalizeCoreMigrationForExistingVitePlus(
+  workspaceInfo: CoreMigrationWorkspace,
+  silent = false,
+  report?: MigrationReport,
+  pending = detectPendingCoreMigration(workspaceInfo),
+): CoreMigrationFinalizationResult {
+  const projectPaths = getCoreMigrationProjectPaths(workspaceInfo);
+  const result: CoreMigrationFinalizationResult = {
+    scripts: false,
+    tsconfigTypes: false,
+    imports: false,
+  };
+
+  if (pending.scripts) {
+    for (const projectPath of projectPaths) {
+      result.scripts = rewriteCorePackageScripts(projectPath) || result.scripts;
+    }
+  }
+
+  if (pending.tsconfigTypes) {
+    for (const projectPath of projectPaths) {
+      result.tsconfigTypes =
+        rewriteTsconfigTypes(projectPath, silent, report) || result.tsconfigTypes;
+    }
+  }
+
+  result.imports = rewriteAllImports(workspaceInfo.rootDir, silent, report);
+
+  return result;
+}
+
+type BootstrapPackageJson = {
+  overrides?: Record<string, string>;
+  resolutions?: Record<string, string>;
+  devDependencies?: Record<string, string>;
+  dependencies?: Record<string, string>;
+  optionalDependencies?: Record<string, string>;
+  pnpm?: {
+    overrides?: Record<string, string>;
+    peerDependencyRules?: {
+      allowAny?: string[];
+      allowedVersions?: Record<string, string>;
+    };
+  };
+  packageManager?: string;
+  devEngines?: { packageManager?: unknown; [key: string]: unknown };
+};
+
+export type VitePlusBootstrapResult = {
+  changed: boolean;
+  packageJson: boolean;
+  packageManagerConfig: boolean;
+  packageManagerField: boolean;
+};
+
+function getVitePlusOverridePackageName(dependencyName: string): string | undefined {
+  if (dependencyName === 'vite') {
+    return '@voidzero-dev/vite-plus-core';
+  }
+  if (dependencyName === 'vitest') {
+    return '@voidzero-dev/vite-plus-test';
+  }
+  return undefined;
+}
+
+function isSemanticVitePlusOverrideSpec(dependencyName: string, spec: string | undefined): boolean {
+  if (!spec) {
+    return false;
+  }
+  if (spec === VITE_PLUS_OVERRIDE_PACKAGES[dependencyName]) {
+    return true;
+  }
+  const packageName = getVitePlusOverridePackageName(dependencyName);
+  return packageName !== undefined && spec.includes(packageName);
+}
+
+function overrideSpecSatisfiesVitePlus(
+  dependencyName: string,
+  spec: string | undefined,
+  catalogDependencyResolver?: CatalogDependencyResolver,
+): boolean {
+  if (!spec) {
+    return false;
+  }
+  if (isSemanticVitePlusOverrideSpec(dependencyName, spec)) {
+    return true;
+  }
+  if (!spec.startsWith('catalog:')) {
+    return false;
+  }
+  return isSemanticVitePlusOverrideSpec(
+    dependencyName,
+    catalogDependencyResolver?.(spec, dependencyName),
+  );
+}
+
+function overridesSatisfyVitePlus(
+  overrides: Record<string, string> | undefined,
+  catalogDependencyResolver?: CatalogDependencyResolver,
+): boolean {
+  return Object.keys(VITE_PLUS_OVERRIDE_PACKAGES).every((dependencyName) =>
+    overrideSpecSatisfiesVitePlus(
+      dependencyName,
+      overrides?.[dependencyName],
+      catalogDependencyResolver,
+    ),
+  );
+}
+
+function hasPackageManagerPin(pkg: BootstrapPackageJson): boolean {
+  return Boolean(pkg.packageManager || pkg.devEngines?.packageManager);
+}
+
+function vitePlusDependencyNeedsConcreteVersion(pkg: BootstrapPackageJson): boolean {
+  const dependencyGroups = [pkg.devDependencies, pkg.dependencies, pkg.optionalDependencies];
+  return dependencyGroups.some(
+    (dependencies) => dependencies?.[VITE_PLUS_NAME]?.startsWith('catalog:') ?? false,
+  );
+}
+
+function defaultCatalogVitePlusDependencyPending(
+  pkg: BootstrapPackageJson,
+  catalogDependencyResolver: CatalogDependencyResolver | undefined,
+): boolean {
+  const dependencyGroups = [pkg.devDependencies, pkg.dependencies, pkg.optionalDependencies];
+  return dependencyGroups.some((dependencies) => {
+    const spec = dependencies?.[VITE_PLUS_NAME];
+    if (spec !== 'catalog:' && spec !== 'catalog:default') {
+      return false;
+    }
+    return catalogDependencyResolver?.(spec, VITE_PLUS_NAME) !== VITE_PLUS_VERSION;
+  });
+}
+
+function pnpmPeerDependencyRulesSatisfyVitePlus(
+  peerDependencyRules:
+    | { allowAny?: string[]; allowedVersions?: Record<string, string> }
+    | undefined,
+): boolean {
+  const overrideKeys = Object.keys(VITE_PLUS_OVERRIDE_PACKAGES);
+  const allowAny = new Set(peerDependencyRules?.allowAny ?? []);
+  const allowedVersions = peerDependencyRules?.allowedVersions ?? {};
+  return overrideKeys.every((key) => allowAny.has(key) && allowedVersions[key] === '*');
+}
+
+function npmVitePlusManagedDependenciesPending(pkg: BootstrapPackageJson): boolean {
+  const dependencyGroups = [pkg.devDependencies, pkg.dependencies, pkg.optionalDependencies];
+  return Object.keys(VITE_PLUS_OVERRIDE_PACKAGES).some((dependencyName) =>
+    dependencyGroups.some(
+      (dependencies) =>
+        dependencies?.[dependencyName] !== undefined &&
+        !overrideSpecSatisfiesVitePlus(dependencyName, dependencies[dependencyName]),
+    ),
+  );
+}
+
+function readPnpmWorkspaceCatalogDependencyResolver(
+  projectPath: string,
+): CatalogDependencyResolver | undefined {
+  const pnpmWorkspaceYamlPath = path.join(projectPath, 'pnpm-workspace.yaml');
+  if (!fs.existsSync(pnpmWorkspaceYamlPath)) {
+    return undefined;
+  }
+  const doc = readYamlFile(pnpmWorkspaceYamlPath) as {
+    catalog?: Record<string, string>;
+    catalogs?: Record<string, Record<string, string>>;
+  } | null;
+  return createCatalogDependencyResolverFromCatalogs(doc?.catalog, doc?.catalogs);
+}
+
+function readPnpmWorkspaceOverrides(projectPath: string): Record<string, string> | undefined {
+  const pnpmWorkspaceYamlPath = path.join(projectPath, 'pnpm-workspace.yaml');
+  if (!fs.existsSync(pnpmWorkspaceYamlPath)) {
+    return undefined;
+  }
+  const doc = readYamlFile(pnpmWorkspaceYamlPath) as { overrides?: Record<string, string> } | null;
+  return doc?.overrides;
+}
+
+function readPnpmWorkspacePeerDependencyRules(
+  projectPath: string,
+): { allowAny?: string[]; allowedVersions?: Record<string, string> } | undefined {
+  const pnpmWorkspaceYamlPath = path.join(projectPath, 'pnpm-workspace.yaml');
+  if (!fs.existsSync(pnpmWorkspaceYamlPath)) {
+    return undefined;
+  }
+  const doc = readYamlFile(pnpmWorkspaceYamlPath) as {
+    peerDependencyRules?: { allowAny?: string[]; allowedVersions?: Record<string, string> };
+  } | null;
+  return doc?.peerDependencyRules;
+}
+
+function yarnrcSatisfiesVitePlus(projectPath: string): boolean {
+  const yarnrcYmlPath = path.join(projectPath, '.yarnrc.yml');
+  if (!fs.existsSync(yarnrcYmlPath)) {
+    return false;
+  }
+  const doc = readYamlFile(yarnrcYmlPath) as {
+    nodeLinker?: string;
+    catalog?: Record<string, string>;
+  } | null;
+  return (
+    !!doc &&
+    Object.hasOwn(doc, 'nodeLinker') &&
+    overridesSatisfyVitePlus(doc.catalog) &&
+    (VITE_PLUS_VERSION.startsWith('file:') || doc.catalog?.[VITE_PLUS_NAME] === VITE_PLUS_VERSION)
+  );
+}
+
+function ensurePnpmWorkspacePackages(projectPath: string, workspacePatterns: string[]): boolean {
+  if (workspacePatterns.length === 0) {
+    return false;
+  }
+  const pnpmWorkspaceYamlPath = path.join(projectPath, 'pnpm-workspace.yaml');
+  let changed = false;
+  editYamlFile(pnpmWorkspaceYamlPath, (doc) => {
+    if (doc.has('packages')) {
+      return;
+    }
+    const packages = new YAMLSeq<Scalar<string>>();
+    for (const pattern of workspacePatterns) {
+      packages.add(scalarString(pattern));
+    }
+    doc.set('packages', packages);
+    changed = true;
+  });
+  return changed;
+}
+
+function readBunCatalogDependencyResolver(pkg: {
+  workspaces?: NpmWorkspaces;
+  catalog?: Record<string, string>;
+  catalogs?: Record<string, Record<string, string>>;
+}): CatalogDependencyResolver {
+  const workspacesObj = pkg.workspaces && !Array.isArray(pkg.workspaces) ? pkg.workspaces : {};
+  const fromWorkspaces = createCatalogDependencyResolverFromCatalogs(
+    workspacesObj.catalog,
+    workspacesObj.catalogs,
+  );
+  const fromPkg = createCatalogDependencyResolverFromCatalogs(pkg.catalog, pkg.catalogs);
+  return (catalogSpec, dependencyName) =>
+    fromWorkspaces(catalogSpec, dependencyName) ?? fromPkg(catalogSpec, dependencyName);
+}
+
+export function detectVitePlusBootstrapPending(
+  projectPath: string,
+  packageManager: PackageManager | undefined,
+): boolean {
+  const packageJsonPath = path.join(projectPath, 'package.json');
+  if (!fs.existsSync(packageJsonPath)) {
+    return false;
+  }
+  const pkg = readJsonFile(packageJsonPath) as BootstrapPackageJson & {
+    workspaces?: NpmWorkspaces;
+    catalog?: Record<string, string>;
+    catalogs?: Record<string, Record<string, string>>;
+  };
+
+  if (!pkg.devDependencies?.[VITE_PLUS_NAME] || !hasPackageManagerPin(pkg)) {
+    return true;
+  }
+
+  if (packageManager === undefined) {
+    return true;
+  }
+
+  if (packageManager === PackageManager.yarn) {
+    return !overridesSatisfyVitePlus(pkg.resolutions) || !yarnrcSatisfiesVitePlus(projectPath);
+  }
+  if (packageManager === PackageManager.npm) {
+    return (
+      vitePlusDependencyNeedsConcreteVersion(pkg) ||
+      !overridesSatisfyVitePlus(pkg.overrides) ||
+      npmVitePlusManagedDependenciesPending(pkg)
+    );
+  }
+  if (packageManager === PackageManager.bun) {
+    return !overridesSatisfyVitePlus(pkg.overrides, readBunCatalogDependencyResolver(pkg));
+  }
+  if (packageManager === PackageManager.pnpm) {
+    if (pkg.pnpm) {
+      return (
+        vitePlusDependencyNeedsConcreteVersion(pkg) ||
+        !overridesSatisfyVitePlus(pkg.pnpm.overrides) ||
+        !pnpmPeerDependencyRulesSatisfyVitePlus(pkg.pnpm.peerDependencyRules)
+      );
+    }
+    const resolver = readPnpmWorkspaceCatalogDependencyResolver(projectPath);
+    return (
+      defaultCatalogVitePlusDependencyPending(pkg, resolver) ||
+      !overridesSatisfyVitePlus(readPnpmWorkspaceOverrides(projectPath), resolver) ||
+      !pnpmPeerDependencyRulesSatisfyVitePlus(readPnpmWorkspacePeerDependencyRules(projectPath))
+    );
+  }
+
+  return false;
+}
+
+function ensureVitePlusDependencySpecs(pkg: BootstrapPackageJson, version: string): boolean {
+  let changed = false;
+  if (version !== 'catalog:') {
+    const dependencyGroups = [pkg.devDependencies, pkg.dependencies, pkg.optionalDependencies];
+    for (const dependencies of dependencyGroups) {
+      if (dependencies?.[VITE_PLUS_NAME]?.startsWith('catalog:')) {
+        dependencies[VITE_PLUS_NAME] = version;
+        changed = true;
+      }
+    }
+  }
+  if (pkg.devDependencies?.[VITE_PLUS_NAME]) {
+    return changed;
+  }
+  pkg.devDependencies = {
+    ...pkg.devDependencies,
+    [VITE_PLUS_NAME]: version,
+  };
+  return true;
+}
+
+function ensureOverrideEntries(
+  overrides: Record<string, string> | undefined,
+  catalogDependencyResolver?: CatalogDependencyResolver,
+): { overrides: Record<string, string>; changed: boolean } {
+  const next = { ...overrides };
+  let changed = false;
+  for (const [dependencyName, overrideSpec] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) {
+    if (
+      !overrideSpecSatisfiesVitePlus(
+        dependencyName,
+        next[dependencyName],
+        catalogDependencyResolver,
+      )
+    ) {
+      next[dependencyName] = overrideSpec;
+      changed = true;
+    }
+  }
+  return { overrides: next, changed };
+}
+
+function ensureNpmVitePlusManagedDependencies(pkg: BootstrapPackageJson): boolean {
+  let changed = false;
+  const dependencyGroups = [pkg.devDependencies, pkg.dependencies, pkg.optionalDependencies];
+  for (const [dependencyName, version] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) {
+    for (const dependencies of dependencyGroups) {
+      if (
+        dependencies?.[dependencyName] !== undefined &&
+        !overrideSpecSatisfiesVitePlus(dependencyName, dependencies[dependencyName])
+      ) {
+        dependencies[dependencyName] = version;
+        changed = true;
+      }
+    }
+  }
+  return changed;
+}
+
+function ensurePnpmPeerDependencyRules(pkg: BootstrapPackageJson): boolean {
+  const overrideKeys = Object.keys(VITE_PLUS_OVERRIDE_PACKAGES);
+  pkg.pnpm ??= {};
+  const peerDependencyRules = {
+    ...pkg.pnpm.peerDependencyRules,
+    allowAny: [...new Set([...(pkg.pnpm.peerDependencyRules?.allowAny ?? []), ...overrideKeys])],
+    allowedVersions: {
+      ...pkg.pnpm.peerDependencyRules?.allowedVersions,
+      ...Object.fromEntries(overrideKeys.map((key) => [key, '*'])),
+    },
+  };
+  const changed =
+    JSON.stringify(pkg.pnpm.peerDependencyRules ?? {}) !== JSON.stringify(peerDependencyRules);
+  pkg.pnpm.peerDependencyRules = peerDependencyRules;
+  return changed;
+}
+
+export function ensureVitePlusBootstrap(
+  workspaceInfo: WorkspaceInfo,
+  report?: MigrationReport,
+): VitePlusBootstrapResult {
+  const projectPath = workspaceInfo.rootDir;
+  const packageJsonPath = path.join(projectPath, 'package.json');
+  const result: VitePlusBootstrapResult = {
+    changed: false,
+    packageJson: false,
+    packageManagerConfig: false,
+    packageManagerField: false,
+  };
+  if (!fs.existsSync(packageJsonPath)) {
+    return result;
+  }
+
+  editJsonFile<
+    BootstrapPackageJson & {
+      workspaces?: NpmWorkspaces;
+      catalog?: Record<string, string>;
+      catalogs?: Record<string, Record<string, string>>;
+    }
+  >(packageJsonPath, (pkg) => {
+    const usePnpmWorkspaceYaml = workspaceInfo.packageManager === PackageManager.pnpm && !pkg.pnpm;
+    const supportCatalog =
+      !VITE_PLUS_VERSION.startsWith('file:') &&
+      (usePnpmWorkspaceYaml || workspaceInfo.packageManager === PackageManager.bun);
+    let packageJsonChanged = ensureVitePlusDependencySpecs(
+      pkg,
+      supportCatalog ? 'catalog:' : VITE_PLUS_VERSION,
+    );
+    if (workspaceInfo.packageManager === PackageManager.npm) {
+      packageJsonChanged = ensureNpmVitePlusManagedDependencies(pkg) || packageJsonChanged;
+    }
+
+    if (workspaceInfo.packageManager === PackageManager.yarn) {
+      const ensured = ensureOverrideEntries(pkg.resolutions);
+      if (ensured.changed) {
+        pkg.resolutions = ensured.overrides;
+        packageJsonChanged = true;
+      }
+    } else if (workspaceInfo.packageManager === PackageManager.npm) {
+      const ensured = ensureOverrideEntries(pkg.overrides);
+      if (ensured.changed) {
+        pkg.overrides = ensured.overrides;
+        packageJsonChanged = true;
+      }
+    } else if (workspaceInfo.packageManager === PackageManager.bun) {
+      const ensured = ensureOverrideEntries(pkg.overrides, readBunCatalogDependencyResolver(pkg));
+      if (ensured.changed) {
+        pkg.overrides = ensured.overrides;
+        packageJsonChanged = true;
+      }
+    } else if (workspaceInfo.packageManager === PackageManager.pnpm && pkg.pnpm) {
+      const ensured = ensureOverrideEntries(pkg.pnpm.overrides);
+      if (ensured.changed) {
+        pkg.pnpm.overrides = ensured.overrides;
+        packageJsonChanged = true;
+      }
+      packageJsonChanged = ensurePnpmPeerDependencyRules(pkg) || packageJsonChanged;
+    }
+
+    result.packageJson = packageJsonChanged;
+    return pkg;
+  });
+
+  if (workspaceInfo.packageManager === PackageManager.pnpm) {
+    const pkg = readJsonFile(packageJsonPath) as BootstrapPackageJson;
+    if (!pkg.pnpm) {
+      const pnpmWorkspaceYamlPath = path.join(projectPath, 'pnpm-workspace.yaml');
+      const before = fs.existsSync(pnpmWorkspaceYamlPath)
+        ? fs.readFileSync(pnpmWorkspaceYamlPath, 'utf-8')
+        : undefined;
+      const catalogDependencyResolver = readPnpmWorkspaceCatalogDependencyResolver(projectPath);
+      if (
+        defaultCatalogVitePlusDependencyPending(pkg, catalogDependencyResolver) ||
+        !overridesSatisfyVitePlus(
+          readPnpmWorkspaceOverrides(projectPath),
+          catalogDependencyResolver,
+        ) ||
+        !pnpmPeerDependencyRulesSatisfyVitePlus(readPnpmWorkspacePeerDependencyRules(projectPath))
+      ) {
+        rewritePnpmWorkspaceYaml(projectPath);
+      }
+      if (fs.existsSync(pnpmWorkspaceYamlPath)) {
+        ensurePnpmWorkspacePackages(projectPath, workspaceInfo.workspacePatterns);
+      }
+      const after = fs.existsSync(pnpmWorkspaceYamlPath)
+        ? fs.readFileSync(pnpmWorkspaceYamlPath, 'utf-8')
+        : undefined;
+      result.packageManagerConfig = before !== after;
+    }
+  } else if (workspaceInfo.packageManager === PackageManager.yarn) {
+    const yarnrcYmlPath = path.join(projectPath, '.yarnrc.yml');
+    const before = fs.existsSync(yarnrcYmlPath)
+      ? fs.readFileSync(yarnrcYmlPath, 'utf-8')
+      : undefined;
+    rewriteYarnrcYml(projectPath);
+    const after = fs.readFileSync(yarnrcYmlPath, 'utf-8');
+    result.packageManagerConfig = before !== after;
+  } else if (workspaceInfo.packageManager === PackageManager.bun) {
+    const before = fs.readFileSync(packageJsonPath, 'utf-8');
+    rewriteBunCatalog(projectPath);
+    const after = fs.readFileSync(packageJsonPath, 'utf-8');
+    result.packageJson = result.packageJson || before !== after;
+  }
+
+  const beforePackageManager = fs.readFileSync(packageJsonPath, 'utf-8');
+  setPackageManager(projectPath, workspaceInfo.downloadPackageManager);
+  const afterPackageManager = fs.readFileSync(packageJsonPath, 'utf-8');
+  result.packageManagerField = beforePackageManager !== afterPackageManager;
+  result.changed = result.packageJson || result.packageManagerConfig || result.packageManagerField;
+  if (result.changed && report) {
+    report.packageManagerBootstrapConfigured = true;
+  }
+  return result;
+}
+
 export function rewritePackageJson(
   pkg: {
     scripts?: Record<string, string>;
@@ -2798,7 +3369,7 @@ function wrapLazyPluginsInViteConfig(
  * This rewrites vite/vitest imports to @voidzero-dev/vite-plus
  * @param projectPath - The root directory to search for files
  */
-function rewriteAllImports(projectPath: string, silent = false, report?: MigrationReport): void {
+function rewriteAllImports(projectPath: string, silent = false, report?: MigrationReport): boolean {
   const result = rewriteImportsInDirectory(projectPath);
   const modified = result.modifiedFiles.length;
   const errors = result.errors.length;
@@ -2833,6 +3404,7 @@ function rewriteAllImports(projectPath: string, silent = false, report?: Migrati
       }
     }
   }
+  return modified > 0;
 }
 
 /**
@@ -2904,6 +3476,18 @@ function removeReplacedHookPackages(packageJsonPath: string): void {
   });
 }
 
+export function detectLegacyGitHooksMigrationCandidate(projectPath: string): boolean {
+  const packageJsonPath = path.join(projectPath, 'package.json');
+  if (!fs.existsSync(packageJsonPath)) {
+    return false;
+  }
+  const pkg = readJsonFile(packageJsonPath) as {
+    scripts?: Record<string, string>;
+    'lint-staged'?: unknown;
+  };
+  return getOldHooksDir(projectPath) !== undefined || pkg['lint-staged'] !== undefined;
+}
+
 /**
  * Walk up from `startPath` looking for `.git` (directory or file — submodules
  * use a `.git` file).  Returns the directory that contains `.git`, or `null`.
diff --git a/packages/cli/src/migration/report.ts b/packages/cli/src/migration/report.ts
index 65c9f8396d..63391ae03a 100644
--- a/packages/cli/src/migration/report.ts
+++ b/packages/cli/src/migration/report.ts
@@ -13,6 +13,7 @@ export interface MigrationReport {
   nodeVersionFileMigrated: boolean;
   gitHooksConfigured: boolean;
   frameworkShimAdded: boolean;
+  packageManagerBootstrapConfigured: boolean;
   warnings: string[];
   manualSteps: string[];
 }
@@ -33,6 +34,7 @@ export function createMigrationReport(): MigrationReport {
     nodeVersionFileMigrated: false,
     gitHooksConfigured: false,
     frameworkShimAdded: false,
+    packageManagerBootstrapConfigured: false,
     warnings: [],
     manualSteps: [],
   };
diff --git a/packages/cli/src/utils/tsconfig.ts b/packages/cli/src/utils/tsconfig.ts
index 899c7216c5..f421dae252 100644
--- a/packages/cli/src/utils/tsconfig.ts
+++ b/packages/cli/src/utils/tsconfig.ts
@@ -168,6 +168,30 @@ export function removeDeprecatedTsconfigFalseOption(filePath: string, optionName
   return true;
 }
 
+const TSCONFIG_TYPE_REPLACEMENTS: Record<string, string> = {
+  'tsdown/client': 'vite-plus/pack/client',
+  'vite/client': 'vite-plus/client',
+};
+
+export function hasTypesToRewriteInTsconfig(filePath: string): boolean {
+  let text: string;
+  try {
+    text = fs.readFileSync(filePath, 'utf-8');
+  } catch {
+    return false;
+  }
+
+  const parsed = parseJsonc(text) as {
+    compilerOptions?: { types?: unknown[] };
+  } | null;
+
+  const types = parsed?.compilerOptions?.types;
+  return (
+    Array.isArray(types) &&
+    types.some((t) => typeof t === 'string' && t in TSCONFIG_TYPE_REPLACEMENTS)
+  );
+}
+
 export function rewriteTypesInTsconfig(filePath: string): boolean {
   let text: string;
   try {
@@ -185,14 +209,11 @@ export function rewriteTypesInTsconfig(filePath: string): boolean {
     return false;
   }
 
-  const REPLACEMENTS: Record<string, string> = {
-    'tsdown/client': 'vite-plus/pack/client',
-    'vite/client': 'vite-plus/client',
-  };
-
   const toReplace = types
     .map((t, i) =>
-      typeof t === 'string' && t in REPLACEMENTS ? { i, newVal: REPLACEMENTS[t] } : null,
+      typeof t === 'string' && t in TSCONFIG_TYPE_REPLACEMENTS
+        ? { i, newVal: TSCONFIG_TYPE_REPLACEMENTS[t] }
+        : null,
     )
     .filter((x): x is { i: number; newVal: string } => x !== null);
 
diff --git a/vite.config.ts b/vite.config.ts
index 4ddb9187f3..52d99d7e93 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -66,6 +66,7 @@ export default defineConfig({
       'packages/cli/snap-tests/check-*/**',
       'packages/cli/snap-tests/fmt-ignore-patterns/src/ignored',
       'packages/cli/snap-tests-global/migration-lint-staged-ts-config',
+      'packages/cli/snap-tests-global/migration-partially-installed-vite-plus/**',
       'ecosystem-ci/*/**',
       'packages/test/**.cjs',
       'packages/test/**.cts',
