Skip to content

Commit 5e77c9b

Browse files
committed
fix(config): auto-install hooks and staged config in prepare script (#1028)
## Summary - When `vp config` runs from a `prepare` script (`npm_lifecycle_event=prepare`), skip the interactive hooks prompt and install automatically — the prepare script implies the project opted into hooks - Auto-create `.vite-hooks/pre-commit` with `vp staged` and ensure default staged config (`{ '*': 'vp check --fix' }`) in `vite.config.ts` when missing, so the pre-commit hook is functional out of the box - Skip agent setup when instruction files already exist (CLAUDE.md, etc.) ## Test plan - [x] New snap test `command-config-prepare-auto-hooks` verifies hooks, pre-commit hook, and staged config are all created automatically - [x] All existing `command-config-*` snap tests pass without changes 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 0e9d243 commit 5e77c9b

7 files changed

Lines changed: 86 additions & 8 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "command-config-prepare-auto-hooks"
3+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
> git init
2+
> vp config # should install hooks automatically without prompting
3+
> git config --local core.hooksPath # should be .vite-hooks/_
4+
.vite-hooks/_
5+
6+
> cat .vite-hooks/pre-commit # should have vp staged
7+
vp staged
8+
9+
> cat vite.config.ts # should have staged config
10+
import { defineConfig } from 'vite-plus';
11+
12+
export default defineConfig({
13+
staged: {
14+
"*": "vp check --fix"
15+
},
16+
17+
});
18+
19+
> vp config # run again to ensure idempotent
20+
> cat .vite-hooks/pre-commit # should remain unchanged
21+
vp staged
22+
23+
> cat vite.config.ts # should remain unchanged
24+
import { defineConfig } from 'vite-plus';
25+
26+
export default defineConfig({
27+
staged: {
28+
"*": "vp check --fix"
29+
},
30+
31+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"env": {
3+
"npm_lifecycle_event": "prepare"
4+
},
5+
"commands": [
6+
{ "command": "git init", "ignoreOutput": true },
7+
"vp config # should install hooks automatically without prompting",
8+
"git config --local core.hooksPath # should be .vite-hooks/_",
9+
"cat .vite-hooks/pre-commit # should have vp staged",
10+
"cat vite.config.ts # should have staged config",
11+
"vp config # run again to ensure idempotent",
12+
"cat .vite-hooks/pre-commit # should remain unchanged",
13+
"cat vite.config.ts # should remain unchanged"
14+
]
15+
}

packages/cli/snap-tests-global/migration-lint-staged-merge-fail/snap.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@ VITE+ - The Unified Toolchain for the Web
44

55
◇ Migrated . to Vite+<repeat>
66
• Node <semver> pnpm <semver>
7-
• Git hooks configured
87
! Warnings:
98
- Failed to merge staged config into vite.config.ts
9+
- Git hooks not configured — Failed to merge staged config into vite.config.ts
10+
11+
Please add staged config to vite.config.ts manually, see https://viteplus.dev/guide/migrate#lint-staged
1012
→ Manual follow-up:
1113
- Please add staged config to vite.config.ts manually, see https://viteplus.dev/guide/migrate#lint-staged
1214

1315
> cat package.json # lint-staged config should be preserved when merge fails
1416
{
1517
"name": "migration-lint-staged-merge-fail",
1618
"devDependencies": {
19+
"lint-staged": "^16.2.6",
1720
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
1821
"vite-plus": "latest"
1922
},
@@ -37,4 +40,4 @@ const config = { plugins: [] };
3740
module.exports = config;
3841

3942
> test -f .vite-hooks/pre-commit && echo 'pre-commit hook exists' || echo 'no pre-commit hook' # should NOT exist when merge fails
40-
no pre-commit hook
43+
pre-commit hook exists

packages/cli/snap-tests-global/migration-lintstagedrc-merge-fail/snap.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@ VITE+ - The Unified Toolchain for the Web
44

55
◇ Migrated . to Vite+<repeat>
66
• Node <semver> pnpm <semver>
7-
• Git hooks configured
87
! Warnings:
98
- Failed to merge staged config into vite.config.ts
9+
- Git hooks not configured — Failed to merge staged config into vite.config.ts
10+
11+
Please add staged config to vite.config.ts manually, see https://viteplus.dev/guide/migrate#lint-staged
1012
→ Manual follow-up:
1113
- Please add staged config to vite.config.ts manually, see https://viteplus.dev/guide/migrate#lint-staged
1214

1315
> cat package.json # check package.json
1416
{
1517
"name": "migration-lintstagedrc-merge-fail",
1618
"devDependencies": {
19+
"lint-staged": "^16.2.6",
1720
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
1821
"vite-plus": "latest"
1922
},
@@ -39,4 +42,4 @@ const config = { plugins: [] };
3942
module.exports = config;
4043

4144
> test -f .vite-hooks/pre-commit && echo 'pre-commit hook exists' || echo 'no pre-commit hook' # should NOT exist when merge fails
42-
no pre-commit hook
45+
pre-commit hook exists

packages/cli/src/config/bin.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { join } from 'node:path';
1010
import mri from 'mri';
1111

1212
import { vitePlusHeader } from '../../binding/index.js';
13+
import { ensurePreCommitHook } from '../migration/migrator.js';
1314
import { updateExistingAgentInstructions } from '../utils/agent.js';
1415
import { renderCliDoc } from '../utils/help.js';
1516
import { defaultInteractive, promptGitHooks } from '../utils/prompts.js';
@@ -53,15 +54,17 @@ async function main() {
5354
const dir = args['hooks-dir'] as string | undefined;
5455
const hooksOnly = args['hooks-only'] as boolean;
5556
const interactive = defaultInteractive();
57+
const isPrepareScript = process.env.npm_lifecycle_event === 'prepare';
5658
const root = process.cwd();
5759

5860
// --- Step 1: Hooks setup ---
5961
const hooksDir = dir ?? '.vite-hooks';
6062
const isFirstHooksRun = !existsSync(join(root, hooksDir, '_', 'pre-commit'));
6163

6264
let shouldSetupHooks = true;
63-
if (interactive && isFirstHooksRun && !dir) {
65+
if (interactive && isFirstHooksRun && !dir && !isPrepareScript) {
6466
// --hooks-dir implies agreement; only prompt when using default dir on first run
67+
// prepare script implies the project opted into hooks — install automatically
6568
shouldSetupHooks = await promptGitHooks({ interactive });
6669
}
6770

@@ -73,6 +76,12 @@ async function main() {
7376
process.exit(1);
7477
}
7578
}
79+
80+
// Only create pre-commit hook when install() succeeded (empty message).
81+
// Skip when hooks were disabled or git is unavailable.
82+
if (!message) {
83+
ensurePreCommitHook(root, hooksDir);
84+
}
7685
}
7786

7887
// --- Step 2: Update agent instructions if Vite+ header exists and is outdated ---

packages/cli/src/migration/migrator.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,7 +1394,7 @@ function mergeAndRemoveJsonConfig(
13941394
* Merge a staged config object into vite.config.ts as `staged: { ... }`.
13951395
* Writes the config to a temp JSON file, calls mergeJsonConfig NAPI, then cleans up.
13961396
*/
1397-
function mergeStagedConfigToViteConfig(
1397+
export function mergeStagedConfigToViteConfig(
13981398
projectPath: string,
13991399
stagedConfig: Record<string, string | string[]>,
14001400
silent = false,
@@ -1440,7 +1440,7 @@ function mergeStagedConfigToViteConfig(
14401440
/**
14411441
* Check if vite.config.ts already has a `staged` config key.
14421442
*/
1443-
function hasStagedConfigInViteConfig(projectPath: string): boolean {
1443+
export function hasStagedConfigInViteConfig(projectPath: string): boolean {
14441444
const configs = detectConfigs(projectPath);
14451445
if (!configs.viteConfig) {
14461446
return false;
@@ -1689,7 +1689,7 @@ export function setupGitHooks(
16891689
const pkgData = readJsonFile<{ 'lint-staged'?: Record<string, string | string[]> }>(
16901690
packageJsonPath,
16911691
);
1692-
const stagedConfig = pkgData?.['lint-staged'] ?? { '*': 'vp check --fix' };
1692+
const stagedConfig = pkgData?.['lint-staged'] ?? DEFAULT_STAGED_CONFIG;
16931693
const updated = rewriteScripts(JSON.stringify(stagedConfig), readRulesYaml());
16941694
const finalConfig: Record<string, string | string[]> = updated
16951695
? JSON.parse(updated)
@@ -1822,6 +1822,20 @@ const STALE_LINT_STAGED_PATTERNS = [
18221822
/^((?:[A-Z_][A-Z0-9_]*(?:=\S*)?\s+)*)lint-staged\b/,
18231823
];
18241824

1825+
const DEFAULT_STAGED_CONFIG: Record<string, string> = { '*': 'vp check --fix' };
1826+
1827+
/**
1828+
* Ensure the pre-commit hook exists with `vp staged`, and that
1829+
* vite.config.ts contains a `staged` block (using the default config
1830+
* if none is present). Called by `vp config` after hook installation.
1831+
*/
1832+
export function ensurePreCommitHook(projectPath: string, dir = '.vite-hooks'): void {
1833+
if (!hasStagedConfigInViteConfig(projectPath)) {
1834+
mergeStagedConfigToViteConfig(projectPath, DEFAULT_STAGED_CONFIG, true);
1835+
}
1836+
createPreCommitHook(projectPath, dir);
1837+
}
1838+
18251839
export function createPreCommitHook(projectPath: string, dir = '.vite-hooks'): void {
18261840
const huskyDir = path.join(projectPath, dir);
18271841
fs.mkdirSync(huskyDir, { recursive: true });

0 commit comments

Comments
 (0)