@pyyupsk/nit 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,6 +10,7 @@ Configure hooks in `package.json` — no extra config files, no dependencies.
10
10
  - Config lives in `package.json` under a `"nit"` key
11
11
  - Auto-installs hooks via the `prepare` lifecycle script
12
12
  - CI-safe `check` command — exits 1 when hooks are out of sync
13
+ - Staged hooks — run commands only on files that match a glob pattern
13
14
  - Works with any package manager (bun, npm, pnpm, yarn)
14
15
 
15
16
  ## Installation
@@ -45,12 +46,34 @@ Add a `"nit"` key to your `package.json`:
45
46
 
46
47
  Any valid [Git hook name](https://git-scm.com/docs/githooks) is supported as a key. The value is the shell command to run.
47
48
 
49
+ ### Staged Hooks
50
+
51
+ Instead of a plain command string, a hook can be defined as a `stages` object. Each key is a glob pattern and the value is the command to run on the matching staged files. Use `{staged_files}` as a placeholder — nit replaces it with the list of matched files before running the command.
52
+
53
+ ```json
54
+ {
55
+ "nit": {
56
+ "hooks": {
57
+ "pre-commit": {
58
+ "stages": {
59
+ "**/*.{ts,js}": "biome check --write {staged_files}",
60
+ "**/*.css": "stylelint --fix {staged_files}"
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ Stages only run when at least one staged file matches the pattern. If no files match a stage, that stage is skipped entirely. Commands without `{staged_files}` run as-is (useful for project-wide checks triggered by any staged file).
69
+
48
70
  ## CLI
49
71
 
50
72
  ```sh
51
- nit install # write hooks to .git/hooks/ (default when run bare)
52
- nit sync # alias for install
53
- nit check # exit 1 if installed hooks differ from config (CI-safe)
73
+ nit install # write hooks to .git/hooks/ (default when run bare)
74
+ nit sync # alias for install
75
+ nit check # exit 1 if installed hooks differ from config (CI-safe)
76
+ nit exec <hook> # run the staged hook for <hook> against currently staged files
54
77
  ```
55
78
 
56
79
  ### `nit install` / `nit sync`
@@ -71,6 +94,14 @@ $ nit check
71
94
  nit: hooks are up to date
72
95
  ```
73
96
 
97
+ ### `nit exec <hook>`
98
+
99
+ Runs a staged hook by name against the currently staged files. This is the command that the generated hook script calls internally — you rarely need to run it directly.
100
+
101
+ ```sh
102
+ nit exec pre-commit
103
+ ```
104
+
74
105
  ## Self-hosting Example
75
106
 
76
107
  `nit` manages its own hooks:
@@ -79,7 +110,12 @@ nit: hooks are up to date
79
110
  {
80
111
  "nit": {
81
112
  "hooks": {
82
- "pre-commit": "bun format && bun check && bun typecheck && bun test && bun run build"
113
+ "pre-commit": {
114
+ "stages": {
115
+ "**/*.{ts,json}": "biome check --write {staged_files}"
116
+ }
117
+ },
118
+ "pre-push": "bun typecheck && bun test && bun run build"
83
119
  }
84
120
  }
85
121
  }
package/dist/cli.js CHANGED
@@ -1,281 +1,10 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/cli.ts
4
- import { join as join6 } from "path";
5
-
6
- // src/core/check.ts
7
- import { existsSync } from "fs";
8
- import { readdir, readFile, stat } from "fs/promises";
9
- import { join } from "path";
10
-
11
- // src/core/hook-script.ts
12
- var NIT_FINGERPRINT = '#!/bin/sh\nif [ "$SKIP_NIT"';
13
- function hookScript(cmd) {
14
- return `#!/bin/sh
2
+ import{join as rt}from"path";import{existsSync as T}from"fs";import{readdir as O,readFile as h,stat as A}from"fs/promises";import{join as k}from"path";async function p(e){try{return[null,await e]}catch(n){return[n instanceof Error?n:new Error(String(n)),null]}}var g=`#!/bin/sh
3
+ if [ "$SKIP_NIT"`;function m(e){return`./node_modules/.bin/nit exec ${e}`}function d(e,n){return`#!/bin/sh
15
4
  if [ "$SKIP_NIT" = "1" ]; then
16
5
  exit 0
17
6
  fi
18
- ${cmd}
19
- `;
20
- }
21
-
22
- // src/core/check.ts
23
- async function checkHooks(hooks, hooksDir) {
24
- try {
25
- await stat(hooksDir);
26
- } catch {
27
- return [new Error(`Hooks directory does not exist: ${hooksDir}`), null];
28
- }
29
- for (const [name, cmd] of Object.entries(hooks)) {
30
- const hookPath = join(hooksDir, name);
31
- if (!existsSync(hookPath)) {
32
- return [null, false];
33
- }
34
- let content;
35
- try {
36
- content = await readFile(hookPath, "utf8");
37
- } catch (e) {
38
- return [
39
- e instanceof Error ? e : new Error(`Failed to read hook "${name}"`),
40
- null
41
- ];
42
- }
43
- const lines = content.split("\n");
44
- const hasCmd = lines.some((line) => line.trim() === cmd.trim());
45
- if (!hasCmd) {
46
- return [null, false];
47
- }
48
- }
49
- try {
50
- const entries = await readdir(hooksDir, { withFileTypes: true });
51
- const configuredNames = new Set(Object.keys(hooks));
52
- for (const e of entries) {
53
- if (!e.isFile() || configuredNames.has(e.name)) continue;
54
- const hookPath = join(hooksDir, e.name);
55
- try {
56
- const content = await readFile(hookPath, "utf8");
57
- if (content.startsWith(NIT_FINGERPRINT)) {
58
- return [null, false];
59
- }
60
- } catch {
61
- }
62
- }
63
- } catch {
64
- }
65
- return [null, true];
66
- }
67
-
68
- // src/core/install.ts
69
- import {
70
- chmod,
71
- readdir as readdir2,
72
- readFile as readFile2,
73
- stat as stat2,
74
- unlink,
75
- writeFile
76
- } from "fs/promises";
77
- import { join as join2 } from "path";
78
- async function installHooks(hooks, hooksDir) {
79
- try {
80
- await stat2(hooksDir);
81
- } catch {
82
- return [new Error(`Hooks directory does not exist: ${hooksDir}`), null];
83
- }
84
- const written = [];
85
- for (const [name, cmd] of Object.entries(hooks)) {
86
- const hookPath = join2(hooksDir, name);
87
- try {
88
- await writeFile(hookPath, hookScript(cmd), "utf8");
89
- written.push(hookPath);
90
- await chmod(hookPath, 493);
91
- } catch (e) {
92
- await Promise.all(written.map((p) => unlink(p).catch(() => {
93
- })));
94
- return [
95
- e instanceof Error ? e : new Error(`Failed to write hook "${name}"`),
96
- null
97
- ];
98
- }
99
- }
100
- try {
101
- const entries = await readdir2(hooksDir, { withFileTypes: true });
102
- const configuredNames = new Set(Object.keys(hooks));
103
- await Promise.all(
104
- entries.filter((e) => e.isFile() && !configuredNames.has(e.name)).map(async (e) => {
105
- const hookPath = join2(hooksDir, e.name);
106
- try {
107
- const content = await readFile2(hookPath, "utf8");
108
- if (content.startsWith(NIT_FINGERPRINT)) {
109
- await unlink(hookPath);
110
- }
111
- } catch {
112
- }
113
- })
114
- );
115
- } catch {
116
- }
117
- return [null, true];
118
- }
119
-
120
- // src/core/validate.ts
121
- import { existsSync as existsSync2 } from "fs";
122
- import { join as join3 } from "path";
123
- function commandExists(bin) {
124
- const sep = process.platform === "win32" ? ";" : ":";
125
- const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
126
- const dirs = (process.env.PATH ?? "").split(sep);
127
- return dirs.some(
128
- (dir) => exts.some((ext) => existsSync2(join3(dir, bin + ext)))
129
- );
130
- }
131
- function validateHooks(hooks) {
132
- for (const [hookName, cmd] of Object.entries(hooks)) {
133
- const bin = cmd.trim().split(/\s+/).at(0);
134
- if (bin === void 0 || bin === "") {
135
- return new Error(`Hook "${hookName}" has an empty command`);
136
- }
137
- if (!commandExists(bin)) {
138
- return new Error(`Hook "${hookName}": command "${bin}" not found in PATH`);
139
- }
140
- }
141
- return null;
142
- }
143
-
144
- // src/utils/config.ts
145
- import { readFile as readFile3 } from "fs/promises";
146
- import { join as join4 } from "path";
147
- async function readConfig(cwd) {
148
- const pkgPath = join4(cwd, "package.json");
149
- let raw;
150
- try {
151
- raw = await readFile3(pkgPath, "utf8");
152
- } catch {
153
- return [new Error(`Cannot find package.json at ${pkgPath}`), null];
154
- }
155
- let pkg;
156
- try {
157
- pkg = JSON.parse(raw);
158
- } catch {
159
- return [new Error(`package.json contains invalid JSON at ${pkgPath}`), null];
160
- }
161
- if (typeof pkg !== "object" || pkg === null) {
162
- return [new Error("package.json must be a JSON object"), null];
163
- }
164
- const nit = pkg.nit;
165
- if (nit === void 0) {
166
- return [null, { hooks: {} }];
167
- }
168
- if (typeof nit !== "object" || nit === null) {
169
- return [new Error('"nit" in package.json must be an object'), null];
170
- }
171
- const nitObj = nit;
172
- const hooks = nitObj.hooks;
173
- if (hooks === void 0) {
174
- return [null, { hooks: {} }];
175
- }
176
- if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) {
177
- return [new Error("hooks must be an object in nit config"), null];
178
- }
179
- const hooksObj = hooks;
180
- for (const [name, cmd] of Object.entries(hooksObj)) {
181
- if (typeof cmd !== "string") {
182
- return [
183
- new Error(
184
- `Hook "${name}" must have a string command, got ${typeof cmd}`
185
- ),
186
- null
187
- ];
188
- }
189
- }
190
- return [null, { hooks: hooksObj }];
191
- }
192
-
193
- // src/utils/git.ts
194
- import { existsSync as existsSync3 } from "fs";
195
- import { stat as stat3 } from "fs/promises";
196
- import { dirname, join as join5, parse } from "path";
197
- async function findGitDir(startPath, stopAt) {
198
- try {
199
- await stat3(startPath);
200
- } catch {
201
- return [new Error(`Path does not exist: ${startPath}`), null];
202
- }
203
- let current = startPath;
204
- const { root } = parse(current);
205
- while (current !== root) {
206
- const candidate = join5(current, ".git");
207
- if (existsSync3(candidate)) {
208
- return [null, candidate];
209
- }
210
- if (stopAt !== void 0 && current === stopAt) break;
211
- const parent = dirname(current);
212
- if (parent === current) break;
213
- current = parent;
214
- }
215
- return [new Error(`Not a git repository (searched from ${startPath})`), null];
216
- }
217
-
218
- // src/cli.ts
219
- var silent = { log: () => {
220
- }, error: () => {
221
- } };
222
- async function run(args, cwd, logger = silent) {
223
- const command = args.at(0) ?? "install";
224
- if (!["install", "sync", "check"].includes(command)) {
225
- logger.error(`nit: unknown command "${command}"`);
226
- logger.error("Usage: nit [install|sync|check]");
227
- return 1;
228
- }
229
- const [cfgErr, config] = await readConfig(cwd);
230
- if (cfgErr !== null) {
231
- logger.error(`nit: ${cfgErr.message}`);
232
- return 1;
233
- }
234
- const [gitErr, gitDir] = await findGitDir(cwd);
235
- if (gitErr !== null) {
236
- if (command !== "check") return 0;
237
- logger.error(`nit: ${gitErr.message}`);
238
- return 1;
239
- }
240
- const hooksDir = join6(gitDir, "hooks");
241
- if (command === "check") {
242
- const [err, inSync] = await checkHooks(config.hooks, hooksDir);
243
- if (err !== null) {
244
- logger.error(`nit: ${err.message}`);
245
- return 1;
246
- }
247
- if (!inSync) {
248
- logger.error("nit: hooks are out of sync \u2014 run `nit install` to fix");
249
- return 1;
250
- }
251
- logger.log("nit: hooks are up to date");
252
- return 0;
253
- }
254
- const validErr = validateHooks(config.hooks);
255
- if (validErr !== null) {
256
- logger.error(`nit: ${validErr.message}`);
257
- return 1;
258
- }
259
- const [installErr] = await installHooks(config.hooks, hooksDir);
260
- if (installErr !== null) {
261
- logger.error(`nit: ${installErr.message}`);
262
- return 1;
263
- }
264
- const hookNames = Object.keys(config.hooks);
265
- if (hookNames.length === 0) {
266
- logger.log("nit: no hooks configured");
267
- } else {
268
- logger.log(`nit: installed ${hookNames.join(", ")}`);
269
- }
270
- return 0;
271
- }
272
- var isMain = typeof process !== "undefined" && /[/\\](?:cli\.[jt]s|nit)$/.test(process.argv[1] ?? "");
273
- if (isMain) {
274
- const args = process.argv.slice(2);
275
- run(args, process.cwd(), console).then((code) => {
276
- process.exit(code);
277
- });
278
- }
279
- export {
280
- run
281
- };
7
+ ${typeof e=="string"?e:m(n)}
8
+ `}async function C(e,n,t){let r=k(t,e);if(!T(r))return[null,!1];let[o,i]=await p(h(r,"utf8"));if(o!==null)return[o,null];let s=typeof n=="string"?n.trim():m(e);return[null,i.split(`
9
+ `).some(c=>c.trim()===s)]}async function I(e,n){try{let t=await O(e,{withFileTypes:!0});for(let r of t)if(!(!r.isFile()||n.has(r.name)))try{if((await h(k(e,r.name),"utf8")).startsWith(g))return!0}catch{}}catch{}return!1}async function y(e,n){try{await A(n)}catch{return[new Error(`Hooks directory does not exist: ${n}`),null]}for(let[t,r]of Object.entries(e)){let[o,i]=await C(t,r,n);if(o!==null)return[o,null];if(!i)return[null,!1]}return await I(n,new Set(Object.keys(e)))?[null,!1]:[null,!0]}import{chmod as D,readdir as F,readFile as L,stat as _,unlink as w,writeFile as G}from"fs/promises";import{join as b}from"path";async function E(e,n){try{await _(n)}catch{return[new Error(`Hooks directory does not exist: ${n}`),null]}let t=[];for(let[r,o]of Object.entries(e)){let i=b(n,r);try{await G(i,d(o,r),"utf8"),t.push(i),await D(i,493)}catch(s){return await Promise.all(t.map(a=>w(a).catch(()=>{}))),[s instanceof Error?s:new Error(`Failed to write hook "${r}"`),null]}}try{let r=await F(n,{withFileTypes:!0}),o=new Set(Object.keys(e));await Promise.all(r.filter(i=>i.isFile()&&!o.has(i.name)).map(async i=>{let s=b(n,i.name);try{(await L(s,"utf8")).startsWith(g)&&await w(s)}catch{}}))}catch{}return[null,!0]}import{spawn as $}from"child_process";var v="{staged_files}";function x(e){return e.replaceAll(/[.+^$|()[\]\\]/g,String.raw`\$&`)}function J(e){let n="",t=0;for(;t<e.length;){let r=e.charAt(t);if(r==="*"&&e.charAt(t+1)==="*")n+=".*",t+=2,e.charAt(t)==="/"&&t++;else if(r==="*")n+="[^/]*",t++;else if(r==="?")n+="[^/]",t++;else if(r==="{"){let o=e.indexOf("}",t);if(o<0)n+=String.raw`\{`,t++;else{let i=e.slice(t+1,o).split(",").map(x).join("|");n+=`(?:${i})`,t=o+1}}else n+=x(r),t++}return new RegExp(`^${n}$`)}function q(e){let n=[],t="",r=0;for(;r<e.length;){let o=e.charAt(r);if(o==='"'||o==="'"){let i=o;for(r++;r<e.length&&e.charAt(r)!==i;)t+=e.charAt(r),r++;r++}else/\s/.test(o)?(t.length>0&&(n.push(t),t=""),r++):(t+=o,r++)}return t.length>0&&n.push(t),n}function z(e,n,t){return new Promise(r=>{let o=[];for(let c of q(e))c===v?o.push(...n):o.push(c);let[i,...s]=o;if(!i)return r(new Error(`Empty command in stage: "${e}"`));let a=$(i,s,{cwd:t,stdio:"inherit"});a.on("close",c=>r(c===0?null:new Error(`Command "${e}" exited with code ${c}`))),a.on("error",r)})}function B(e){return new Promise(n=>{let t=$("git",["diff","--cached","--name-only","--diff-filter=ACMR"],{cwd:e,stdio:["ignore","pipe","ignore"]}),r="";t.stdout.on("data",o=>{r+=o.toString()}),t.on("close",()=>n([null,r.split(`
10
+ `).filter(Boolean)])),t.on("error",o=>n([o,null]))})}async function j(e,n){let[t,r]=await B(n);if(t!==null)return[t,null];if(r.length===0)return[null,!0];let o=r.map(i=>{let s=i.replaceAll("\\","/");return{file:s,basename:s.split("/").at(-1)??s}});for(let[i,s]of Object.entries(e)){let a=J(i),c=o.filter(({file:u,basename:f})=>a.test(u)||a.test(f)).map(({file:u})=>u);if(c.length===0)continue;let l=await z(s,c,n);if(l!==null)return[l,null]}return[null,!0]}import{existsSync as K}from"fs";import{join as M}from"path";function W(e){let n=process.platform==="win32"?";":":",t=process.platform==="win32"?[".exe",".cmd",".bat",""]:[""];return(process.env.PATH??"").split(n).some(o=>t.some(i=>K(M(o,e+i))))}function S(e,n){let t=n.trim().split(/\s+/).at(0);return t===void 0||t===""?new Error(`Hook "${e}" has an empty command`):W(t)?null:new Error(`Hook "${e}": command "${t}" not found in PATH`)}function R(e){for(let[n,t]of Object.entries(e))if(typeof t=="string"){let r=S(n,t);if(r!==null)return r}else for(let[r,o]of Object.entries(t.stages)){let i=S(`${n}[${r}]`,o);if(i!==null)return i}return null}import{readFile as U}from"fs/promises";import{join as V}from"path";function Q(e,n){if(typeof n=="string")return[null,n];if(typeof n=="object"&&n!==null&&!Array.isArray(n)&&Object.hasOwn(n,"stages")){let t=n.stages;if(typeof t!="object"||t===null||Array.isArray(t))return[new Error(`Hook "${e}" has invalid stages: stages must be a plain object`),null];let r=t;for(let[o,i]of Object.entries(r))if(typeof i!="string")return[new Error(`Hook "${e}" stage "${o}" must have a string command, got ${typeof i}`),null];return[null,{stages:r}]}return[new Error(`Hook "${e}" must be a string or a stages object, got ${typeof n}`),null]}async function H(e){let n=V(e,"package.json"),t;try{t=await U(n,"utf8")}catch{return[new Error(`Cannot find package.json at ${n}`),null]}let r;try{r=JSON.parse(t)}catch{return[new Error(`package.json contains invalid JSON at ${n}`),null]}if(typeof r!="object"||r===null)return[new Error("package.json must be a JSON object"),null];let o=r.nit;if(o===void 0)return[null,{hooks:{}}];if(typeof o!="object"||o===null)return[new Error('"nit" in package.json must be an object'),null];let s=o.hooks;if(s===void 0)return[null,{hooks:{}}];if(typeof s!="object"||s===null||Array.isArray(s))return[new Error("hooks must be an object in nit config"),null];let a=s,c={};for(let[l,u]of Object.entries(a)){let[f,N]=Q(l,u);if(f!==null)return[f,null];c[l]=N}return[null,{hooks:c}]}import{existsSync as X}from"fs";import{stat as Y}from"fs/promises";import{dirname as Z,join as tt,parse as nt}from"path";async function P(e,n){try{await Y(e)}catch{return[new Error(`Path does not exist: ${e}`),null]}let t=e,{root:r}=nt(t);for(;t!==r;){let o=tt(t,".git");if(X(o))return[null,o];if(n!==void 0&&t===n)break;let i=Z(t);if(i===t)break;t=i}return[new Error(`Not a git repository (searched from ${e})`),null]}var et={log:()=>{},error:()=>{}};async function ot(e,n,t,r){let o=e.at(1);if(!o)return r.error("nit: exec requires a hook name"),1;let i=n.hooks[o];if(i===void 0||typeof i=="string")return r.error(`nit: "${o}" is not a staged hook`),1;let[s]=await j(i.stages,t);return s!==null?(r.error(`nit: ${s.message}`),1):0}async function it(e,n,t){let[r,o]=await y(e.hooks,n);return r!==null?(t.error(`nit: ${r.message}`),1):o?(t.log("nit: hooks are up to date"),0):(t.error("nit: hooks are out of sync \u2014 run `nit install` to fix"),1)}async function st(e,n,t){let r=R(e.hooks);if(r!==null)return t.error(`nit: ${r.message}`),1;let[o]=await E(e.hooks,n);if(o!==null)return t.error(`nit: ${o.message}`),1;let i=Object.keys(e.hooks);return t.log(i.length===0?"nit: no hooks configured":`nit: installed ${i.join(", ")}`),0}async function ct(e,n,t=et){let r=e.at(0)??"install";if(!["install","sync","check","exec"].includes(r))return t.error(`nit: unknown command "${r}"`),t.error("Usage: nit [install|sync|check|exec]"),1;let[o,i]=await H(n);if(o!==null)return t.error(`nit: ${o.message}`),1;if(r==="exec")return ot(e,i,n,t);let[s,a]=await P(n);if(s!==null)return r!=="check"?0:(t.error(`nit: ${s.message}`),1);let c=rt(a,"hooks");return r==="check"?it(i,c,t):st(i,c,t)}var at=typeof process<"u"&&/[/\\](?:cli\.[jt]s|nit)$/.test(process.argv[1]??"");if(at){let e=process.argv.slice(2),n=await ct(e,process.cwd(),console);process.exit(n)}export{ct as run};
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@pyyupsk/nit",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Lightweight zero-dependency Git hooks manager for JavaScript/Node.js projects",
5
5
  "license": "MIT",
6
6
  "packageManager": "bun@1.3.11",
7
7
  "type": "module",
8
8
  "main": "dist/cli.js",
9
- "module": "dist/cli.js",
10
9
  "bin": {
11
10
  "nit": "dist/cli.js"
12
11
  },
@@ -21,7 +20,8 @@
21
20
  "check": "biome check .",
22
21
  "dev": "tsup --watch",
23
22
  "format": "biome format --write .",
24
- "prepare": "bun run ./src/cli.ts install",
23
+ "knip": "knip",
24
+ "prepare": "bun run build && ln -sf ../../dist/cli.js node_modules/.bin/nit && bun run ./src/cli.ts install",
25
25
  "test": "vitest run",
26
26
  "test:watch": "vitest",
27
27
  "typecheck": "tsc --noEmit"
@@ -30,13 +30,19 @@
30
30
  "@biomejs/biome": "^2.4.11",
31
31
  "@types/bun": "^1.3.12",
32
32
  "@vitest/coverage-v8": "^4.1.4",
33
+ "knip": "^6.4.0",
33
34
  "tsup": "^8.5.1",
34
35
  "typescript": "^6.0.2",
35
36
  "vitest": "^4.1.4"
36
37
  },
37
38
  "nit": {
38
39
  "hooks": {
39
- "pre-commit": "bun format && bun check && bun typecheck && bun test && bun run build"
40
+ "pre-commit": {
41
+ "stages": {
42
+ "**/*.{ts,json}": "biome check --write {staged_files}"
43
+ }
44
+ },
45
+ "pre-push": "bun typecheck && bun test && bun run build"
40
46
  }
41
47
  }
42
48
  }
package/dist/cli.cjs DELETED
@@ -1,298 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
10
- };
11
- var __copyProps = (to, from, except, desc) => {
12
- if (from && typeof from === "object" || typeof from === "function") {
13
- for (let key of __getOwnPropNames(from))
14
- if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
- }
17
- return to;
18
- };
19
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
-
21
- // src/cli.ts
22
- var cli_exports = {};
23
- __export(cli_exports, {
24
- run: () => run
25
- });
26
- module.exports = __toCommonJS(cli_exports);
27
- var import_node_path6 = require("path");
28
-
29
- // src/core/check.ts
30
- var import_node_fs = require("fs");
31
- var import_promises = require("fs/promises");
32
- var import_node_path = require("path");
33
-
34
- // src/core/hook-script.ts
35
- var NIT_FINGERPRINT = '#!/bin/sh\nif [ "$SKIP_NIT"';
36
- function hookScript(cmd) {
37
- return `#!/bin/sh
38
- if [ "$SKIP_NIT" = "1" ]; then
39
- exit 0
40
- fi
41
- ${cmd}
42
- `;
43
- }
44
-
45
- // src/core/check.ts
46
- async function checkHooks(hooks, hooksDir) {
47
- try {
48
- await (0, import_promises.stat)(hooksDir);
49
- } catch {
50
- return [new Error(`Hooks directory does not exist: ${hooksDir}`), null];
51
- }
52
- for (const [name, cmd] of Object.entries(hooks)) {
53
- const hookPath = (0, import_node_path.join)(hooksDir, name);
54
- if (!(0, import_node_fs.existsSync)(hookPath)) {
55
- return [null, false];
56
- }
57
- let content;
58
- try {
59
- content = await (0, import_promises.readFile)(hookPath, "utf8");
60
- } catch (e) {
61
- return [
62
- e instanceof Error ? e : new Error(`Failed to read hook "${name}"`),
63
- null
64
- ];
65
- }
66
- const lines = content.split("\n");
67
- const hasCmd = lines.some((line) => line.trim() === cmd.trim());
68
- if (!hasCmd) {
69
- return [null, false];
70
- }
71
- }
72
- try {
73
- const entries = await (0, import_promises.readdir)(hooksDir, { withFileTypes: true });
74
- const configuredNames = new Set(Object.keys(hooks));
75
- for (const e of entries) {
76
- if (!e.isFile() || configuredNames.has(e.name)) continue;
77
- const hookPath = (0, import_node_path.join)(hooksDir, e.name);
78
- try {
79
- const content = await (0, import_promises.readFile)(hookPath, "utf8");
80
- if (content.startsWith(NIT_FINGERPRINT)) {
81
- return [null, false];
82
- }
83
- } catch {
84
- }
85
- }
86
- } catch {
87
- }
88
- return [null, true];
89
- }
90
-
91
- // src/core/install.ts
92
- var import_promises2 = require("fs/promises");
93
- var import_node_path2 = require("path");
94
- async function installHooks(hooks, hooksDir) {
95
- try {
96
- await (0, import_promises2.stat)(hooksDir);
97
- } catch {
98
- return [new Error(`Hooks directory does not exist: ${hooksDir}`), null];
99
- }
100
- const written = [];
101
- for (const [name, cmd] of Object.entries(hooks)) {
102
- const hookPath = (0, import_node_path2.join)(hooksDir, name);
103
- try {
104
- await (0, import_promises2.writeFile)(hookPath, hookScript(cmd), "utf8");
105
- written.push(hookPath);
106
- await (0, import_promises2.chmod)(hookPath, 493);
107
- } catch (e) {
108
- await Promise.all(written.map((p) => (0, import_promises2.unlink)(p).catch(() => {
109
- })));
110
- return [
111
- e instanceof Error ? e : new Error(`Failed to write hook "${name}"`),
112
- null
113
- ];
114
- }
115
- }
116
- try {
117
- const entries = await (0, import_promises2.readdir)(hooksDir, { withFileTypes: true });
118
- const configuredNames = new Set(Object.keys(hooks));
119
- await Promise.all(
120
- entries.filter((e) => e.isFile() && !configuredNames.has(e.name)).map(async (e) => {
121
- const hookPath = (0, import_node_path2.join)(hooksDir, e.name);
122
- try {
123
- const content = await (0, import_promises2.readFile)(hookPath, "utf8");
124
- if (content.startsWith(NIT_FINGERPRINT)) {
125
- await (0, import_promises2.unlink)(hookPath);
126
- }
127
- } catch {
128
- }
129
- })
130
- );
131
- } catch {
132
- }
133
- return [null, true];
134
- }
135
-
136
- // src/core/validate.ts
137
- var import_node_fs2 = require("fs");
138
- var import_node_path3 = require("path");
139
- function commandExists(bin) {
140
- const sep = process.platform === "win32" ? ";" : ":";
141
- const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
142
- const dirs = (process.env.PATH ?? "").split(sep);
143
- return dirs.some(
144
- (dir) => exts.some((ext) => (0, import_node_fs2.existsSync)((0, import_node_path3.join)(dir, bin + ext)))
145
- );
146
- }
147
- function validateHooks(hooks) {
148
- for (const [hookName, cmd] of Object.entries(hooks)) {
149
- const bin = cmd.trim().split(/\s+/).at(0);
150
- if (bin === void 0 || bin === "") {
151
- return new Error(`Hook "${hookName}" has an empty command`);
152
- }
153
- if (!commandExists(bin)) {
154
- return new Error(`Hook "${hookName}": command "${bin}" not found in PATH`);
155
- }
156
- }
157
- return null;
158
- }
159
-
160
- // src/utils/config.ts
161
- var import_promises3 = require("fs/promises");
162
- var import_node_path4 = require("path");
163
- async function readConfig(cwd) {
164
- const pkgPath = (0, import_node_path4.join)(cwd, "package.json");
165
- let raw;
166
- try {
167
- raw = await (0, import_promises3.readFile)(pkgPath, "utf8");
168
- } catch {
169
- return [new Error(`Cannot find package.json at ${pkgPath}`), null];
170
- }
171
- let pkg;
172
- try {
173
- pkg = JSON.parse(raw);
174
- } catch {
175
- return [new Error(`package.json contains invalid JSON at ${pkgPath}`), null];
176
- }
177
- if (typeof pkg !== "object" || pkg === null) {
178
- return [new Error("package.json must be a JSON object"), null];
179
- }
180
- const nit = pkg.nit;
181
- if (nit === void 0) {
182
- return [null, { hooks: {} }];
183
- }
184
- if (typeof nit !== "object" || nit === null) {
185
- return [new Error('"nit" in package.json must be an object'), null];
186
- }
187
- const nitObj = nit;
188
- const hooks = nitObj.hooks;
189
- if (hooks === void 0) {
190
- return [null, { hooks: {} }];
191
- }
192
- if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) {
193
- return [new Error("hooks must be an object in nit config"), null];
194
- }
195
- const hooksObj = hooks;
196
- for (const [name, cmd] of Object.entries(hooksObj)) {
197
- if (typeof cmd !== "string") {
198
- return [
199
- new Error(
200
- `Hook "${name}" must have a string command, got ${typeof cmd}`
201
- ),
202
- null
203
- ];
204
- }
205
- }
206
- return [null, { hooks: hooksObj }];
207
- }
208
-
209
- // src/utils/git.ts
210
- var import_node_fs3 = require("fs");
211
- var import_promises4 = require("fs/promises");
212
- var import_node_path5 = require("path");
213
- async function findGitDir(startPath, stopAt) {
214
- try {
215
- await (0, import_promises4.stat)(startPath);
216
- } catch {
217
- return [new Error(`Path does not exist: ${startPath}`), null];
218
- }
219
- let current = startPath;
220
- const { root } = (0, import_node_path5.parse)(current);
221
- while (current !== root) {
222
- const candidate = (0, import_node_path5.join)(current, ".git");
223
- if ((0, import_node_fs3.existsSync)(candidate)) {
224
- return [null, candidate];
225
- }
226
- if (stopAt !== void 0 && current === stopAt) break;
227
- const parent = (0, import_node_path5.dirname)(current);
228
- if (parent === current) break;
229
- current = parent;
230
- }
231
- return [new Error(`Not a git repository (searched from ${startPath})`), null];
232
- }
233
-
234
- // src/cli.ts
235
- var silent = { log: () => {
236
- }, error: () => {
237
- } };
238
- async function run(args, cwd, logger = silent) {
239
- const command = args.at(0) ?? "install";
240
- if (!["install", "sync", "check"].includes(command)) {
241
- logger.error(`nit: unknown command "${command}"`);
242
- logger.error("Usage: nit [install|sync|check]");
243
- return 1;
244
- }
245
- const [cfgErr, config] = await readConfig(cwd);
246
- if (cfgErr !== null) {
247
- logger.error(`nit: ${cfgErr.message}`);
248
- return 1;
249
- }
250
- const [gitErr, gitDir] = await findGitDir(cwd);
251
- if (gitErr !== null) {
252
- if (command !== "check") return 0;
253
- logger.error(`nit: ${gitErr.message}`);
254
- return 1;
255
- }
256
- const hooksDir = (0, import_node_path6.join)(gitDir, "hooks");
257
- if (command === "check") {
258
- const [err, inSync] = await checkHooks(config.hooks, hooksDir);
259
- if (err !== null) {
260
- logger.error(`nit: ${err.message}`);
261
- return 1;
262
- }
263
- if (!inSync) {
264
- logger.error("nit: hooks are out of sync \u2014 run `nit install` to fix");
265
- return 1;
266
- }
267
- logger.log("nit: hooks are up to date");
268
- return 0;
269
- }
270
- const validErr = validateHooks(config.hooks);
271
- if (validErr !== null) {
272
- logger.error(`nit: ${validErr.message}`);
273
- return 1;
274
- }
275
- const [installErr] = await installHooks(config.hooks, hooksDir);
276
- if (installErr !== null) {
277
- logger.error(`nit: ${installErr.message}`);
278
- return 1;
279
- }
280
- const hookNames = Object.keys(config.hooks);
281
- if (hookNames.length === 0) {
282
- logger.log("nit: no hooks configured");
283
- } else {
284
- logger.log(`nit: installed ${hookNames.join(", ")}`);
285
- }
286
- return 0;
287
- }
288
- var isMain = typeof process !== "undefined" && /[/\\](?:cli\.[jt]s|nit)$/.test(process.argv[1] ?? "");
289
- if (isMain) {
290
- const args = process.argv.slice(2);
291
- run(args, process.cwd(), console).then((code) => {
292
- process.exit(code);
293
- });
294
- }
295
- // Annotate the CommonJS export names for ESM import in node:
296
- 0 && (module.exports = {
297
- run
298
- });