Skip to content

Commit 880214d

Browse files
authored
fix: install() should replace husky hook paths instead of skipping (#772)
When `core.hooksPath` is set to `.husky/_` or `.husky`, `install()` was treating it as a genuinely custom path and refusing to overwrite. This caused both `vp config` and `vp migrate` to skip hooks setup. Now husky paths are recognized and replaced with `.vite-hooks/_`.
1 parent 0c40c50 commit 880214d

5 files changed

Lines changed: 141 additions & 59 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-replace-husky-hookspath"
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
> git init
2+
> git config core.hooksPath .husky/_
3+
> vp config --hooks-only # should replace .husky/_ with .vite-hooks/_
4+
> git config --local core.hooksPath # should be .vite-hooks/_
5+
.vite-hooks/_
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"commands": [
3+
{ "command": "git init", "ignoreOutput": true },
4+
{ "command": "git config core.hooksPath .husky/_", "ignoreOutput": true },
5+
"vp config --hooks-only # should replace .husky/_ with .vite-hooks/_",
6+
"git config --local core.hooksPath # should be .vite-hooks/_"
7+
]
8+
}

packages/cli/src/config/hooks.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,12 @@ export function install(dir = '.vite-hooks'): InstallResult {
8787
const target = rel ? `${rel}/${dir}/_` : `${dir}/_`;
8888
const checkResult = spawnSync('git', ['config', '--local', 'core.hooksPath']);
8989
const existingHooksPath = checkResult.status === 0 ? checkResult.stdout?.toString().trim() : '';
90-
if (existingHooksPath && existingHooksPath !== target) {
90+
if (
91+
existingHooksPath &&
92+
existingHooksPath !== target &&
93+
existingHooksPath !== '.husky' &&
94+
!existingHooksPath.startsWith('.husky/')
95+
) {
9196
return {
9297
message: `core.hooksPath is already set to "${existingHooksPath}", skipping`,
9398
isError: false,
Lines changed: 119 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Summary
44

5-
Add `vp config` and `vp staged` as built-in commands. `vp config` is a unified configuration command that sets up git hooks (husky-compatible reimplementation, not a bundled dependency) and agent integration. `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.
5+
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.
66

77
## Motivation
88

@@ -21,50 +21,129 @@ Pain points:
2121

2222
By building these capabilities into vite-plus, projects get pre-commit hooks with zero extra devDependencies. Both `vp create` and `vp migrate` set this up automatically.
2323

24-
## Command Syntax
24+
## User Workflows
25+
26+
There are three distinct entry points, each with a different responsibility:
27+
28+
### New projects: `vp create`
29+
30+
`vp create` scaffolds a new project and optionally sets up the full hooks pipeline:
31+
32+
1. Prompts "Set up pre-commit hooks?" (default: yes)
33+
2. If accepted, calls `installGitHooks()` which:
34+
- Adds `"prepare": "vp config"` to `package.json`
35+
- Adds `staged` config to `vite.config.ts`
36+
- Creates `.vite-hooks/pre-commit` containing `vp staged`
37+
- Runs `vp config --hooks-only` to install hook shims and set `core.hooksPath`
38+
39+
Flags: `--hooks` (force), `--no-hooks` (skip)
40+
41+
### Existing projects: `vp migrate`
42+
43+
`vp migrate` migrates from husky/lint-staged and sets up the full hooks pipeline:
44+
45+
1. Runs pre-flight checks (husky version, other tools, subdirectory detection)
46+
2. Prompts "Set up pre-commit hooks?" (default: yes)
47+
3. If accepted, after migration rewrite, calls `installGitHooks()` which:
48+
- Detects old husky directory from `scripts.prepare`
49+
- Rewrites `"prepare": "husky"``"prepare": "vp config"` via `rewritePrepareScript()`
50+
- Migrates `lint-staged` config from `package.json` to `staged` in `vite.config.ts`
51+
- Copies `.husky/` hooks to `.vite-hooks/` (or preserves custom dir)
52+
- Creates `.vite-hooks/pre-commit` containing `vp staged`
53+
- Runs `vp config --hooks-only` to install hook shims and set `core.hooksPath`
54+
- Removes husky and lint-staged from `devDependencies`
55+
56+
Flags: `--hooks` (force), `--no-hooks` (skip)
57+
58+
### Ongoing use: `vp config` (prepare lifecycle)
59+
60+
`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`.
61+
62+
```json
63+
{ "scripts": { "prepare": "vp config" } }
64+
```
65+
66+
When `npm_lifecycle_event=prepare` (set by npm/pnpm/yarn during `npm install`), agent setup is skipped automatically — only hooks are reinstalled.
67+
68+
### Manual setup (without `vp create`/`vp migrate`)
69+
70+
For users who want to set up hooks manually, four steps are required:
71+
72+
1. **Add prepare script** to `package.json`:
73+
```json
74+
{ "scripts": { "prepare": "vp config" } }
75+
```
76+
2. **Add staged config** to `vite.config.ts`:
77+
```typescript
78+
export default defineConfig({
79+
staged: { '*': 'vp check --fix' },
80+
});
81+
```
82+
3. **Create pre-commit hook** at `.vite-hooks/pre-commit`:
83+
```sh
84+
vp staged
85+
```
86+
4. **Run `vp config`** to install hook shims and set `core.hooksPath`
87+
88+
## Commands
89+
90+
### `vp config`
2591

2692
```bash
27-
# Configure project (hooks + agent integration)
28-
vp config
93+
vp config # Configure project (hooks + agent integration)
2994
vp config -h # Show help
3095
vp config --hooks-dir .husky # Custom hooks directory (default: .vite-hooks)
96+
```
3197

32-
# Run staged linters on staged files (runs bundled lint-staged with staged config)
33-
vp staged
98+
Behavior:
3499

35-
# Control hooks setup during create/migrate
36-
vp create --hooks # Force hooks setup
37-
vp create --no-hooks # Skip hooks setup
38-
vp migrate --hooks # Force hooks setup
39-
vp migrate --no-hooks # Skip hooks setup
100+
1. Built-in husky-compatible install logic (reimplementation of husky v9, not a bundled dependency)
101+
2. Sets `core.hooksPath` to `<hooks-dir>/_` (default: `.vite-hooks/_`)
102+
3. Creates hook scripts in `<hooks-dir>/_/` that source the user-defined hooks in `<hooks-dir>/`
103+
4. Agent integration: injects agent instructions and MCP config (skipped during `prepare` lifecycle — see point 9)
104+
5. Safe to run multiple times (idempotent)
105+
6. Exits 0 and skips hooks if `VITE_GIT_HOOKS=0` or `HUSKY=0` environment variable is set (backwards compatible)
106+
7. Exits 0 and skips hooks if `.git` directory doesn't exist (safe during `npm install` in consumer projects)
107+
8. Exits 1 on real errors (git command not found, `git config` failed)
108+
9. `prepare` lifecycle detection: when `npm_lifecycle_event=prepare`, agent setup is skipped. This ensures `"prepare": "vp config"` only installs hooks during install — agent setup is handled by `vp create`/`vp migrate`
109+
10. Interactive mode: prompts on first run for hooks and agent setup; updates silently on subsequent runs
110+
11. Non-interactive mode: runs everything by default
111+
112+
### `vp staged`
113+
114+
```bash
115+
vp staged # Run staged linters on staged files
40116
```
41117

118+
Behavior:
119+
120+
1. Reads config from `staged` key in `vite.config.ts` via `resolveConfig()`
121+
2. If `staged` key not found, exits with a warning and setup instructions
122+
3. Passes config to bundled lint-staged via its programmatic API
123+
4. Runs configured commands on git-staged files only
124+
5. Exits with non-zero code if any command fails
125+
6. Does not support custom config file paths — config must be in `vite.config.ts`
126+
42127
Both commands are listed under "Core Commands" in `vp -h` (global and local CLI).
43128

44-
## User-Facing Configuration
129+
## Configuration
45130

46-
### vite.config.ts + package.json (zero extra devDependencies)
131+
### `vite.config.ts`
47132

48133
```typescript
49-
// vite.config.ts
50134
export default defineConfig({
51135
staged: {
52136
'*': 'vp check --fix',
53137
},
54138
});
55139
```
56140

57-
```json
58-
// package.json (new project)
59-
{
60-
"scripts": {
61-
"prepare": "vp config"
62-
}
63-
}
64-
```
141+
`vp staged` reads config from the `staged` key via Vite's `resolveConfig()`. If no `staged` key is found, it exits with a warning and instructions to add the config. Standalone config files (`.lintstagedrc.*`, `lint-staged.config.*`) are not supported by the migration — projects using those formats are warned to migrate manually.
142+
143+
### `package.json`
65144

66145
```json
67-
// package.json (migrated from husky — default .husky dir migrates to .vite-hooks)
146+
// New project
68147
{
69148
"scripts": {
70149
"prepare": "vp config"
@@ -73,7 +152,7 @@ export default defineConfig({
73152
```
74153

75154
```json
76-
// package.json (migrated from husky with custom dir — dir is preserved)
155+
// Migrated from husky with custom dir — dir is preserved
77156
{
78157
"scripts": {
79158
"prepare": "vp config --hooks-dir .config/husky"
@@ -91,7 +170,7 @@ If the project already has a prepare script, `vp config` is prepended:
91170
}
92171
```
93172

94-
### .vite-hooks/pre-commit (or custom dir for projects with non-default husky dir)
173+
### `.vite-hooks/pre-commit`
95174

96175
```
97176
vp staged
@@ -101,36 +180,7 @@ vp staged
101180

102181
`vp check --fix` already handles unsupported file types gracefully (it only processes files that match known extensions). Using `*` simplifies the configuration — no need to maintain a list of extensions.
103182

104-
### Config Discovery
105-
106-
`vp staged` reads config from the `staged` key in `vite.config.ts` via Vite's `resolveConfig()`. If no `staged` key is found, it exits with a warning and instructions to add the config. Standalone config files (`.lintstagedrc.*`, `lint-staged.config.*`) are not supported by the migration — projects using those formats are warned to migrate manually.
107-
108-
## Behavior
109-
110-
### `vp config`
111-
112-
1. Built-in husky-compatible install logic (reimplementation of husky v9, not a bundled dependency)
113-
2. Sets `core.hooksPath` to `<hooks-dir>/_` (default: `.vite-hooks/_`)
114-
3. Creates hook scripts in `<hooks-dir>/_/` that source the user-defined hooks in `<hooks-dir>/`
115-
4. Agent integration: injects agent instructions and MCP config (skipped during `prepare` lifecycle — see point 11)
116-
5. Safe to run multiple times (idempotent)
117-
6. Exits 0 and skips hooks if `VITE_GIT_HOOKS=0` or `HUSKY=0` environment variable is set (backwards compatible)
118-
7. Exits 0 and skips hooks if `.git` directory doesn't exist (safe during `npm install` in consumer projects)
119-
8. Exits 1 on real errors (git command not found, `git config` failed)
120-
9. Interactive mode: prompts on first run for hooks and agent setup; updates silently on subsequent runs
121-
10. Non-interactive mode: runs everything by default
122-
11. `prepare` lifecycle detection: when `npm_lifecycle_event=prepare` (set by npm/pnpm/yarn during `npm install`), agent setup is skipped automatically. This ensures `"prepare": "vp config"` only installs hooks during install — agent setup is a one-time operation handled by `vp create`/`vp migrate`, not repeated on every `npm install`
123-
124-
### `vp staged`
125-
126-
1. Reads config from `staged` key in `vite.config.ts` via `resolveConfig()`
127-
2. If `staged` key not found, exits with a warning and setup instructions
128-
3. Passes config to bundled lint-staged via its programmatic API
129-
4. Runs configured commands on git-staged files only
130-
5. Exits with non-zero code if any command fails
131-
6. Does not support custom config file paths — config must be in vite.config.ts
132-
133-
### Automatic Setup
183+
## Automatic Setup
134184

135185
Both `vp create` and `vp migrate` prompt the user before setting up pre-commit hooks:
136186

@@ -139,14 +189,21 @@ Both `vp create` and `vp migrate` prompt the user before setting up pre-commit h
139189
- **`--hooks` flag**: Force hooks setup (no prompt)
140190
- **`--no-hooks` flag**: Skip hooks setup entirely (no prompt)
141191

142-
#### `vp create`
192+
```bash
193+
vp create --hooks # Force hooks setup
194+
vp create --no-hooks # Skip hooks setup
195+
vp migrate --hooks # Force hooks setup
196+
vp migrate --no-hooks # Skip hooks setup
197+
```
198+
199+
### `vp create`
143200

144201
- After project creation and migration rewrite, prompts for hooks setup
145202
- If accepted, calls `rewritePrepareScript()` then `setupGitHooks()` — same as `vp migrate`
146203
- `rewritePrepareScript()` rewrites any template-provided `"prepare": "husky"` to `"prepare": "vp config"` before `setupGitHooks()` runs
147204
- Creates `.vite-hooks/pre-commit` with `vp staged`
148205

149-
#### `vp migrate`
206+
### `vp migrate`
150207

151208
Migration rewrite (`rewritePackageJson`) uses `vite-tools.yml` rules to rewrite tool commands (vite, oxlint, vitest, etc.) in all scripts. Crucially, the husky rule is **not** in `vite-tools.yml` — it lives in a separate `vite-prepare.yml` and is only applied to `scripts.prepare` via `rewritePrepareScript()`. This ensures husky is never accidentally rewritten in non-prepare scripts.
152209

@@ -163,10 +220,14 @@ Hook setup behavior:
163220
- **Has `husky install`**`rewritePrepareScript()` collapses `"husky install"``"husky"` before applying the ast-grep rule, so `"husky install .hooks"` becomes `"vp config --hooks-dir .hooks"` (custom dir preserved)
164221
- **Has existing prepare script** (e.g. `"npm run build"`) — composes as `"vp config && npm run build"` (prepend so hooks are active before other prepare tasks; idempotent if already contains `vp config`)
165222
- **Has lint-staged** — migrates `"lint-staged"` key to `staged` in vite.config.ts, keeps existing config (already rewritten by migration rules), removes lint-staged from devDeps
223+
224+
## Migration Edge Cases
225+
166226
- **Has husky <9.0.0** — detected **before** migration rewrite. Warns "please upgrade to husky v9+ first", skips hooks setup, and also skips lint-staged migration (`skipStagedMigration` flag). This preserves the `lint-staged` config in package.json and standalone config files, since `.husky/pre-commit` still references `npx lint-staged`.
167227
- **Has other tool (simple-git-hooks, lefthook, yorkie)** — warns and skips
168228
- **Subdirectory project** (e.g. `vp migrate foo`) — if the project path differs from the git root, warns "Subdirectory project detected" and skips hooks setup entirely. This prevents `vp config` from setting `core.hooksPath` to a subdirectory path, which would hijack the repo-wide hooks.
169229
- **No .git directory** — adds package.json config and creates hook pre-commit file, but skips `vp config` hook install (no `core.hooksPath` to set)
230+
- **Standalone lint-staged config** (`.lintstagedrc.*`, `lint-staged.config.*`) — not supported by auto-migration. Projects using those formats are warned to migrate manually.
170231
- After creating the pre-commit hook, runs `vp config` directly to install hook shims (does not rely on npm install lifecycle, which may not run in CI or snap test contexts)
171232

172233
## Implementation Architecture
@@ -218,7 +279,7 @@ Husky <9.0.0 is not supported by auto migration — `vp migrate` detects unsuppo
218279
| ---------------- | -------------------------------------- | --------------------------- |
219280
| `vp check` | Format + lint + type check | Manual or CI |
220281
| `vp check --fix` | Auto-fix format + lint issues | Manual or pre-commit |
221-
| **`vp config`** | **Configure project (hooks + agent)** | **npm `prepare` lifecycle** |
282+
| **`vp config`** | **Reinstall hook shims + agent setup** | **npm `prepare` lifecycle** |
222283
| **`vp staged`** | **Run staged linters on staged files** | **Pre-commit hook** |
223284

224285
## Comparison with Other Tools

0 commit comments

Comments
 (0)