@isentinel/hooks 1.2.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/LICENSE +24 -0
- package/README.md +85 -0
- package/dist/events.d.mts +47 -0
- package/dist/hooks/clear-lint-state.d.mts +1 -0
- package/dist/hooks/clear-lint-state.mjs +13 -0
- package/dist/hooks/lint-guard.d.mts +1 -0
- package/dist/hooks/lint-guard.mjs +15 -0
- package/dist/hooks/lint-stop.d.mts +1 -0
- package/dist/hooks/lint-stop.mjs +39 -0
- package/dist/hooks/lint.d.mts +1 -0
- package/dist/hooks/lint.mjs +28 -0
- package/dist/hooks/type-check-stop.d.mts +1 -0
- package/dist/hooks/type-check-stop.mjs +48 -0
- package/dist/hooks/type-check.d.mts +1 -0
- package/dist/hooks/type-check.mjs +29 -0
- package/dist/io.mjs +29 -0
- package/dist/scripts/lint.d.mts +58 -0
- package/dist/scripts/lint.mjs +398 -0
- package/dist/scripts/type-check.d.mts +49 -0
- package/dist/scripts/type-check.mjs +176 -0
- package/package.json +94 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-PRESENT Christopher Buss <christopher.buss@pm.me>
|
|
4
|
+
|
|
5
|
+
Copyright for portions of this project are held by Anthony Fu 2025, as part of
|
|
6
|
+
antfu/skills. All other copyright are held by Christopher Buss
|
|
7
|
+
<christopher.buss@pm.me>, 2026.
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
10
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
11
|
+
the Software without restriction, including without limitation the rights to
|
|
12
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
13
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
14
|
+
subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
21
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
22
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
23
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
24
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Roblox Skills & Claude Code Extensions
|
|
2
|
+
|
|
3
|
+
Personal collection of [Agent Skills](https://agentskills.io/home), hooks, and
|
|
4
|
+
plugins for Claude Code, focused on Roblox development.
|
|
5
|
+
|
|
6
|
+
This started as a fork of [antfu/skills](https://github.com/antfu/skills). I'm
|
|
7
|
+
repurposing it for my own workflow but keeping it open source in case others
|
|
8
|
+
find it useful.
|
|
9
|
+
|
|
10
|
+
## What's here
|
|
11
|
+
|
|
12
|
+
- **Skills** - Agent skills for Roblox tooling, Luau, and related ecosystems
|
|
13
|
+
- **Hooks** - Custom Claude Code hooks for my workflow
|
|
14
|
+
- **Plugins** - Any other extensions I end up building
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpx skills add christopher-buss/skills -skill='*'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or install everything globally:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpx skills add christopher-buss/skills -skill='*' -g
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
More on the CLI at [skills](https://github.com/vercel-labs/skills).
|
|
29
|
+
|
|
30
|
+
## Skills
|
|
31
|
+
|
|
32
|
+
### Hand-maintained
|
|
33
|
+
|
|
34
|
+
Manually written with personal preferences and best practices.
|
|
35
|
+
|
|
36
|
+
| Skill | Description |
|
|
37
|
+
| ----------------------------- | ------------------------------------------------------------- |
|
|
38
|
+
| [isentinel](skills/isentinel) | isentinel's opinionated preferences for roblox-ts development |
|
|
39
|
+
| [roblox-ts](skills/roblox-ts) | TypeScript to Roblox Lua transpiler |
|
|
40
|
+
| [test-driven-development](skills/test-driven-development) | How to write tests and design for testability in Roblox projects |
|
|
41
|
+
| [ecs-design](skills/ecs-design) | Best practices for designing Entity Component Systems in Roblox |
|
|
42
|
+
|
|
43
|
+
### Generated from documentation
|
|
44
|
+
|
|
45
|
+
Generated from official docs.
|
|
46
|
+
|
|
47
|
+
| Skill | Description | Source |
|
|
48
|
+
| --------------------------------- | --------------------------------------------- | ------------------------------------------------------------- |
|
|
49
|
+
| [jecs](skills/jecs) | Entity Component System for Roblox | [Ukendio/jecs](https://github.com/Ukendio/jecs) |
|
|
50
|
+
| [pnpm](skills/pnpm) | Fast, disk-efficient package manager | [pnpm/pnpm.io](https://github.com/pnpm/pnpm.io) |
|
|
51
|
+
| [roblox-ts](skills/robloxTs) | TypeScript to Roblox Lua transpiler | [roblox-ts/roblox-ts](https://github.com/roblox-ts/roblox-ts) |
|
|
52
|
+
| [superpowers](skills/superpowers) | Agent workflow skills (customized for Roblox) | [obra/superpowers](https://github.com/obra/superpowers) |
|
|
53
|
+
|
|
54
|
+
### Vendored
|
|
55
|
+
|
|
56
|
+
Synced from external repos that maintain their own skills.
|
|
57
|
+
|
|
58
|
+
| Skill | Description | Source |
|
|
59
|
+
| --------------------------------------- | ------------------------------------ | ------------------------------------------------------- |
|
|
60
|
+
| [humanizer](skills/humanizer) | Remove AI writing patterns from text | [blader/humanizer](https://github.com/blader/humanizer) |
|
|
61
|
+
| [writing-skills](skills/writing-skills) | How to write agent skills | [obra/superpowers](https://github.com/obra/superpowers) |
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
See [AGENTS.md](AGENTS.md) for how skills are generated and maintained.
|
|
66
|
+
|
|
67
|
+
## Adding your own
|
|
68
|
+
|
|
69
|
+
1. Fork this repo
|
|
70
|
+
2. `pnpm install`
|
|
71
|
+
3. Update `meta.ts` with your projects
|
|
72
|
+
4. `nr start cleanup` to clear existing submodules
|
|
73
|
+
5. `nr start init` to clone fresh
|
|
74
|
+
6. `nr start sync` for vendored skills
|
|
75
|
+
7. Have your agent generate skills one project at a time
|
|
76
|
+
|
|
77
|
+
## Attribution
|
|
78
|
+
|
|
79
|
+
Forked from [Anthony Fu's skills](https://github.com/antfu/skills). The original
|
|
80
|
+
project's approach of using git submodules to reference source documentation is
|
|
81
|
+
clever - skills stay current with upstream changes without manual updates.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
[MIT](LICENSE.md). Vendored skills keep their original licenses.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region node_modules/.pnpm/@constellos+claude-code-kit@0.4.0/node_modules/@constellos/claude-code-kit/dist/types/hooks/base.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Base output fields available for all hook events
|
|
4
|
+
*/
|
|
5
|
+
interface BaseHookOutput {
|
|
6
|
+
/**
|
|
7
|
+
* Whether Claude should continue after hook execution.
|
|
8
|
+
* Setting to false will terminate execution and require user input.
|
|
9
|
+
* @default true
|
|
10
|
+
*/
|
|
11
|
+
continue?: boolean;
|
|
12
|
+
/** Message shown to user when continue is false */
|
|
13
|
+
stopReason?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Hide stdout from transcript mode.
|
|
16
|
+
* Useful for suppressing verbose output.
|
|
17
|
+
* @default false
|
|
18
|
+
*/
|
|
19
|
+
suppressOutput?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Optional warning message shown to the user.
|
|
22
|
+
* Useful when "continue" is true but a message to the user is still needed.
|
|
23
|
+
*/
|
|
24
|
+
systemMessage?: string;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region node_modules/.pnpm/@constellos+claude-code-kit@0.4.0/node_modules/@constellos/claude-code-kit/dist/types/hooks/events.d.ts
|
|
28
|
+
/**
|
|
29
|
+
* PostToolUse hook output
|
|
30
|
+
*/
|
|
31
|
+
type PostToolUseHookOutput = BaseHookOutput & ({
|
|
32
|
+
decision: undefined;
|
|
33
|
+
reason?: string;
|
|
34
|
+
hookSpecificOutput?: {
|
|
35
|
+
hookEventName: "PostToolUse";
|
|
36
|
+
additionalContext?: string;
|
|
37
|
+
};
|
|
38
|
+
} | {
|
|
39
|
+
decision: "block";
|
|
40
|
+
reason?: string; /** Required when decision is "block" */
|
|
41
|
+
hookSpecificOutput: {
|
|
42
|
+
hookEventName: "PostToolUse"; /** Required when decision is "block" */
|
|
43
|
+
additionalContext: string;
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
//#endregion
|
|
47
|
+
export { PostToolUseHookOutput as t };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { clearEditedFiles, clearLintAttempts, clearStopAttempts } from "../scripts/lint.mjs";
|
|
2
|
+
import { clearTypecheckStopAttempts } from "../scripts/type-check.mjs";
|
|
3
|
+
import { t as readStdinJson } from "../io.mjs";
|
|
4
|
+
|
|
5
|
+
//#region hooks/clear-lint-state.ts
|
|
6
|
+
const input = await readStdinJson();
|
|
7
|
+
clearLintAttempts();
|
|
8
|
+
clearStopAttempts();
|
|
9
|
+
clearTypecheckStopAttempts();
|
|
10
|
+
clearEditedFiles(input.session_id);
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { isProtectedFile } from "../scripts/lint.mjs";
|
|
2
|
+
import { n as writeStdoutJson, t as readStdinJson } from "../io.mjs";
|
|
3
|
+
import { basename } from "node:path";
|
|
4
|
+
|
|
5
|
+
//#region hooks/lint-guard.ts
|
|
6
|
+
const input = await readStdinJson();
|
|
7
|
+
if (input.tool_name === "Write" || input.tool_name === "Edit") {
|
|
8
|
+
if (isProtectedFile(basename(input.tool_input.file_path))) writeStdoutJson({
|
|
9
|
+
decision: "block",
|
|
10
|
+
reason: "Modifying linter config is forbidden. Report to user if a rule blocks your task."
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { findSourceRoot, getTransitiveDependents, isLintableFile, lint, readEditedFiles, readLintAttempts, readSettings, readStopAttempts, stopDecision, writeStopAttempts } from "../scripts/lint.mjs";
|
|
2
|
+
import { n as writeStdoutJson, t as readStdinJson } from "../io.mjs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
|
|
6
|
+
//#region hooks/lint-stop.ts
|
|
7
|
+
const settings = readSettings();
|
|
8
|
+
if (!settings.lint) process.exit(0);
|
|
9
|
+
const SESSION_ID = (await readStdinJson()).session_id;
|
|
10
|
+
const editedFiles = readEditedFiles(SESSION_ID);
|
|
11
|
+
if (editedFiles.length === 0) process.exit(0);
|
|
12
|
+
const dependents = /* @__PURE__ */ new Set();
|
|
13
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14
|
+
for (const file of editedFiles) {
|
|
15
|
+
const sourceRoot = findSourceRoot(resolve(file));
|
|
16
|
+
if (sourceRoot === void 0 || seen.has(sourceRoot)) continue;
|
|
17
|
+
seen.add(sourceRoot);
|
|
18
|
+
for (const dependent of getTransitiveDependents(editedFiles, sourceRoot, settings.runner)) dependents.add(dependent);
|
|
19
|
+
}
|
|
20
|
+
const files = [...new Set([...editedFiles, ...dependents])].filter((file) => isLintableFile(file));
|
|
21
|
+
if (files.length === 0) process.exit(0);
|
|
22
|
+
const errorFiles = [];
|
|
23
|
+
for (const file of files) if (lint(file, ["--fix"], settings) !== void 0) errorFiles.push(file);
|
|
24
|
+
const result = stopDecision({
|
|
25
|
+
errorFiles,
|
|
26
|
+
lintAttempts: readLintAttempts(),
|
|
27
|
+
maxLintAttempts: settings.maxLintAttempts,
|
|
28
|
+
stopAttempts: readStopAttempts()
|
|
29
|
+
});
|
|
30
|
+
if (result === void 0) process.exit(0);
|
|
31
|
+
if (result.resetStopAttempts) {
|
|
32
|
+
writeStopAttempts(0);
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
writeStopAttempts(readStopAttempts() + 1);
|
|
36
|
+
writeStdoutJson(result);
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { lint, readLintAttempts, readSettings, writeEditedFile, writeLintAttempts } from "../scripts/lint.mjs";
|
|
2
|
+
import { n as writeStdoutJson, t as readStdinJson } from "../io.mjs";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
|
|
5
|
+
//#region hooks/lint.ts
|
|
6
|
+
const settings = readSettings();
|
|
7
|
+
if (!settings.lint) process.exit(0);
|
|
8
|
+
const input = await readStdinJson();
|
|
9
|
+
if (input.tool_name !== "Write" && input.tool_name !== "Edit") process.exit(0);
|
|
10
|
+
function run(filePath) {
|
|
11
|
+
const attempts = readLintAttempts();
|
|
12
|
+
const result = lint(filePath, ["--fix"], settings);
|
|
13
|
+
if (result !== void 0) {
|
|
14
|
+
const count = (attempts[filePath] ?? 0) + 1;
|
|
15
|
+
attempts[filePath] = count;
|
|
16
|
+
writeLintAttempts(attempts);
|
|
17
|
+
if (count >= settings.maxLintAttempts && result.hookSpecificOutput) result.hookSpecificOutput.additionalContext = `CRITICAL: ${filePath} failed linting ${count} times. STOP editing this file and report lint errors to user.\n${result.hookSpecificOutput.additionalContext}`;
|
|
18
|
+
writeStdoutJson(result);
|
|
19
|
+
} else if (filePath in attempts) {
|
|
20
|
+
delete attempts[filePath];
|
|
21
|
+
writeLintAttempts(attempts);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
run(input.tool_input.file_path);
|
|
25
|
+
writeEditedFile(input.session_id, input.tool_input.file_path);
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { findSourceRoot, getTransitiveDependents, readEditedFiles, readLintAttempts, readSettings } from "../scripts/lint.mjs";
|
|
2
|
+
import { isTypeCheckable, readTypecheckStopAttempts, resolveTsconfig, runTypeCheck, typecheckStopDecision, writeTypecheckStopAttempts } from "../scripts/type-check.mjs";
|
|
3
|
+
import { n as writeStdoutJson, t as readStdinJson } from "../io.mjs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
|
|
7
|
+
//#region hooks/type-check-stop.ts
|
|
8
|
+
const settings = readSettings();
|
|
9
|
+
if (!settings.typecheck) process.exit(0);
|
|
10
|
+
const SESSION_ID = (await readStdinJson()).session_id;
|
|
11
|
+
const PROJECT_ROOT = process.env["CLAUDE_PROJECT_DIR"] ?? process.cwd();
|
|
12
|
+
const editedFiles = readEditedFiles(SESSION_ID);
|
|
13
|
+
if (editedFiles.length === 0) process.exit(0);
|
|
14
|
+
const allFiles = new Set(editedFiles);
|
|
15
|
+
const seenRoots = /* @__PURE__ */ new Set();
|
|
16
|
+
for (const file of editedFiles) {
|
|
17
|
+
const sourceRoot = findSourceRoot(resolve(file));
|
|
18
|
+
if (sourceRoot === void 0 || seenRoots.has(sourceRoot)) continue;
|
|
19
|
+
seenRoots.add(sourceRoot);
|
|
20
|
+
for (const dependent of getTransitiveDependents(editedFiles, sourceRoot, settings.runner)) allFiles.add(dependent);
|
|
21
|
+
}
|
|
22
|
+
const files = [...allFiles].filter((file) => isTypeCheckable(file));
|
|
23
|
+
if (files.length === 0) process.exit(0);
|
|
24
|
+
const errorFiles = [];
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
const tsconfig = resolveTsconfig(join(PROJECT_ROOT, file), PROJECT_ROOT);
|
|
27
|
+
if (tsconfig === void 0) continue;
|
|
28
|
+
const output = runTypeCheck(tsconfig, settings.runner, settings.typecheckArgs);
|
|
29
|
+
if (output !== void 0) {
|
|
30
|
+
if (/error TS/i.test(output)) errorFiles.push(file);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const result = typecheckStopDecision({
|
|
34
|
+
errorFiles,
|
|
35
|
+
lintAttempts: readLintAttempts(),
|
|
36
|
+
maxLintAttempts: settings.maxLintAttempts,
|
|
37
|
+
stopAttempts: readTypecheckStopAttempts()
|
|
38
|
+
});
|
|
39
|
+
if (result === void 0) process.exit(0);
|
|
40
|
+
if (result.resetStopAttempts) {
|
|
41
|
+
writeTypecheckStopAttempts(0);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
writeTypecheckStopAttempts(readTypecheckStopAttempts() + 1);
|
|
45
|
+
writeStdoutJson(result);
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readLintAttempts, readSettings, writeEditedFile, writeLintAttempts } from "../scripts/lint.mjs";
|
|
2
|
+
import { typeCheck } from "../scripts/type-check.mjs";
|
|
3
|
+
import { n as writeStdoutJson, t as readStdinJson } from "../io.mjs";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
|
|
6
|
+
//#region hooks/type-check.ts
|
|
7
|
+
const settings = readSettings();
|
|
8
|
+
if (!settings.typecheck) process.exit(0);
|
|
9
|
+
const input = await readStdinJson();
|
|
10
|
+
if (input.tool_name !== "Write" && input.tool_name !== "Edit") process.exit(0);
|
|
11
|
+
function run(filePath) {
|
|
12
|
+
const attempts = readLintAttempts();
|
|
13
|
+
const result = typeCheck(filePath, settings);
|
|
14
|
+
if (result !== void 0) {
|
|
15
|
+
const count = (attempts[filePath] ?? 0) + 1;
|
|
16
|
+
attempts[filePath] = count;
|
|
17
|
+
writeLintAttempts(attempts);
|
|
18
|
+
if (count >= settings.maxLintAttempts && result.hookSpecificOutput) result.hookSpecificOutput.additionalContext = `CRITICAL: ${filePath} failed type-check ${count} times. STOP editing this file and report type errors to user.\n${result.hookSpecificOutput.additionalContext}`;
|
|
19
|
+
writeStdoutJson(result);
|
|
20
|
+
} else if (filePath in attempts) {
|
|
21
|
+
delete attempts[filePath];
|
|
22
|
+
writeLintAttempts(attempts);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
run(input.tool_input.file_path);
|
|
26
|
+
writeEditedFile(input.session_id, input.tool_input.file_path);
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
export { };
|
package/dist/io.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
|
|
4
|
+
//#region hooks/io.ts
|
|
5
|
+
async function readStdinJson() {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const chunks = [];
|
|
8
|
+
process.stdin.on("data", (chunk) => {
|
|
9
|
+
chunks.push(chunk);
|
|
10
|
+
});
|
|
11
|
+
process.stdin.on("end", () => {
|
|
12
|
+
try {
|
|
13
|
+
const data = Buffer.concat(chunks).toString("utf8");
|
|
14
|
+
resolve(JSON.parse(data));
|
|
15
|
+
} catch (err) {
|
|
16
|
+
reject(/* @__PURE__ */ new Error(`Failed to parse JSON input: ${err}`));
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
process.stdin.on("error", (error) => {
|
|
20
|
+
reject(/* @__PURE__ */ new Error(`Failed to read stdin: ${error}`));
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function writeStdoutJson(output) {
|
|
25
|
+
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
export { writeStdoutJson as n, readStdinJson as t };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { t as PostToolUseHookOutput } from "../events.mjs";
|
|
2
|
+
|
|
3
|
+
//#region scripts/lint.d.ts
|
|
4
|
+
interface LintSettings {
|
|
5
|
+
cacheBust: Array<string>;
|
|
6
|
+
eslint: boolean;
|
|
7
|
+
lint: boolean;
|
|
8
|
+
maxLintAttempts: number;
|
|
9
|
+
oxlint: boolean;
|
|
10
|
+
runner: string;
|
|
11
|
+
typecheck: boolean;
|
|
12
|
+
typecheckArgs: Array<string>;
|
|
13
|
+
}
|
|
14
|
+
type DependencyGraph = Record<string, Array<string>>;
|
|
15
|
+
declare function isProtectedFile(filename: string): boolean;
|
|
16
|
+
declare function readEditedFiles(sessionId: string): Array<string>;
|
|
17
|
+
declare function writeEditedFile(sessionId: string, filePath: string): void;
|
|
18
|
+
declare function clearEditedFiles(sessionId: string): void;
|
|
19
|
+
declare function getTransitiveDependents(files: Array<string>, sourceRoot: string, runner?: string): Array<string>;
|
|
20
|
+
declare const DEFAULT_CACHE_BUST: string[];
|
|
21
|
+
interface StopDecisionResult {
|
|
22
|
+
decision?: "block";
|
|
23
|
+
reason?: string;
|
|
24
|
+
resetStopAttempts?: true;
|
|
25
|
+
}
|
|
26
|
+
interface StopDecisionInput {
|
|
27
|
+
errorFiles: Array<string>;
|
|
28
|
+
lintAttempts: Record<string, number>;
|
|
29
|
+
maxLintAttempts: number;
|
|
30
|
+
stopAttempts: number;
|
|
31
|
+
}
|
|
32
|
+
declare function readSettings(): LintSettings;
|
|
33
|
+
declare function getChangedFiles(): Array<string>;
|
|
34
|
+
declare function isLintableFile(filePath: string, extensions?: string[]): boolean;
|
|
35
|
+
declare function findEntryPoints(sourceRoot: string): Array<string>;
|
|
36
|
+
declare function getDependencyGraph(sourceRoot: string, entryPoints: Array<string>, runner?: string): DependencyGraph;
|
|
37
|
+
declare function invertGraph(graph: DependencyGraph, target: string): Array<string>;
|
|
38
|
+
declare function findSourceRoot(filePath: string): string | undefined;
|
|
39
|
+
declare function readLintAttempts(): Record<string, number>;
|
|
40
|
+
declare function writeLintAttempts(attempts: Record<string, number>): void;
|
|
41
|
+
declare function readStopAttempts(): number;
|
|
42
|
+
declare function writeStopAttempts(count: number): void;
|
|
43
|
+
declare function stopDecision(input: StopDecisionInput): StopDecisionResult | undefined;
|
|
44
|
+
declare function clearStopAttempts(): void;
|
|
45
|
+
declare function clearLintAttempts(): void;
|
|
46
|
+
declare function resolveBustFiles(patterns: Array<string>): Array<string>;
|
|
47
|
+
declare function shouldBustCache(patterns: Array<string>): boolean;
|
|
48
|
+
declare function clearCache(): void;
|
|
49
|
+
declare function invalidateCacheEntries(filePaths: Array<string>): void;
|
|
50
|
+
declare function runOxlint(filePath: string, extraFlags?: Array<string>, runner?: string): string | undefined;
|
|
51
|
+
declare function runEslint(filePath: string, extraFlags?: Array<string>, runner?: string): string | undefined;
|
|
52
|
+
declare function restartDaemon(runner?: string): void;
|
|
53
|
+
declare function formatErrors(output: string): Array<string>;
|
|
54
|
+
declare function buildHookOutput(filePath: string, errors: Array<string>): PostToolUseHookOutput;
|
|
55
|
+
declare function lint(filePath: string, extraFlags?: Array<string>, settings?: LintSettings): PostToolUseHookOutput | undefined;
|
|
56
|
+
declare function main(targets: Array<string>, settings?: LintSettings): void;
|
|
57
|
+
//#endregion
|
|
58
|
+
export { DEFAULT_CACHE_BUST, DependencyGraph, LintSettings, StopDecisionResult, buildHookOutput, clearCache, clearEditedFiles, clearLintAttempts, clearStopAttempts, findEntryPoints, findSourceRoot, formatErrors, getChangedFiles, getDependencyGraph, getTransitiveDependents, invalidateCacheEntries, invertGraph, isLintableFile, isProtectedFile, lint, main, readEditedFiles, readLintAttempts, readSettings, readStopAttempts, resolveBustFiles, restartDaemon, runEslint, runOxlint, shouldBustCache, stopDecision, writeEditedFile, writeLintAttempts, writeStopAttempts };
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { createFromFile } from "file-entry-cache";
|
|
2
|
+
import { execSync, spawn } from "node:child_process";
|
|
3
|
+
import { existsSync, globSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
|
|
7
|
+
//#region scripts/lint.ts
|
|
8
|
+
const PROTECTED_PATTERNS = [
|
|
9
|
+
"eslint.config.",
|
|
10
|
+
"oxlint.config.",
|
|
11
|
+
".eslintrc",
|
|
12
|
+
".oxlintrc."
|
|
13
|
+
];
|
|
14
|
+
function isProtectedFile(filename) {
|
|
15
|
+
return PROTECTED_PATTERNS.some((pattern) => filename.startsWith(pattern) || filename === pattern);
|
|
16
|
+
}
|
|
17
|
+
const LINT_STATE_PATH = ".claude/state/lint-attempts.json";
|
|
18
|
+
const STOP_STATE_PATH = ".claude/state/stop-attempts.json";
|
|
19
|
+
const EDITED_FILES_PATH = ".claude/state/edited-files.json";
|
|
20
|
+
function readEditedFiles(sessionId) {
|
|
21
|
+
if (!existsSync(EDITED_FILES_PATH)) return [];
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(readFileSync(EDITED_FILES_PATH, "utf-8"))[sessionId] ?? [];
|
|
24
|
+
} catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function writeEditedFile(sessionId, filePath) {
|
|
29
|
+
let state = {};
|
|
30
|
+
if (existsSync(EDITED_FILES_PATH)) try {
|
|
31
|
+
state = JSON.parse(readFileSync(EDITED_FILES_PATH, "utf-8"));
|
|
32
|
+
} catch {
|
|
33
|
+
state = {};
|
|
34
|
+
}
|
|
35
|
+
const files = state[sessionId] ?? [];
|
|
36
|
+
if (!files.includes(filePath)) files.push(filePath);
|
|
37
|
+
state[sessionId] = files;
|
|
38
|
+
mkdirSync(dirname(EDITED_FILES_PATH), { recursive: true });
|
|
39
|
+
writeFileSync(EDITED_FILES_PATH, JSON.stringify(state));
|
|
40
|
+
}
|
|
41
|
+
function clearEditedFiles(sessionId) {
|
|
42
|
+
if (!existsSync(EDITED_FILES_PATH)) return;
|
|
43
|
+
try {
|
|
44
|
+
const state = JSON.parse(readFileSync(EDITED_FILES_PATH, "utf-8"));
|
|
45
|
+
delete state[sessionId];
|
|
46
|
+
if (Object.keys(state).length === 0) unlinkSync(EDITED_FILES_PATH);
|
|
47
|
+
else writeFileSync(EDITED_FILES_PATH, JSON.stringify(state));
|
|
48
|
+
} catch {
|
|
49
|
+
unlinkSync(EDITED_FILES_PATH);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function getTransitiveDependents(files, sourceRoot, runner = DEFAULT_SETTINGS.runner) {
|
|
53
|
+
const entryPoints = findEntryPoints(sourceRoot);
|
|
54
|
+
if (entryPoints.length === 0) return [];
|
|
55
|
+
const graph = getDependencyGraph(sourceRoot, entryPoints, runner);
|
|
56
|
+
const visited = /* @__PURE__ */ new Set();
|
|
57
|
+
const queue = [];
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
const relativePath = relative(sourceRoot, resolve(file)).replaceAll("\\", "/");
|
|
60
|
+
if (!visited.has(relativePath)) {
|
|
61
|
+
visited.add(relativePath);
|
|
62
|
+
queue.push(relativePath);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let current = queue.shift();
|
|
66
|
+
while (current !== void 0) {
|
|
67
|
+
const importers = invertGraph(graph, current);
|
|
68
|
+
for (const importer of importers) if (!visited.has(importer)) {
|
|
69
|
+
visited.add(importer);
|
|
70
|
+
queue.push(importer);
|
|
71
|
+
}
|
|
72
|
+
current = queue.shift();
|
|
73
|
+
}
|
|
74
|
+
const originals = new Set(files.map((file) => relative(sourceRoot, resolve(file)).replaceAll("\\", "/")));
|
|
75
|
+
return [...visited].filter((file) => !originals.has(file)).map((file) => join(sourceRoot, file));
|
|
76
|
+
}
|
|
77
|
+
const ESLINT_CACHE_PATH = ".eslintcache";
|
|
78
|
+
const DEFAULT_EXTENSIONS = [
|
|
79
|
+
".ts",
|
|
80
|
+
".tsx",
|
|
81
|
+
".js",
|
|
82
|
+
".jsx",
|
|
83
|
+
".mjs",
|
|
84
|
+
".mts",
|
|
85
|
+
".json"
|
|
86
|
+
];
|
|
87
|
+
const ENTRY_CANDIDATES = [
|
|
88
|
+
"index.ts",
|
|
89
|
+
"cli.ts",
|
|
90
|
+
"main.ts"
|
|
91
|
+
];
|
|
92
|
+
const MAX_ERRORS = 5;
|
|
93
|
+
const SETTINGS_FILE = ".claude/sentinel.local.md";
|
|
94
|
+
const DEFAULT_CACHE_BUST = ["*.config.*", "**/tsconfig*.json"];
|
|
95
|
+
const DEFAULT_MAX_LINT_ATTEMPTS = 1;
|
|
96
|
+
const DEFAULT_MAX_STOP_ATTEMPTS = 1;
|
|
97
|
+
const DEFAULT_SETTINGS = {
|
|
98
|
+
cacheBust: [...DEFAULT_CACHE_BUST],
|
|
99
|
+
eslint: true,
|
|
100
|
+
lint: true,
|
|
101
|
+
maxLintAttempts: DEFAULT_MAX_LINT_ATTEMPTS,
|
|
102
|
+
oxlint: false,
|
|
103
|
+
runner: "pnpm exec",
|
|
104
|
+
typecheck: true,
|
|
105
|
+
typecheckArgs: []
|
|
106
|
+
};
|
|
107
|
+
function readSettings() {
|
|
108
|
+
if (!existsSync(SETTINGS_FILE)) return { ...DEFAULT_SETTINGS };
|
|
109
|
+
const fields = parseFrontmatter(readFileSync(SETTINGS_FILE, "utf-8"));
|
|
110
|
+
const cacheBustRaw = fields.get("cache-bust") ?? "";
|
|
111
|
+
const userPatterns = cacheBustRaw ? cacheBustRaw.split(",").map((entry) => entry.trim()).filter(Boolean) : [];
|
|
112
|
+
const maxAttemptsRaw = fields.get("max-lint-attempts");
|
|
113
|
+
const maxLintAttempts = maxAttemptsRaw !== void 0 ? Number(maxAttemptsRaw) : DEFAULT_MAX_LINT_ATTEMPTS;
|
|
114
|
+
return {
|
|
115
|
+
cacheBust: [...DEFAULT_CACHE_BUST, ...userPatterns],
|
|
116
|
+
eslint: fields.get("eslint") !== "false",
|
|
117
|
+
lint: fields.get("lint") !== "false",
|
|
118
|
+
maxLintAttempts,
|
|
119
|
+
oxlint: fields.get("oxlint") === "true",
|
|
120
|
+
runner: fields.get("runner") ?? DEFAULT_SETTINGS.runner,
|
|
121
|
+
typecheck: fields.get("typecheck") !== "false",
|
|
122
|
+
typecheckArgs: (fields.get("typecheck-args") ?? "").split(",").map((entry) => entry.trim()).filter(Boolean)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function getChangedFiles() {
|
|
126
|
+
const options = {
|
|
127
|
+
encoding: "utf-8",
|
|
128
|
+
stdio: "pipe"
|
|
129
|
+
};
|
|
130
|
+
const changed = execSync("git diff --name-only --diff-filter=d HEAD", options);
|
|
131
|
+
const untracked = execSync("git ls-files --others --exclude-standard", options);
|
|
132
|
+
return [...changed.trim().split("\n"), ...untracked.trim().split("\n")].filter(Boolean);
|
|
133
|
+
}
|
|
134
|
+
function isLintableFile(filePath, extensions = DEFAULT_EXTENSIONS) {
|
|
135
|
+
return extensions.some((extension) => filePath.endsWith(extension));
|
|
136
|
+
}
|
|
137
|
+
function findEntryPoints(sourceRoot) {
|
|
138
|
+
return ENTRY_CANDIDATES.map((name) => join(sourceRoot, name)).filter((path) => {
|
|
139
|
+
return existsSync(path);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function getDependencyGraph(sourceRoot, entryPoints, runner = DEFAULT_SETTINGS.runner) {
|
|
143
|
+
execSync("which madge", {
|
|
144
|
+
stdio: "pipe",
|
|
145
|
+
timeout: 1e3
|
|
146
|
+
});
|
|
147
|
+
const output = execSync(`${runner} madge --json ${entryPoints.map((ep) => `"${ep}"`).join(" ")}`, {
|
|
148
|
+
cwd: sourceRoot,
|
|
149
|
+
encoding: "utf-8",
|
|
150
|
+
stdio: [
|
|
151
|
+
"pipe",
|
|
152
|
+
"pipe",
|
|
153
|
+
"pipe"
|
|
154
|
+
],
|
|
155
|
+
timeout: 3e4
|
|
156
|
+
});
|
|
157
|
+
return JSON.parse(output);
|
|
158
|
+
}
|
|
159
|
+
function invertGraph(graph, target) {
|
|
160
|
+
const importers = [];
|
|
161
|
+
for (const [file, dependencies] of Object.entries(graph)) if (dependencies.includes(target)) importers.push(file);
|
|
162
|
+
return importers;
|
|
163
|
+
}
|
|
164
|
+
function findSourceRoot(filePath) {
|
|
165
|
+
let current = dirname(filePath);
|
|
166
|
+
while (current !== dirname(current)) {
|
|
167
|
+
if (existsSync(join(current, "package.json"))) {
|
|
168
|
+
const sourceDirectory = join(current, "src");
|
|
169
|
+
if (existsSync(sourceDirectory)) return sourceDirectory;
|
|
170
|
+
return current;
|
|
171
|
+
}
|
|
172
|
+
current = dirname(current);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function readLintAttempts() {
|
|
176
|
+
if (!existsSync(LINT_STATE_PATH)) return {};
|
|
177
|
+
try {
|
|
178
|
+
return JSON.parse(readFileSync(LINT_STATE_PATH, "utf-8"));
|
|
179
|
+
} catch {
|
|
180
|
+
return {};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function writeLintAttempts(attempts) {
|
|
184
|
+
mkdirSync(dirname(LINT_STATE_PATH), { recursive: true });
|
|
185
|
+
writeFileSync(LINT_STATE_PATH, JSON.stringify(attempts));
|
|
186
|
+
}
|
|
187
|
+
function readStopAttempts() {
|
|
188
|
+
if (!existsSync(STOP_STATE_PATH)) return 0;
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(readFileSync(STOP_STATE_PATH, "utf-8"));
|
|
191
|
+
} catch {
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function writeStopAttempts(count) {
|
|
196
|
+
mkdirSync(dirname(STOP_STATE_PATH), { recursive: true });
|
|
197
|
+
writeFileSync(STOP_STATE_PATH, JSON.stringify(count));
|
|
198
|
+
}
|
|
199
|
+
function stopDecision(input) {
|
|
200
|
+
if (input.errorFiles.length === 0) {
|
|
201
|
+
if (input.stopAttempts > 0) return { resetStopAttempts: true };
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (input.errorFiles.every((file) => {
|
|
205
|
+
return findAttempts(file, input.lintAttempts) >= input.maxLintAttempts;
|
|
206
|
+
})) return;
|
|
207
|
+
if (input.stopAttempts >= DEFAULT_MAX_STOP_ATTEMPTS) return { reason: `Unresolved lint errors in: ${input.errorFiles.join(", ")}. These may be pre-existing.` };
|
|
208
|
+
return {
|
|
209
|
+
decision: "block",
|
|
210
|
+
reason: `Lint errors detected in: ${input.errorFiles.join(", ")}. If related to your changes, please fix before finishing.`
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function clearStopAttempts() {
|
|
214
|
+
if (existsSync(STOP_STATE_PATH)) unlinkSync(STOP_STATE_PATH);
|
|
215
|
+
}
|
|
216
|
+
function clearLintAttempts() {
|
|
217
|
+
if (existsSync(LINT_STATE_PATH)) unlinkSync(LINT_STATE_PATH);
|
|
218
|
+
}
|
|
219
|
+
function resolveBustFiles(patterns) {
|
|
220
|
+
const positive = patterns.filter((pattern) => !pattern.startsWith("!"));
|
|
221
|
+
const negative = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
|
|
222
|
+
const matched = positive.flatMap((pattern) => globSync(pattern));
|
|
223
|
+
if (negative.length === 0) return matched;
|
|
224
|
+
const excluded = new Set(negative.flatMap((pattern) => globSync(pattern)));
|
|
225
|
+
return matched.filter((file) => !excluded.has(file));
|
|
226
|
+
}
|
|
227
|
+
function shouldBustCache(patterns) {
|
|
228
|
+
if (patterns.length === 0) return false;
|
|
229
|
+
if (!existsSync(ESLINT_CACHE_PATH)) return false;
|
|
230
|
+
const files = resolveBustFiles(patterns);
|
|
231
|
+
if (files.length === 0) return false;
|
|
232
|
+
const cacheMtime = statSync(ESLINT_CACHE_PATH).mtimeMs;
|
|
233
|
+
return files.some((file) => statSync(file).mtimeMs > cacheMtime);
|
|
234
|
+
}
|
|
235
|
+
function clearCache() {
|
|
236
|
+
if (existsSync(ESLINT_CACHE_PATH)) unlinkSync(ESLINT_CACHE_PATH);
|
|
237
|
+
}
|
|
238
|
+
function invalidateCacheEntries(filePaths) {
|
|
239
|
+
if (filePaths.length === 0) return;
|
|
240
|
+
if (!existsSync(ESLINT_CACHE_PATH)) return;
|
|
241
|
+
const cache = createFromFile(ESLINT_CACHE_PATH);
|
|
242
|
+
for (const file of filePaths) cache.removeEntry(file);
|
|
243
|
+
cache.reconcile();
|
|
244
|
+
}
|
|
245
|
+
function runOxlint(filePath, extraFlags = [], runner = DEFAULT_SETTINGS.runner) {
|
|
246
|
+
const flags = extraFlags.length > 0 ? `${extraFlags.join(" ")} ` : "";
|
|
247
|
+
try {
|
|
248
|
+
execSync(`${runner} oxlint ${flags}"${filePath}"`, { stdio: "pipe" });
|
|
249
|
+
return;
|
|
250
|
+
} catch (err_) {
|
|
251
|
+
const err = err_;
|
|
252
|
+
const stdout = err.stdout?.toString() ?? "";
|
|
253
|
+
const stderr = err.stderr?.toString() ?? "";
|
|
254
|
+
const message = err.message ?? "";
|
|
255
|
+
return stdout || stderr || message;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function runEslint(filePath, extraFlags = [], runner = DEFAULT_SETTINGS.runner) {
|
|
259
|
+
const flags = ["--cache", ...extraFlags].join(" ");
|
|
260
|
+
try {
|
|
261
|
+
execSync(`${runner} eslint_d ${flags} "${filePath}"`, {
|
|
262
|
+
env: {
|
|
263
|
+
...process.env,
|
|
264
|
+
ESLINT_IN_EDITOR: "true"
|
|
265
|
+
},
|
|
266
|
+
stdio: "pipe"
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
} catch (err_) {
|
|
270
|
+
const err = err_;
|
|
271
|
+
const stdout = err.stdout?.toString() ?? "";
|
|
272
|
+
const stderr = err.stderr?.toString() ?? "";
|
|
273
|
+
const message = err.message ?? "";
|
|
274
|
+
return stdout || stderr || message;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function restartDaemon(runner = DEFAULT_SETTINGS.runner) {
|
|
278
|
+
const [command = "pnpm", ...prefixArgs] = runner.split(/\s+/);
|
|
279
|
+
const child = spawn(command, [
|
|
280
|
+
...prefixArgs,
|
|
281
|
+
"eslint_d",
|
|
282
|
+
"restart"
|
|
283
|
+
], {
|
|
284
|
+
detached: true,
|
|
285
|
+
env: {
|
|
286
|
+
...process.env,
|
|
287
|
+
ESLINT_IN_EDITOR: "true"
|
|
288
|
+
},
|
|
289
|
+
stdio: "pipe"
|
|
290
|
+
});
|
|
291
|
+
child.stderr.on("data", (data) => {
|
|
292
|
+
process.stderr.write(`[eslint_d restart] ${data.toString()}`);
|
|
293
|
+
});
|
|
294
|
+
child.on("error", (error) => {
|
|
295
|
+
process.stderr.write(`[eslint_d restart] failed: ${error.message}\n`);
|
|
296
|
+
});
|
|
297
|
+
child.unref();
|
|
298
|
+
}
|
|
299
|
+
function formatErrors(output) {
|
|
300
|
+
return output.split("\n").filter((line) => /error/i.test(line)).slice(0, MAX_ERRORS);
|
|
301
|
+
}
|
|
302
|
+
function buildHookOutput(filePath, errors) {
|
|
303
|
+
const errorText = errors.join("\n");
|
|
304
|
+
const isTruncated = errors.length >= MAX_ERRORS;
|
|
305
|
+
const userMessage = `⚠️ Lint errors in ${filePath}:\n${errorText}${isTruncated ? "\n..." : ""}`;
|
|
306
|
+
return {
|
|
307
|
+
decision: void 0,
|
|
308
|
+
hookSpecificOutput: {
|
|
309
|
+
additionalContext: `⚠️ Lint errors in ${filePath}:\n${errorText}${isTruncated ? "\n(run lint to view more)" : ""}`,
|
|
310
|
+
hookEventName: "PostToolUse"
|
|
311
|
+
},
|
|
312
|
+
systemMessage: userMessage
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function lint(filePath, extraFlags = [], settings = DEFAULT_SETTINGS) {
|
|
316
|
+
if (shouldBustCache(settings.cacheBust)) clearCache();
|
|
317
|
+
else invalidateCacheEntries(findImporters(filePath, settings.runner));
|
|
318
|
+
const outputs = [];
|
|
319
|
+
if (settings.oxlint) {
|
|
320
|
+
const output = runOxlint(filePath, extraFlags, settings.runner);
|
|
321
|
+
if (output !== void 0) outputs.push(output);
|
|
322
|
+
}
|
|
323
|
+
if (settings.eslint) {
|
|
324
|
+
const output = runEslint(filePath, extraFlags, settings.runner);
|
|
325
|
+
if (output !== void 0) outputs.push(output);
|
|
326
|
+
}
|
|
327
|
+
if (settings.eslint) restartDaemon(settings.runner);
|
|
328
|
+
if (outputs.length > 0) {
|
|
329
|
+
const errors = formatErrors(outputs.join("\n"));
|
|
330
|
+
if (errors.length > 0) return buildHookOutput(filePath, errors);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function main(targets, settings = DEFAULT_SETTINGS) {
|
|
334
|
+
if (shouldBustCache(settings.cacheBust)) clearCache();
|
|
335
|
+
else invalidateCacheEntries(getChangedFiles());
|
|
336
|
+
let hasErrors = false;
|
|
337
|
+
for (const target of targets) {
|
|
338
|
+
const outputs = [];
|
|
339
|
+
if (settings.oxlint) {
|
|
340
|
+
const output = runOxlint(target, ["--color"], settings.runner);
|
|
341
|
+
if (output !== void 0) outputs.push(output);
|
|
342
|
+
}
|
|
343
|
+
if (settings.eslint) {
|
|
344
|
+
const output = runEslint(target, ["--color"], settings.runner);
|
|
345
|
+
if (output !== void 0) outputs.push(output);
|
|
346
|
+
}
|
|
347
|
+
for (const output of outputs) {
|
|
348
|
+
hasErrors = true;
|
|
349
|
+
const filtered = output.split("\n").filter((line) => !line.startsWith("[")).join("\n").trim();
|
|
350
|
+
if (filtered.length > 0) process.stderr.write(`${filtered}\n`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (settings.eslint) restartDaemon(settings.runner);
|
|
354
|
+
if (hasErrors) process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
function parseFrontmatter(content) {
|
|
357
|
+
const fields = /* @__PURE__ */ new Map();
|
|
358
|
+
const frontmatter = /^---\n([\s\S]*?)\n---/m.exec(content)?.[1];
|
|
359
|
+
if (frontmatter === void 0) return fields;
|
|
360
|
+
for (const line of frontmatter.split("\n")) {
|
|
361
|
+
const colon = line.indexOf(":");
|
|
362
|
+
if (colon > 0) {
|
|
363
|
+
const key = line.slice(0, colon).trim();
|
|
364
|
+
const value = line.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
|
|
365
|
+
fields.set(key, value);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return fields;
|
|
369
|
+
}
|
|
370
|
+
function endsWithSegment(haystack, needle) {
|
|
371
|
+
if (haystack === needle) return true;
|
|
372
|
+
if (!needle.includes("/")) return false;
|
|
373
|
+
return haystack.endsWith(`/${needle}`);
|
|
374
|
+
}
|
|
375
|
+
function findAttempts(file, lintAttempts) {
|
|
376
|
+
if (file in lintAttempts) return lintAttempts[file];
|
|
377
|
+
const normalized = file.replaceAll("\\", "/");
|
|
378
|
+
for (const [key, count] of Object.entries(lintAttempts)) {
|
|
379
|
+
const normalizedKey = key.replaceAll("\\", "/");
|
|
380
|
+
if (endsWithSegment(normalizedKey, normalized) || endsWithSegment(normalized, normalizedKey)) return count;
|
|
381
|
+
}
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
function findImporters(filePath, runner = DEFAULT_SETTINGS.runner) {
|
|
385
|
+
const absPath = resolve(filePath);
|
|
386
|
+
const sourceRoot = findSourceRoot(absPath);
|
|
387
|
+
if (sourceRoot === void 0) return [];
|
|
388
|
+
const entryPoints = findEntryPoints(sourceRoot);
|
|
389
|
+
if (entryPoints.length === 0) return [];
|
|
390
|
+
return invertGraph(getDependencyGraph(sourceRoot, entryPoints, runner), relative(sourceRoot, absPath).replaceAll("\\", "/")).map((file) => join(sourceRoot, file));
|
|
391
|
+
}
|
|
392
|
+
if (process.argv[1]?.endsWith("scripts/lint.ts") === true) {
|
|
393
|
+
const settings = readSettings();
|
|
394
|
+
main(process.argv.length > 2 ? process.argv.slice(2) : ["."], settings);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
//#endregion
|
|
398
|
+
export { DEFAULT_CACHE_BUST, buildHookOutput, clearCache, clearEditedFiles, clearLintAttempts, clearStopAttempts, findEntryPoints, findSourceRoot, formatErrors, getChangedFiles, getDependencyGraph, getTransitiveDependents, invalidateCacheEntries, invertGraph, isLintableFile, isProtectedFile, lint, main, readEditedFiles, readLintAttempts, readSettings, readStopAttempts, resolveBustFiles, restartDaemon, runEslint, runOxlint, shouldBustCache, stopDecision, writeEditedFile, writeLintAttempts, writeStopAttempts };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { t as PostToolUseHookOutput } from "../events.mjs";
|
|
2
|
+
|
|
3
|
+
//#region scripts/type-check.d.ts
|
|
4
|
+
interface TsconfigCache {
|
|
5
|
+
hashes: Record<string, string>;
|
|
6
|
+
mappings: Record<string, string>;
|
|
7
|
+
projectRoot: string;
|
|
8
|
+
}
|
|
9
|
+
declare function readTsconfigCache(projectRoot: string): TsconfigCache | undefined;
|
|
10
|
+
declare function writeTsconfigCache(projectRoot: string, cache: TsconfigCache): void;
|
|
11
|
+
declare function resolveTsconfig(filePath: string, projectRoot: string): string | undefined;
|
|
12
|
+
declare function isTypeCheckable(filePath: string): boolean;
|
|
13
|
+
declare function resolveViaReferences(directory: string, configPath: string, targetFile: string): string | undefined;
|
|
14
|
+
declare function findTsconfigForFile(targetFile: string, projectRoot: string): string | undefined;
|
|
15
|
+
declare function runTypeCheck(tsconfig: string, runner?: string, extraArgs?: Array<string>): string | undefined;
|
|
16
|
+
interface TypeCheckSettings {
|
|
17
|
+
runner: string;
|
|
18
|
+
typecheck: boolean;
|
|
19
|
+
typecheckArgs?: Array<string>;
|
|
20
|
+
}
|
|
21
|
+
interface TypecheckStopDecisionResult {
|
|
22
|
+
decision?: "block";
|
|
23
|
+
reason?: string;
|
|
24
|
+
resetStopAttempts?: true;
|
|
25
|
+
}
|
|
26
|
+
interface TypeCheckOutputOptions {
|
|
27
|
+
dependencyErrors: Array<string>;
|
|
28
|
+
fileErrors: Array<string>;
|
|
29
|
+
totalDependencyErrors: number;
|
|
30
|
+
totalFileErrors: number;
|
|
31
|
+
}
|
|
32
|
+
interface TypecheckStopDecisionInput {
|
|
33
|
+
errorFiles: Array<string>;
|
|
34
|
+
lintAttempts: Record<string, number>;
|
|
35
|
+
maxLintAttempts: number;
|
|
36
|
+
stopAttempts: number;
|
|
37
|
+
}
|
|
38
|
+
declare function partitionErrors(errors: Array<string>, filePath: string, projectRoot: string): {
|
|
39
|
+
dependencyErrors: Array<string>;
|
|
40
|
+
fileErrors: Array<string>;
|
|
41
|
+
};
|
|
42
|
+
declare function buildTypeCheckOutput(options: TypeCheckOutputOptions): PostToolUseHookOutput;
|
|
43
|
+
declare function typeCheck(filePath: string, settings: TypeCheckSettings): PostToolUseHookOutput | undefined;
|
|
44
|
+
declare function typecheckStopDecision(input: TypecheckStopDecisionInput): TypecheckStopDecisionResult | undefined;
|
|
45
|
+
declare function readTypecheckStopAttempts(): number;
|
|
46
|
+
declare function writeTypecheckStopAttempts(count: number): void;
|
|
47
|
+
declare function clearTypecheckStopAttempts(): void;
|
|
48
|
+
//#endregion
|
|
49
|
+
export { TsconfigCache, TypeCheckSettings, TypecheckStopDecisionResult, buildTypeCheckOutput, clearTypecheckStopAttempts, findTsconfigForFile, isTypeCheckable, partitionErrors, readTsconfigCache, readTypecheckStopAttempts, resolveTsconfig, resolveViaReferences, runTypeCheck, typeCheck, typecheckStopDecision, writeTsconfigCache, writeTypecheckStopAttempts };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, relative } from "node:path";
|
|
4
|
+
import { createFilesMatcher, parseTsconfig } from "get-tsconfig";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
//#region scripts/type-check.ts
|
|
8
|
+
const TYPE_CHECK_EXTENSIONS = [".ts", ".tsx"];
|
|
9
|
+
const CACHE_PATH = join(".claude", "state", "tsconfig-cache.json");
|
|
10
|
+
const STOP_STATE_PATH = join(".claude", "state", "typecheck-stop-attempts.json");
|
|
11
|
+
const DEFAULT_MAX_STOP_ATTEMPTS = 3;
|
|
12
|
+
function readTsconfigCache(projectRoot) {
|
|
13
|
+
const cachePath = join(projectRoot, CACHE_PATH);
|
|
14
|
+
if (!existsSync(cachePath)) return;
|
|
15
|
+
const content = readFileSync(cachePath, "utf-8");
|
|
16
|
+
return JSON.parse(content);
|
|
17
|
+
}
|
|
18
|
+
function writeTsconfigCache(projectRoot, cache) {
|
|
19
|
+
mkdirSync(join(projectRoot, ".claude", "state"), { recursive: true });
|
|
20
|
+
writeFileSync(join(projectRoot, CACHE_PATH), JSON.stringify(cache));
|
|
21
|
+
}
|
|
22
|
+
function resolveTsconfig(filePath, projectRoot) {
|
|
23
|
+
const cache = readTsconfigCache(projectRoot);
|
|
24
|
+
if (cache?.projectRoot === projectRoot) {
|
|
25
|
+
const cachedTsconfig = cache.mappings[filePath];
|
|
26
|
+
if (cachedTsconfig !== void 0 && existsSync(cachedTsconfig)) {
|
|
27
|
+
if (hashFileContent(cachedTsconfig) === cache.hashes[cachedTsconfig]) return cachedTsconfig;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const tsconfig = findTsconfigForFile(filePath, projectRoot);
|
|
31
|
+
if (tsconfig !== void 0) {
|
|
32
|
+
const hash = hashFileContent(tsconfig);
|
|
33
|
+
writeTsconfigCache(projectRoot, {
|
|
34
|
+
hashes: {
|
|
35
|
+
...cache?.hashes,
|
|
36
|
+
[tsconfig]: hash
|
|
37
|
+
},
|
|
38
|
+
mappings: {
|
|
39
|
+
...cache?.mappings,
|
|
40
|
+
[filePath]: tsconfig
|
|
41
|
+
},
|
|
42
|
+
projectRoot
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return tsconfig;
|
|
46
|
+
}
|
|
47
|
+
function hashFileContent(filePath) {
|
|
48
|
+
const content = readFileSync(filePath, "utf-8");
|
|
49
|
+
return createHash("sha256").update(content).digest("hex");
|
|
50
|
+
}
|
|
51
|
+
const DEFAULT_RUNNER = "pnpm exec";
|
|
52
|
+
function isTypeCheckable(filePath) {
|
|
53
|
+
return TYPE_CHECK_EXTENSIONS.some((extension) => filePath.endsWith(extension));
|
|
54
|
+
}
|
|
55
|
+
function resolveViaReferences(directory, configPath, targetFile) {
|
|
56
|
+
const { references } = parseTsconfig(configPath);
|
|
57
|
+
if (references === void 0 || references.length === 0) return;
|
|
58
|
+
for (const ref of references) {
|
|
59
|
+
const refPath = join(directory, ref.path);
|
|
60
|
+
const refConfigPath = refPath.endsWith(".json") ? refPath : join(refPath, "tsconfig.json");
|
|
61
|
+
if (!existsSync(refConfigPath)) continue;
|
|
62
|
+
if (createFilesMatcher({
|
|
63
|
+
config: parseTsconfig(refConfigPath),
|
|
64
|
+
path: refConfigPath
|
|
65
|
+
})(targetFile) !== void 0) return refConfigPath;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function findTsconfigForFile(targetFile, projectRoot) {
|
|
69
|
+
let directory = dirname(targetFile);
|
|
70
|
+
while (directory.length >= projectRoot.length) {
|
|
71
|
+
const candidate = join(directory, "tsconfig.json");
|
|
72
|
+
if (existsSync(candidate)) return resolveViaReferences(directory, candidate, targetFile) ?? candidate;
|
|
73
|
+
const parent = dirname(directory);
|
|
74
|
+
if (parent === directory) break;
|
|
75
|
+
directory = parent;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function runTypeCheck(tsconfig, runner = DEFAULT_RUNNER, extraArgs = []) {
|
|
79
|
+
const args = [`tsgo -p "${tsconfig}" --noEmit --pretty false`, ...extraArgs].join(" ");
|
|
80
|
+
try {
|
|
81
|
+
execSync(`${runner} ${args}`, { stdio: "pipe" });
|
|
82
|
+
return;
|
|
83
|
+
} catch (err_) {
|
|
84
|
+
const err = err_;
|
|
85
|
+
const stdout = err.stdout?.toString() ?? "";
|
|
86
|
+
const stderr = err.stderr?.toString() ?? "";
|
|
87
|
+
const message = err.message ?? "";
|
|
88
|
+
return stdout || stderr || message;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const MAX_ERRORS = 5;
|
|
92
|
+
function partitionErrors(errors, filePath, projectRoot) {
|
|
93
|
+
const relativePath = relative(projectRoot, filePath).replaceAll("\\", "/");
|
|
94
|
+
const fileErrors = [];
|
|
95
|
+
const dependencyErrors = [];
|
|
96
|
+
for (const error of errors) if (error.startsWith(relativePath)) fileErrors.push(error);
|
|
97
|
+
else dependencyErrors.push(error);
|
|
98
|
+
return {
|
|
99
|
+
dependencyErrors,
|
|
100
|
+
fileErrors
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function buildTypeCheckOutput(options) {
|
|
104
|
+
const sections = [];
|
|
105
|
+
if (options.totalFileErrors > 0) {
|
|
106
|
+
const text = options.fileErrors.join("\n");
|
|
107
|
+
const errorSuffix = options.totalFileErrors === 1 ? "error" : "errors";
|
|
108
|
+
sections.push(`TypeScript found ${options.totalFileErrors} type ${errorSuffix} in edited file:\n${text}`);
|
|
109
|
+
}
|
|
110
|
+
if (options.totalDependencyErrors > 0) {
|
|
111
|
+
const text = options.dependencyErrors.join("\n");
|
|
112
|
+
const errorSuffix = options.totalDependencyErrors === 1 ? "error" : "errors";
|
|
113
|
+
sections.push(`TypeScript found ${options.totalDependencyErrors} type ${errorSuffix} in other files:\n${text}`);
|
|
114
|
+
}
|
|
115
|
+
const isTruncated = options.fileErrors.length < options.totalFileErrors || options.dependencyErrors.length < options.totalDependencyErrors;
|
|
116
|
+
const suffix = isTruncated ? "\n..." : "";
|
|
117
|
+
const claudeSuffix = isTruncated ? "\n(run typecheck to view more)" : "";
|
|
118
|
+
const userMessage = sections.join("\n\n") + suffix;
|
|
119
|
+
return {
|
|
120
|
+
decision: void 0,
|
|
121
|
+
hookSpecificOutput: {
|
|
122
|
+
additionalContext: sections.join("\n\n") + claudeSuffix,
|
|
123
|
+
hookEventName: "PostToolUse"
|
|
124
|
+
},
|
|
125
|
+
systemMessage: userMessage
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function typeCheck(filePath, settings) {
|
|
129
|
+
if (!isTypeCheckable(filePath)) return;
|
|
130
|
+
const projectRoot = process.env["CLAUDE_PROJECT_DIR"] ?? process.cwd();
|
|
131
|
+
const tsconfig = resolveTsconfig(filePath, projectRoot);
|
|
132
|
+
if (tsconfig === void 0) return;
|
|
133
|
+
const output = runTypeCheck(tsconfig, settings.runner, settings.typecheckArgs);
|
|
134
|
+
if (output === void 0) return;
|
|
135
|
+
const allErrors = output.split("\n").filter((line) => /error TS/i.test(line));
|
|
136
|
+
if (allErrors.length === 0) return;
|
|
137
|
+
const { dependencyErrors, fileErrors } = partitionErrors(allErrors, filePath, projectRoot);
|
|
138
|
+
return buildTypeCheckOutput({
|
|
139
|
+
dependencyErrors: dependencyErrors.slice(0, MAX_ERRORS),
|
|
140
|
+
fileErrors: fileErrors.slice(0, MAX_ERRORS),
|
|
141
|
+
totalDependencyErrors: dependencyErrors.length,
|
|
142
|
+
totalFileErrors: fileErrors.length
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function typecheckStopDecision(input) {
|
|
146
|
+
if (input.errorFiles.length === 0) {
|
|
147
|
+
if (input.stopAttempts > 0) return { resetStopAttempts: true };
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (input.errorFiles.every((file) => {
|
|
151
|
+
return (input.lintAttempts[file] ?? 0) >= input.maxLintAttempts;
|
|
152
|
+
})) return;
|
|
153
|
+
if (input.stopAttempts >= DEFAULT_MAX_STOP_ATTEMPTS) return { reason: `Unresolved type errors in: ${input.errorFiles.join(", ")}. These may be pre-existing.` };
|
|
154
|
+
return {
|
|
155
|
+
decision: "block",
|
|
156
|
+
reason: `Type errors detected in: ${input.errorFiles.join(", ")}. If related to your changes, fix before finishing.`
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function readTypecheckStopAttempts() {
|
|
160
|
+
if (!existsSync(STOP_STATE_PATH)) return 0;
|
|
161
|
+
try {
|
|
162
|
+
return JSON.parse(readFileSync(STOP_STATE_PATH, "utf-8"));
|
|
163
|
+
} catch {
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function writeTypecheckStopAttempts(count) {
|
|
168
|
+
mkdirSync(dirname(STOP_STATE_PATH), { recursive: true });
|
|
169
|
+
writeFileSync(STOP_STATE_PATH, JSON.stringify(count));
|
|
170
|
+
}
|
|
171
|
+
function clearTypecheckStopAttempts() {
|
|
172
|
+
if (existsSync(STOP_STATE_PATH)) unlinkSync(STOP_STATE_PATH);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
//#endregion
|
|
176
|
+
export { buildTypeCheckOutput, clearTypecheckStopAttempts, findTsconfigForFile, isTypeCheckable, partitionErrors, readTsconfigCache, readTypecheckStopAttempts, resolveTsconfig, resolveViaReferences, runTypeCheck, typeCheck, typecheckStopDecision, writeTsconfigCache, writeTypecheckStopAttempts };
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@isentinel/hooks",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Claude Code hooks for linting and type-checking TypeScript projects",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude",
|
|
7
|
+
"hooks",
|
|
8
|
+
"eslint",
|
|
9
|
+
"typescript",
|
|
10
|
+
"linting"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/christopher-buss/skills",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/christopher-buss/skills/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/christopher-buss/skills.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Christopher Buss <christopher.buss@pm.me> (https://github.com/christopher-buss)",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
"./hooks/*": "./dist/hooks/*.mjs",
|
|
25
|
+
"./lint": {
|
|
26
|
+
"types": "./dist/scripts/lint.d.mts",
|
|
27
|
+
"default": "./dist/scripts/lint.mjs"
|
|
28
|
+
},
|
|
29
|
+
"./type-check": {
|
|
30
|
+
"types": "./dist/scripts/type-check.d.mts",
|
|
31
|
+
"default": "./dist/scripts/type-check.mjs"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"types": "./dist/scripts/lint.d.mts",
|
|
35
|
+
"files": [
|
|
36
|
+
"dist/"
|
|
37
|
+
],
|
|
38
|
+
"simple-git-hooks": {
|
|
39
|
+
"pre-commit": "pnpm lint-staged"
|
|
40
|
+
},
|
|
41
|
+
"lint-staged": {
|
|
42
|
+
"*.{ts,mts}": "eslint --fix"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"eslint_d": "^14.3.0",
|
|
46
|
+
"file-entry-cache": "^11.1.2",
|
|
47
|
+
"get-tsconfig": "^4.13.6",
|
|
48
|
+
"madge": "^8.0.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@clack/prompts": "^0.11.0",
|
|
52
|
+
"@constellos/claude-code-kit": "^0.4.0",
|
|
53
|
+
"@isentinel/eslint-config": "5.0.0-beta.8",
|
|
54
|
+
"@isentinel/tsconfig": "^1.2.0",
|
|
55
|
+
"@types/node": "^25.0.10",
|
|
56
|
+
"@typescript/native-preview": "7.0.0-dev.20260212.1",
|
|
57
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
58
|
+
"@vitest/eslint-plugin": "^1.6.9",
|
|
59
|
+
"better-typescript-lib": "^2.12.0",
|
|
60
|
+
"bumpp": "^10.4.1",
|
|
61
|
+
"eslint": "^9.39.2",
|
|
62
|
+
"eslint-plugin-n": "^17.23.2",
|
|
63
|
+
"eslint-plugin-pnpm": "^1.5.0",
|
|
64
|
+
"jiti": "^2.6.1",
|
|
65
|
+
"lint-staged": "^15.5.1",
|
|
66
|
+
"publint": "^0.3.18",
|
|
67
|
+
"simple-git-hooks": "^2.11.1",
|
|
68
|
+
"tsdown": "^0.20.3",
|
|
69
|
+
"type-fest": "^5.4.4",
|
|
70
|
+
"typescript": "^5.9.3",
|
|
71
|
+
"vitest": "^4.0.18"
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"@typescript/native-preview": ">=7"
|
|
75
|
+
},
|
|
76
|
+
"peerDependenciesMeta": {
|
|
77
|
+
"@typescript/native-preview": {
|
|
78
|
+
"optional": true
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"engines": {
|
|
82
|
+
"node": ">=24.11.0"
|
|
83
|
+
},
|
|
84
|
+
"scripts": {
|
|
85
|
+
"build": "tsdown",
|
|
86
|
+
"lint": "eslint --cache",
|
|
87
|
+
"pack": "pnpm pack",
|
|
88
|
+
"release": "bumpp",
|
|
89
|
+
"start": "node scripts/cli.ts",
|
|
90
|
+
"test": "vitest",
|
|
91
|
+
"test:coverage": "vitest run --coverage",
|
|
92
|
+
"typecheck": "tsgo --noEmit"
|
|
93
|
+
}
|
|
94
|
+
}
|