diff --git a/packages/cli/snap-tests-global/command-config-auto-hooks/package.json b/packages/cli/snap-tests-global/command-config-auto-hooks/package.json new file mode 100644 index 0000000000..b7343b121e --- /dev/null +++ b/packages/cli/snap-tests-global/command-config-auto-hooks/package.json @@ -0,0 +1,3 @@ +{ + "name": "command-config-auto-hooks" +} diff --git a/packages/cli/snap-tests-global/command-config-auto-hooks/snap.txt b/packages/cli/snap-tests-global/command-config-auto-hooks/snap.txt new file mode 100644 index 0000000000..5dbe4396a5 --- /dev/null +++ b/packages/cli/snap-tests-global/command-config-auto-hooks/snap.txt @@ -0,0 +1,16 @@ +> git init +> vp config # should install hooks automatically without prompting (staged config exists) +> git config --local core.hooksPath # should be .vite-hooks/_ +.vite-hooks/_ + +> cat .vite-hooks/pre-commit # should have vp staged +vp staged + +> cat vite.config.ts # should remain unchanged +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + staged: { + '*': 'vp check --fix', + }, +}); diff --git a/packages/cli/snap-tests-global/command-config-auto-hooks/steps.json b/packages/cli/snap-tests-global/command-config-auto-hooks/steps.json new file mode 100644 index 0000000000..2be32c221f --- /dev/null +++ b/packages/cli/snap-tests-global/command-config-auto-hooks/steps.json @@ -0,0 +1,9 @@ +{ + "commands": [ + { "command": "git init", "ignoreOutput": true }, + "vp config # should install hooks automatically without prompting (staged config exists)", + "git config --local core.hooksPath # should be .vite-hooks/_", + "cat .vite-hooks/pre-commit # should have vp staged", + "cat vite.config.ts # should remain unchanged" + ] +} diff --git a/packages/cli/snap-tests-global/command-config-auto-hooks/vite.config.ts b/packages/cli/snap-tests-global/command-config-auto-hooks/vite.config.ts new file mode 100644 index 0000000000..e5a41e6a07 --- /dev/null +++ b/packages/cli/snap-tests-global/command-config-auto-hooks/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + staged: { + '*': 'vp check --fix', + }, +}); diff --git a/packages/cli/snap-tests-global/command-config-postinstall-auto-hooks/package.json b/packages/cli/snap-tests-global/command-config-postinstall-auto-hooks/package.json new file mode 100644 index 0000000000..4a3ab85a04 --- /dev/null +++ b/packages/cli/snap-tests-global/command-config-postinstall-auto-hooks/package.json @@ -0,0 +1,3 @@ +{ + "name": "command-config-postinstall-auto-hooks" +} diff --git a/packages/cli/snap-tests-global/command-config-postinstall-auto-hooks/snap.txt b/packages/cli/snap-tests-global/command-config-postinstall-auto-hooks/snap.txt new file mode 100644 index 0000000000..057f3cbc3d --- /dev/null +++ b/packages/cli/snap-tests-global/command-config-postinstall-auto-hooks/snap.txt @@ -0,0 +1,17 @@ +> git init +> vp config # should install hooks automatically without prompting +> git config --local core.hooksPath # should be .vite-hooks/_ +.vite-hooks/_ + +> cat .vite-hooks/pre-commit # should have vp staged +vp staged + +> cat vite.config.ts # should have staged config +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + staged: { + "*": "vp check --fix" + }, + +}); diff --git a/packages/cli/snap-tests-global/command-config-postinstall-auto-hooks/steps.json b/packages/cli/snap-tests-global/command-config-postinstall-auto-hooks/steps.json new file mode 100644 index 0000000000..93a1fb5cba --- /dev/null +++ b/packages/cli/snap-tests-global/command-config-postinstall-auto-hooks/steps.json @@ -0,0 +1,12 @@ +{ + "env": { + "npm_lifecycle_event": "postinstall" + }, + "commands": [ + { "command": "git init", "ignoreOutput": true }, + "vp config # should install hooks automatically without prompting", + "git config --local core.hooksPath # should be .vite-hooks/_", + "cat .vite-hooks/pre-commit # should have vp staged", + "cat vite.config.ts # should have staged config" + ] +} diff --git a/packages/cli/src/config/bin.ts b/packages/cli/src/config/bin.ts index b1b29eb951..4d2667fe69 100644 --- a/packages/cli/src/config/bin.ts +++ b/packages/cli/src/config/bin.ts @@ -10,7 +10,7 @@ import { join } from 'node:path'; import mri from 'mri'; import { vitePlusHeader } from '../../binding/index.js'; -import { ensurePreCommitHook } from '../migration/migrator.js'; +import { ensurePreCommitHook, hasStagedConfigInViteConfig } from '../migration/migrator.js'; import { updateExistingAgentInstructions } from '../utils/agent.js'; import { renderCliDoc } from '../utils/help.js'; import { defaultInteractive, promptGitHooks } from '../utils/prompts.js'; @@ -54,7 +54,8 @@ async function main() { const dir = args['hooks-dir'] as string | undefined; const hooksOnly = args['hooks-only'] as boolean; const interactive = defaultInteractive(); - const isPrepareScript = process.env.npm_lifecycle_event === 'prepare'; + const lifecycleEvent = process.env.npm_lifecycle_event; + const isLifecycleScript = lifecycleEvent === 'prepare' || lifecycleEvent === 'postinstall'; const root = process.cwd(); // --- Step 1: Hooks setup --- @@ -62,9 +63,16 @@ async function main() { const isFirstHooksRun = !existsSync(join(root, hooksDir, '_', 'pre-commit')); let shouldSetupHooks = true; - if (interactive && isFirstHooksRun && !dir && !isPrepareScript) { + if ( + interactive && + isFirstHooksRun && + !dir && + !isLifecycleScript && + !hasStagedConfigInViteConfig(root) + ) { // --hooks-dir implies agreement; only prompt when using default dir on first run - // prepare script implies the project opted into hooks — install automatically + // lifecycle script (prepare/postinstall) implies the project opted into hooks — install automatically + // existing staged config in vite.config.ts implies the project already opted in shouldSetupHooks = await promptGitHooks({ interactive }); } diff --git a/rfcs/config-and-staged-commands.md b/rfcs/config-and-staged-commands.md index 7a1f71edc8..1b40224ce4 100644 --- a/rfcs/config-and-staged-commands.md +++ b/rfcs/config-and-staged-commands.md @@ -2,7 +2,7 @@ ## Summary -Add `vp config` and `vp staged` as built-in commands. `vp config` is a `prepare`-lifecycle command that installs git hook shims (husky-compatible reimplementation, not a bundled dependency). `vp staged` bundles lint-staged and reads config from the `staged` key in `vite.config.ts`. Projects get a zero-config pre-commit hook that runs `vp check --fix` on staged files — no extra devDependencies needed. +Add `vp config` and `vp staged` as built-in commands. `vp config` is a lifecycle command (`prepare` or `postinstall`) that installs git hook shims (husky-compatible reimplementation, not a bundled dependency). `vp staged` bundles lint-staged and reads config from the `staged` key in `vite.config.ts`. Projects get a zero-config pre-commit hook that runs `vp check --fix` on staged files — no extra devDependencies needed. ## Motivation @@ -55,24 +55,27 @@ Flags: `--hooks` (force), `--no-hooks` (skip) Flags: `--hooks` (force), `--no-hooks` (skip) -### Ongoing use: `vp config` (prepare lifecycle) +### Ongoing use: `vp config` (lifecycle script) -`vp config` is the command that runs on every `npm install` via the `prepare` script. It reinstalls hook shims — it does **not** create the `staged` config or the pre-commit hook file. Those are created by `vp create`/`vp migrate`. +`vp config` is the command that runs on every `npm install` via the `prepare` or `postinstall` script. It reinstalls hook shims — it does **not** create the `staged` config or the pre-commit hook file. Those are created by `vp create`/`vp migrate`. ```json { "scripts": { "prepare": "vp config" } } +// or +{ "scripts": { "postinstall": "vp config" } } ``` -When `npm_lifecycle_event=prepare` (set by npm/pnpm/yarn during `npm install`), agent setup is skipped automatically — only hooks are reinstalled. +When running from a lifecycle script (`npm_lifecycle_event` is `prepare` or `postinstall`), hooks are installed automatically without prompting. ### Manual setup (without `vp create`/`vp migrate`) For users who want to set up hooks manually, four steps are required: -1. **Add prepare script** to `package.json`: +1. **Add lifecycle script** to `package.json`: ```json { "scripts": { "prepare": "vp config" } } ``` + Or use `postinstall` if `prepare` is not suitable for your project. 2. **Add staged config** to `vite.config.ts`: ```typescript export default defineConfig({ @@ -105,9 +108,10 @@ Behavior: 6. Exits 0 and skips hooks if `VITE_GIT_HOOKS=0` or `HUSKY=0` environment variable is set (backwards compatible) 7. Exits 0 and skips hooks if `.git` directory doesn't exist (safe during `npm install` in consumer projects) 8. Exits 1 on real errors (git command not found, `git config` failed) -9. Agent update runs uniformly in all modes (`prepare`, interactive, non-interactive). New agent file creation is handled by `vp create`/`vp migrate`. -10. Interactive mode: prompts on first run for hooks setup -11. Non-interactive mode: sets up hooks by default +9. Agent update runs uniformly in all modes (lifecycle script, interactive, non-interactive). New agent file creation is handled by `vp create`/`vp migrate`. +10. Lifecycle script mode (`prepare`/`postinstall`): sets up hooks automatically without prompting +11. Interactive mode: prompts on first run — unless the project already has `staged` config in `vite.config.ts` (which implies prior opt-in) +12. Non-interactive mode: sets up hooks by default ### `vp staged` @@ -275,12 +279,54 @@ Husky <9.0.0 is not supported by auto migration — `vp migrate` detects unsuppo ## Relationship to Existing Commands -| Command | Purpose | When | -| ---------------- | -------------------------------------- | --------------------------- | -| `vp check` | Format + lint + type check | Manual or CI | -| `vp check --fix` | Auto-fix format + lint issues | Manual or pre-commit | -| **`vp config`** | **Reinstall hook shims + agent setup** | **npm `prepare` lifecycle** | -| **`vp staged`** | **Run staged linters on staged files** | **Pre-commit hook** | +| Command | Purpose | When | +| ---------------- | -------------------------------------- | ------------------------------------------- | +| `vp check` | Format + lint + type check | Manual or CI | +| `vp check --fix` | Auto-fix format + lint issues | Manual or pre-commit | +| **`vp config`** | **Reinstall hook shims + agent setup** | **npm lifecycle (`prepare`/`postinstall`)** | +| **`vp staged`** | **Run staged linters on staged files** | **Pre-commit hook** | + +## `vp config` Hooks Setup Flow + +``` +vp config +│ +├─ VITE_GIT_HOOKS=0 or HUSKY=0? ──→ Skip hooks (exit 0) +│ +├─ Not inside a git repo? ──→ Skip hooks (exit 0) +│ +├─ Should prompt user? +│ Prompt ONLY when ALL of these are true: +│ • Interactive terminal (not CI, not piped) +│ • First run (hook shims don't exist yet) +│ • No --hooks-dir flag +│ • Not running from lifecycle script (prepare/postinstall) +│ • No staged config in vite.config.ts +│ +│ YES → Prompt "Set up pre-commit hooks?" +│ User declines → skip hooks +│ NO → Auto-install hooks +│ +├─ core.hooksPath already set to a custom path? +│ (not .vite-hooks/_, not .husky) +│ └─ YES → Skip hooks, preserve custom config +│ +├─ Set core.hooksPath → .vite-hooks/_ +├─ Create hook shims in .vite-hooks/_/ +├─ Ensure staged config in vite.config.ts +└─ Ensure .vite-hooks/pre-commit contains "vp staged" +``` + +### When does the prompt appear? + +| Caller | Prompts? | Why | +| ------------------------------------- | -------- | ---------------------------------- | +| `npm install` → prepare/postinstall | No | lifecycle script = auto-install | +| Manual, project has `staged` config | No | staged config = already opted in | +| Manual, no `staged` config, first run | **Yes** | No signal that project wants hooks | +| Manual, already ran before | No | Hook shims exist = not first run | +| CI / non-interactive | No | Non-interactive = auto-install | +| `--hooks-dir` flag | No | Explicit flag = intent to install | ## Comparison with Other Tools