cc4pm 1.8.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/.claude-plugin/README.md +17 -0
- package/.claude-plugin/plugin.json +25 -0
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/README.zh-CN.md +134 -0
- package/contexts/dev.md +20 -0
- package/contexts/research.md +26 -0
- package/contexts/review.md +22 -0
- package/examples/CLAUDE.md +100 -0
- package/examples/statusline.json +19 -0
- package/examples/user-CLAUDE.md +109 -0
- package/install.sh +17 -0
- package/manifests/install-components.json +173 -0
- package/manifests/install-modules.json +335 -0
- package/manifests/install-profiles.json +75 -0
- package/package.json +117 -0
- package/schemas/ecc-install-config.schema.json +58 -0
- package/schemas/hooks.schema.json +197 -0
- package/schemas/install-components.schema.json +56 -0
- package/schemas/install-modules.schema.json +105 -0
- package/schemas/install-profiles.schema.json +45 -0
- package/schemas/install-state.schema.json +210 -0
- package/schemas/package-manager.schema.json +23 -0
- package/schemas/plugin.schema.json +58 -0
- package/scripts/ci/catalog.js +83 -0
- package/scripts/ci/validate-agents.js +81 -0
- package/scripts/ci/validate-commands.js +135 -0
- package/scripts/ci/validate-hooks.js +239 -0
- package/scripts/ci/validate-install-manifests.js +211 -0
- package/scripts/ci/validate-no-personal-paths.js +63 -0
- package/scripts/ci/validate-rules.js +81 -0
- package/scripts/ci/validate-skills.js +54 -0
- package/scripts/claw.js +468 -0
- package/scripts/doctor.js +110 -0
- package/scripts/ecc.js +194 -0
- package/scripts/hooks/auto-tmux-dev.js +88 -0
- package/scripts/hooks/check-console-log.js +71 -0
- package/scripts/hooks/check-hook-enabled.js +12 -0
- package/scripts/hooks/cost-tracker.js +78 -0
- package/scripts/hooks/doc-file-warning.js +63 -0
- package/scripts/hooks/evaluate-session.js +100 -0
- package/scripts/hooks/insaits-security-monitor.py +269 -0
- package/scripts/hooks/insaits-security-wrapper.js +88 -0
- package/scripts/hooks/post-bash-build-complete.js +27 -0
- package/scripts/hooks/post-bash-pr-created.js +36 -0
- package/scripts/hooks/post-edit-console-warn.js +54 -0
- package/scripts/hooks/post-edit-format.js +109 -0
- package/scripts/hooks/post-edit-typecheck.js +96 -0
- package/scripts/hooks/pre-bash-dev-server-block.js +187 -0
- package/scripts/hooks/pre-bash-git-push-reminder.js +28 -0
- package/scripts/hooks/pre-bash-tmux-reminder.js +33 -0
- package/scripts/hooks/pre-compact.js +48 -0
- package/scripts/hooks/pre-write-doc-warn.js +9 -0
- package/scripts/hooks/quality-gate.js +168 -0
- package/scripts/hooks/run-with-flags-shell.sh +32 -0
- package/scripts/hooks/run-with-flags.js +120 -0
- package/scripts/hooks/session-end-marker.js +15 -0
- package/scripts/hooks/session-end.js +299 -0
- package/scripts/hooks/session-start.js +97 -0
- package/scripts/hooks/suggest-compact.js +80 -0
- package/scripts/install-apply.js +137 -0
- package/scripts/install-plan.js +254 -0
- package/scripts/lib/hook-flags.js +74 -0
- package/scripts/lib/install/apply.js +23 -0
- package/scripts/lib/install/config.js +82 -0
- package/scripts/lib/install/request.js +113 -0
- package/scripts/lib/install/runtime.js +42 -0
- package/scripts/lib/install-executor.js +605 -0
- package/scripts/lib/install-lifecycle.js +763 -0
- package/scripts/lib/install-manifests.js +305 -0
- package/scripts/lib/install-state.js +120 -0
- package/scripts/lib/install-targets/antigravity-project.js +9 -0
- package/scripts/lib/install-targets/claude-home.js +10 -0
- package/scripts/lib/install-targets/codex-home.js +10 -0
- package/scripts/lib/install-targets/cursor-project.js +10 -0
- package/scripts/lib/install-targets/helpers.js +89 -0
- package/scripts/lib/install-targets/opencode-home.js +10 -0
- package/scripts/lib/install-targets/registry.js +64 -0
- package/scripts/lib/orchestration-session.js +299 -0
- package/scripts/lib/package-manager.d.ts +119 -0
- package/scripts/lib/package-manager.js +431 -0
- package/scripts/lib/project-detect.js +428 -0
- package/scripts/lib/resolve-formatter.js +185 -0
- package/scripts/lib/session-adapters/canonical-session.js +138 -0
- package/scripts/lib/session-adapters/claude-history.js +149 -0
- package/scripts/lib/session-adapters/dmux-tmux.js +80 -0
- package/scripts/lib/session-adapters/registry.js +111 -0
- package/scripts/lib/session-aliases.d.ts +136 -0
- package/scripts/lib/session-aliases.js +481 -0
- package/scripts/lib/session-manager.d.ts +131 -0
- package/scripts/lib/session-manager.js +464 -0
- package/scripts/lib/shell-split.js +86 -0
- package/scripts/lib/skill-improvement/amendify.js +89 -0
- package/scripts/lib/skill-improvement/evaluate.js +59 -0
- package/scripts/lib/skill-improvement/health.js +118 -0
- package/scripts/lib/skill-improvement/observations.js +108 -0
- package/scripts/lib/tmux-worktree-orchestrator.js +491 -0
- package/scripts/lib/utils.d.ts +183 -0
- package/scripts/lib/utils.js +543 -0
- package/scripts/list-installed.js +90 -0
- package/scripts/orchestrate-codex-worker.sh +92 -0
- package/scripts/orchestrate-worktrees.js +108 -0
- package/scripts/orchestration-status.js +62 -0
- package/scripts/repair.js +97 -0
- package/scripts/session-inspect.js +150 -0
- package/scripts/setup-package-manager.js +204 -0
- package/scripts/skill-create-output.js +244 -0
- package/scripts/uninstall.js +96 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const MAX_STDIN = 1024 * 1024;
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { splitShellSegments } = require('../lib/shell-split');
|
|
7
|
+
|
|
8
|
+
const DEV_COMMAND_WORDS = new Set([
|
|
9
|
+
'npm',
|
|
10
|
+
'pnpm',
|
|
11
|
+
'yarn',
|
|
12
|
+
'bun',
|
|
13
|
+
'npx',
|
|
14
|
+
'tmux'
|
|
15
|
+
]);
|
|
16
|
+
const SKIPPABLE_PREFIX_WORDS = new Set(['env', 'command', 'builtin', 'exec', 'noglob', 'sudo', 'nohup']);
|
|
17
|
+
const PREFIX_OPTION_VALUE_WORDS = {
|
|
18
|
+
env: new Set(['-u', '-C', '-S', '--unset', '--chdir', '--split-string']),
|
|
19
|
+
sudo: new Set([
|
|
20
|
+
'-u',
|
|
21
|
+
'-g',
|
|
22
|
+
'-h',
|
|
23
|
+
'-p',
|
|
24
|
+
'-r',
|
|
25
|
+
'-t',
|
|
26
|
+
'-C',
|
|
27
|
+
'--user',
|
|
28
|
+
'--group',
|
|
29
|
+
'--host',
|
|
30
|
+
'--prompt',
|
|
31
|
+
'--role',
|
|
32
|
+
'--type',
|
|
33
|
+
'--close-from'
|
|
34
|
+
])
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function readToken(input, startIndex) {
|
|
38
|
+
let index = startIndex;
|
|
39
|
+
while (index < input.length && /\s/.test(input[index])) index += 1;
|
|
40
|
+
if (index >= input.length) return null;
|
|
41
|
+
|
|
42
|
+
let token = '';
|
|
43
|
+
let quote = null;
|
|
44
|
+
|
|
45
|
+
while (index < input.length) {
|
|
46
|
+
const ch = input[index];
|
|
47
|
+
|
|
48
|
+
if (quote) {
|
|
49
|
+
if (ch === quote) {
|
|
50
|
+
quote = null;
|
|
51
|
+
index += 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (ch === '\\' && quote === '"' && index + 1 < input.length) {
|
|
56
|
+
token += input[index + 1];
|
|
57
|
+
index += 2;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
token += ch;
|
|
62
|
+
index += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (ch === '"' || ch === "'") {
|
|
67
|
+
quote = ch;
|
|
68
|
+
index += 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (/\s/.test(ch)) break;
|
|
73
|
+
|
|
74
|
+
if (ch === '\\' && index + 1 < input.length) {
|
|
75
|
+
token += input[index + 1];
|
|
76
|
+
index += 2;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
token += ch;
|
|
81
|
+
index += 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { token, end: index };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function shouldSkipOptionValue(wrapper, optionToken) {
|
|
88
|
+
if (!wrapper || !optionToken || optionToken.includes('=')) return false;
|
|
89
|
+
const optionSet = PREFIX_OPTION_VALUE_WORDS[wrapper];
|
|
90
|
+
return Boolean(optionSet && optionSet.has(optionToken));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isOptionToken(token) {
|
|
94
|
+
return token.startsWith('-') && token.length > 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeCommandWord(token) {
|
|
98
|
+
if (!token) return '';
|
|
99
|
+
const base = path.basename(token).toLowerCase();
|
|
100
|
+
return base.replace(/\.(cmd|exe|bat)$/i, '');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getLeadingCommandWord(segment) {
|
|
104
|
+
let index = 0;
|
|
105
|
+
let activeWrapper = null;
|
|
106
|
+
let skipNextValue = false;
|
|
107
|
+
|
|
108
|
+
while (index < segment.length) {
|
|
109
|
+
const parsed = readToken(segment, index);
|
|
110
|
+
if (!parsed) return null;
|
|
111
|
+
index = parsed.end;
|
|
112
|
+
|
|
113
|
+
const token = parsed.token;
|
|
114
|
+
if (!token) continue;
|
|
115
|
+
|
|
116
|
+
if (skipNextValue) {
|
|
117
|
+
skipNextValue = false;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (token === '--') {
|
|
122
|
+
activeWrapper = null;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token)) continue;
|
|
127
|
+
|
|
128
|
+
const normalizedToken = normalizeCommandWord(token);
|
|
129
|
+
|
|
130
|
+
if (SKIPPABLE_PREFIX_WORDS.has(normalizedToken)) {
|
|
131
|
+
activeWrapper = normalizedToken;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (activeWrapper && isOptionToken(token)) {
|
|
136
|
+
if (shouldSkipOptionValue(activeWrapper, token)) {
|
|
137
|
+
skipNextValue = true;
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return normalizedToken;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let raw = '';
|
|
149
|
+
process.stdin.setEncoding('utf8');
|
|
150
|
+
process.stdin.on('data', chunk => {
|
|
151
|
+
if (raw.length < MAX_STDIN) {
|
|
152
|
+
const remaining = MAX_STDIN - raw.length;
|
|
153
|
+
raw += chunk.substring(0, remaining);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
process.stdin.on('end', () => {
|
|
158
|
+
try {
|
|
159
|
+
const input = JSON.parse(raw);
|
|
160
|
+
const cmd = String(input.tool_input?.command || '');
|
|
161
|
+
|
|
162
|
+
if (process.platform !== 'win32') {
|
|
163
|
+
const segments = splitShellSegments(cmd);
|
|
164
|
+
const tmuxLauncher = /^\s*tmux\s+(new|new-session|new-window|split-window)\b/;
|
|
165
|
+
const devPattern = /\b(npm\s+run\s+dev|pnpm(?:\s+run)?\s+dev|yarn\s+dev|bun\s+run\s+dev)\b/;
|
|
166
|
+
|
|
167
|
+
const hasBlockedDev = segments.some(segment => {
|
|
168
|
+
const commandWord = getLeadingCommandWord(segment);
|
|
169
|
+
if (!commandWord || !DEV_COMMAND_WORDS.has(commandWord)) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
return devPattern.test(segment) && !tmuxLauncher.test(segment);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (hasBlockedDev) {
|
|
176
|
+
console.error('[Hook] BLOCKED: Dev server must run in tmux for log access');
|
|
177
|
+
console.error('[Hook] Use: tmux new-session -d -s dev "npm run dev"');
|
|
178
|
+
console.error('[Hook] Then: tmux attach -t dev');
|
|
179
|
+
process.exit(2);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// ignore parse errors and pass through
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
process.stdout.write(raw);
|
|
187
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const MAX_STDIN = 1024 * 1024;
|
|
5
|
+
let raw = '';
|
|
6
|
+
|
|
7
|
+
process.stdin.setEncoding('utf8');
|
|
8
|
+
process.stdin.on('data', chunk => {
|
|
9
|
+
if (raw.length < MAX_STDIN) {
|
|
10
|
+
const remaining = MAX_STDIN - raw.length;
|
|
11
|
+
raw += chunk.substring(0, remaining);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
process.stdin.on('end', () => {
|
|
16
|
+
try {
|
|
17
|
+
const input = JSON.parse(raw);
|
|
18
|
+
const cmd = String(input.tool_input?.command || '');
|
|
19
|
+
if (/\bgit\s+push\b/.test(cmd)) {
|
|
20
|
+
console.error('[Hook] Review changes before push...');
|
|
21
|
+
console.error('[Hook] Continuing with push (remove this hook to add interactive review)');
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
// ignore parse errors and pass through
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
process.stdout.write(raw);
|
|
28
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const MAX_STDIN = 1024 * 1024;
|
|
5
|
+
let raw = '';
|
|
6
|
+
|
|
7
|
+
process.stdin.setEncoding('utf8');
|
|
8
|
+
process.stdin.on('data', chunk => {
|
|
9
|
+
if (raw.length < MAX_STDIN) {
|
|
10
|
+
const remaining = MAX_STDIN - raw.length;
|
|
11
|
+
raw += chunk.substring(0, remaining);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
process.stdin.on('end', () => {
|
|
16
|
+
try {
|
|
17
|
+
const input = JSON.parse(raw);
|
|
18
|
+
const cmd = String(input.tool_input?.command || '');
|
|
19
|
+
|
|
20
|
+
if (
|
|
21
|
+
process.platform !== 'win32' &&
|
|
22
|
+
!process.env.TMUX &&
|
|
23
|
+
/(npm (install|test)|pnpm (install|test)|yarn (install|test)?|bun (install|test)|cargo build|make\b|docker\b|pytest|vitest|playwright)/.test(cmd)
|
|
24
|
+
) {
|
|
25
|
+
console.error('[Hook] Consider running in tmux for session persistence');
|
|
26
|
+
console.error('[Hook] tmux new -s dev | tmux attach -t dev');
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
// ignore parse errors and pass through
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
process.stdout.write(raw);
|
|
33
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PreCompact Hook - Save state before context compaction
|
|
4
|
+
*
|
|
5
|
+
* Cross-platform (Windows, macOS, Linux)
|
|
6
|
+
*
|
|
7
|
+
* Runs before Claude compacts context, giving you a chance to
|
|
8
|
+
* preserve important state that might get lost in summarization.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const {
|
|
13
|
+
getSessionsDir,
|
|
14
|
+
getDateTimeString,
|
|
15
|
+
getTimeString,
|
|
16
|
+
findFiles,
|
|
17
|
+
ensureDir,
|
|
18
|
+
appendFile,
|
|
19
|
+
log
|
|
20
|
+
} = require('../lib/utils');
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
const sessionsDir = getSessionsDir();
|
|
24
|
+
const compactionLog = path.join(sessionsDir, 'compaction-log.txt');
|
|
25
|
+
|
|
26
|
+
ensureDir(sessionsDir);
|
|
27
|
+
|
|
28
|
+
// Log compaction event with timestamp
|
|
29
|
+
const timestamp = getDateTimeString();
|
|
30
|
+
appendFile(compactionLog, `[${timestamp}] Context compaction triggered\n`);
|
|
31
|
+
|
|
32
|
+
// If there's an active session file, note the compaction
|
|
33
|
+
const sessions = findFiles(sessionsDir, '*-session.tmp');
|
|
34
|
+
|
|
35
|
+
if (sessions.length > 0) {
|
|
36
|
+
const activeSession = sessions[0].path;
|
|
37
|
+
const timeStr = getTimeString();
|
|
38
|
+
appendFile(activeSession, `\n---\n**[Compaction occurred at ${timeStr}]** - Context was summarized\n`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
log('[PreCompact] State saved before compaction');
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
main().catch(err => {
|
|
46
|
+
console.error('[PreCompact] Error:', err.message);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Quality Gate Hook
|
|
4
|
+
*
|
|
5
|
+
* Runs lightweight quality checks after file edits.
|
|
6
|
+
* - Targets one file when file_path is provided
|
|
7
|
+
* - Falls back to no-op when language/tooling is unavailable
|
|
8
|
+
*
|
|
9
|
+
* For JS/TS files with Biome, this hook is skipped because
|
|
10
|
+
* post-edit-format.js already runs `biome check --write`.
|
|
11
|
+
* This hook still handles .json/.md files for Biome, and all
|
|
12
|
+
* Prettier / Go / Python checks.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { spawnSync } = require('child_process');
|
|
20
|
+
|
|
21
|
+
const { findProjectRoot, detectFormatter, resolveFormatterBin } = require('../lib/resolve-formatter');
|
|
22
|
+
|
|
23
|
+
const MAX_STDIN = 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Execute a command synchronously, returning the spawnSync result.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} command - Executable path or name
|
|
29
|
+
* @param {string[]} args - Arguments to pass
|
|
30
|
+
* @param {string} [cwd] - Working directory (defaults to process.cwd())
|
|
31
|
+
* @returns {import('child_process').SpawnSyncReturns<string>}
|
|
32
|
+
*/
|
|
33
|
+
function exec(command, args, cwd = process.cwd()) {
|
|
34
|
+
return spawnSync(command, args, {
|
|
35
|
+
cwd,
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
env: process.env,
|
|
38
|
+
timeout: 15000
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Write a message to stderr for logging.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} msg - Message to log
|
|
46
|
+
*/
|
|
47
|
+
function log(msg) {
|
|
48
|
+
process.stderr.write(`${msg}\n`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run quality-gate checks for a single file based on its extension.
|
|
53
|
+
* Skips JS/TS files when Biome is configured (handled by post-edit-format).
|
|
54
|
+
*
|
|
55
|
+
* @param {string} filePath - Path to the edited file
|
|
56
|
+
*/
|
|
57
|
+
function maybeRunQualityGate(filePath) {
|
|
58
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Resolve to absolute path so projectRoot-relative comparisons work
|
|
63
|
+
filePath = path.resolve(filePath);
|
|
64
|
+
|
|
65
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
66
|
+
const fix = String(process.env.ECC_QUALITY_GATE_FIX || '').toLowerCase() === 'true';
|
|
67
|
+
const strict = String(process.env.ECC_QUALITY_GATE_STRICT || '').toLowerCase() === 'true';
|
|
68
|
+
|
|
69
|
+
if (['.ts', '.tsx', '.js', '.jsx', '.json', '.md'].includes(ext)) {
|
|
70
|
+
const projectRoot = findProjectRoot(path.dirname(filePath));
|
|
71
|
+
const formatter = detectFormatter(projectRoot);
|
|
72
|
+
|
|
73
|
+
if (formatter === 'biome') {
|
|
74
|
+
// JS/TS already handled by post-edit-format via `biome check --write`
|
|
75
|
+
if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// .json / .md — still need quality gate
|
|
80
|
+
const resolved = resolveFormatterBin(projectRoot, 'biome');
|
|
81
|
+
if (!resolved) return;
|
|
82
|
+
const args = [...resolved.prefix, 'check', filePath];
|
|
83
|
+
if (fix) args.push('--write');
|
|
84
|
+
const result = exec(resolved.bin, args, projectRoot);
|
|
85
|
+
if (result.status !== 0 && strict) {
|
|
86
|
+
log(`[QualityGate] Biome check failed for ${filePath}`);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (formatter === 'prettier') {
|
|
92
|
+
const resolved = resolveFormatterBin(projectRoot, 'prettier');
|
|
93
|
+
if (!resolved) return;
|
|
94
|
+
const args = [...resolved.prefix, fix ? '--write' : '--check', filePath];
|
|
95
|
+
const result = exec(resolved.bin, args, projectRoot);
|
|
96
|
+
if (result.status !== 0 && strict) {
|
|
97
|
+
log(`[QualityGate] Prettier check failed for ${filePath}`);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// No formatter configured — skip
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (ext === '.go') {
|
|
107
|
+
if (fix) {
|
|
108
|
+
const r = exec('gofmt', ['-w', filePath]);
|
|
109
|
+
if (r.status !== 0 && strict) {
|
|
110
|
+
log(`[QualityGate] gofmt failed for ${filePath}`);
|
|
111
|
+
}
|
|
112
|
+
} else if (strict) {
|
|
113
|
+
const r = exec('gofmt', ['-l', filePath]);
|
|
114
|
+
if (r.status !== 0) {
|
|
115
|
+
log(`[QualityGate] gofmt failed for ${filePath}`);
|
|
116
|
+
} else if (r.stdout && r.stdout.trim()) {
|
|
117
|
+
log(`[QualityGate] gofmt check failed for ${filePath}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (ext === '.py') {
|
|
124
|
+
const args = ['format'];
|
|
125
|
+
if (!fix) args.push('--check');
|
|
126
|
+
args.push(filePath);
|
|
127
|
+
const r = exec('ruff', args);
|
|
128
|
+
if (r.status !== 0 && strict) {
|
|
129
|
+
log(`[QualityGate] Ruff check failed for ${filePath}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Core logic — exported so run-with-flags.js can call directly.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} rawInput - Raw JSON string from stdin
|
|
138
|
+
* @returns {string} The original input (pass-through)
|
|
139
|
+
*/
|
|
140
|
+
function run(rawInput) {
|
|
141
|
+
try {
|
|
142
|
+
const input = JSON.parse(rawInput);
|
|
143
|
+
const filePath = String(input.tool_input?.file_path || '');
|
|
144
|
+
maybeRunQualityGate(filePath);
|
|
145
|
+
} catch {
|
|
146
|
+
// Ignore parse errors.
|
|
147
|
+
}
|
|
148
|
+
return rawInput;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── stdin entry point (backwards-compatible) ────────────────────
|
|
152
|
+
if (require.main === module) {
|
|
153
|
+
let raw = '';
|
|
154
|
+
process.stdin.setEncoding('utf8');
|
|
155
|
+
process.stdin.on('data', chunk => {
|
|
156
|
+
if (raw.length < MAX_STDIN) {
|
|
157
|
+
const remaining = MAX_STDIN - raw.length;
|
|
158
|
+
raw += chunk.substring(0, remaining);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
process.stdin.on('end', () => {
|
|
163
|
+
const result = run(raw);
|
|
164
|
+
process.stdout.write(result);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = { run };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
HOOK_ID="${1:-}"
|
|
5
|
+
REL_SCRIPT_PATH="${2:-}"
|
|
6
|
+
PROFILES_CSV="${3:-standard,strict}"
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "${SCRIPT_DIR}/../.." && pwd)}"
|
|
9
|
+
|
|
10
|
+
# Preserve stdin for passthrough or script execution
|
|
11
|
+
INPUT="$(cat)"
|
|
12
|
+
|
|
13
|
+
if [[ -z "$HOOK_ID" || -z "$REL_SCRIPT_PATH" ]]; then
|
|
14
|
+
printf '%s' "$INPUT"
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# Ask Node helper if this hook is enabled
|
|
19
|
+
ENABLED="$(node "${PLUGIN_ROOT}/scripts/hooks/check-hook-enabled.js" "$HOOK_ID" "$PROFILES_CSV" 2>/dev/null || echo yes)"
|
|
20
|
+
if [[ "$ENABLED" != "yes" ]]; then
|
|
21
|
+
printf '%s' "$INPUT"
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
SCRIPT_PATH="${PLUGIN_ROOT}/${REL_SCRIPT_PATH}"
|
|
26
|
+
if [[ ! -f "$SCRIPT_PATH" ]]; then
|
|
27
|
+
echo "[Hook] Script not found for ${HOOK_ID}: ${SCRIPT_PATH}" >&2
|
|
28
|
+
printf '%s' "$INPUT"
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
printf '%s' "$INPUT" | "$SCRIPT_PATH"
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Executes a hook script only when enabled by cc4pm hook profile flags.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node run-with-flags.js <hookId> <scriptRelativePath> [profilesCsv]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { spawnSync } = require('child_process');
|
|
14
|
+
const { isHookEnabled } = require('../lib/hook-flags');
|
|
15
|
+
|
|
16
|
+
const MAX_STDIN = 1024 * 1024;
|
|
17
|
+
|
|
18
|
+
function readStdinRaw() {
|
|
19
|
+
return new Promise(resolve => {
|
|
20
|
+
let raw = '';
|
|
21
|
+
process.stdin.setEncoding('utf8');
|
|
22
|
+
process.stdin.on('data', chunk => {
|
|
23
|
+
if (raw.length < MAX_STDIN) {
|
|
24
|
+
const remaining = MAX_STDIN - raw.length;
|
|
25
|
+
raw += chunk.substring(0, remaining);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
process.stdin.on('end', () => resolve(raw));
|
|
29
|
+
process.stdin.on('error', () => resolve(raw));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getPluginRoot() {
|
|
34
|
+
if (process.env.CLAUDE_PLUGIN_ROOT && process.env.CLAUDE_PLUGIN_ROOT.trim()) {
|
|
35
|
+
return process.env.CLAUDE_PLUGIN_ROOT;
|
|
36
|
+
}
|
|
37
|
+
return path.resolve(__dirname, '..', '..');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function main() {
|
|
41
|
+
const [, , hookId, relScriptPath, profilesCsv] = process.argv;
|
|
42
|
+
const raw = await readStdinRaw();
|
|
43
|
+
|
|
44
|
+
if (!hookId || !relScriptPath) {
|
|
45
|
+
process.stdout.write(raw);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!isHookEnabled(hookId, { profiles: profilesCsv })) {
|
|
50
|
+
process.stdout.write(raw);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const pluginRoot = getPluginRoot();
|
|
55
|
+
const resolvedRoot = path.resolve(pluginRoot);
|
|
56
|
+
const scriptPath = path.resolve(pluginRoot, relScriptPath);
|
|
57
|
+
|
|
58
|
+
// Prevent path traversal outside the plugin root
|
|
59
|
+
if (!scriptPath.startsWith(resolvedRoot + path.sep)) {
|
|
60
|
+
process.stderr.write(`[Hook] Path traversal rejected for ${hookId}: ${scriptPath}\n`);
|
|
61
|
+
process.stdout.write(raw);
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!fs.existsSync(scriptPath)) {
|
|
66
|
+
process.stderr.write(`[Hook] Script not found for ${hookId}: ${scriptPath}\n`);
|
|
67
|
+
process.stdout.write(raw);
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Prefer direct require() when the hook exports a run(rawInput) function.
|
|
72
|
+
// This eliminates one Node.js process spawn (~50-100ms savings per hook).
|
|
73
|
+
//
|
|
74
|
+
// SAFETY: Only require() hooks that export run(). Legacy hooks execute
|
|
75
|
+
// side effects at module scope (stdin listeners, process.exit, main() calls)
|
|
76
|
+
// which would interfere with the parent process or cause double execution.
|
|
77
|
+
let hookModule;
|
|
78
|
+
const src = fs.readFileSync(scriptPath, 'utf8');
|
|
79
|
+
const hasRunExport = /\bmodule\.exports\b/.test(src) && /\brun\b/.test(src);
|
|
80
|
+
|
|
81
|
+
if (hasRunExport) {
|
|
82
|
+
try {
|
|
83
|
+
hookModule = require(scriptPath);
|
|
84
|
+
} catch (requireErr) {
|
|
85
|
+
process.stderr.write(`[Hook] require() failed for ${hookId}: ${requireErr.message}\n`);
|
|
86
|
+
// Fall through to legacy spawnSync path
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (hookModule && typeof hookModule.run === 'function') {
|
|
91
|
+
try {
|
|
92
|
+
const output = hookModule.run(raw);
|
|
93
|
+
if (output !== null && output !== undefined) process.stdout.write(output);
|
|
94
|
+
} catch (runErr) {
|
|
95
|
+
process.stderr.write(`[Hook] run() error for ${hookId}: ${runErr.message}\n`);
|
|
96
|
+
process.stdout.write(raw);
|
|
97
|
+
}
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Legacy path: spawn a child Node process for hooks without run() export
|
|
102
|
+
const result = spawnSync('node', [scriptPath], {
|
|
103
|
+
input: raw,
|
|
104
|
+
encoding: 'utf8',
|
|
105
|
+
env: process.env,
|
|
106
|
+
cwd: process.cwd(),
|
|
107
|
+
timeout: 30000
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
111
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
112
|
+
|
|
113
|
+
const code = Number.isInteger(result.status) ? result.status : 0;
|
|
114
|
+
process.exit(code);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
main().catch(err => {
|
|
118
|
+
process.stderr.write(`[Hook] run-with-flags error: ${err.message}\n`);
|
|
119
|
+
process.exit(0);
|
|
120
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const MAX_STDIN = 1024 * 1024;
|
|
5
|
+
let raw = '';
|
|
6
|
+
process.stdin.setEncoding('utf8');
|
|
7
|
+
process.stdin.on('data', chunk => {
|
|
8
|
+
if (raw.length < MAX_STDIN) {
|
|
9
|
+
const remaining = MAX_STDIN - raw.length;
|
|
10
|
+
raw += chunk.substring(0, remaining);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
process.stdin.on('end', () => {
|
|
14
|
+
process.stdout.write(raw);
|
|
15
|
+
});
|