diff --git a/.agents/skills/migrate-to-rstack-cli/references/git-hooks.md b/.agents/skills/migrate-to-rstack-cli/references/git-hooks.md index 8625a249..35e01c2a 100644 --- a/.agents/skills/migrate-to-rstack-cli/references/git-hooks.md +++ b/.agents/skills/migrate-to-rstack-cli/references/git-hooks.md @@ -1,17 +1,17 @@ # Git Hook Migration -Migrate [Husky](https://typicode.github.io/husky/) or [simple-git-hooks](https://github.com/toplenboren/simple-git-hooks#readme) to [`rs setup`](https://rstack.rs/guide/cli/setup). If a hook calls lint-staged or nano-staged, also read [lint-staged.md](lint-staged.md). +Migrate [Husky](https://typicode.github.io/husky/) or [simple-git-hooks](https://github.com/toplenboren/simple-git-hooks#readme) to [`rs hooks`](https://rstack.rs/guide/cli/hooks). If a hook calls lint-staged or nano-staged, also read [lint-staged.md](lint-staged.md). ## Shared Steps 1. Inspect the hook manager configuration, hook scripts, lifecycle scripts, custom paths, environment overrides, and `git config --show-scope --get core.hooksPath`. -2. Inventory every active hook before editing. Confirm each hook is [supported by `rs setup`](https://rstack.rs/guide/cli/setup#supported-hooks); stop or design an explicit alternative for unsupported hooks. -3. Create each migrated hook in the selected hooks directory, `.rstack/hooks` by default, before running `rs setup`, because the command changes the repository's `core.hooksPath`. Preserve commands and any explicit directory changes. -4. Ensure the `prepare` script in the root `package.json` runs `rs setup`, adding it if necessary. Remove the old installer invocation from any lifecycle script while preserving other commands. Use `--hooks-dir` consistently when choosing a custom directory. -5. If the previous manager's hooks or `core.hooksPath` block installation, run `rs setup --force` once after migrating every required hook. The command preserves the previous files but makes them inactive. Do not add `--force` to the lifecycle script. +2. Inventory every active hook before editing. Confirm each hook is [supported by `rs hooks`](https://rstack.rs/guide/cli/hooks#supported-hooks); stop or design an explicit alternative for unsupported hooks. +3. Create each migrated hook in the selected hooks directory, `.rstack/hooks` by default, before running `rs hooks`, because the command changes the repository's `core.hooksPath`. Preserve commands and any explicit directory changes. +4. Ensure the `prepare` script in the root `package.json` runs `rs hooks`, adding it if necessary. Remove the old installer invocation from any lifecycle script while preserving other commands. Use `--hooks-dir` consistently when choosing a custom directory. +5. If the previous manager's hooks or `core.hooksPath` block installation, run `rs hooks --force` once after migrating every required hook. The command preserves the previous files but makes them inactive. Do not add `--force` to the lifecycle script. 6. Exercise the migrated hooks, then remove the old dependency, configuration, and generated hook files only after behavior matches and their ownership and paths are confirmed. -`rs setup` creates `.rstack/hooks/_/.gitignore`. Do not list `.rstack/hooks/_` in the root `.gitignore`. +`rs hooks` creates `.rstack/hooks/_/.gitignore`. Do not list `.rstack/hooks/_` in the root `.gitignore`. ## Husky @@ -19,7 +19,7 @@ Migrate [Husky](https://typicode.github.io/husky/) or [simple-git-hooks](https:/ - Husky v5 and newer: files such as `.husky/pre-commit`; exclude the generated `.husky/_` directory. - Husky v4: `package.json#husky.hooks` or `.huskyrc*` configuration. 2. Move each hook body to the corresponding file in the selected hooks directory. Remove lines that source `.husky/_/husky.sh`; keep the reusable POSIX shell commands. -3. Replace Husky lifecycle invocations such as `husky`, `husky install`, or a custom Husky directory command with `rs setup` or `rs setup --hooks-dir `. +3. Replace Husky lifecycle invocations such as `husky`, `husky install`, or a custom Husky directory command with `rs hooks` or `rs hooks --hooks-dir `. 4. Replace `HUSKY=0`, `HUSKY_SKIP_HOOKS`, and `HUSKY_SKIP_INSTALL` usage with `RSTACK_HOOKS=0`. Replace `HUSKY_GIT_PARAMS` with the positional arguments expected by each hook. For example, change `commitlint -E HUSKY_GIT_PARAMS` to `commitlint --edit "$1"`. 5. Tell users who rely on `$XDG_CONFIG_HOME/husky/init.sh`, `~/.config/husky/init.sh`, or the deprecated `~/.huskyrc` to move the required shell setup to `$XDG_CONFIG_HOME/rstack/hooks-init.sh` or `~/.config/rstack/hooks-init.sh`. Do not edit user-level files without permission. 6. After validation, remove the Husky dependency and old hook directory. @@ -42,9 +42,9 @@ pnpm test 1. Load the active configuration from `package.json#simple-git-hooks`, a `.simple-git-hooks.{js,cjs,mjs,json}` or `simple-git-hooks.{js,cjs,mjs,json}` file, or the custom path passed to its CLI. Use the configuration values instead of copying generated files from the Git hooks directory. 2. Create one file in the selected hooks directory for each configured command, using the hook name as the file name. Ignore `preserveUnused` as a command, but inspect and migrate any existing hooks that it preserves. -3. Replace the simple-git-hooks lifecycle command with `rs setup`, preserving other chained commands. +3. Replace the simple-git-hooks lifecycle command with `rs hooks`, preserving other chained commands. 4. Replace `SKIP_INSTALL_SIMPLE_GIT_HOOKS=1` and `SKIP_SIMPLE_GIT_HOOKS=1` usage with `RSTACK_HOOKS=0`. Move required commands from the file referenced by `SIMPLE_GIT_HOOKS_RC` to the Rstack user initialization file, with user permission. -5. Do not run the simple-git-hooks uninstall script after `rs setup`; it follows the current `core.hooksPath` and can delete Rstack's generated hook shims. +5. Do not run the simple-git-hooks uninstall script after `rs hooks`; it follows the current `core.hooksPath` and can delete Rstack's generated hook shims. 6. After validation, remove the simple-git-hooks dependency, config, installer, old generated hook files, and stale package-manager metadata such as pnpm `allowBuilds`. Confirm the generated files' ownership and paths before removing them. For example, migrate: @@ -61,7 +61,7 @@ For example, migrate: } ``` -to `"prepare": "existing-command && rs setup"` and these hook files: +to `"prepare": "existing-command && rs hooks"` and these hook files: ```sh title=".rstack/hooks/pre-commit" pnpm lint diff --git a/README.md b/README.md index 6285e4c8..7ef8120e 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ It also covers local development needs outside Rstack's scope, with Prettier for | [`rs check`](https://rstack.rs/guide/cli/check) | Run static checks, including lint and format | | [`rs lib`](https://rstack.rs/guide/cli/lib) | Build library | | [`rs doc`](https://rstack.rs/guide/cli/doc) | Serve or build docs | -| [`rs setup`](https://rstack.rs/guide/cli/setup) | Install Git hooks | +| [`rs hooks`](https://rstack.rs/guide/cli/hooks) | Manage Git hooks | | [`rs staged`](https://rstack.rs/guide/cli/staged) | Run tasks on staged Git files | Rstack CLI fits into your existing project workflow. It does not replace your runtime, package manager, or task runner, such as [pnpm](https://github.com/pnpm/pnpm), [Bun](https://github.com/oven-sh/bun), [Turborepo](https://github.com/vercel/turborepo), [Nx](https://github.com/nrwl/nx), and [Nub](https://github.com/nubjs/nub). @@ -63,7 +63,7 @@ bun add -d rstack "lib": "rs lib", "doc": "rs doc", "format": "rs fmt", - "prepare": "rs setup" + "prepare": "rs hooks" } } ``` diff --git a/package.json b/package.json index a241757d..36aa501e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "license": "MIT", "type": "module", "scripts": { - "benchmark:fmt-discovery": "node scripts/benchmark-fmt-discovery.js", "build": "pnpm --filter \"./packages/**\" build", "check": "rs check --type-check", "check:spell": "pnpm dlx cspell && heading-case", @@ -13,7 +12,7 @@ "doc:build": "node --run build && pnpm --dir website build", "format": "rs fmt && heading-case --write", "lint": "rs lint --type-check", - "prepare": "node scripts/rs.js setup", + "prepare": "node scripts/rs.js hooks", "release:prepare": "node scripts/prepare-release.js", "test": "pnpm --parallel --filter \"./packages/**\" test" }, diff --git a/packages/create-rstack/package.json b/packages/create-rstack/package.json index 1dacc8b8..9f3ce506 100644 --- a/packages/create-rstack/package.json +++ b/packages/create-rstack/package.json @@ -1,6 +1,6 @@ { "name": "create-rstack", - "version": "3.2.3", + "version": "3.2.4", "description": "Create a new Rstack project", "homepage": "https://rstack.rs", "bugs": { diff --git a/packages/create-rstack/src/index.ts b/packages/create-rstack/src/index.ts index 1429da78..34d951fc 100644 --- a/packages/create-rstack/src/index.ts +++ b/packages/create-rstack/src/index.ts @@ -198,7 +198,7 @@ const injectStagedSetup = async ({ }; packageJson.scripts = Object.fromEntries( - Object.entries({ ...packageJson.scripts, prepare: 'rs setup' }).sort( + Object.entries({ ...packageJson.scripts, prepare: 'rs hooks' }).sort( ([left], [right]) => left.localeCompare(right), ), ); diff --git a/packages/create-rstack/template-app-lit-ts/package.json b/packages/create-rstack/template-app-lit-ts/package.json index f9b2b75e..256b8e2a 100644 --- a/packages/create-rstack/template-app-lit-ts/package.json +++ b/packages/create-rstack/template-app-lit-ts/package.json @@ -19,7 +19,7 @@ "devDependencies": { "@types/node": "^24.13.3", "happy-dom": "^20.11.2", - "rstack": "^0.6.3", + "rstack": "^0.6.4", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-app-lit/package.json b/packages/create-rstack/template-app-lit/package.json index 9d33cd4e..1e218aa6 100644 --- a/packages/create-rstack/template-app-lit/package.json +++ b/packages/create-rstack/template-app-lit/package.json @@ -18,6 +18,6 @@ }, "devDependencies": { "happy-dom": "^20.11.2", - "rstack": "^0.6.3" + "rstack": "^0.6.4" } } diff --git a/packages/create-rstack/template-app-preact-ts/package.json b/packages/create-rstack/template-app-preact-ts/package.json index b7c9850b..8c4e5b3c 100644 --- a/packages/create-rstack/template-app-preact-ts/package.json +++ b/packages/create-rstack/template-app-preact-ts/package.json @@ -22,7 +22,7 @@ "@testing-library/preact": "^3.2.4", "@types/node": "^24.13.3", "happy-dom": "^20.11.2", - "rstack": "^0.6.3", + "rstack": "^0.6.4", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-app-preact/package.json b/packages/create-rstack/template-app-preact/package.json index 16cbc48e..3895d8fe 100644 --- a/packages/create-rstack/template-app-preact/package.json +++ b/packages/create-rstack/template-app-preact/package.json @@ -21,6 +21,6 @@ "@testing-library/jest-dom": "^7.0.1", "@testing-library/preact": "^3.2.4", "happy-dom": "^20.11.2", - "rstack": "^0.6.3" + "rstack": "^0.6.4" } } diff --git a/packages/create-rstack/template-app-react-ts/package.json b/packages/create-rstack/template-app-react-ts/package.json index 1972e3c2..397fc308 100644 --- a/packages/create-rstack/template-app-react-ts/package.json +++ b/packages/create-rstack/template-app-react-ts/package.json @@ -26,7 +26,7 @@ "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", "happy-dom": "^20.11.2", - "rstack": "^0.6.3", + "rstack": "^0.6.4", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-app-react/package.json b/packages/create-rstack/template-app-react/package.json index b442af05..ee72465b 100644 --- a/packages/create-rstack/template-app-react/package.json +++ b/packages/create-rstack/template-app-react/package.json @@ -23,6 +23,6 @@ "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.2", "happy-dom": "^20.11.2", - "rstack": "^0.6.3" + "rstack": "^0.6.4" } } diff --git a/packages/create-rstack/template-app-solid-ts/package.json b/packages/create-rstack/template-app-solid-ts/package.json index caeff60e..d2bbc0c0 100644 --- a/packages/create-rstack/template-app-solid-ts/package.json +++ b/packages/create-rstack/template-app-solid-ts/package.json @@ -23,7 +23,7 @@ "@testing-library/jest-dom": "^7.0.1", "@types/node": "^24.13.3", "happy-dom": "^20.11.2", - "rstack": "^0.6.3", + "rstack": "^0.6.4", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-app-solid/package.json b/packages/create-rstack/template-app-solid/package.json index c9710c81..b9badb93 100644 --- a/packages/create-rstack/template-app-solid/package.json +++ b/packages/create-rstack/template-app-solid/package.json @@ -22,6 +22,6 @@ "@solidjs/testing-library": "^0.8.10", "@testing-library/jest-dom": "^7.0.1", "happy-dom": "^20.11.2", - "rstack": "^0.6.3" + "rstack": "^0.6.4" } } diff --git a/packages/create-rstack/template-app-svelte-ts/package.json b/packages/create-rstack/template-app-svelte-ts/package.json index 29ee4cdb..97acb112 100644 --- a/packages/create-rstack/template-app-svelte-ts/package.json +++ b/packages/create-rstack/template-app-svelte-ts/package.json @@ -23,7 +23,7 @@ "@types/node": "^24.13.3", "happy-dom": "^20.11.2", "prettier-plugin-svelte": "^4.1.1", - "rstack": "^0.6.3", + "rstack": "^0.6.4", "svelte-check": "^4.7.6", "typescript": "^6.0.3" } diff --git a/packages/create-rstack/template-app-svelte/package.json b/packages/create-rstack/template-app-svelte/package.json index 1c04678f..651f7f31 100644 --- a/packages/create-rstack/template-app-svelte/package.json +++ b/packages/create-rstack/template-app-svelte/package.json @@ -22,6 +22,6 @@ "@testing-library/svelte": "^5.4.2", "happy-dom": "^20.11.2", "prettier-plugin-svelte": "^4.1.1", - "rstack": "^0.6.3" + "rstack": "^0.6.4" } } diff --git a/packages/create-rstack/template-app-vanilla-ts/package.json b/packages/create-rstack/template-app-vanilla-ts/package.json index 8f00acb6..8ae3628d 100644 --- a/packages/create-rstack/template-app-vanilla-ts/package.json +++ b/packages/create-rstack/template-app-vanilla-ts/package.json @@ -18,7 +18,7 @@ "@testing-library/jest-dom": "^7.0.1", "@types/node": "^24.13.3", "happy-dom": "^20.11.2", - "rstack": "^0.6.3", + "rstack": "^0.6.4", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-app-vanilla/package.json b/packages/create-rstack/template-app-vanilla/package.json index 7185e366..f23e19e7 100644 --- a/packages/create-rstack/template-app-vanilla/package.json +++ b/packages/create-rstack/template-app-vanilla/package.json @@ -17,6 +17,6 @@ "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^7.0.1", "happy-dom": "^20.11.2", - "rstack": "^0.6.3" + "rstack": "^0.6.4" } } diff --git a/packages/create-rstack/template-app-vue-ts/package.json b/packages/create-rstack/template-app-vue-ts/package.json index c6f81f88..60102f5a 100644 --- a/packages/create-rstack/template-app-vue-ts/package.json +++ b/packages/create-rstack/template-app-vue-ts/package.json @@ -22,7 +22,7 @@ "@types/node": "^24.13.3", "@vue/test-utils": "^2.4.11", "happy-dom": "^20.11.2", - "rstack": "^0.6.3", + "rstack": "^0.6.4", "typescript": "^6.0.3", "vue-tsc": "^3.3.10" } diff --git a/packages/create-rstack/template-app-vue/package.json b/packages/create-rstack/template-app-vue/package.json index 559baa2d..7a617276 100644 --- a/packages/create-rstack/template-app-vue/package.json +++ b/packages/create-rstack/template-app-vue/package.json @@ -21,6 +21,6 @@ "@testing-library/jest-dom": "^7.0.1", "@vue/test-utils": "^2.4.11", "happy-dom": "^20.11.2", - "rstack": "^0.6.3" + "rstack": "^0.6.4" } } diff --git a/packages/create-rstack/template-doc-i18n/package.json b/packages/create-rstack/template-doc-i18n/package.json index d15b4c05..67b5bb96 100644 --- a/packages/create-rstack/template-doc-i18n/package.json +++ b/packages/create-rstack/template-doc-i18n/package.json @@ -18,7 +18,7 @@ "@types/react-dom": "^19.2.4", "react": "^19.2.8", "react-dom": "^19.2.8", - "rstack": "^0.6.3", + "rstack": "^0.6.4", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-doc/package.json b/packages/create-rstack/template-doc/package.json index 526c8535..e4566431 100644 --- a/packages/create-rstack/template-doc/package.json +++ b/packages/create-rstack/template-doc/package.json @@ -18,7 +18,7 @@ "@types/react-dom": "^19.2.4", "react": "^19.2.8", "react-dom": "^19.2.8", - "rstack": "^0.6.3", + "rstack": "^0.6.4", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-lib-node-ts/package.json b/packages/create-rstack/template-lib-node-ts/package.json index bb50a0bf..8eeb57ee 100644 --- a/packages/create-rstack/template-lib-node-ts/package.json +++ b/packages/create-rstack/template-lib-node-ts/package.json @@ -25,7 +25,7 @@ }, "devDependencies": { "@types/node": "^24.13.3", - "rstack": "^0.6.3", + "rstack": "^0.6.4", "typescript": "^7.0.2" }, "engines": { diff --git a/packages/create-rstack/template-lib-node/package.json b/packages/create-rstack/template-lib-node/package.json index 75df9220..b7306bb8 100644 --- a/packages/create-rstack/template-lib-node/package.json +++ b/packages/create-rstack/template-lib-node/package.json @@ -18,7 +18,7 @@ "test:watch": "rs test --watch" }, "devDependencies": { - "rstack": "^0.6.3" + "rstack": "^0.6.4" }, "engines": { "node": ">=22.12.0" diff --git a/packages/create-rstack/template-lib-react-ts/package.json b/packages/create-rstack/template-lib-react-ts/package.json index 96801542..7f36e9c2 100644 --- a/packages/create-rstack/template-lib-react-ts/package.json +++ b/packages/create-rstack/template-lib-react-ts/package.json @@ -33,7 +33,7 @@ "happy-dom": "^20.11.2", "react": "^19.2.8", "react-dom": "^19.2.8", - "rstack": "^0.6.3", + "rstack": "^0.6.4", "typescript": "^7.0.2" }, "peerDependencies": { diff --git a/packages/create-rstack/template-lib-react/package.json b/packages/create-rstack/template-lib-react/package.json index beb4e88b..f318cef8 100644 --- a/packages/create-rstack/template-lib-react/package.json +++ b/packages/create-rstack/template-lib-react/package.json @@ -25,7 +25,7 @@ "happy-dom": "^20.11.2", "react": "^19.2.8", "react-dom": "^19.2.8", - "rstack": "^0.6.3" + "rstack": "^0.6.4" }, "peerDependencies": { "react": ">=18.0.0", diff --git a/packages/create-rstack/template-lib-solid-ts/package.json b/packages/create-rstack/template-lib-solid-ts/package.json index 66909ca2..1bc5d337 100644 --- a/packages/create-rstack/template-lib-solid-ts/package.json +++ b/packages/create-rstack/template-lib-solid-ts/package.json @@ -30,7 +30,7 @@ "@testing-library/jest-dom": "^7.0.1", "@types/node": "^24.13.3", "happy-dom": "^20.11.2", - "rstack": "^0.6.3", + "rstack": "^0.6.4", "solid-js": "^1.9.15", "typescript": "^7.0.2" }, diff --git a/packages/create-rstack/template-lib-solid/package.json b/packages/create-rstack/template-lib-solid/package.json index ccd84f93..7e54da13 100644 --- a/packages/create-rstack/template-lib-solid/package.json +++ b/packages/create-rstack/template-lib-solid/package.json @@ -27,7 +27,7 @@ "@solidjs/testing-library": "^0.8.10", "@testing-library/jest-dom": "^7.0.1", "happy-dom": "^20.11.2", - "rstack": "^0.6.3", + "rstack": "^0.6.4", "solid-js": "^1.9.15" }, "peerDependencies": { diff --git a/packages/create-rstack/template-lib-svelte-ts/package.json b/packages/create-rstack/template-lib-svelte-ts/package.json index 11670082..59d50628 100644 --- a/packages/create-rstack/template-lib-svelte-ts/package.json +++ b/packages/create-rstack/template-lib-svelte-ts/package.json @@ -27,7 +27,7 @@ "@types/node": "^24.13.3", "happy-dom": "^20.11.2", "prettier-plugin-svelte": "^4.1.1", - "rstack": "^0.6.3", + "rstack": "^0.6.4", "svelte": "^5.56.9", "svelte-check": "^4.7.6", "svelte2tsx": "^0.7.61", diff --git a/packages/create-rstack/template-lib-svelte/package.json b/packages/create-rstack/template-lib-svelte/package.json index 461f55b1..5c7ca3da 100644 --- a/packages/create-rstack/template-lib-svelte/package.json +++ b/packages/create-rstack/template-lib-svelte/package.json @@ -20,7 +20,7 @@ "@rsbuild/plugin-svelte": "^2.0.1", "happy-dom": "^20.11.2", "prettier-plugin-svelte": "^4.1.1", - "rstack": "^0.6.3", + "rstack": "^0.6.4", "svelte": "^5.56.9" }, "peerDependencies": { diff --git a/packages/create-rstack/template-lib-vue-ts/package.json b/packages/create-rstack/template-lib-vue-ts/package.json index 3759973f..85b9c79a 100644 --- a/packages/create-rstack/template-lib-vue-ts/package.json +++ b/packages/create-rstack/template-lib-vue-ts/package.json @@ -28,7 +28,7 @@ "@types/node": "^24.13.3", "@vue/test-utils": "^2.4.11", "happy-dom": "^20.11.2", - "rstack": "^0.6.3", + "rstack": "^0.6.4", "typescript": "^6.0.3", "vue": "^3.5.41", "vue-tsc": "^3.3.10" diff --git a/packages/create-rstack/template-lib-vue/package.json b/packages/create-rstack/template-lib-vue/package.json index 949ac1b8..eff5ec7d 100644 --- a/packages/create-rstack/template-lib-vue/package.json +++ b/packages/create-rstack/template-lib-vue/package.json @@ -21,7 +21,7 @@ "@testing-library/jest-dom": "^7.0.1", "@vue/test-utils": "^2.4.11", "happy-dom": "^20.11.2", - "rstack": "^0.6.3", + "rstack": "^0.6.4", "vue": "^3.5.41" }, "peerDependencies": { diff --git a/packages/create-rstack/tests/create.test.ts b/packages/create-rstack/tests/create.test.ts index 63a44649..a6170b8f 100644 --- a/packages/create-rstack/tests/create.test.ts +++ b/packages/create-rstack/tests/create.test.ts @@ -137,7 +137,7 @@ const expectStagedSetup = async ( configExtension: string, scripts: Record, ): Promise => { - expect(scripts.prepare).toBe('rs setup'); + expect(scripts.prepare).toBe('rs hooks'); expect( await readFile( path.join(projectDirectory, '.rstack', 'hooks', 'pre-commit'), diff --git a/packages/rstack/binding.cjs b/packages/rstack/binding.cjs index 77c7626e..37dce54b 100644 --- a/packages/rstack/binding.cjs +++ b/packages/rstack/binding.cjs @@ -77,8 +77,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-android-arm64') const bindingPackageVersion = require('@rstackjs/cli-android-arm64/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -93,8 +93,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-android-arm-eabi') const bindingPackageVersion = require('@rstackjs/cli-android-arm-eabi/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -114,8 +114,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-win32-x64-gnu') const bindingPackageVersion = require('@rstackjs/cli-win32-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -130,8 +130,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-win32-x64-msvc') const bindingPackageVersion = require('@rstackjs/cli-win32-x64-msvc/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -147,8 +147,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-win32-ia32-msvc') const bindingPackageVersion = require('@rstackjs/cli-win32-ia32-msvc/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -163,8 +163,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-win32-arm64-msvc') const bindingPackageVersion = require('@rstackjs/cli-win32-arm64-msvc/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -182,8 +182,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-darwin-universal') const bindingPackageVersion = require('@rstackjs/cli-darwin-universal/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -198,8 +198,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-darwin-x64') const bindingPackageVersion = require('@rstackjs/cli-darwin-x64/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -214,8 +214,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-darwin-arm64') const bindingPackageVersion = require('@rstackjs/cli-darwin-arm64/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -234,8 +234,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-freebsd-x64') const bindingPackageVersion = require('@rstackjs/cli-freebsd-x64/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -250,8 +250,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-freebsd-arm64') const bindingPackageVersion = require('@rstackjs/cli-freebsd-arm64/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -271,8 +271,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-x64-musl') const bindingPackageVersion = require('@rstackjs/cli-linux-x64-musl/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -287,8 +287,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-x64-gnu') const bindingPackageVersion = require('@rstackjs/cli-linux-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -305,8 +305,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-arm64-musl') const bindingPackageVersion = require('@rstackjs/cli-linux-arm64-musl/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -321,8 +321,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-arm64-gnu') const bindingPackageVersion = require('@rstackjs/cli-linux-arm64-gnu/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -339,8 +339,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-arm-musleabihf') const bindingPackageVersion = require('@rstackjs/cli-linux-arm-musleabihf/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -355,8 +355,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-arm-gnueabihf') const bindingPackageVersion = require('@rstackjs/cli-linux-arm-gnueabihf/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -373,8 +373,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-loong64-musl') const bindingPackageVersion = require('@rstackjs/cli-linux-loong64-musl/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -389,8 +389,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-loong64-gnu') const bindingPackageVersion = require('@rstackjs/cli-linux-loong64-gnu/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -407,8 +407,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-riscv64-musl') const bindingPackageVersion = require('@rstackjs/cli-linux-riscv64-musl/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -423,8 +423,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-riscv64-gnu') const bindingPackageVersion = require('@rstackjs/cli-linux-riscv64-gnu/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -440,8 +440,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-ppc64-gnu') const bindingPackageVersion = require('@rstackjs/cli-linux-ppc64-gnu/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -456,8 +456,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-s390x-gnu') const bindingPackageVersion = require('@rstackjs/cli-linux-s390x-gnu/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -476,8 +476,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-openharmony-arm64') const bindingPackageVersion = require('@rstackjs/cli-openharmony-arm64/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -492,8 +492,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-openharmony-x64') const bindingPackageVersion = require('@rstackjs/cli-openharmony-x64/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -508,8 +508,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-openharmony-arm') const bindingPackageVersion = require('@rstackjs/cli-openharmony-arm/package.json').version - if (bindingPackageVersion !== '0.6.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -648,8 +648,8 @@ if (!nativeBinding || forceWasi) { if (!candidateFailed) { if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { const bindingPackageVersion = require('@rstackjs/cli-wasm32-wasi/package.json').version - if (bindingPackageVersion !== '0.6.3') { - throw new Error(`WASI binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.4') { + throw new Error(`WASI binding package version mismatch, expected 0.6.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } } wasiBinding = require('@rstackjs/cli-wasm32-wasi') diff --git a/packages/rstack/package.json b/packages/rstack/package.json index 073f88c8..dea1a96b 100644 --- a/packages/rstack/package.json +++ b/packages/rstack/package.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.6.3", + "version": "0.6.4", "description": "One CLI for JavaScript development, powered by Rstack.", "homepage": "https://rstack.rs", "bugs": { diff --git a/packages/rstack/src/cli/commandHelp.ts b/packages/rstack/src/cli/commandHelp.ts index a844a1b4..550fb81d 100644 --- a/packages/rstack/src/cli/commandHelp.ts +++ b/packages/rstack/src/cli/commandHelp.ts @@ -45,6 +45,8 @@ export type HelpTopic = | 'lint' | 'fmt' | 'staged' + | 'hooks' + | 'hooks uninstall' | 'setup'; const CONFIG_OPTION: HelpItem = [ @@ -132,7 +134,7 @@ const HELP_DEFINITIONS = { ['check', 'Run static checks, including lint and format'], ['test', 'Run tests'], ['staged', 'Run tasks on staged Git files'], - ['setup', 'Install Git hooks'], + ['hooks', 'Manage Git hooks'], ], }, { @@ -491,6 +493,41 @@ const HELP_DEFINITIONS = { }, ], }, + hooks: { + usage: 'rs hooks [command] [options]', + description: 'Manage Git hooks in the current repository', + sections: [ + { + title: 'Commands', + items: [ + ['[options]', 'Install or update Git hooks (default)'], + ['uninstall', 'Uninstall Git hooks'], + ], + }, + commandHint('hooks'), + { + title: 'Options', + items: [ + ['-f, --force', 'Install despite an existing Git hooks setup'], + [ + '--hooks-dir ', + 'Specify hooks directory relative to the Git repository root', + ], + HELP_OPTION, + ], + }, + ], + }, + 'hooks uninstall': { + usage: 'rs hooks uninstall [options]', + description: 'Uninstall Git hooks from the current repository', + sections: [ + { + title: 'Options', + items: [HELP_OPTION], + }, + ], + }, setup: { usage: 'rs setup [options]', description: 'Install Git hooks in the current repository', diff --git a/packages/rstack/src/cli/commands.ts b/packages/rstack/src/cli/commands.ts index 891f67d5..624788ce 100644 --- a/packages/rstack/src/cli/commands.ts +++ b/packages/rstack/src/cli/commands.ts @@ -252,12 +252,12 @@ export async function setupCommands(): Promise { return; } - if (command === 'setup') { - const { runSetupCLI } = await import( - /* rspackChunkName: 'setup' */ + if (command === 'hooks' || command === 'setup') { + const { runHooksCLI } = await import( + /* rspackChunkName: 'hooks' */ '../setup/index.ts' ); - await runSetupCLI(args.slice(1)); + await runHooksCLI(args.slice(1), command); return; } diff --git a/packages/rstack/src/setup/git.ts b/packages/rstack/src/setup/git.ts new file mode 100644 index 00000000..0805a3ac --- /dev/null +++ b/packages/rstack/src/setup/git.ts @@ -0,0 +1,197 @@ +import { type SpawnSyncReturns, spawnSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; + +export const generatedDirectoryName = '_'; +export const ownerFileName = '.owner'; + +export type FailedHooksResult = { + status: 'failed'; + reason: string; + message: string; +}; + +export type GitContext = { + defaultHooksDirectory: string; + effectiveHooksDirectory: string; + gitRoot: string; + projectPath: string; +}; + +export type HooksPathScope = + 'command' | 'worktree' | 'local' | 'global' | 'system'; + +export const fail = (reason: string, message: string): FailedHooksResult => ({ + status: 'failed', + reason, + message, +}); + +export const runGit = (cwd: string, args: string[]): SpawnSyncReturns => + spawnSync('git', args, { cwd, encoding: 'utf8' }); + +const removeLineEnding = (value: string): string => + value.replace(/\r?\n$/u, ''); + +export const gitFailure = ( + error: NodeJS.ErrnoException | undefined, + stderr: string, +): FailedHooksResult => { + if (error?.code === 'ENOENT') { + return fail('git-not-found', 'Git command not found.'); + } + + return fail( + 'git-command-failed', + `Failed to run Git: ${error?.message || stderr.trim()}`, + ); +}; + +export const resolveHooksPathScope = ( + cwd: string, +): HooksPathScope | FailedHooksResult | undefined => { + const configured = runGit(cwd, [ + 'config', + '--show-scope', + '--get', + 'core.hooksPath', + ]); + if (configured.error || configured.status === null) { + return gitFailure(configured.error, configured.stderr); + } + + // Exit status 1 means core.hooksPath is not configured yet. + if (configured.status === 1) { + return undefined; + } + if (configured.status !== 0) { + return fail( + 'git-config-failed', + `Failed to resolve the core.hooksPath scope: ${configured.stderr.trim()}`, + ); + } + + const separator = configured.stdout.indexOf('\t'); + const scope = separator === -1 ? '' : configured.stdout.slice(0, separator); + if ( + scope === 'command' || + scope === 'worktree' || + scope === 'local' || + scope === 'global' || + scope === 'system' + ) { + return scope; + } + + return fail( + 'git-config-failed', + 'Failed to resolve the core.hooksPath scope.', + ); +}; + +export const resolveGitHooksPath = ( + cwd: string, +): string | FailedHooksResult => { + const hooksDirectory = runGit(cwd, [ + 'rev-parse', + '--path-format=absolute', + '--git-path', + 'hooks', + ]); + if (hooksDirectory.error || hooksDirectory.status === null) { + return gitFailure(hooksDirectory.error, hooksDirectory.stderr); + } + if (hooksDirectory.status !== 0) { + return fail( + 'git-command-failed', + `Failed to resolve the Git hooks path: ${hooksDirectory.stderr.trim()}`, + ); + } + + const resolvedDirectory = removeLineEnding(hooksDirectory.stdout); + if (!resolvedDirectory) { + return fail('git-command-failed', 'Failed to resolve the Git hooks path.'); + } + return resolvedDirectory; +}; + +export const resolveGitContext = ( + cwd: string, +): + | GitContext + | FailedHooksResult + | { status: 'skipped'; reason: 'not-git-repository' } => { + // Resolve every repository path in one Git process. `--git-path hooks` + // accounts for the effective core.hooksPath configuration across Git scopes. + const repository = runGit(cwd, [ + 'rev-parse', + '--is-inside-work-tree', + '--path-format=absolute', + '--show-toplevel', + '--show-prefix', + '--git-common-dir', + '--git-path', + 'hooks', + ]); + if (repository.error || repository.status === null) { + return gitFailure(repository.error, repository.stderr); + } + + const [ + insideWorkTree = '', + gitRoot = '', + repositoryPrefix = '', + gitCommonDirectory = '', + effectiveHooksDirectory = '', + ] = removeLineEnding(repository.stdout).split(/\r?\n/u); + + if (insideWorkTree !== 'true') { + return { status: 'skipped', reason: 'not-git-repository' }; + } + + if (repository.status !== 0) { + return fail( + 'git-command-failed', + `Failed to resolve the Git repository paths: ${repository.stderr.trim()}`, + ); + } + + if (!gitRoot || !gitCommonDirectory || !effectiveHooksDirectory) { + return fail( + 'git-command-failed', + 'Failed to resolve the Git repository paths.', + ); + } + + return { + defaultHooksDirectory: path.join(gitCommonDirectory, 'hooks'), + effectiveHooksDirectory, + gitRoot, + projectPath: + repositoryPrefix.replaceAll('\\', '/').replace(/\/$/u, '') || '.', + }; +}; + +export const isSamePath = (first: string, second: string): boolean => + path.resolve(first) === path.resolve(second); + +export const readOwner = (directory: string): string | undefined => { + try { + const content = readFileSync(path.join(directory, ownerFileName), 'utf8'); + const owner = removeLineEnding(content); + return content === `${owner}\n` && + owner.length > 0 && + !/[\r\n]/u.test(owner) + ? owner + : undefined; + } catch { + return undefined; + } +}; + +export const displayPath = (gitRoot: string, filePath: string): string => { + const relativePath = path.relative(gitRoot, filePath).replaceAll('\\', '/'); + return relativePath.length > 0 && !relativePath.startsWith('../') + ? relativePath + : filePath; +}; diff --git a/packages/rstack/src/setup/hooks.ts b/packages/rstack/src/setup/hooks.ts index 7a6e523c..2b068fcc 100644 --- a/packages/rstack/src/setup/hooks.ts +++ b/packages/rstack/src/setup/hooks.ts @@ -50,7 +50,7 @@ rs_init="\${XDG_CONFIG_HOME:-$HOME/.config}/rstack/hooks-init.sh" IFS= read -r rs_project_path < "$rs_dir/.owner" || exit 1 [ -n "$rs_project_path" ] || exit 1 -# Fall back to the Node.js executable that ran rs setup when GUI clients omit +# Fall back to the Node.js executable that ran rs hooks when GUI clients omit # it from PATH. Keep an existing Node.js environment ahead of this fallback. rs_node_fallback=${quoteShellPath(nodeExecutable)} if ! command -v node >/dev/null 2>&1 && [ -x "$rs_node_fallback" ]; then diff --git a/packages/rstack/src/setup/index.ts b/packages/rstack/src/setup/index.ts index 5b37d61d..6d7c9ef2 100644 --- a/packages/rstack/src/setup/index.ts +++ b/packages/rstack/src/setup/index.ts @@ -2,8 +2,12 @@ import { color, logger } from 'rslog'; import { parseArgs } from '../cli/args.ts'; import { printCommandHelp } from '../cli/help.ts'; import { installHooks } from './install.ts'; +import { uninstallHooks } from './uninstall.ts'; -export const runSetupCLI = async (args: string[]): Promise => { +const runInstallCLI = async ( + args: string[], + command: 'hooks' | 'setup', +): Promise => { const { values } = parseArgs({ args, options: { @@ -25,30 +29,31 @@ export const runSetupCLI = async (args: string[]): Promise => { const hooksDir = hooksDirs?.[0]; if (values.help) { - await printCommandHelp('setup'); + await printCommandHelp(command); return; } const result = installHooks({ force: values.force, hooksDir }); if (result.status === 'installed') { - // Warn when `--force` preserves an existing hooks setup but makes it inactive. + // Explain the result when `--force` preserves an existing hooks setup but makes it inactive. if (result.inactiveHooks) { const { hooks, path, restore } = result.inactiveHooks; const hooksMessage = hooks.length ? `: ${color.yellow(hooks.join(', '))}` : ''; - logger.warn( - `The previous Git hooks path "${color.yellow(path)}" is now inactive${hooksMessage}.`, + logger.info( + `Rstack now manages Git hooks at "${color.yellow(result.hooksPath)}".`, + ); + logger.info( + `Existing hooks in "${color.yellow(path)}" were preserved but will no longer run${hooksMessage}.`, ); if (restore === 'unset') { - logger.info( - `The existing files were preserved and will become active again if ${color.yellow('core.hooksPath')} is unset.`, - ); + logger.info(`Unset ${color.yellow('core.hooksPath')} to restore them.`); } else { logger.info( - `The existing files were preserved. Set ${color.yellow('core.hooksPath')} back to this path to use them again.`, + `Set ${color.yellow('core.hooksPath')} back to "${color.yellow(path)}" to restore them.`, ); } } @@ -67,7 +72,7 @@ export const runSetupCLI = async (args: string[]): Promise => { result.reason === 'hooks-path-conflict' ) { logger.info( - `To continue, run ${color.yellow('rs setup --force')}. Existing hook files will be preserved but become inactive.`, + `To continue, run ${color.yellow('rs hooks --force')}. Existing hook files will be preserved but become inactive.`, ); } return; @@ -83,3 +88,48 @@ export const runSetupCLI = async (args: string[]): Promise => { throw new Error(result.message); }; + +const runUninstallCLI = async (args: string[]): Promise => { + const { values } = parseArgs({ + args, + options: { + help: { type: 'boolean', short: 'h' }, + }, + allowPositionals: false, + strict: true, + }); + + if (values.help) { + await printCommandHelp('hooks uninstall'); + return; + } + + const result = uninstallHooks(); + if (result.status === 'uninstalled') { + logger.info( + `Rstack Git hooks uninstalled from "${color.yellow(result.hooksPath)}".`, + ); + logger.info( + `Remove ${color.yellow('rs hooks')} from the ${color.yellow('prepare')} script in package.json to keep hooks uninstalled.`, + ); + return; + } + + if (result.status === 'unchanged') { + return; + } + + throw new Error(result.message); +}; + +export const runHooksCLI = async ( + args: string[], + command: 'hooks' | 'setup' = 'hooks', +): Promise => { + if (command === 'hooks' && args[0] === 'uninstall') { + await runUninstallCLI(args.slice(1)); + return; + } + + await runInstallCLI(args, command); +}; diff --git a/packages/rstack/src/setup/install.ts b/packages/rstack/src/setup/install.ts index a0dabd99..bd0bb407 100644 --- a/packages/rstack/src/setup/install.ts +++ b/packages/rstack/src/setup/install.ts @@ -1,4 +1,3 @@ -import { spawnSync } from 'node:child_process'; import { chmodSync, existsSync, @@ -8,11 +7,23 @@ import { writeFileSync, } from 'node:fs'; import path from 'node:path'; +import { + displayPath, + fail, + type FailedHooksResult, + generatedDirectoryName, + gitFailure, + isSamePath, + ownerFileName, + readOwner, + resolveGitContext, + resolveGitHooksPath, + resolveHooksPathScope, + runGit, +} from './git.ts'; import { createHookFiles, hookNames } from './hooks.ts'; const defaultHooksDir = '.rstack/hooks'; -const generatedDirectoryName = '_'; -const ownerFileName = '.owner'; const gitignore = '*\n'; type InstallHooksOptions = { @@ -27,11 +38,7 @@ type InactiveHooks = { restore: 'configure' | 'unset'; }; -type FailedInstallResult = { - status: 'failed'; - reason: string; - message: string; -}; +type FailedInstallResult = FailedHooksResult; type SkippedInstallResult = { status: 'skipped'; @@ -45,21 +52,8 @@ type InstallResult = | SkippedInstallResult | FailedInstallResult; -type GitContext = { - defaultHooksDirectory: string; - effectiveHooksDirectory: string; - gitRoot: string; - projectPath: string; -}; - type GitConfigScopeOption = '--local' | '--worktree'; -const fail = (reason: string, message: string): FailedInstallResult => ({ - status: 'failed', - reason, - message, -}); - const skip = (reason: string, message?: string): SkippedInstallResult => ({ status: 'skipped', reason, @@ -93,145 +87,23 @@ const resolveHooksDir = (hooksDir: string): string | FailedInstallResult => { return resolvedDir; }; -const runGit = (cwd: string, args: string[]) => - spawnSync('git', args, { cwd, encoding: 'utf8' }); - -const removeLineEnding = (value: string): string => - value.replace(/\r?\n$/u, ''); - -const gitFailure = ( - error: NodeJS.ErrnoException | undefined, - stderr: string, -): FailedInstallResult => { - if (error?.code === 'ENOENT') { - return fail('git-not-found', 'Git command not found.'); - } - - return fail( - 'git-command-failed', - `Failed to run Git: ${error?.message || stderr.trim()}`, - ); -}; - -const resolveHooksPathScope = ( +const resolveInstallConfigScope = ( cwd: string, ): GitConfigScopeOption | FailedInstallResult => { - const configured = runGit(cwd, [ - 'config', - '--show-scope', - '--get', - 'core.hooksPath', - ]); - if (configured.error || configured.status === null) { - return gitFailure(configured.error, configured.stderr); - } - - // Exit status 1 means core.hooksPath is not configured yet. - if (configured.status === 1) { - return '--local'; + const scope = resolveHooksPathScope(cwd); + if (typeof scope === 'object') { + return scope; } - if (configured.status !== 0) { - return fail( - 'git-config-failed', - `Failed to resolve the core.hooksPath scope: ${configured.stderr.trim()}`, - ); - } - - const separator = configured.stdout.indexOf('\t'); - const scope = separator === -1 ? '' : configured.stdout.slice(0, separator); if (scope === 'worktree') { return '--worktree'; } if (scope === 'command') { return fail( 'hooks-path-command-scope', - "Cannot configure core.hooksPath because it is set in Git's command scope. Remove the command-scoped override and rerun rs setup.", + "Cannot configure core.hooksPath because it is set in Git's command scope. Remove the command-scoped override and rerun rs hooks.", ); } - if (scope === 'system' || scope === 'global' || scope === 'local') { - return '--local'; - } - - return fail( - 'git-config-failed', - 'Failed to resolve the core.hooksPath scope.', - ); -}; - -const resolveGitHooksPath = (cwd: string): string | FailedInstallResult => { - const hooksDirectory = runGit(cwd, [ - 'rev-parse', - '--path-format=absolute', - '--git-path', - 'hooks', - ]); - if (hooksDirectory.error || hooksDirectory.status === null) { - return gitFailure(hooksDirectory.error, hooksDirectory.stderr); - } - if (hooksDirectory.status !== 0) { - return fail( - 'git-command-failed', - `Failed to resolve the Git hooks path: ${hooksDirectory.stderr.trim()}`, - ); - } - - const resolvedDirectory = removeLineEnding(hooksDirectory.stdout); - if (!resolvedDirectory) { - return fail('git-command-failed', 'Failed to resolve the Git hooks path.'); - } - return resolvedDirectory; -}; - -const resolveGitContext = (cwd: string): GitContext | InstallResult => { - // Resolve every repository path in one Git process. `--git-path hooks` - // accounts for the effective core.hooksPath configuration across Git scopes. - const repository = runGit(cwd, [ - 'rev-parse', - '--is-inside-work-tree', - '--path-format=absolute', - '--show-toplevel', - '--show-prefix', - '--git-common-dir', - '--git-path', - 'hooks', - ]); - if (repository.error || repository.status === null) { - return gitFailure(repository.error, repository.stderr); - } - - const [ - insideWorkTree = '', - gitRoot = '', - repositoryPrefix = '', - gitCommonDirectory = '', - effectiveHooksDirectory = '', - ] = removeLineEnding(repository.stdout).split(/\r?\n/u); - - if (insideWorkTree !== 'true') { - return skip('not-git-repository'); - } - - if (repository.status !== 0) { - return fail( - 'git-command-failed', - `Failed to resolve the Git repository paths: ${repository.stderr.trim()}`, - ); - } - - if (!gitRoot || !gitCommonDirectory || !effectiveHooksDirectory) { - return fail( - 'git-command-failed', - 'Failed to resolve the Git repository paths.', - ); - } - - return { - defaultHooksDirectory: path.join(gitCommonDirectory, 'hooks'), - effectiveHooksDirectory, - gitRoot, - projectPath: - repositoryPrefix.replaceAll('\\', '/').replace(/\/$/u, '') || '.', - }; + return '--local'; }; const isCurrentFile = ( @@ -252,30 +124,6 @@ const isCurrentFile = ( } }; -const isSamePath = (first: string, second: string): boolean => - path.resolve(first) === path.resolve(second); - -const readOwner = (directory: string): string | undefined => { - try { - const content = readFileSync(path.join(directory, ownerFileName), 'utf8'); - const owner = removeLineEnding(content); - return content === `${owner}\n` && - owner.length > 0 && - !/[\r\n]/u.test(owner) - ? owner - : undefined; - } catch { - return undefined; - } -}; - -const displayPath = (gitRoot: string, filePath: string): string => { - const relativePath = path.relative(gitRoot, filePath).replaceAll('\\', '/'); - return relativePath.length > 0 && !relativePath.startsWith('../') - ? relativePath - : filePath; -}; - const ownerConflict = (project: string): SkippedInstallResult => skip( 'owned-by-another-project', @@ -402,7 +250,9 @@ export const installHooks = ({ } // Preserve a worktree-scoped override instead of writing a shadowed local value. - const configScope = hooksPathMatches ? '--local' : resolveHooksPathScope(cwd); + const configScope = hooksPathMatches + ? '--local' + : resolveInstallConfigScope(cwd); if (typeof configScope !== 'string') { return configScope; } diff --git a/packages/rstack/src/setup/uninstall.ts b/packages/rstack/src/setup/uninstall.ts new file mode 100644 index 00000000..39eb9fed --- /dev/null +++ b/packages/rstack/src/setup/uninstall.ts @@ -0,0 +1,146 @@ +import { rmSync } from 'node:fs'; +import path from 'node:path'; +import { + displayPath, + fail, + type FailedHooksResult, + generatedDirectoryName, + gitFailure, + isSamePath, + readOwner, + resolveGitContext, + resolveGitHooksPath, + resolveHooksPathScope, + runGit, +} from './git.ts'; + +export type UninstallHooksOptions = { + cwd?: string; +}; + +export type UninstallHooksResult = + | { status: 'uninstalled'; hooksPath: string } + | { + status: 'unchanged'; + reason: 'not-git-repository' | 'not-installed'; + } + | FailedHooksResult; + +const isInsideRepository = (gitRoot: string, directory: string): boolean => { + const relativePath = path.relative(gitRoot, directory); + return ( + relativePath.length > 0 && + relativePath !== '..' && + !relativePath.startsWith(`..${path.sep}`) && + !path.isAbsolute(relativePath) + ); +}; + +const unmanagedDirectory = ( + gitRoot: string, + directory: string, +): FailedHooksResult => + fail( + 'hooks-directory-conflict', + `Cannot uninstall Git hooks because "${displayPath(gitRoot, directory)}" is not managed by Rstack. No files were removed.`, + ); + +export const uninstallHooks = ({ + cwd = process.cwd(), +}: UninstallHooksOptions = {}): UninstallHooksResult => { + const context = resolveGitContext(cwd); + if ('status' in context) { + return context.status === 'skipped' + ? { status: 'unchanged', reason: 'not-git-repository' } + : context; + } + + const { + defaultHooksDirectory, + effectiveHooksDirectory, + gitRoot, + projectPath, + } = context; + if (isSamePath(effectiveHooksDirectory, defaultHooksDirectory)) { + return { status: 'unchanged', reason: 'not-installed' }; + } + + if ( + path.basename(effectiveHooksDirectory) !== generatedDirectoryName || + !isInsideRepository(gitRoot, effectiveHooksDirectory) + ) { + return unmanagedDirectory(gitRoot, effectiveHooksDirectory); + } + + const owner = readOwner(effectiveHooksDirectory); + if (!owner) { + return unmanagedDirectory(gitRoot, effectiveHooksDirectory); + } + if (owner !== projectPath) { + return fail( + 'owned-by-another-project', + `Cannot uninstall Git hooks because "${displayPath(gitRoot, effectiveHooksDirectory)}" is owned by Rstack project "${owner}". No files were removed.`, + ); + } + + const scope = resolveHooksPathScope(cwd); + if (typeof scope === 'object') { + return scope; + } + if (scope === 'command') { + return fail( + 'hooks-path-command-scope', + "Cannot uninstall Git hooks because core.hooksPath is set in Git's command scope. Remove the command-scoped override and rerun rs hooks uninstall.", + ); + } + if (scope !== 'local' && scope !== 'worktree') { + const scopeName = scope ? `Git's ${scope} scope` : 'an unknown Git scope'; + return fail( + 'hooks-path-scope-conflict', + `Cannot uninstall Git hooks because core.hooksPath is set in ${scopeName}. No files were removed.`, + ); + } + + const configScope = scope === 'worktree' ? '--worktree' : '--local'; + const unset = runGit(cwd, [ + 'config', + configScope, + '--unset', + 'core.hooksPath', + ]); + if (unset.error || unset.status === null) { + return gitFailure(unset.error, unset.stderr); + } + if (unset.status !== 0) { + return fail( + 'git-config-failed', + `Failed to unset core.hooksPath in Git's ${scope} scope: ${unset.stderr.trim() || `Git exited with status ${unset.status}`}`, + ); + } + + const remainingHooksDirectory = resolveGitHooksPath(cwd); + if (typeof remainingHooksDirectory !== 'string') { + return remainingHooksDirectory; + } + if (isSamePath(remainingHooksDirectory, effectiveHooksDirectory)) { + return fail( + 'git-config-failed', + `Failed to deactivate Rstack Git hooks because core.hooksPath still resolves to "${displayPath(gitRoot, effectiveHooksDirectory)}". Generated files were preserved.`, + ); + } + + try { + rmSync(effectiveHooksDirectory, { recursive: true }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return fail( + 'remove-failed', + `Failed to remove generated Git hook files: ${message}`, + ); + } + + return { + status: 'uninstalled', + hooksPath: displayPath(gitRoot, effectiveHooksDirectory), + }; +}; diff --git a/packages/rstack/tests/cli/__snapshots__/help.test.ts.snap b/packages/rstack/tests/cli/__snapshots__/help.test.ts.snap index 6198d4dd..65e3a481 100644 --- a/packages/rstack/tests/cli/__snapshots__/help.test.ts.snap +++ b/packages/rstack/tests/cli/__snapshots__/help.test.ts.snap @@ -352,7 +352,7 @@ Commands: check Run static checks, including lint and format test Run tests staged Run tasks on staged Git files - setup Install Git hooks + hooks Manage Git hooks For command-specific options, run: $ rs -h diff --git a/packages/rstack/tests/cli/hooks/__snapshots__/index.test.ts.snap b/packages/rstack/tests/cli/hooks/__snapshots__/index.test.ts.snap new file mode 100644 index 00000000..ae0b783b --- /dev/null +++ b/packages/rstack/tests/cli/hooks/__snapshots__/index.test.ts.snap @@ -0,0 +1,36 @@ +// Rstest Snapshot v1 + +exports[`displays hooks help without installing hooks 1`] = ` +"Rstack v + +Usage: + $ rs hooks [command] [options] + +Manage Git hooks in the current repository + +Commands: + [options] Install or update Git hooks (default) + uninstall Uninstall Git hooks + +For command-specific options, run: + $ rs hooks -h + +Options: + -f, --force Install despite an existing Git hooks setup + --hooks-dir Specify hooks directory relative to the Git repository root + -h, --help Display this help message +" +`; + +exports[`displays uninstall help 1`] = ` +"Rstack v + +Usage: + $ rs hooks uninstall [options] + +Uninstall Git hooks from the current repository + +Options: + -h, --help Display this help message +" +`; diff --git a/packages/rstack/tests/cli/hooks/index.test.ts b/packages/rstack/tests/cli/hooks/index.test.ts new file mode 100644 index 00000000..563bd04d --- /dev/null +++ b/packages/rstack/tests/cli/hooks/index.test.ts @@ -0,0 +1,278 @@ +import { spawnSync } from 'node:child_process'; +import { + chmodSync, + existsSync, + mkdirSync, + mkdtempSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import path from 'node:path'; +import { afterEach, beforeEach } from 'rstack/test'; +import { normalizeHelpOutput, RSTACK_BIN_PATH, test } from '#test-helpers'; + +const hooksPath = '.rstack/hooks/_'; + +let cwd: string; +let env: NodeJS.ProcessEnv; + +const git = (args: string[]): string => { + const result = spawnSync('git', args, { cwd, encoding: 'utf8', env }); + if (result.status !== 0) { + throw new Error(result.stderr || `Git exited with status ${result.status}`); + } + return result.stdout.trim(); +}; + +const initRepository = (): void => { + git(['init', '--quiet']); +}; + +const runHooks = (args: string[], runCwd: string = cwd) => + spawnSync(process.execPath, [RSTACK_BIN_PATH, 'hooks', ...args], { + cwd: runCwd, + encoding: 'utf8', + env, + }); + +const runHooksSuccessfully = (args: string[], runCwd: string = cwd): string => { + const result = runHooks(args, runCwd); + if (result.status !== 0) { + throw new Error( + result.stderr || result.error?.message || `Exited with ${result.status}`, + ); + } + return `${result.stdout}${result.stderr}`; +}; + +beforeEach(() => { + cwd = mkdtempSync(path.join(import.meta.dirname, 'test-temp-rstack hooks ')); + env = { + ...process.env, + // Keep Git from treating the fixture as part of this repository. + GIT_CEILING_DIRECTORIES: import.meta.dirname, + GIT_CONFIG_GLOBAL: path.join(cwd, 'global.gitconfig'), + GIT_CONFIG_NOSYSTEM: '1', + }; +}); + +afterEach(() => { + rmSync(cwd, { force: true, recursive: true }); +}); + +test('displays hooks help without installing hooks', ({ execCli, expect }) => { + initRepository(); + const output = execCli('hooks --help', { cwd, env }); + + expect(execCli('hooks -h', { cwd, env })).toBe(output); + expect(normalizeHelpOutput(output)).toMatchSnapshot(); + expect(existsSync(path.join(cwd, '.rstack'))).toBe(false); +}); + +test('rejects unknown hooks positionals and options', ({ execCli, expect }) => { + expect(() => execCli('hooks install', { cwd })).toThrow(); + expect(() => execCli('hooks --unknown', { cwd })).toThrow(); + expect(() => execCli('hooks --dir custom-hooks', { cwd })).toThrow(); + expect(() => execCli('hooks -d custom-hooks', { cwd })).toThrow(); +}); + +test('displays uninstall help', ({ execCli, expect }) => { + const output = execCli('hooks uninstall --help', { cwd }); + + expect(execCli('hooks uninstall -h', { cwd })).toBe(output); + expect(normalizeHelpOutput(output)).toMatchSnapshot(); +}); + +test('rejects unsupported uninstall arguments', ({ expect }) => { + for (const args of [ + ['extra'], + ['--force'], + ['--hooks-dir', 'custom-hooks'], + ]) { + expect(runHooks(['uninstall', ...args]).status).toBe(1); + } +}); + +test('reports missing and repeated hooks directory options', ({ expect }) => { + const missing = runHooks(['--hooks-dir']); + expect(missing.status).toBe(1); + expect(missing.stderr).toContain('--hooks-dir'); + + const repeated = runHooks(['--hooks-dir', 'first', '--hooks-dir', 'second']); + expect(repeated.status).toBe(1); + expect(repeated.stderr).toContain( + 'The --hooks-dir option cannot be specified more than once.', + ); +}); + +test('rejects invalid hooks directory options', ({ expect }) => { + const empty = runHooks(['--hooks-dir', '']); + expect(empty.status).toBe(1); + expect(empty.stderr).toContain('Git hooks directory must not be empty.'); + + const absolute = runHooks(['--hooks-dir', path.join(cwd, 'hooks')]); + expect(absolute.status).toBe(1); + expect(absolute.stderr).toContain( + 'Git hooks directory must be relative to the Git repository root.', + ); + + const parent = runHooks(['--hooks-dir', '../hooks']); + expect(parent.status).toBe(1); + expect(parent.stderr).toContain('Git hooks directory must not contain "..".'); +}); + +test('installs hooks silently without loading Rstack config', ({ + execCli, + expect, +}) => { + initRepository(); + writeFileSync( + path.join(cwd, 'rstack.config.ts'), + 'throw new Error("must not load");\n', + ); + + expect(execCli('hooks', { cwd, env })).toBe(''); + expect(git(['config', '--local', '--get', 'core.hooksPath'])).toBe(hooksPath); + expect(existsSync(path.join(cwd, hooksPath, 'runner'))).toBe(true); + expect(existsSync(path.join(cwd, '.rstack', 'hooks', 'pre-commit'))).toBe( + false, + ); + + expect(execCli('hooks', { cwd, env })).toBe(''); +}); + +test('uninstalls hooks and prints the prepare reminder', ({ + execCli, + expect, +}) => { + initRepository(); + expect(execCli('hooks', { cwd, env })).toBe(''); + + const output = execCli('hooks uninstall', { cwd, env }); + expect(output).toContain( + 'info Rstack Git hooks uninstalled from ".rstack/hooks/_".', + ); + expect(output).toContain( + 'info Remove rs hooks from the prepare script in package.json to keep hooks uninstalled.', + ); + expect(existsSync(path.join(cwd, hooksPath))).toBe(false); +}); + +test('guides and forces installation while preserving existing hooks', ({ + expect, +}) => { + initRepository(); + const existingHook = path.join(cwd, '.git', 'hooks', 'pre-commit'); + writeFileSync( + existingHook, + "#!/usr/bin/env sh\nprintf 'ran\\n' > old-hook-ran\n", + ); + chmodSync(existingHook, 0o755); + + const skippedOutput = runHooksSuccessfully([]); + expect(skippedOutput).toContain( + 'Git hooks setup skipped: existing Git hooks were found: pre-commit.', + ); + expect(skippedOutput).toContain( + 'To continue, run rs hooks --force. Existing hook files will be preserved but become inactive.', + ); + + const forcedOutput = runHooksSuccessfully(['--force']); + expect(forcedOutput).toContain( + 'info Rstack now manages Git hooks at ".rstack/hooks/_".', + ); + expect(forcedOutput).toContain( + 'Existing hooks in ".git/hooks" were preserved but will no longer run: pre-commit.', + ); + expect(forcedOutput).toContain('Unset core.hooksPath to restore them.'); + expect(git(['config', '--local', '--get', 'core.hooksPath'])).toBe(hooksPath); + + git(['hook', 'run', 'pre-commit']); + expect(existsSync(path.join(cwd, 'old-hook-ran'))).toBe(false); + + git(['config', '--local', '--unset', 'core.hooksPath']); + git(['hook', 'run', 'pre-commit']); + expect(existsSync(path.join(cwd, 'old-hook-ran'))).toBe(true); + + expect(runHooksSuccessfully(['-f'])).toContain( + 'Existing hooks in ".git/hooks" were preserved but will no longer run: pre-commit.', + ); +}); + +test('reports how to restore a replaced hooks path', ({ expect }) => { + initRepository(); + const existingDirectory = path.join(cwd, '.husky', '_'); + mkdirSync(existingDirectory, { recursive: true }); + writeFileSync( + path.join(existingDirectory, 'pre-commit'), + '#!/usr/bin/env sh\n', + ); + git(['config', '--local', 'core.hooksPath', '.husky/_']); + + const output = runHooksSuccessfully(['--force']); + expect(output).toContain( + 'info Rstack now manages Git hooks at ".rstack/hooks/_".', + ); + expect(output).toContain( + 'Existing hooks in ".husky/_" were preserved but will no longer run: pre-commit.', + ); + expect(output).toContain( + 'Set core.hooksPath back to ".husky/_" to restore them.', + ); +}); + +test('installs root-relative hooks and reports owner conflicts', ({ + execCli, + expect, +}) => { + initRepository(); + const frontend = path.join(cwd, 'frontend'); + const docs = path.join(cwd, 'docs'); + mkdirSync(frontend); + mkdirSync(docs); + + expect( + execCli('hooks --hooks-dir "custom hooks"', { cwd: frontend, env }), + ).toBe(''); + expect(git(['config', '--local', '--get', 'core.hooksPath'])).toBe( + 'custom hooks/_', + ); + expect(existsSync(path.join(cwd, 'custom hooks', '_', 'runner'))).toBe(true); + + expect(runHooksSuccessfully(['--hooks-dir', 'custom hooks'], docs)).toContain( + 'Git hooks are already managed by Rstack project "frontend"', + ); +}); + +test('skips non-Git directories without creating files', ({ + execCli, + expect, +}) => { + expect(execCli('hooks', { cwd, env })).toContain( + 'info Git hooks setup skipped: not a Git repository.', + ); + expect(existsSync(path.join(cwd, '.rstack'))).toBe(false); +}); + +test('skips installation when hooks are disabled', ({ execCli, expect }) => { + const output = execCli('hooks', { + cwd, + env: { ...env, RSTACK_HOOKS: '0' }, + }); + + expect(output).toContain( + 'info Git hooks setup skipped: disabled by RSTACK_HOOKS.', + ); + expect(existsSync(path.join(cwd, '.rstack'))).toBe(false); +}); + +test('exits with an error when Git is unavailable', ({ expect }) => { + const result = spawnSync(process.execPath, [RSTACK_BIN_PATH, 'hooks'], { + cwd, + encoding: 'utf8', + env: { ...env, PATH: '', Path: '' }, + }); + + expect(result.status).toBe(1); + expect(result.stderr).toContain('Git command not found.'); +}); diff --git a/packages/rstack/tests/cli/setup/__snapshots__/index.test.ts.snap b/packages/rstack/tests/cli/setup/__snapshots__/index.test.ts.snap deleted file mode 100644 index 71351935..00000000 --- a/packages/rstack/tests/cli/setup/__snapshots__/index.test.ts.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Rstest Snapshot v1 - -exports[`displays setup help 1`] = ` -"Rstack v - -Usage: - $ rs setup [options] - -Install Git hooks in the current repository - -Options: - -f, --force Install despite an existing Git hooks setup - --hooks-dir Specify hooks directory relative to the Git repository root - -h, --help Display this help message -" -`; diff --git a/packages/rstack/tests/cli/setup/index.test.ts b/packages/rstack/tests/cli/setup/index.test.ts index 93bda958..6fdc1501 100644 --- a/packages/rstack/tests/cli/setup/index.test.ts +++ b/packages/rstack/tests/cli/setup/index.test.ts @@ -1,17 +1,8 @@ import { spawnSync } from 'node:child_process'; -import { - chmodSync, - existsSync, - mkdirSync, - mkdtempSync, - rmSync, - writeFileSync, -} from 'node:fs'; +import { existsSync, mkdtempSync, rmSync } from 'node:fs'; import path from 'node:path'; import { afterEach, beforeEach } from 'rstack/test'; -import { normalizeHelpOutput, RSTACK_BIN_PATH, test } from '#test-helpers'; - -const hooksPath = '.rstack/hooks/_'; +import { test } from '#test-helpers'; let cwd: string; let env: NodeJS.ProcessEnv; @@ -24,32 +15,10 @@ const git = (args: string[]): string => { return result.stdout.trim(); }; -const initRepository = (): void => { - git(['init', '--quiet']); -}; - -const runSetup = (args: string[], runCwd: string = cwd) => - spawnSync(process.execPath, [RSTACK_BIN_PATH, 'setup', ...args], { - cwd: runCwd, - encoding: 'utf8', - env, - }); - -const runSetupSuccessfully = (args: string[], runCwd: string = cwd): string => { - const result = runSetup(args, runCwd); - if (result.status !== 0) { - throw new Error( - result.stderr || result.error?.message || `Exited with ${result.status}`, - ); - } - return `${result.stdout}${result.stderr}`; -}; - beforeEach(() => { cwd = mkdtempSync(path.join(import.meta.dirname, 'test-temp-rstack setup ')); env = { ...process.env, - // Keep Git from treating the fixture as part of this repository. GIT_CEILING_DIRECTORIES: import.meta.dirname, GIT_CONFIG_GLOBAL: path.join(cwd, 'global.gitconfig'), GIT_CONFIG_NOSYSTEM: '1', @@ -60,173 +29,17 @@ afterEach(() => { rmSync(cwd, { force: true, recursive: true }); }); -test('displays setup help', ({ execCli, expect }) => { - const output = execCli('setup --help', { cwd }); - - expect(execCli('setup -h', { cwd })).toBe(output); - expect(normalizeHelpOutput(output)).toMatchSnapshot(); -}); - -test('rejects unknown setup options', ({ execCli, expect }) => { - expect(() => execCli('setup --unknown', { cwd })).toThrow(); -}); - -test('reports missing and repeated hooks directory options', ({ expect }) => { - const missing = runSetup(['--hooks-dir']); - expect(missing.status).toBe(1); - expect(missing.stderr).toContain('--hooks-dir'); - - const repeated = runSetup(['--hooks-dir', 'first', '--hooks-dir', 'second']); - expect(repeated.status).toBe(1); - expect(repeated.stderr).toContain( - 'The --hooks-dir option cannot be specified more than once.', - ); -}); - -test('rejects invalid hooks directory options', ({ expect }) => { - const empty = runSetup(['--hooks-dir', '']); - expect(empty.status).toBe(1); - expect(empty.stderr).toContain('Git hooks directory must not be empty.'); - - const absolute = runSetup(['--hooks-dir', path.join(cwd, 'hooks')]); - expect(absolute.status).toBe(1); - expect(absolute.stderr).toContain( - 'Git hooks directory must be relative to the Git repository root.', - ); - - const parent = runSetup(['--hooks-dir', '../hooks']); - expect(parent.status).toBe(1); - expect(parent.stderr).toContain('Git hooks directory must not contain "..".'); -}); - -test('installs hooks silently without loading Rstack config', ({ - execCli, - expect, -}) => { - initRepository(); - writeFileSync( - path.join(cwd, 'rstack.config.ts'), - 'throw new Error("must not load");\n', - ); - - expect(execCli('setup', { cwd, env })).toBe(''); - expect(git(['config', '--local', '--get', 'core.hooksPath'])).toBe(hooksPath); - expect(existsSync(path.join(cwd, hooksPath, 'runner'))).toBe(true); - expect(existsSync(path.join(cwd, '.rstack', 'hooks', 'pre-commit'))).toBe( - false, - ); - - expect(execCli('setup', { cwd, env })).toBe(''); -}); - -test('guides and forces setup while preserving existing hooks', ({ - expect, -}) => { - initRepository(); - const existingHook = path.join(cwd, '.git', 'hooks', 'pre-commit'); - writeFileSync( - existingHook, - "#!/usr/bin/env sh\nprintf 'ran\\n' > old-hook-ran\n", - ); - chmodSync(existingHook, 0o755); - - const skippedOutput = runSetupSuccessfully([]); - expect(skippedOutput).toContain( - 'Git hooks setup skipped: existing Git hooks were found: pre-commit.', - ); - expect(skippedOutput).toContain( - 'To continue, run rs setup --force. Existing hook files will be preserved but become inactive.', - ); - - const forcedOutput = runSetupSuccessfully(['--force']); - expect(forcedOutput).toContain( - 'The previous Git hooks path ".git/hooks" is now inactive: pre-commit.', - ); - expect(forcedOutput).toContain( - 'The existing files were preserved and will become active again if core.hooksPath is unset.', - ); - expect(git(['config', '--local', '--get', 'core.hooksPath'])).toBe(hooksPath); - - git(['hook', 'run', 'pre-commit']); - expect(existsSync(path.join(cwd, 'old-hook-ran'))).toBe(false); - - git(['config', '--local', '--unset', 'core.hooksPath']); - git(['hook', 'run', 'pre-commit']); - expect(existsSync(path.join(cwd, 'old-hook-ran'))).toBe(true); - - expect(runSetupSuccessfully(['-f'])).toContain( - 'The previous Git hooks path ".git/hooks" is now inactive: pre-commit.', - ); -}); - -test('reports how to restore a replaced hooks path', ({ expect }) => { - initRepository(); - const existingDirectory = path.join(cwd, '.husky', '_'); - mkdirSync(existingDirectory, { recursive: true }); - writeFileSync( - path.join(existingDirectory, 'pre-commit'), - '#!/usr/bin/env sh\n', - ); - git(['config', '--local', 'core.hooksPath', '.husky/_']); - - const output = runSetupSuccessfully(['--force']); - expect(output).toContain( - 'The previous Git hooks path ".husky/_" is now inactive: pre-commit.', - ); - expect(output).toContain( - 'The existing files were preserved. Set core.hooksPath back to this path to use them again.', - ); -}); +test('keeps setup as a compatibility alias', ({ execCli, expect }) => { + git(['init', '--quiet']); -test('installs root-relative hooks and reports owner conflicts', ({ - execCli, - expect, -}) => { - initRepository(); - const frontend = path.join(cwd, 'frontend'); - const docs = path.join(cwd, 'docs'); - mkdirSync(frontend); - mkdirSync(docs); + const help = execCli('setup --help', { cwd, env }); + expect(execCli('setup -h', { cwd, env })).toBe(help); + expect(help).toContain('$ rs setup [options]'); - expect( - execCli('setup --hooks-dir "custom hooks"', { cwd: frontend, env }), - ).toBe(''); + expect(execCli('setup --hooks-dir "compat hooks"', { cwd, env })).toBe(''); expect(git(['config', '--local', '--get', 'core.hooksPath'])).toBe( - 'custom hooks/_', - ); - expect(existsSync(path.join(cwd, 'custom hooks', '_', 'runner'))).toBe(true); - - expect(runSetupSuccessfully(['--hooks-dir', 'custom hooks'], docs)).toContain( - 'Git hooks are already managed by Rstack project "frontend"', - ); -}); - -test('skips non-Git directories without creating files', ({ - execCli, - expect, -}) => { - expect(execCli('setup', { cwd, env })).toContain( - 'info Git hooks setup skipped: not a Git repository.', + 'compat hooks/_', ); - expect(existsSync(path.join(cwd, '.rstack'))).toBe(false); -}); - -test('skips setup when hooks are disabled', ({ execCli, expect }) => { - const output = execCli('setup', { cwd, env: { ...env, RSTACK_HOOKS: '0' } }); - - expect(output).toContain( - 'info Git hooks setup skipped: disabled by RSTACK_HOOKS.', - ); - expect(existsSync(path.join(cwd, '.rstack'))).toBe(false); -}); - -test('exits with an error when Git is unavailable', ({ expect }) => { - const result = spawnSync(process.execPath, [RSTACK_BIN_PATH, 'setup'], { - cwd, - encoding: 'utf8', - env: { ...env, PATH: '', Path: '' }, - }); - - expect(result.status).toBe(1); - expect(result.stderr).toContain('Git command not found.'); + expect(existsSync(path.join(cwd, 'compat hooks', '_', 'runner'))).toBe(true); + expect(execCli('hooks --hooks-dir "compat hooks"', { cwd, env })).toBe(''); }); diff --git a/packages/rstack/tests/setup/uninstall.test.ts b/packages/rstack/tests/setup/uninstall.test.ts new file mode 100644 index 00000000..30985e6a --- /dev/null +++ b/packages/rstack/tests/setup/uninstall.test.ts @@ -0,0 +1,165 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { expect, test } from 'rstack/test'; +import { installHooks } from '../../src/setup/install.ts'; +import { uninstallHooks } from '../../src/setup/uninstall.ts'; +import { + git, + hooksPath, + restoreEnv, + runGit, + withDirectory, + withRepository, +} from './helpers.ts'; + +test.each([ + ['default', undefined, hooksPath], + ['custom', 'config/custom hooks', 'config/custom hooks/_'], +] as const)( + 'uninstalls a %s installation and preserves user hooks', + (_, hooksDir, installedHooksPath) => { + withRepository((cwd) => { + const userHook = path.join( + cwd, + path.dirname(installedHooksPath), + 'pre-commit', + ); + mkdirSync(path.dirname(userHook), { recursive: true }); + writeFileSync(userHook, 'echo user hook\n'); + + expect( + installHooks({ cwd, ...(hooksDir ? { hooksDir } : {}) }).status, + ).toBe('installed'); + const originalValue = process.env.RSTACK_HOOKS; + process.env.RSTACK_HOOKS = '0'; + + try { + expect(uninstallHooks({ cwd })).toEqual({ + status: 'uninstalled', + hooksPath: installedHooksPath, + }); + } finally { + restoreEnv('RSTACK_HOOKS', originalValue); + } + + expect( + git(cwd, ['config', '--local', '--get', 'core.hooksPath']).status, + ).toBe(1); + expect(existsSync(path.join(cwd, installedHooksPath))).toBe(false); + expect(readFileSync(userHook, 'utf8')).toBe('echo user hook\n'); + expect(uninstallHooks({ cwd }).status).toBe('unchanged'); + }); + }, +); + +test('uninstalls one linked worktree without affecting another', () => { + withRepository((cwd) => { + runGit(cwd, [ + '-c', + 'user.name=Rstack', + '-c', + 'user.email=rstack@example.com', + 'commit', + '--allow-empty', + '--quiet', + '-m', + 'initial', + ]); + runGit(cwd, ['config', '--local', 'extensions.worktreeConfig', 'true']); + expect(installHooks({ cwd }).status).toBe('installed'); + const mainHooksDirectory = path.join(cwd, hooksPath); + runGit(cwd, ['config', '--local', 'core.hooksPath', mainHooksDirectory]); + + const linkedWorktree = path.join(cwd, 'linked'); + runGit(cwd, [ + 'worktree', + 'add', + '--quiet', + '--detach', + linkedWorktree, + 'HEAD', + ]); + runGit(linkedWorktree, [ + 'config', + '--worktree', + 'core.hooksPath', + hooksPath, + ]); + expect(installHooks({ cwd: linkedWorktree }).status).toBe('installed'); + + expect(uninstallHooks({ cwd: linkedWorktree }).status).toBe('uninstalled'); + expect( + git(linkedWorktree, ['config', '--worktree', '--get', 'core.hooksPath']) + .status, + ).toBe(1); + expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe( + mainHooksDirectory, + ); + expect(existsSync(mainHooksDirectory)).toBe(true); + }); +}); + +test('refuses to remove hooks owned by another Rstack project', () => { + withRepository((cwd) => { + const frontend = path.join(cwd, 'frontend'); + const docs = path.join(cwd, 'docs'); + mkdirSync(frontend); + mkdirSync(docs); + + expect(installHooks({ cwd: frontend }).status).toBe('installed'); + expect(uninstallHooks({ cwd: docs })).toMatchObject({ + status: 'failed', + reason: 'owned-by-another-project', + }); + expect(existsSync(path.join(cwd, hooksPath, 'runner'))).toBe(true); + }); +}); + +test('refuses to remove an unmanaged hooks directory', () => { + withRepository((cwd) => { + const unmanagedDirectory = path.join(cwd, '.husky', '_'); + const unmanagedHook = path.join(unmanagedDirectory, 'pre-commit'); + mkdirSync(unmanagedDirectory, { recursive: true }); + writeFileSync(unmanagedHook, '#!/usr/bin/env sh\n'); + runGit(cwd, ['config', '--local', 'core.hooksPath', '.husky/_']); + + expect(uninstallHooks({ cwd })).toMatchObject({ + status: 'failed', + reason: 'hooks-directory-conflict', + }); + expect(readFileSync(unmanagedHook, 'utf8')).toBe('#!/usr/bin/env sh\n'); + }); +}); + +test('is unchanged outside a Git repository', () => { + withDirectory((cwd) => { + expect(uninstallHooks({ cwd })).toEqual({ + status: 'unchanged', + reason: 'not-git-repository', + }); + expect(existsSync(path.join(cwd, '.rstack'))).toBe(false); + }); +}); + +test.each([ + [ + 'unsetting fails', + (cwd: string) => writeFileSync(path.join(cwd, '.git', 'config.lock'), ''), + ], + [ + 'the path remains active', + (cwd: string) => + runGit(cwd, ['config', '--global', 'core.hooksPath', hooksPath]), + ], +] as const)('preserves generated hooks when %s', (_, arrangeFailure) => { + withRepository((cwd) => { + expect(installHooks({ cwd }).status).toBe('installed'); + arrangeFailure(cwd); + + expect(uninstallHooks({ cwd })).toMatchObject({ + status: 'failed', + reason: 'git-config-failed', + }); + expect(existsSync(path.join(cwd, hooksPath, 'runner'))).toBe(true); + }); +}); diff --git a/scripts/benchmark-fmt-discovery.js b/scripts/benchmark-fmt-discovery.js deleted file mode 100644 index 2891d082..00000000 --- a/scripts/benchmark-fmt-discovery.js +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env node -import { stat } from 'node:fs/promises'; -import path from 'node:path'; -import { performance } from 'node:perf_hooks'; -import { discoverFmtPaths } from '../packages/rstack/src/fmt/discoverPaths.ts'; - -const usage = `Usage: - pnpm benchmark:fmt-discovery -- [options] - -Build the optimized native binding before recording comparison data: - pnpm --filter rstack build:native:release - -Options: - --cwd Directory used to resolve inputs (default: current directory) - --pattern Input path or glob; may be repeated - --explicit-count Benchmark the first N discovered files as explicit inputs - --warmup Warmup runs excluded from timing (default: 5) - --runs Number of measured runs (default: 30) - -h, --help Display this help message -`; - -const readValue = (args, index, flag) => { - const value = args[index + 1]; - if (value === undefined) { - throw new Error(`${flag} requires a value.`); - } - return value; -}; - -const parseInteger = (value, flag, minimum) => { - const result = Number(value); - if (!Number.isSafeInteger(result) || result < minimum) { - throw new Error( - `${flag} must be an integer greater than or equal to ${minimum}.`, - ); - } - return result; -}; - -const parseArgs = (args) => { - const options = { - cwd: process.cwd(), - explicitCount: undefined, - patterns: [], - runs: 30, - warmup: 5, - }; - - for (let index = 0; index < args.length; index++) { - const arg = args[index]; - switch (arg) { - case '--': - break; - case '--cwd': - options.cwd = readValue(args, index, arg); - index++; - break; - case '--pattern': - options.patterns.push(readValue(args, index, arg)); - index++; - break; - case '--explicit-count': - options.explicitCount = parseInteger( - readValue(args, index, arg), - arg, - 1, - ); - index++; - break; - case '--runs': - options.runs = parseInteger(readValue(args, index, arg), arg, 1); - index++; - break; - case '--warmup': - options.warmup = parseInteger(readValue(args, index, arg), arg, 0); - index++; - break; - case '-h': - case '--help': - process.stdout.write(usage); - process.exit(0); - break; - default: - throw new Error(`Unknown option: ${arg}`); - } - } - - if (options.explicitCount !== undefined && options.patterns.length > 0) { - throw new Error('--explicit-count cannot be combined with --pattern.'); - } - return options; -}; - -const percentile = (sortedValues, ratio) => { - const position = (sortedValues.length - 1) * ratio; - const lowerIndex = Math.floor(position); - const upperIndex = Math.ceil(position); - const lower = sortedValues[lowerIndex]; - const upper = sortedValues[upperIndex]; - return lower + (upper - lower) * (position - lowerIndex); -}; - -const roundMilliseconds = (value) => Math.round(value * 1000) / 1000; - -const main = async () => { - const options = parseArgs(process.argv.slice(2)); - const cwd = path.resolve(options.cwd); - const cwdStats = await stat(cwd); - if (!cwdStats.isDirectory()) { - throw new Error(`Benchmark cwd is not a directory: ${cwd}`); - } - - let patterns = options.patterns.length > 0 ? options.patterns : undefined; - let mode = patterns ? 'patterns' : 'directory'; - if (options.explicitCount !== undefined) { - const discovered = await discoverFmtPaths({ cwd }); - if (discovered.length < options.explicitCount) { - throw new Error( - `Only ${discovered.length} files were discovered; cannot select ${options.explicitCount}.`, - ); - } - patterns = discovered.slice(0, options.explicitCount); - mode = 'explicit'; - } - - let expectedFileCount; - const runOnce = async () => { - const startTime = performance.now(); - const files = await discoverFmtPaths({ cwd, patterns }); - const duration = performance.now() - startTime; - expectedFileCount ??= files.length; - if (files.length !== expectedFileCount) { - throw new Error( - `Discovered file count changed between runs: ${expectedFileCount} -> ${files.length}.`, - ); - } - return duration; - }; - - for (let index = 0; index < options.warmup; index++) { - await runOnce(); - } - - const durations = []; - for (let index = 0; index < options.runs; index++) { - durations.push(await runOnce()); - } - durations.sort((left, right) => left - right); - - process.stdout.write( - `${JSON.stringify( - { - cwd, - mode, - patternCount: patterns?.length ?? 0, - fileCount: expectedFileCount, - warmup: options.warmup, - runs: options.runs, - medianMs: roundMilliseconds(percentile(durations, 0.5)), - p95Ms: roundMilliseconds(percentile(durations, 0.95)), - }, - undefined, - 2, - )}\n`, - ); -}; - -main().catch((error) => { - console.error(error instanceof Error ? error.message : error); - process.exitCode = 1; -}); diff --git a/website/docs/en/guide/cli/_meta.json b/website/docs/en/guide/cli/_meta.json index 5357606a..c228ad7a 100644 --- a/website/docs/en/guide/cli/_meta.json +++ b/website/docs/en/guide/cli/_meta.json @@ -8,6 +8,6 @@ "check", "lint", "fmt", - "setup", + "hooks", "staged" ] diff --git a/website/docs/en/guide/cli/hooks.mdx b/website/docs/en/guide/cli/hooks.mdx new file mode 100644 index 00000000..1011313a --- /dev/null +++ b/website/docs/en/guide/cli/hooks.mdx @@ -0,0 +1,247 @@ +--- +description: 'Install, configure, troubleshoot, and safely remove repository-level Git hooks with Rstack CLI.' +--- + +# hooks + +import { PackageManagerTabs } from '@rspress/core/theme'; + +The `rs hooks` command installs, updates, and removes repository-level [Git hooks](https://git-scm.com/docs/githooks). Hook scripts run in the project that installed them. + +## Usage + +```bash +rs hooks [options] +rs hooks uninstall +``` + +Run `rs hooks` without a subcommand to install or update hooks. + +:::tip Alias +`rs setup` is an alias for `rs hooks`. +::: + +## Install hooks + +By default, hook scripts are stored in `.rstack/hooks` at the Git repository root. If the current directory is outside a Git repository, the command skips installation. + +Add `rs hooks` to the `prepare` script of the project that owns the repository hooks: + +```json title="package.json" +{ + "scripts": { + "prepare": "rs hooks" + } +} +``` + +Run the script once to install the hooks: + + + +For example, create a `pre-commit` hook that runs [`rs staged`](./staged): + +```sh title=".rstack/hooks/pre-commit" +rs staged +``` + +You can safely run the installation more than once, and it does not load `rstack.config.*`. Run `rs hooks` again after cloning the repository or if the generated hook files are missing. + +:::warning Existing Git hook managers + +`rs hooks` updates the repository's [`core.hooksPath`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-corehooksPath). If it detects another hooks path or existing Git hooks, it skips installation. Run `rs hooks --force` to let Rstack manage hooks instead. + +::: + +## Install options + +### `--force` + +`--force` (or `-f`) installs Rstack hooks even when another Git hook setup already exists: + +```bash +rs hooks --force +``` + +Rstack keeps the existing hook files and points `core.hooksPath` to its generated hooks directory. While this setting is active, Git does not run hooks from the previous location. + +`--force` cannot replace hooks owned by another Rstack project. + +:::tip +Run `rs hooks --force` only once. Use `rs hooks` without `--force` in the `prepare` script. +::: + +### `--hooks-dir` + +Specifies a custom directory for hook scripts, relative to the Git repository root. + +```bash +rs hooks --hooks-dir config/git-hooks + +# Quote paths that contain spaces +rs hooks --hooks-dir "config/git hooks" +``` + +When using a custom directory, add the full command to the `prepare` script of the project that manages hooks: + +```json title="package.json" +{ + "scripts": { + "prepare": "rs hooks --hooks-dir config/git-hooks" + } +} +``` + +> The path must not contain `..`. This prevents hook files from being created or overwritten outside the repository through a parent directory path. + +### `--help` + +`--help` (or `-h`) displays the command's usage, subcommands, and installation options. + +```bash +rs hooks --help +``` + +## Uninstall hooks + +Run the command from the project that installed the hooks: + +```bash +rs hooks uninstall +``` + +Rstack automatically finds the active hooks and removes the `core.hooksPath` setting and the generated `_` directory. It does not delete project hook files such as `.rstack/hooks/pre-commit`. Hooks managed by another project or tool are left untouched. + +Remove `rs hooks` from the `prepare` script if you do not want the hooks to be installed again. + +If `--force` previously took over hooks in `.git/hooks`, those hooks become active again after uninstalling. + +## Hook files + +The default directory structure is: + +```text +.rstack/ +└── hooks/ + ├── pre-commit # Repository hook script: edit and commit + └── _/ # Generated by rs hooks; ignored by Git + ├── .gitignore + ├── .owner + ├── runner + ├── pre-commit + ├── commit-msg + └── ... +``` + +Files alongside `_` are repository hook scripts. The `_` directory contains generated files and is ignored by Git. `rs hooks` points `core.hooksPath` to `.rstack/hooks/_`. + +## Supported hooks + +Rstack CLI supports these client-side Git hooks: + +- `pre-commit` +- `pre-merge-commit` +- `prepare-commit-msg` +- `commit-msg` +- `post-commit` +- `applypatch-msg` +- `pre-applypatch` +- `post-applypatch` +- `pre-rebase` +- `post-rewrite` +- `post-checkout` +- `post-merge` +- `pre-push` +- `pre-auto-gc` + +Create a file with the matching name alongside the `_` directory. + +## Hook runtime + +Rstack CLI runs hook scripts with POSIX `sh -e`. It forwards Git's arguments and standard input, then returns the hook's exit code. Before running a hook, Rstack changes to the project that installed the hooks and prepends that project's `node_modules/.bin` to `PATH`. + +### Disable and debug + +Set `RSTACK_HOOKS=0` to skip installation or hook execution: + +```bash +RSTACK_HOOKS=0 git commit -m "Skip hooks" +``` + +Set `RSTACK_HOOKS=2` to trace the Rstack CLI hook runtime, including how it invokes the hook script and handles its exit code; to trace commands inside the hook script, add `set -x` to the script: + +```bash +RSTACK_HOOKS=2 git commit -m "Trace hooks" +``` + +### Configure the hook environment + +Before running a hook script, Rstack CLI loads this optional POSIX shell file: + +```text +${XDG_CONFIG_HOME:-$HOME/.config}/rstack/hooks-init.sh +``` + +Use it to initialize a Node.js version manager, update `PATH`, or set `RSTACK_HOOKS=0` for the current user. + +## Ownership safety + +Each generated hooks directory records its owning project. Only that project should include `rs hooks` in its `prepare` script. Rstack skips installation from any other project, even with `--force`, and refuses to remove hooks owned by another project. + +To transfer ownership: + +1. Remove `rs hooks` from the previous owner's `prepare` script. +2. Run `rs hooks uninstall` from the previous owner. +3. Add `rs hooks` to the new owner's `prepare` script and run it once. + +## Monorepo + +In a monorepo, the project that provides Rstack CLI may live in a subdirectory such as `frontend/`. Running `rs hooks` there still installs hooks at the Git repository root: + +```text +repo/.rstack/hooks/ +repo/.rstack/hooks/_/ +core.hooksPath=.rstack/hooks/_ +``` + +Rstack records `frontend` as the owning project. The hook scripts stay at the repository root but run from `frontend`, allowing them to use its configuration and dependencies without an explicit `cd`: + +```sh title=".rstack/hooks/pre-commit" +rs staged +``` + +## Worktree behavior + +Rstack preserves the Git configuration scope of the active `core.hooksPath`. If a linked worktree uses a worktree-scoped hooks path, `rs hooks --force` replaces it in that scope instead of writing a local setting that Git would ignore. + +`rs hooks uninstall` also removes the setting from that scope. Removing a worktree-scoped installation affects only the current worktree and leaves hooks in other linked checkouts unchanged. A local `core.hooksPath` setting is shared across linked checkouts, so changing or removing it affects every checkout that does not override it. + +## Troubleshooting + +### Hook does not run + +- Check that the hook script has a [supported name](#supported-hooks) and is next to the `_` directory. +- Run `git config --show-scope --get core.hooksPath` and verify the effective scope and path. +- Rerun `rs hooks` to restore generated files and executable permissions. +- Check that `RSTACK_HOOKS` is not set to `0` in the environment or initialization file. +- If another hook setup is detected, run `rs hooks --force`. +- If another project is reported as the hooks owner, follow the steps in [Ownership safety](#ownership-safety). + +Hook scripts do not need to be executable because Rstack CLI runs them with `sh`. + +### Command not found + +For exit code 127, Rstack CLI prints the effective `PATH`. If a GUI Git client cannot find Node.js or the package manager, initialize them in `hooks-init.sh`. + +### Windows and Yarn + +On Windows, hooks run in the POSIX shell included with [Git for Windows](https://gitforwindows.org/). Use LF line endings and `/` path separators in hooks. + +[Yarn PnP](https://yarnpkg.com/features/pnp) does not provide `node_modules/.bin`. Run tools through a Yarn script, such as `yarn run test`, and make Node.js and Yarn available through `hooks-init.sh` when needed. diff --git a/website/docs/en/guide/cli/setup.mdx b/website/docs/en/guide/cli/setup.mdx deleted file mode 100644 index 6a960433..00000000 --- a/website/docs/en/guide/cli/setup.mdx +++ /dev/null @@ -1,231 +0,0 @@ -# setup - -import { PackageManagerTabs } from '@rspress/core/theme'; - -The `rs setup` command installs repository-level [Git hooks](https://git-scm.com/docs/githooks) and runs them in the project that invokes the command. - -## Usage - -```bash -rs setup [options] -``` - -By default, hook scripts are stored in `.rstack/hooks`, relative to the Git repository root. If the current directory is not inside a Git repository, the command skips installation. - -Add `rs setup` to the `prepare` script of the project that should manage the repository hooks: - -```json title="package.json" -{ - "scripts": { - "prepare": "rs setup" - } -} -``` - -Run the script once to generate the hook files: - - - -For example, create a `pre-commit` hook that runs [`rs staged`](./staged): - -```sh title=".rstack/hooks/pre-commit" -rs staged -``` - -:::warning Existing Git hook managers - -`rs setup` updates the repository's [`core.hooksPath`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-corehooksPath). It skips installation when another hooks path or existing Git hook is detected. Run `rs setup --force` to install Rstack hooks anyway. - -::: - -## Options - -### `--force` - -`--force` (or `-f`) installs Rstack hooks even when an existing Git hooks setup is detected: - -```bash -rs setup --force -``` - -Rstack preserves the existing hook files and sets `core.hooksPath` to its generated hooks directory. While this setting is active, Git no longer runs hooks from the previous location. - -:::tip -Run `rs setup --force` only once. Use `rs setup` without `--force` in the `prepare` script. -::: - -### `--hooks-dir` - -Sets the directory for hook scripts, relative to the Git repository root. - -```bash -rs setup --hooks-dir config/git-hooks - -# Quote paths that contain spaces -rs setup --hooks-dir "config/git hooks" -``` - -When using a custom directory, add the full command to the `prepare` script of the project that manages hooks: - -```json title="package.json" -{ - "scripts": { - "prepare": "rs setup --hooks-dir config/git-hooks" - } -} -``` - -> To prevent Git hook files from being created or overwritten outside the repository through parent directory paths, the path must not contain `..`. - -### `--help` - -`--help` (or `-h`) displays the command's usage and options. - -```bash -rs setup --help -``` - -## Hook files - -The default directory structure is: - -```text -.rstack/ -└── hooks/ - ├── pre-commit # Repository hook script: edit and commit - └── _/ # Generated by rs setup; ignored by Git - ├── .gitignore - ├── .owner - ├── runner - ├── pre-commit - ├── commit-msg - └── ... -``` - -Files next to `_` are repository hook scripts. The `_` directory contains generated files and is ignored by Git. `rs setup` points `core.hooksPath` to `.rstack/hooks/_`; rerun it after cloning the repository or when generated files are missing. - -## Supported hooks - -Rstack CLI supports these client-side Git hooks: - -- `pre-commit` -- `pre-merge-commit` -- `prepare-commit-msg` -- `commit-msg` -- `post-commit` -- `applypatch-msg` -- `pre-applypatch` -- `post-applypatch` -- `pre-rebase` -- `post-rewrite` -- `post-checkout` -- `post-merge` -- `pre-push` -- `pre-auto-gc` - -Create a file with the matching name next to the `_` directory. - -## Hook runtime - -Rstack CLI runs hook scripts with POSIX `sh -e`, forwards Git's arguments and standard input, and returns the hook's exit code. Before running a hook, it changes to the project that installed the hooks and prepends that project's `node_modules/.bin` to `PATH`. - -### Disable and debug - -Set `RSTACK_HOOKS=0` to skip installation or hook execution: - -```bash -RSTACK_HOOKS=0 git commit -m "Skip hooks" -``` - -Set `RSTACK_HOOKS=2` to trace the Rstack CLI hook runtime, including how it invokes the hook script and handles its exit code; to trace commands inside the hook script, add `set -x` to the script: - -```bash -RSTACK_HOOKS=2 git commit -m "Trace hooks" -``` - -### Configure the hook environment - -Before running a hook script, Rstack CLI loads this optional POSIX shell file: - -```text -${XDG_CONFIG_HOME:-$HOME/.config}/rstack/hooks-init.sh -``` - -Use it to initialize a Node.js version manager, update `PATH`, or set `RSTACK_HOOKS=0` for the current user. - -## Monorepo - -In a monorepo, the project that provides Rstack CLI may be located in a subdirectory such as `frontend/`. Running `rs setup` from that directory still installs hooks at the Git repository root: - -```text -repo/.rstack/hooks/ -repo/.rstack/hooks/_/ -core.hooksPath=.rstack/hooks/_ -``` - -Rstack CLI records `frontend` as the project that owns the hooks. Hook scripts remain at the repository root, but run from `frontend`, so they can use its configuration and dependencies without an explicit `cd`: - -```sh title=".rstack/hooks/pre-commit" -rs staged -``` - -A Git repository has one hooks owner. Only that project should include `rs setup` in its `prepare` script. Calls from another project are skipped with a warning. - -To change the owner, remove `rs setup` from the previous project's `prepare` script, delete the generated `_` directory, and then run `rs setup` from the new project. - -## Remove hooks - -To remove hooks managed by Rstack CLI: - -1. Remove `rs setup` from the `prepare` script. -2. Check which Git configuration scope defines the active hooks path: - - ```bash - git config --show-scope --get core.hooksPath - ``` - -3. Unset the hooks path in the reported scope. For `local`, run: - - ```bash - git config --local --unset core.hooksPath - ``` - - For `worktree`, run: - - ```bash - git config --worktree --unset core.hooksPath - ``` - - If `--force` previously preserved hooks under `.git/hooks`, unsetting the path reactivates those files. Delete any obsolete files first if you do not want them to run. - -4. Delete `.rstack/hooks/`, or the directory passed to `--hooks-dir`. - -## Troubleshooting - -### Hook does not run - -- Check that the hook script has a [supported name](#supported-hooks) and is next to the `_` directory. -- Run `git config --show-scope --get core.hooksPath` and verify the effective scope and path. -- Rerun `rs setup` to restore generated files and executable permissions. -- Check that `RSTACK_HOOKS` is not set to `0` in the environment or initialization file. -- If another hooks setup is reported, run `rs setup --force`. -- If another project is reported as the hooks owner, follow the ownership transfer steps in [Monorepo](#monorepo). - -Hook scripts do not need to be executable because Rstack CLI runs them with `sh`. - -### Command not found - -For exit code 127, Rstack CLI prints the effective `PATH`. If a GUI Git client cannot find Node.js or the package manager, initialize them in `hooks-init.sh`. - -### Windows and Yarn - -On Windows, hooks run in the POSIX shell included with [Git for Windows](https://gitforwindows.org/). Use LF line endings and `/` path separators in hooks. - -[Yarn PnP](https://yarnpkg.com/features/pnp) does not provide `node_modules/.bin`. Run tools through a Yarn script, such as `yarn run test`, and make Node.js and Yarn available through `hooks-init.sh` when needed. diff --git a/website/docs/en/guide/cli/staged.mdx b/website/docs/en/guide/cli/staged.mdx index d3938e6b..941a26bc 100644 --- a/website/docs/en/guide/cli/staged.mdx +++ b/website/docs/en/guide/cli/staged.mdx @@ -1,10 +1,14 @@ +--- +description: 'Run lint-staged tasks against Git-staged files with the rs staged command.' +--- + # staged The `rs staged` command uses [lint-staged](https://github.com/lint-staged/lint-staged) to run tasks against files staged in Git. `rs staged` can run linters, formatters, or other checks on staged files before committing code. -A common pattern is to pair `rs staged` with [`rs setup`](./setup) and run staged-file tasks from a `pre-commit` hook. +A common pattern is to pair `rs staged` with [`rs hooks`](./hooks) and run staged-file tasks from a `pre-commit` hook. ## Usage diff --git a/website/docs/en/guide/git-hooks.mdx b/website/docs/en/guide/git-hooks.mdx index 800f9016..e59c6ff7 100644 --- a/website/docs/en/guide/git-hooks.mdx +++ b/website/docs/en/guide/git-hooks.mdx @@ -6,18 +6,18 @@ description: 'Set up repository Git hooks with Rstack CLI and automatically lint import { PackageManagerTabs } from '@rspress/core/theme'; -Use [`rs setup`](./cli/setup) to manage repository-level Git hooks that run project commands. By default, hook scripts live in `.rstack/hooks`. You can use them to validate commit messages, check code before pushing, or format files before committing. +Use [`rs hooks`](./cli/hooks) to manage repository-level Git hooks that run project commands. By default, hook scripts live in `.rstack/hooks`. You can use them to validate commit messages, check code before pushing, or format files before committing. -This page uses `pre-commit` as an example: first install Git hooks with `rs setup`, then run [`rs staged`](./cli/staged) from the `pre-commit` hook to lint and format the files staged for the commit. +This page uses `pre-commit` as an example: first install Git hooks with `rs hooks`, then run [`rs staged`](./cli/staged) from the `pre-commit` hook to lint and format the files staged for the commit. ## Set up hooks -Add `rs setup` to the `prepare` script of the project that owns the repository hooks: +Add `rs hooks` to the `prepare` script of the project that owns the repository hooks: ```json title="package.json" { "scripts": { - "prepare": "rs setup" + "prepare": "rs hooks" } } ``` @@ -33,7 +33,7 @@ Run the script once to install the hooks: }} /> -`rs setup` sets the repository's `core.hooksPath` to `.rstack/hooks/_`. Verify the installation with: +`rs hooks` sets the repository's `core.hooksPath` to `.rstack/hooks/_`. Verify the installation with: ```bash git config --get core.hooksPath @@ -43,7 +43,7 @@ git config --get core.hooksPath :::tip - The `_` directory is generated dynamically and ignored by Git by default. -- If `rs setup` detects another hooks path or existing Git hooks, it skips installation. Run `rs setup --force` to install Rstack hooks anyway. See the [`rs setup` guide](./cli/setup) for details. +- If `rs hooks` detects another hooks path or existing Git hooks, it skips installation. Run `rs hooks --force` to install Rstack hooks anyway. See the [`rs hooks` guide](./cli/hooks) for details. ::: @@ -74,7 +74,7 @@ rs staged ### How it works -When you run `git commit`, Git invokes the hook installed by `rs setup`. The hook executes `.rstack/hooks/pre-commit`, and `rs staged` then runs the configured tasks on the staged files. +When you run `git commit`, Git invokes the hook installed by `rs hooks`. The hook executes `.rstack/hooks/pre-commit`, and `rs staged` then runs the configured tasks on the staged files. `rs staged` passes matching staged files to each command. Commands in an array run in order: [`rs lint --fix`](./cli/lint) first applies available fixes, then [`rs fmt`](./cli/fmt) formats the result. Remove `--fix` if lint errors should block the commit without changing files. diff --git a/website/docs/en/guide/quick-start.mdx b/website/docs/en/guide/quick-start.mdx index ec64840d..452237d1 100644 --- a/website/docs/en/guide/quick-start.mdx +++ b/website/docs/en/guide/quick-start.mdx @@ -148,7 +148,7 @@ The following commands are available: - [`rs check`](./cli/check): Run linting and formatting checks, with optional TypeScript type checking. - [`rs lint`](./cli/lint): Lint source code with Rslint. - [`rs fmt`](./cli/fmt): Format code. -- [`rs setup`](./cli/setup): Install repository-level Git hooks. +- [`rs hooks`](./cli/hooks): Install, update, or uninstall repository-level Git hooks. - [`rs staged`](./cli/staged): Run tasks against files staged in Git with lint-staged. ## Configure Rstack CLI \{#configure-rstack} diff --git a/website/docs/zh/guide/cli/_meta.json b/website/docs/zh/guide/cli/_meta.json index 5357606a..c228ad7a 100644 --- a/website/docs/zh/guide/cli/_meta.json +++ b/website/docs/zh/guide/cli/_meta.json @@ -8,6 +8,6 @@ "check", "lint", "fmt", - "setup", + "hooks", "staged" ] diff --git a/website/docs/zh/guide/cli/hooks.mdx b/website/docs/zh/guide/cli/hooks.mdx new file mode 100644 index 00000000..9131cc5c --- /dev/null +++ b/website/docs/zh/guide/cli/hooks.mdx @@ -0,0 +1,247 @@ +--- +description: '使用 Rstack CLI 安装、配置、排查并安全移除仓库级 Git hooks。' +--- + +# hooks + +import { PackageManagerTabs } from '@rspress/core/theme'; + +`rs hooks` 命令用于安装、更新和卸载仓库级 [Git hooks](https://git-scm.com/docs/githooks)。hook 脚本会在执行安装命令的项目目录中运行。 + +## 用法 \{#usage} + +```bash +rs hooks [options] +rs hooks uninstall +``` + +直接运行 `rs hooks` 即可安装或更新 hooks。 + +:::tip 别名 +`rs setup` 是 `rs hooks` 的别名。 +::: + +## 安装 hooks \{#install-hooks} + +hook 脚本默认存放在 Git 仓库根目录的 `.rstack/hooks` 中。如果当前目录不在 Git 仓库内,命令会跳过安装。 + +在负责管理仓库 hooks 的项目中,将 `rs hooks` 添加到 `package.json` 的 `prepare` 脚本: + +```json title="package.json" +{ + "scripts": { + "prepare": "rs hooks" + } +} +``` + +执行一次该脚本,完成 hooks 安装: + + + +例如,创建一个 `pre-commit` hook,并在其中运行 [`rs staged`](./staged): + +```sh title=".rstack/hooks/pre-commit" +rs staged +``` + +安装命令可以安全地重复执行,并且不会加载 `rstack.config.*`。克隆仓库后或生成的 hook 文件缺失时,请再次运行 `rs hooks`。 + +:::warning 已有 Git hook 管理工具 + +`rs hooks` 会更新仓库的 [`core.hooksPath`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-corehooksPath)。如果检测到其他 hooks 路径或已有 Git hooks,命令会跳过安装。可运行 `rs hooks --force`,改用 Rstack 管理 hooks。 + +::: + +## 安装选项 \{#install-options} + +### `--force` + +即使已经存在其他 Git hooks 配置,也可以使用 `--force`(或 `-f`)安装 Rstack hooks: + +```bash +rs hooks --force +``` + +Rstack 会保留原有 hook 文件,并将 `core.hooksPath` 指向其生成的 hooks 目录。该配置生效期间,Git 不会执行原路径下的 hooks。 + +`--force` 无法替换由其他 Rstack 项目管理的 hooks。 + +:::tip +`rs hooks --force` 只需运行一次。`prepare` 脚本中应使用不带 `--force` 的 `rs hooks`。 +::: + +### `--hooks-dir` + +指定 hook 脚本的自定义存放目录,路径相对于 Git 仓库根目录。 + +```bash +rs hooks --hooks-dir config/git-hooks + +# 路径包含空格时需要使用引号 +rs hooks --hooks-dir "config/git hooks" +``` + +使用自定义目录时,请将完整命令写入负责管理 hooks 的项目 `package.json`: + +```json title="package.json" +{ + "scripts": { + "prepare": "rs hooks --hooks-dir config/git-hooks" + } +} +``` + +> 路径中不能包含 `..`,以免通过父目录路径在仓库之外创建或覆盖 hook 文件。 + +### `--help` + +`--help`(或 `-h`)用于显示命令的用法、子命令和安装选项。 + +```bash +rs hooks --help +``` + +## 卸载 hooks \{#uninstall-hooks} + +请在安装 hooks 的项目目录中运行: + +```bash +rs hooks uninstall +``` + +Rstack 会自动定位当前生效的 hooks,并移除 `core.hooksPath` 配置和生成的 `_` 目录,但不会删除 `.rstack/hooks/pre-commit` 等项目 hook 文件。由其他项目或工具管理的 hooks 不会被删除。 + +如果不希望再次安装 hooks,请从 `prepare` 脚本中移除 `rs hooks`。 + +如果之前使用 `--force` 接管了 `.git/hooks` 中的 hooks,卸载后这些 hooks 会重新生效。 + +## Hook 文件 \{#hook-files} + +默认目录结构如下: + +```text +.rstack/ +└── hooks/ + ├── pre-commit # 仓库 hook 脚本:编辑并提交 + └── _/ # 由 rs hooks 生成;默认被 Git 忽略 + ├── .gitignore + ├── .owner + ├── runner + ├── pre-commit + ├── commit-msg + └── ... +``` + +与 `_` 同级的文件是仓库 hook 脚本。`_` 目录包含生成文件,并由 Git 忽略。`rs hooks` 会将 `core.hooksPath` 指向 `.rstack/hooks/_`。 + +## 支持的 hooks \{#supported-hooks} + +Rstack CLI 支持以下客户端 Git hooks: + +- `pre-commit` +- `pre-merge-commit` +- `prepare-commit-msg` +- `commit-msg` +- `post-commit` +- `applypatch-msg` +- `pre-applypatch` +- `post-applypatch` +- `pre-rebase` +- `post-rewrite` +- `post-checkout` +- `post-merge` +- `pre-push` +- `pre-auto-gc` + +在与 `_` 同级的位置创建对应的同名文件即可。 + +## Hook 运行时 \{#hook-runtime} + +Rstack CLI 使用 POSIX `sh -e` 运行 hook 脚本。它会转发 Git 提供的参数和标准输入,并返回 hook 的退出码。运行 hook 前,Rstack 会切换到安装 hooks 的项目,并将该项目的 `node_modules/.bin` 添加到 `PATH` 开头。 + +### 禁用与调试 \{#disable-and-debug} + +将 `RSTACK_HOOKS` 设为 `0`,可以跳过安装或 hook 执行: + +```bash +RSTACK_HOOKS=0 git commit -m "Skip hooks" +``` + +将 `RSTACK_HOOKS` 设为 `2`,可以跟踪 Rstack CLI 的 hook 运行时,包括调用 hook 脚本和处理退出码等步骤;如需跟踪 hook 脚本内部的命令,请在脚本中添加 `set -x`: + +```bash +RSTACK_HOOKS=2 git commit -m "Trace hooks" +``` + +### 配置 hook 运行环境 \{#configure-the-hook-environment} + +运行 hook 脚本前,Rstack CLI 会加载以下可选的 POSIX shell 文件: + +```text +${XDG_CONFIG_HOME:-$HOME/.config}/rstack/hooks-init.sh +``` + +可以在其中初始化 Node.js 版本管理器、更新 `PATH`,或为当前用户设置 `RSTACK_HOOKS=0`。 + +## 所有权保护 \{#ownership-safety} + +每个生成的 hooks 目录都会记录所属项目。只有该项目应在 `prepare` 脚本中调用 `rs hooks`。其他项目即使使用 `--force` 调用安装命令,也会被跳过;卸载命令同样会拒绝删除其他项目的 hooks。 + +如需转移所有权: + +1. 从原项目的 `prepare` 脚本中移除 `rs hooks`。 +2. 在原项目中运行 `rs hooks uninstall`。 +3. 将 `rs hooks` 添加到新项目的 `prepare` 脚本,并运行一次。 + +## Monorepo \{#monorepo} + +在 monorepo 中,提供 Rstack CLI 的项目可能位于 `frontend/` 等子目录。在该目录中运行 `rs hooks`,hooks 仍会安装到 Git 仓库根目录: + +```text +repo/.rstack/hooks/ +repo/.rstack/hooks/_/ +core.hooksPath=.rstack/hooks/_ +``` + +Rstack 会将 `frontend` 记录为 hooks 所属项目。hook 脚本仍保存在仓库根目录,但会从 `frontend` 目录运行,因此可以直接使用其中的配置和依赖,无需显式执行 `cd`: + +```sh title=".rstack/hooks/pre-commit" +rs staged +``` + +## Worktree 行为 \{#worktree-behavior} + +Rstack 会保留当前生效的 `core.hooksPath` 所在的 Git 配置作用域。如果 linked worktree 使用的是 worktree 作用域配置,`rs hooks --force` 会在同一作用域中替换它,而不会写入一个被 Git 忽略的 local 配置。 + +`rs hooks uninstall` 也会从同一作用域中移除配置。卸载 worktree 作用域的安装只影响当前 worktree,不会改变其他 worktree 中的 hooks。local 作用域的 `core.hooksPath` 会在多个 worktree 之间共享,因此更改或移除该配置会影响所有未覆盖它的 worktree。 + +## 故障排查 \{#troubleshooting} + +### Hook 未运行 \{#hook-does-not-run} + +- 确认 hook 脚本使用[支持的名称](#supported-hooks),并与 `_` 目录同级。 +- 运行 `git config --show-scope --get core.hooksPath`,检查当前生效的配置作用域和路径。 +- 重新运行 `rs hooks`,恢复生成文件及其可执行权限。 +- 检查环境变量或初始化文件中是否设置了 `RSTACK_HOOKS=0`。 +- 如果命令检测到其他 hooks 配置,请运行 `rs hooks --force`。 +- 如果命令提示其他项目是 hooks owner,请按照[所有权保护](#ownership-safety)中的步骤转移 owner。 + +hook 脚本不需要可执行权限,因为 Rstack CLI 会使用 `sh` 运行它。 + +### 找不到命令 \{#command-not-found} + +退出码为 127 时,Rstack CLI 会打印实际生效的 `PATH`。如果 GUI Git 客户端找不到 Node.js 或包管理器,请在 `hooks-init.sh` 中初始化相关环境。 + +### Windows 与 Yarn \{#windows-and-yarn} + +在 Windows 上,hooks 会通过 [Git for Windows](https://gitforwindows.org/) 自带的 POSIX shell 运行。请在 hook 中使用 LF 换行符和 `/` 路径分隔符。 + +[Yarn PnP](https://yarnpkg.com/features/pnp) 不提供 `node_modules/.bin`。请通过 Yarn 脚本运行工具,例如 `yarn run test`;必要时可通过 `hooks-init.sh` 配置 Node.js 和 Yarn。 diff --git a/website/docs/zh/guide/cli/setup.mdx b/website/docs/zh/guide/cli/setup.mdx deleted file mode 100644 index cdef8ab9..00000000 --- a/website/docs/zh/guide/cli/setup.mdx +++ /dev/null @@ -1,231 +0,0 @@ -# setup - -import { PackageManagerTabs } from '@rspress/core/theme'; - -`rs setup` 命令用于安装仓库级 [Git hooks](https://git-scm.com/docs/githooks),并在调用该命令的项目中运行 hooks。 - -## 用法 \{#usage} - -```bash -rs setup [options] -``` - -hook 脚本默认存放在 Git 仓库根目录下的 `.rstack/hooks`。如果当前目录不属于 Git 仓库,命令会跳过安装。 - -在负责管理仓库 hooks 的项目 `package.json` 中添加 `prepare` 脚本: - -```json title="package.json" -{ - "scripts": { - "prepare": "rs setup" - } -} -``` - -执行一次该脚本,生成 hook 文件: - - - -例如,创建一个 `pre-commit` hook,并在其中运行 [`rs staged`](./staged): - -```sh title=".rstack/hooks/pre-commit" -rs staged -``` - -:::warning 已有 Git hook 管理工具 - -`rs setup` 会更新仓库的 [`core.hooksPath`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-corehooksPath)。检测到其他 hooks 路径或已有 Git hook 时,命令会跳过安装。可运行 `rs setup --force` 强制安装 Rstack hooks。 - -::: - -## 选项 \{#options} - -### `--force` - -检测到已有 Git hooks 配置时,可使用 `--force`(或 `-f`)强制安装 Rstack hooks: - -```bash -rs setup --force -``` - -Rstack 会保留原有 hook 文件,并将 `core.hooksPath` 指向 Rstack 生成的 hooks 目录。该配置生效期间,Git 不再执行原路径下的 hooks。 - -:::tip -`rs setup --force` 只需运行一次。`prepare` 脚本中应使用不带 `--force` 的 `rs setup`。 -::: - -### `--hooks-dir` - -设置 hook 脚本的存放目录,路径相对于 Git 仓库根目录。 - -```bash -rs setup --hooks-dir config/git-hooks - -# 路径包含空格时需要使用引号 -rs setup --hooks-dir "config/git hooks" -``` - -使用自定义目录时,请将完整命令写入负责管理 hooks 的项目 `package.json`: - -```json title="package.json" -{ - "scripts": { - "prepare": "rs setup --hooks-dir config/git-hooks" - } -} -``` - -> 为避免通过父目录路径在仓库之外创建或覆盖 Git hook 文件,路径中不能包含 `..`。 - -### `--help` - -`--help`(或 `-h`)用于显示命令的用法和选项。 - -```bash -rs setup --help -``` - -## Hook 文件 \{#hook-files} - -默认目录结构如下: - -```text -.rstack/ -└── hooks/ - ├── pre-commit # 仓库 hook 脚本:编辑并提交 - └── _/ # 由 rs setup 生成;默认被 Git 忽略 - ├── .gitignore - ├── .owner - ├── runner - ├── pre-commit - ├── commit-msg - └── ... -``` - -与 `_` 同级的文件是仓库 hook 脚本。`_` 目录包含生成文件,并由 Git 忽略。`rs setup` 会将 `core.hooksPath` 指向 `.rstack/hooks/_`;克隆仓库后或生成文件缺失时,请重新运行该命令。 - -## 支持的 hooks \{#supported-hooks} - -Rstack CLI 支持以下客户端 Git hooks: - -- `pre-commit` -- `pre-merge-commit` -- `prepare-commit-msg` -- `commit-msg` -- `post-commit` -- `applypatch-msg` -- `pre-applypatch` -- `post-applypatch` -- `pre-rebase` -- `post-rewrite` -- `post-checkout` -- `post-merge` -- `pre-push` -- `pre-auto-gc` - -在与 `_` 同级的位置创建对应的同名文件即可。 - -## Hook 运行时 \{#hook-runtime} - -Rstack CLI 使用 POSIX `sh -e` 运行 hook 脚本,并转发 Git 提供的参数和标准输入,同时返回 hook 的退出码。运行 hook 前,Rstack CLI 会切换到安装 hooks 的项目,并将该项目的 `node_modules/.bin` 添加到 `PATH` 开头。 - -### 禁用与调试 \{#disable-and-debug} - -将 `RSTACK_HOOKS` 设为 `0`,可以跳过安装或 hook 执行: - -```bash -RSTACK_HOOKS=0 git commit -m "Skip hooks" -``` - -将 `RSTACK_HOOKS` 设为 `2`,可以跟踪 Rstack CLI 的 hook 运行时,包括调用 hook 脚本和处理退出码等步骤;如需跟踪 hook 脚本内部的命令,请在脚本中添加 `set -x`: - -```bash -RSTACK_HOOKS=2 git commit -m "Trace hooks" -``` - -### 配置 hook 运行环境 \{#configure-the-hook-environment} - -运行 hook 脚本前,Rstack CLI 会加载以下可选的 POSIX shell 文件: - -```text -${XDG_CONFIG_HOME:-$HOME/.config}/rstack/hooks-init.sh -``` - -可以在其中初始化 Node.js 版本管理器、更新 `PATH`,或为当前用户设置 `RSTACK_HOOKS=0`。 - -## Monorepo \{#monorepo} - -在 monorepo 中,提供 Rstack CLI 的项目可能位于 `frontend/` 等子目录。从该目录运行 `rs setup` 时,hooks 仍会安装到 Git 仓库根目录: - -```text -repo/.rstack/hooks/ -repo/.rstack/hooks/_/ -core.hooksPath=.rstack/hooks/_ -``` - -Rstack CLI 会将 `frontend` 记录为负责管理 hooks 的项目。hook 脚本仍位于仓库根目录,但会从 `frontend` 目录运行,因此可以直接使用其中的配置和依赖,无需显式执行 `cd`: - -```sh title=".rstack/hooks/pre-commit" -rs staged -``` - -一个 Git 仓库只能有一个 hooks owner。只有负责管理 hooks 的项目应在 `prepare` 脚本中调用 `rs setup`。其他项目调用时会收到警告并跳过。 - -如需更换 owner,请先从原项目的 `prepare` 脚本中移除 `rs setup`,删除生成的 `_` 目录,再从新项目运行 `rs setup`。 - -## 移除 hooks \{#remove-hooks} - -如需移除由 Rstack CLI 管理的 hooks: - -1. 从 `prepare` 脚本中移除 `rs setup`。 -2. 检查当前生效的 hooks 路径来自哪个 Git 配置作用域: - - ```bash - git config --show-scope --get core.hooksPath - ``` - -3. 根据输出,在对应作用域中取消 hooks 路径配置。作用域为 `local` 时运行: - - ```bash - git config --local --unset core.hooksPath - ``` - - 作用域为 `worktree` 时运行: - - ```bash - git config --worktree --unset core.hooksPath - ``` - - 如果之前通过 `--force` 保留了 `.git/hooks` 下的 hooks,取消路径配置会重新启用这些文件。如果不希望它们运行,请先删除不再需要的文件。 - -4. 删除 `.rstack/hooks/` 或通过 `--hooks-dir` 指定的目录。 - -## 故障排查 \{#troubleshooting} - -### Hook 未运行 \{#hook-does-not-run} - -- 确认 hook 脚本使用[支持的名称](#supported-hooks),并与 `_` 目录同级。 -- 运行 `git config --show-scope --get core.hooksPath`,检查当前生效的配置作用域和路径。 -- 重新运行 `rs setup`,恢复生成文件及其可执行权限。 -- 检查环境变量或初始化文件中是否设置了 `RSTACK_HOOKS=0`。 -- 如果命令提示存在其他 hooks 配置,请运行 `rs setup --force`。 -- 如果命令提示其他项目是 hooks owner,请按照 [Monorepo](#monorepo) 中的步骤转移 owner。 - -hook 脚本不需要可执行权限,因为 Rstack CLI 会使用 `sh` 运行它。 - -### 找不到命令 \{#command-not-found} - -退出码为 127 时,Rstack CLI 会打印实际生效的 `PATH`。如果 GUI Git 客户端找不到 Node.js 或包管理器,请在 `hooks-init.sh` 中初始化相关环境。 - -### Windows 与 Yarn \{#windows-and-yarn} - -在 Windows 上,hooks 会通过 [Git for Windows](https://gitforwindows.org/) 自带的 POSIX shell 运行。请在 hook 中使用 LF 换行符和 `/` 路径分隔符。 - -[Yarn PnP](https://yarnpkg.com/features/pnp) 不提供 `node_modules/.bin`。请通过 Yarn 脚本运行工具,例如 `yarn run test`;必要时可通过 `hooks-init.sh` 配置 Node.js 和 Yarn。 diff --git a/website/docs/zh/guide/cli/staged.mdx b/website/docs/zh/guide/cli/staged.mdx index 4821c3ca..897c7f79 100644 --- a/website/docs/zh/guide/cli/staged.mdx +++ b/website/docs/zh/guide/cli/staged.mdx @@ -1,10 +1,14 @@ +--- +description: '使用 rs staged 命令对 Git 暂存文件运行 lint-staged 任务。' +--- + # staged `rs staged` 命令使用 [lint-staged](https://github.com/lint-staged/lint-staged) 对 Git 暂存文件运行任务。 `rs staged` 可用于在提交代码前,对暂存文件运行 linter、格式化工具或其他检查。 -常见做法是将 `rs staged` 与 [`rs setup`](./setup) 配合使用,通过 `pre-commit` hook 运行暂存文件任务。 +常见做法是将 `rs staged` 与 [`rs hooks`](./hooks) 配合使用,通过 `pre-commit` hook 运行暂存文件任务。 ## 用法 \{#usage} diff --git a/website/docs/zh/guide/git-hooks.mdx b/website/docs/zh/guide/git-hooks.mdx index 3a04d20f..bdb3be75 100644 --- a/website/docs/zh/guide/git-hooks.mdx +++ b/website/docs/zh/guide/git-hooks.mdx @@ -6,18 +6,18 @@ description: '使用 Rstack CLI 配置仓库级 Git hooks,并在提交前自 import { PackageManagerTabs } from '@rspress/core/theme'; -使用 [`rs setup`](./cli/setup) 可以统一管理仓库级 Git hooks,并通过 hook 脚本运行项目命令。hook 脚本默认存放在 `.rstack/hooks` 中,可用于校验提交信息、推送前检查代码、提交前格式化文件等场景。 +使用 [`rs hooks`](./cli/hooks) 可以统一管理仓库级 Git hooks,并通过 hook 脚本运行项目命令。hook 脚本默认存放在 `.rstack/hooks` 中,可用于校验提交信息、推送前检查代码、提交前格式化文件等场景。 -下面以 `pre-commit` 为例:先通过 `rs setup` 安装 Git hooks,再在 `pre-commit` hook 中运行 [`rs staged`](./cli/staged),对本次提交的暂存文件进行代码检查和格式化。 +下面以 `pre-commit` 为例:先通过 `rs hooks` 安装 Git hooks,再在 `pre-commit` hook 中运行 [`rs staged`](./cli/staged),对本次提交的暂存文件进行代码检查和格式化。 ## 安装 hooks \{#set-up-hooks} -在负责管理仓库 hooks 的项目中,将 `rs setup` 添加到 `package.json` 的 `prepare` 脚本: +在负责管理仓库 hooks 的项目中,将 `rs hooks` 添加到 `package.json` 的 `prepare` 脚本: ```json title="package.json" { "scripts": { - "prepare": "rs setup" + "prepare": "rs hooks" } } ``` @@ -33,7 +33,7 @@ import { PackageManagerTabs } from '@rspress/core/theme'; }} /> -`rs setup` 会将仓库的 `core.hooksPath` 设为 `.rstack/hooks/_`,可以通过以下命令确认是否安装成功: +`rs hooks` 会将仓库的 `core.hooksPath` 设为 `.rstack/hooks/_`,可以通过以下命令确认是否安装成功: ```bash git config --get core.hooksPath @@ -43,7 +43,7 @@ git config --get core.hooksPath :::tip - `_` 目录由命令动态生成,且默认被 Git 忽略。 -- 如果检测到其他 hooks 路径或已有 Git hooks,`rs setup` 会跳过安装。可运行 `rs setup --force` 强制安装 Rstack hooks。详细说明请参考 [`rs setup` 指南](./cli/setup)。 +- 如果检测到其他 hooks 路径或已有 Git hooks,`rs hooks` 会跳过安装。可运行 `rs hooks --force` 强制安装 Rstack hooks。详细说明请参考 [`rs hooks` 指南](./cli/hooks)。 ::: @@ -74,7 +74,7 @@ rs staged ### 执行流程 \{#how-it-works} -运行 `git commit` 时,Git 会调用 `rs setup` 安装的 hook。该 hook 会执行 `.rstack/hooks/pre-commit`,再由 `rs staged` 对暂存文件运行配置的任务。 +运行 `git commit` 时,Git 会调用 `rs hooks` 安装的 hook。该 hook 会执行 `.rstack/hooks/pre-commit`,再由 `rs staged` 对暂存文件运行配置的任务。 `rs staged` 会将匹配的暂存文件传给对应命令,数组中的命令按顺序执行,[`rs lint --fix`](./cli/lint) 先修复可自动处理的问题,再由 [`rs fmt`](./cli/fmt) 统一格式。如果只希望代码检查阻止提交而不修改文件,可以移除 `--fix`。 diff --git a/website/docs/zh/guide/quick-start.mdx b/website/docs/zh/guide/quick-start.mdx index 4cc43cca..074a84f7 100644 --- a/website/docs/zh/guide/quick-start.mdx +++ b/website/docs/zh/guide/quick-start.mdx @@ -148,7 +148,7 @@ Rstack CLI 提供以下命令: - [`rs check`](./cli/check):运行 lint 和格式检查,并可选启用 TypeScript 类型检查。 - [`rs lint`](./cli/lint):使用 Rslint 检查源代码。 - [`rs fmt`](./cli/fmt):格式化代码。 -- [`rs setup`](./cli/setup):安装仓库级 Git hooks。 +- [`rs hooks`](./cli/hooks):安装、更新或卸载仓库级 Git hooks。 - [`rs staged`](./cli/staged):使用 lint-staged 对 Git 暂存区中的文件运行任务。 ## 配置 Rstack CLI \{#configure-rstack}