@nolrm/contextkit 0.16.4 → 0.18.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 +17 -4
- package/lib/commands/install.js +4 -0
- package/lib/commands/update.js +4 -0
- package/lib/integrations/claude-integration.js +43 -0
- package/lib/integrations/claude-integration.md +39 -0
- package/lib/utils/claude-settings.js +67 -0
- package/lib/utils/claude-settings.md +52 -0
- package/lib/utils/hook-detector.js +138 -0
- package/lib/utils/hook-detector.md +42 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,7 +114,7 @@ Each platform generates bridge files that the AI tool auto-reads. If a bridge fi
|
|
|
114
114
|
/fix # diagnose and fix bugs
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
**Claude Code** — `CLAUDE.md` uses `@` imports to auto-load all standards into context every session (no manual reads needed, saves tokens). Skills in `.claude/skills/`.
|
|
117
|
+
**Claude Code** — `CLAUDE.md` uses `@` imports to auto-load all standards into context every session (no manual reads needed, saves tokens). Skills in `.claude/skills/`. Also writes a PostToolUse hook to `.claude/settings.json` that runs format+lint after every file edit — auto-detected for Node.js (npm/pnpm/yarn/bun), Go, and Python.
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
120
|
/analyze # scan codebase and generate standards
|
|
@@ -164,7 +164,8 @@ ContextKit installs reusable slash commands for supported platforms:
|
|
|
164
164
|
| `/squad-test` | Classify test levels, write and run tests against acceptance criteria |
|
|
165
165
|
| `/squad-review` | Review the full pipeline and give a verdict |
|
|
166
166
|
| `/squad-doc` | Create companion `.md` files for new/modified code after review passes |
|
|
167
|
-
| `/squad-
|
|
167
|
+
| `/squad-go` | Extract tasks from the current conversation and run the full pipeline immediately — no second command needed |
|
|
168
|
+
| `/squad-auto` | Auto-run the full pipeline after `/squad` kickoff (sequential) |
|
|
168
169
|
| `/squad-auto-parallel` | Auto-run the pipeline in parallel using Claude Code agents (Claude Code only) |
|
|
169
170
|
| `/ck` | Health check — verify setup, standards, and integrations |
|
|
170
171
|
| `/agent-push-checklist` | Pre-push quality checklist for agents to self-check before `git push` |
|
|
@@ -195,12 +196,22 @@ The squad workflow turns a single AI session into a structured multi-role pipeli
|
|
|
195
196
|
| 5 | Reviewer | `/squad-review` | Reviews everything and gives a PASS or NEEDS-WORK verdict |
|
|
196
197
|
| 6 | Doc Writer | `/squad-doc` | Creates companion `.md` files for every new/modified code file |
|
|
197
198
|
|
|
199
|
+
### Express Flow (conversation-first)
|
|
200
|
+
|
|
201
|
+
After discussing a feature or fix with your AI tool, run a single command to go hands-free:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
/squad-go
|
|
205
|
+
# Reads tasks from the current conversation, confirms the list, writes specs,
|
|
206
|
+
# and immediately runs architect → dev → test → review → doc — no second command needed
|
|
207
|
+
```
|
|
208
|
+
|
|
198
209
|
### Single-Task Flow
|
|
199
210
|
|
|
200
211
|
```bash
|
|
201
212
|
/squad "add dark mode support" # PO writes the spec
|
|
202
213
|
|
|
203
|
-
/squad-auto # Auto-runs architect → dev → test → review → doc
|
|
214
|
+
/squad-auto # Auto-runs architect → dev → test → review → doc
|
|
204
215
|
# — or step through manually —
|
|
205
216
|
/squad-architect # Architect designs the plan
|
|
206
217
|
/squad-dev # Dev implements the code
|
|
@@ -258,7 +269,9 @@ If you have a screenshot, mockup, or design image relevant to the task, paste or
|
|
|
258
269
|
|
|
259
270
|
---
|
|
260
271
|
|
|
261
|
-
##
|
|
272
|
+
## Hooks & Quality Gates
|
|
273
|
+
|
|
274
|
+
ContextKit installs two kinds of hooks. **Git hooks** (pre-push, commit-msg) enforce quality at push time for the whole team. For **Claude Code** installs, a **PostToolUse hook** is also written to `.claude/settings.json` — it runs format+lint after every file edit in a Claude Code session, catching failures immediately rather than at push time.
|
|
262
275
|
|
|
263
276
|
ContextKit can optionally install Git hooks during `ck install`. Uses `git config core.hooksPath` to point Git at `.contextkit/hooks/` — no external dependencies like Husky required. Works in any git repo, not just Node.js projects.
|
|
264
277
|
|
package/lib/commands/install.js
CHANGED
|
@@ -973,6 +973,10 @@ Any design decisions, trade-offs, or open questions to resolve before coding.
|
|
|
973
973
|
`${this.repoUrl}/commands/squad/squad-ci.md`,
|
|
974
974
|
'.contextkit/commands/squad/squad-ci.md'
|
|
975
975
|
);
|
|
976
|
+
await this.downloadManager.downloadFile(
|
|
977
|
+
`${this.repoUrl}/commands/squad/squad-go.md`,
|
|
978
|
+
'.contextkit/commands/squad/squad-go.md'
|
|
979
|
+
);
|
|
976
980
|
await this.downloadManager.downloadFile(
|
|
977
981
|
`${this.repoUrl}/commands/dev/health-check.md`,
|
|
978
982
|
'.contextkit/commands/dev/health-check.md'
|
package/lib/commands/update.js
CHANGED
|
@@ -337,6 +337,10 @@ class UpdateCommand {
|
|
|
337
337
|
`${this.repoUrl}/commands/squad/squad-ci.md`,
|
|
338
338
|
'.contextkit/commands/squad/squad-ci.md'
|
|
339
339
|
);
|
|
340
|
+
await this.downloadManager.downloadFile(
|
|
341
|
+
`${this.repoUrl}/commands/squad/squad-go.md`,
|
|
342
|
+
'.contextkit/commands/squad/squad-go.md'
|
|
343
|
+
);
|
|
340
344
|
await this.downloadManager.downloadFile(
|
|
341
345
|
`${this.repoUrl}/commands/dev/health-check.md`,
|
|
342
346
|
'.contextkit/commands/dev/health-check.md'
|
|
@@ -26,6 +26,7 @@ class ClaudeIntegration extends BaseIntegration {
|
|
|
26
26
|
'.claude/skills/squad-auto-parallel/SKILL.md',
|
|
27
27
|
'.claude/skills/squad-reset/SKILL.md',
|
|
28
28
|
'.claude/skills/squad-doc/SKILL.md',
|
|
29
|
+
'.claude/skills/squad-go/SKILL.md',
|
|
29
30
|
'.claude/skills/spec/SKILL.md',
|
|
30
31
|
'.claude/skills/ck/SKILL.md',
|
|
31
32
|
'.claude/skills/doc-arch/SKILL.md',
|
|
@@ -44,6 +45,25 @@ class ClaudeIntegration extends BaseIntegration {
|
|
|
44
45
|
await fs.ensureDir('.claude/skills');
|
|
45
46
|
await this.addToGitignore('.claude/settings.local.json');
|
|
46
47
|
await this.removeLegacyFiles();
|
|
48
|
+
await this._installHook();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async _installHook() {
|
|
52
|
+
try {
|
|
53
|
+
const HookDetector = require('../utils/hook-detector');
|
|
54
|
+
const ClaudeSettings = require('../utils/claude-settings');
|
|
55
|
+
const command = await new HookDetector().detect();
|
|
56
|
+
if (command) {
|
|
57
|
+
await new ClaudeSettings().addPostToolUseHook(command);
|
|
58
|
+
console.log(chalk.green(` ✓ PostToolUse hook: ${command}`));
|
|
59
|
+
} else {
|
|
60
|
+
console.log(
|
|
61
|
+
chalk.dim(chalk.yellow(' ⚠ No format/lint tooling detected — PostToolUse hook skipped'))
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.log(chalk.yellow(` ⚠ Could not install PostToolUse hook: ${err.message}`));
|
|
66
|
+
}
|
|
47
67
|
}
|
|
48
68
|
|
|
49
69
|
async addToGitignore(entry) {
|
|
@@ -452,6 +472,24 @@ After review passes, create or update companion .md files for every new/modified
|
|
|
452
472
|
`
|
|
453
473
|
);
|
|
454
474
|
|
|
475
|
+
await this.writeGeneratedFile(
|
|
476
|
+
'.claude/skills/squad-go/SKILL.md',
|
|
477
|
+
`---
|
|
478
|
+
description: Extract tasks from conversation and run the full pipeline immediately — no checkpoint pause
|
|
479
|
+
argument-hint: '"<optional task override>"'
|
|
480
|
+
allowed-tools: Read, Edit, Write, Glob, Grep, Bash
|
|
481
|
+
effort: high
|
|
482
|
+
context: fork
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
Read \`.contextkit/commands/squad/squad-go.md\` and execute the express pipeline workflow.
|
|
486
|
+
|
|
487
|
+
Reads tasks from the current conversation, writes PO specs, and immediately runs architect → dev → test → review → doc. No checkpoint pause — single invocation, hands-free.
|
|
488
|
+
|
|
489
|
+
Use \`/squad\` instead when you want to review specs before execution starts.
|
|
490
|
+
`
|
|
491
|
+
);
|
|
492
|
+
|
|
455
493
|
// Doc family skills
|
|
456
494
|
await this.writeGeneratedFile(
|
|
457
495
|
'.claude/skills/doc-arch/SKILL.md',
|
|
@@ -571,6 +609,11 @@ Load and apply the project's ContextKit standards before taking action in an age
|
|
|
571
609
|
console.log(chalk.dim(' /squad-dev — Implement the code'));
|
|
572
610
|
console.log(chalk.dim(' /squad-test — Write and run tests'));
|
|
573
611
|
console.log(chalk.dim(' /squad-review — Review and write verdict'));
|
|
612
|
+
console.log(
|
|
613
|
+
chalk.dim(
|
|
614
|
+
' /squad-go — Extract tasks from conversation and run full pipeline'
|
|
615
|
+
)
|
|
616
|
+
);
|
|
574
617
|
console.log(chalk.dim(' /squad-auto — Auto-run full pipeline (recommended)'));
|
|
575
618
|
console.log(
|
|
576
619
|
chalk.dim(' /squad-auto-parallel — Auto-run pipeline in parallel (batch, fastest)')
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# claude-integration.js
|
|
2
|
+
|
|
3
|
+
The Claude Code platform integration. Installs `CLAUDE.md`, `.claude/rules/`, `.claude/skills/`, and a PostToolUse hook in `.claude/settings.json`.
|
|
4
|
+
|
|
5
|
+
## Extends
|
|
6
|
+
|
|
7
|
+
`BaseIntegration`
|
|
8
|
+
|
|
9
|
+
## install(force = false)
|
|
10
|
+
|
|
11
|
+
Runs the full Claude Code setup:
|
|
12
|
+
1. Writes bridge file (`CLAUDE.md`) and generated files (rules, skills) via `super.install()`
|
|
13
|
+
2. Ensures `.claude/skills/` directory exists
|
|
14
|
+
3. Adds `.claude/settings.local.json` to `.gitignore`
|
|
15
|
+
4. Removes legacy `.claude/commands/` files
|
|
16
|
+
5. Detects project tooling and installs a PostToolUse hook in `.claude/settings.json`
|
|
17
|
+
|
|
18
|
+
### Hook Installation (step 5)
|
|
19
|
+
|
|
20
|
+
Uses `HookDetector` to detect the right format+lint command for the project (Node.js, Go, or Python). If a command is found, `ClaudeSettings.addPostToolUseHook()` writes it to `.claude/settings.json` with a `_contextkit: true` marker.
|
|
21
|
+
|
|
22
|
+
- On success: logs `✓ PostToolUse hook: <command>`
|
|
23
|
+
- No tooling detected: logs a dim yellow skip message
|
|
24
|
+
- Any error: logs a yellow warning and continues (graceful degradation)
|
|
25
|
+
|
|
26
|
+
Re-running `install()` (e.g. via `ck update → refreshIntegrations()`) replaces the existing ContextKit hook rather than duplicating it.
|
|
27
|
+
|
|
28
|
+
## generatedFiles
|
|
29
|
+
|
|
30
|
+
Listed in the constructor. Does **not** include `.claude/settings.json` — that file is a merge target, not overwritten.
|
|
31
|
+
|
|
32
|
+
## Key Files Written
|
|
33
|
+
|
|
34
|
+
| File | Type | Purpose |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `CLAUDE.md` | Bridge (merged) | Auto-loaded every Claude Code session |
|
|
37
|
+
| `.claude/rules/contextkit-*.md` | Generated | Scoped rules for standards, testing, code style |
|
|
38
|
+
| `.claude/skills/*/SKILL.md` | Generated | All slash commands |
|
|
39
|
+
| `.claude/settings.json` | Merged | PostToolUse hook entry (`_contextkit: true`) |
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
|
|
4
|
+
const SETTINGS_PATH = '.claude/settings.json';
|
|
5
|
+
|
|
6
|
+
class ClaudeSettings {
|
|
7
|
+
async read() {
|
|
8
|
+
if (!(await fs.pathExists(SETTINGS_PATH))) return {};
|
|
9
|
+
try {
|
|
10
|
+
const raw = await fs.readFile(SETTINGS_PATH, 'utf8');
|
|
11
|
+
return JSON.parse(raw);
|
|
12
|
+
} catch {
|
|
13
|
+
console.warn(
|
|
14
|
+
chalk.yellow(` ⚠ .claude/settings.json contains invalid JSON — skipping hook merge`)
|
|
15
|
+
);
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async write(settings) {
|
|
21
|
+
await fs.ensureDir('.claude');
|
|
22
|
+
await fs.writeFile(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async addPostToolUseHook(command) {
|
|
26
|
+
if (!command) throw new Error('hook command is required');
|
|
27
|
+
|
|
28
|
+
const settings = await this.read();
|
|
29
|
+
settings.hooks = settings.hooks || {};
|
|
30
|
+
|
|
31
|
+
if (!Array.isArray(settings.hooks.PostToolUse)) {
|
|
32
|
+
if (settings.hooks.PostToolUse !== undefined) {
|
|
33
|
+
settings.hooks._contextkit_original_invalid = settings.hooks.PostToolUse;
|
|
34
|
+
}
|
|
35
|
+
settings.hooks.PostToolUse = [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter((entry) => !entry._contextkit);
|
|
39
|
+
|
|
40
|
+
settings.hooks.PostToolUse.push({
|
|
41
|
+
matcher: 'Edit|Write',
|
|
42
|
+
hooks: [{ type: 'command', command }],
|
|
43
|
+
_contextkit: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await this.write(settings);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async removeContextKitHooks() {
|
|
50
|
+
const settings = await this.read();
|
|
51
|
+
if (!Array.isArray(settings.hooks?.PostToolUse)) return;
|
|
52
|
+
|
|
53
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter((entry) => !entry._contextkit);
|
|
54
|
+
|
|
55
|
+
if (settings.hooks.PostToolUse.length === 0) {
|
|
56
|
+
delete settings.hooks.PostToolUse;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
60
|
+
delete settings.hooks;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await this.write(settings);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = ClaudeSettings;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# claude-settings.js
|
|
2
|
+
|
|
3
|
+
Reads, merges, and writes `.claude/settings.json` safely — adding or removing ContextKit-owned entries without clobbering user content.
|
|
4
|
+
|
|
5
|
+
## Exports
|
|
6
|
+
|
|
7
|
+
`ClaudeSettings` — class
|
|
8
|
+
|
|
9
|
+
## Public API
|
|
10
|
+
|
|
11
|
+
### `read() → Promise<object>`
|
|
12
|
+
|
|
13
|
+
Returns the parsed contents of `.claude/settings.json`, or `{}` if the file does not exist. On invalid JSON, logs a yellow warning and returns `{}` (does not throw).
|
|
14
|
+
|
|
15
|
+
### `write(settings) → Promise<void>`
|
|
16
|
+
|
|
17
|
+
Writes the settings object to `.claude/settings.json` with 2-space indentation and a trailing newline. Creates `.claude/` if it does not exist.
|
|
18
|
+
|
|
19
|
+
### `addPostToolUseHook(command) → Promise<void>`
|
|
20
|
+
|
|
21
|
+
Adds a ContextKit-owned PostToolUse hook entry. Replaces any existing `_contextkit: true` entry rather than duplicating. Merges into existing file content — other keys (permissions, other hooks) are preserved.
|
|
22
|
+
|
|
23
|
+
Throws `Error('hook command is required')` if `command` is falsy.
|
|
24
|
+
|
|
25
|
+
Hook entry written:
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"matcher": "Edit|Write",
|
|
29
|
+
"hooks": [{ "type": "command", "command": "<command>" }],
|
|
30
|
+
"_contextkit": true
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### `removeContextKitHooks() → Promise<void>`
|
|
35
|
+
|
|
36
|
+
Removes all `PostToolUse` entries where `_contextkit: true`. If the array becomes empty, deletes the `PostToolUse` key. If `hooks` becomes empty, deletes the `hooks` key. No-op if the file is absent.
|
|
37
|
+
|
|
38
|
+
## Usage Example
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
const ClaudeSettings = require('./claude-settings');
|
|
42
|
+
|
|
43
|
+
const cs = new ClaudeSettings();
|
|
44
|
+
await cs.addPostToolUseHook('pnpm run format && pnpm run lint --fix 2>&1 | tail -20');
|
|
45
|
+
// → writes .claude/settings.json, merging into existing content
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Edge Cases & Notes
|
|
49
|
+
|
|
50
|
+
- `PostToolUse` that is not an array: the original value is preserved under `_contextkit_original_invalid`, and a fresh array is started for ContextKit's entry
|
|
51
|
+
- Invalid JSON in existing file: returns `{}` with a warning — the file is NOT overwritten (protects corrupted user files)
|
|
52
|
+
- Thread safety: no file locking; not a concern for a CLI tool
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
class HookDetector {
|
|
6
|
+
_commandExists(name) {
|
|
7
|
+
try {
|
|
8
|
+
execSync(`which ${name}`, { stdio: 'ignore' });
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
_pmPrefix(pm) {
|
|
16
|
+
switch (pm) {
|
|
17
|
+
case 'pnpm':
|
|
18
|
+
return 'pnpm run';
|
|
19
|
+
case 'yarn':
|
|
20
|
+
return 'yarn';
|
|
21
|
+
case 'bun':
|
|
22
|
+
return 'bun run';
|
|
23
|
+
default:
|
|
24
|
+
return 'npm run';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_lintFixCmd(pm, hasLintFix, hasLint) {
|
|
29
|
+
if (hasLintFix) {
|
|
30
|
+
switch (pm) {
|
|
31
|
+
case 'yarn':
|
|
32
|
+
return 'yarn lint:fix';
|
|
33
|
+
default:
|
|
34
|
+
return `${this._pmPrefix(pm)} lint:fix`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (hasLint) {
|
|
38
|
+
switch (pm) {
|
|
39
|
+
case 'pnpm':
|
|
40
|
+
return 'pnpm run lint --fix';
|
|
41
|
+
case 'yarn':
|
|
42
|
+
return 'yarn lint --fix';
|
|
43
|
+
case 'bun':
|
|
44
|
+
return 'bun run lint --fix';
|
|
45
|
+
default:
|
|
46
|
+
return 'npm run lint -- --fix';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async _detectNode(projectDir) {
|
|
53
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
54
|
+
if (!(await fs.pathExists(pkgPath))) return null;
|
|
55
|
+
|
|
56
|
+
let packageJson;
|
|
57
|
+
try {
|
|
58
|
+
const raw = await fs.readFile(pkgPath, 'utf8');
|
|
59
|
+
packageJson = JSON.parse(raw);
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const scripts = packageJson.scripts || {};
|
|
65
|
+
|
|
66
|
+
let pm = 'npm';
|
|
67
|
+
if (await fs.pathExists(path.join(projectDir, 'bun.lockb'))) {
|
|
68
|
+
pm = 'bun';
|
|
69
|
+
} else if (await fs.pathExists(path.join(projectDir, 'pnpm-lock.yaml'))) {
|
|
70
|
+
pm = 'pnpm';
|
|
71
|
+
} else if (await fs.pathExists(path.join(projectDir, 'yarn.lock'))) {
|
|
72
|
+
pm = 'yarn';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const hasFormat = Boolean(scripts.format);
|
|
76
|
+
const hasLintFix = Boolean(scripts['lint:fix']);
|
|
77
|
+
const hasLint = Boolean(scripts.lint);
|
|
78
|
+
|
|
79
|
+
const parts = [];
|
|
80
|
+
|
|
81
|
+
if (hasFormat) {
|
|
82
|
+
const prefix = this._pmPrefix(pm);
|
|
83
|
+
parts.push(pm === 'yarn' ? `yarn format` : `${prefix} format`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const lintCmd = this._lintFixCmd(pm, hasLintFix, hasLint);
|
|
87
|
+
if (lintCmd) {
|
|
88
|
+
parts.push(lintCmd);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (parts.length === 0) return null;
|
|
92
|
+
|
|
93
|
+
return parts.join(' && ') + ' 2>&1 | tail -20';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async _detectGo(projectDir) {
|
|
97
|
+
if (!(await fs.pathExists(path.join(projectDir, 'go.mod')))) return null;
|
|
98
|
+
|
|
99
|
+
const parts = [];
|
|
100
|
+
if (this._commandExists('gofmt')) parts.push('gofmt -w .');
|
|
101
|
+
if (this._commandExists('golangci-lint')) parts.push('golangci-lint run 2>&1 | tail -20');
|
|
102
|
+
|
|
103
|
+
if (parts.length === 0) return null;
|
|
104
|
+
|
|
105
|
+
const cmd = parts.join(' && ');
|
|
106
|
+
// If only gofmt, add tail ourselves; if golangci-lint is present, it already has tail
|
|
107
|
+
return parts.length === 1 && parts[0] === 'gofmt -w .' ? cmd : cmd;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async _detectPython(projectDir) {
|
|
111
|
+
const hasPyproject = await fs.pathExists(path.join(projectDir, 'pyproject.toml'));
|
|
112
|
+
const hasRequirements = await fs.pathExists(path.join(projectDir, 'requirements.txt'));
|
|
113
|
+
if (!hasPyproject && !hasRequirements) return null;
|
|
114
|
+
|
|
115
|
+
const parts = [];
|
|
116
|
+
if (this._commandExists('black')) parts.push('black .');
|
|
117
|
+
if (this._commandExists('ruff')) parts.push('ruff check --fix . 2>&1 | tail -20');
|
|
118
|
+
|
|
119
|
+
if (parts.length === 0) return null;
|
|
120
|
+
|
|
121
|
+
return parts.join(' && ');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async detect(projectDir = process.cwd()) {
|
|
125
|
+
const node = await this._detectNode(projectDir);
|
|
126
|
+
if (node) return node;
|
|
127
|
+
|
|
128
|
+
const go = await this._detectGo(projectDir);
|
|
129
|
+
if (go) return go;
|
|
130
|
+
|
|
131
|
+
const python = await this._detectPython(projectDir);
|
|
132
|
+
if (python) return python;
|
|
133
|
+
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = HookDetector;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# hook-detector.js
|
|
2
|
+
|
|
3
|
+
Detects the right PostToolUse hook command for a project based on its language, package manager, and available scripts.
|
|
4
|
+
|
|
5
|
+
## Exports
|
|
6
|
+
|
|
7
|
+
`HookDetector` — class
|
|
8
|
+
|
|
9
|
+
## Public API
|
|
10
|
+
|
|
11
|
+
### `detect(projectDir = process.cwd()) → Promise<string | null>`
|
|
12
|
+
|
|
13
|
+
Returns a shell command string suitable for use as a Claude Code PostToolUse hook, or `null` if no format/lint tooling is detected.
|
|
14
|
+
|
|
15
|
+
Tries three detectors in order: Node.js → Go → Python.
|
|
16
|
+
|
|
17
|
+
## Usage Example
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
const HookDetector = require('./hook-detector');
|
|
21
|
+
|
|
22
|
+
const command = await new HookDetector().detect('/path/to/project');
|
|
23
|
+
// → "pnpm run format && pnpm run lint --fix 2>&1 | tail -20"
|
|
24
|
+
// → null (if no tooling found)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Detection Logic
|
|
28
|
+
|
|
29
|
+
| Stack | Trigger | PM detection | Command built from |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| Node.js | `package.json` exists | lockfile (`bun.lockb` > `pnpm-lock.yaml` > `yarn.lock` > `npm`) | `scripts.format`, `scripts['lint:fix']`, `scripts.lint` |
|
|
32
|
+
| Go | `go.mod` exists | n/a | `gofmt` and/or `golangci-lint` (via `which`) |
|
|
33
|
+
| Python | `pyproject.toml` or `requirements.txt` | n/a | `black` and/or `ruff` (via `which`) |
|
|
34
|
+
|
|
35
|
+
For Node.js: `lint:fix` takes priority over `lint`. If only `lint` is present, `--fix` is appended (npm uses `-- --fix` separator; pnpm/yarn/bun pass args directly).
|
|
36
|
+
|
|
37
|
+
## Edge Cases & Notes
|
|
38
|
+
|
|
39
|
+
- Malformed `package.json` → returns `null`, does not throw
|
|
40
|
+
- No scripts in `package.json` → returns `null`
|
|
41
|
+
- Go/Python tools not on PATH → returns `null`
|
|
42
|
+
- Node.js takes priority when multiple stack markers coexist
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nolrm/contextkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "ContextKit - Context Engineering for AI Development. Provide rich context to AI through structured MD files with standards, code guides, and documentation. Works with Cursor, Claude, Aider, VS Code Copilot, and more.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|