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,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Claude Plugin Configuration",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["name"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"name": { "type": "string" },
|
|
8
|
+
"version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
|
|
9
|
+
"description": { "type": "string" },
|
|
10
|
+
"author": {
|
|
11
|
+
"oneOf": [
|
|
12
|
+
{ "type": "string" },
|
|
13
|
+
{
|
|
14
|
+
"type": "object",
|
|
15
|
+
"properties": {
|
|
16
|
+
"name": { "type": "string" },
|
|
17
|
+
"url": { "type": "string", "format": "uri" }
|
|
18
|
+
},
|
|
19
|
+
"required": ["name"]
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"homepage": { "type": "string", "format": "uri" },
|
|
24
|
+
"repository": { "type": "string" },
|
|
25
|
+
"license": { "type": "string" },
|
|
26
|
+
"keywords": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"items": { "type": "string" }
|
|
29
|
+
},
|
|
30
|
+
"skills": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"items": { "type": "string" }
|
|
33
|
+
},
|
|
34
|
+
"agents": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": { "type": "string" }
|
|
37
|
+
},
|
|
38
|
+
"features": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": {
|
|
41
|
+
"agents": { "type": "integer", "minimum": 0 },
|
|
42
|
+
"commands": { "type": "integer", "minimum": 0 },
|
|
43
|
+
"skills": { "type": "integer", "minimum": 0 },
|
|
44
|
+
"configAssets": { "type": "boolean" },
|
|
45
|
+
"hookEvents": {
|
|
46
|
+
"type": "array",
|
|
47
|
+
"items": { "type": "string" }
|
|
48
|
+
},
|
|
49
|
+
"customTools": {
|
|
50
|
+
"type": "array",
|
|
51
|
+
"items": { "type": "string" }
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"additionalProperties": false
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"additionalProperties": false
|
|
58
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Catalog agents, commands, and skills from the repo.
|
|
4
|
+
* Outputs JSON with counts and lists for CI/docs sync.
|
|
5
|
+
*
|
|
6
|
+
* Usage: node scripts/ci/catalog.js [--json|--md]
|
|
7
|
+
* Default: --json to stdout
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const ROOT = path.join(__dirname, '../..');
|
|
14
|
+
const AGENTS_DIR = path.join(ROOT, 'agents');
|
|
15
|
+
const COMMANDS_DIR = path.join(ROOT, 'commands');
|
|
16
|
+
const SKILLS_DIR = path.join(ROOT, 'skills');
|
|
17
|
+
|
|
18
|
+
function listAgents() {
|
|
19
|
+
if (!fs.existsSync(AGENTS_DIR)) return [];
|
|
20
|
+
try {
|
|
21
|
+
return fs.readdirSync(AGENTS_DIR)
|
|
22
|
+
.filter(f => f.endsWith('.md'))
|
|
23
|
+
.map(f => f.slice(0, -3))
|
|
24
|
+
.sort();
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new Error(`Failed to read agents directory (${AGENTS_DIR}): ${error.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function listCommands() {
|
|
31
|
+
if (!fs.existsSync(COMMANDS_DIR)) return [];
|
|
32
|
+
try {
|
|
33
|
+
return fs.readdirSync(COMMANDS_DIR)
|
|
34
|
+
.filter(f => f.endsWith('.md'))
|
|
35
|
+
.map(f => f.slice(0, -3))
|
|
36
|
+
.sort();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new Error(`Failed to read commands directory (${COMMANDS_DIR}): ${error.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function listSkills() {
|
|
43
|
+
if (!fs.existsSync(SKILLS_DIR)) return [];
|
|
44
|
+
try {
|
|
45
|
+
const entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
|
|
46
|
+
return entries
|
|
47
|
+
.filter(e => e.isDirectory() && fs.existsSync(path.join(SKILLS_DIR, e.name, 'SKILL.md')))
|
|
48
|
+
.map(e => e.name)
|
|
49
|
+
.sort();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new Error(`Failed to read skills directory (${SKILLS_DIR}): ${error.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function run() {
|
|
56
|
+
const agents = listAgents();
|
|
57
|
+
const commands = listCommands();
|
|
58
|
+
const skills = listSkills();
|
|
59
|
+
|
|
60
|
+
const catalog = {
|
|
61
|
+
agents: { count: agents.length, list: agents },
|
|
62
|
+
commands: { count: commands.length, list: commands },
|
|
63
|
+
skills: { count: skills.length, list: skills }
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const format = process.argv[2] === '--md' ? 'md' : 'json';
|
|
67
|
+
if (format === 'md') {
|
|
68
|
+
console.log('# cc4pm Catalog (generated)\n');
|
|
69
|
+
console.log(`- **Agents:** ${catalog.agents.count}`);
|
|
70
|
+
console.log(`- **Commands:** ${catalog.commands.count}`);
|
|
71
|
+
console.log(`- **Skills:** ${catalog.skills.count}\n`);
|
|
72
|
+
console.log('## Agents\n');
|
|
73
|
+
catalog.agents.list.forEach(a => { console.log(`- ${a}`); });
|
|
74
|
+
console.log('\n## Commands\n');
|
|
75
|
+
catalog.commands.list.forEach(c => { console.log(`- ${c}`); });
|
|
76
|
+
console.log('\n## Skills\n');
|
|
77
|
+
catalog.skills.list.forEach(s => { console.log(`- ${s}`); });
|
|
78
|
+
} else {
|
|
79
|
+
console.log(JSON.stringify(catalog, null, 2));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
run();
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validate agent markdown files have required frontmatter
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const AGENTS_DIR = path.join(__dirname, '../../agents');
|
|
10
|
+
const REQUIRED_FIELDS = ['model', 'tools'];
|
|
11
|
+
const VALID_MODELS = ['haiku', 'sonnet', 'opus'];
|
|
12
|
+
|
|
13
|
+
function extractFrontmatter(content) {
|
|
14
|
+
// Strip BOM if present (UTF-8 BOM: \uFEFF)
|
|
15
|
+
const cleanContent = content.replace(/^\uFEFF/, '');
|
|
16
|
+
// Support both LF and CRLF line endings
|
|
17
|
+
const match = cleanContent.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
18
|
+
if (!match) return null;
|
|
19
|
+
|
|
20
|
+
const frontmatter = {};
|
|
21
|
+
const lines = match[1].split(/\r?\n/);
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
const colonIdx = line.indexOf(':');
|
|
24
|
+
if (colonIdx > 0) {
|
|
25
|
+
const key = line.slice(0, colonIdx).trim();
|
|
26
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
27
|
+
frontmatter[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return frontmatter;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function validateAgents() {
|
|
34
|
+
if (!fs.existsSync(AGENTS_DIR)) {
|
|
35
|
+
console.log('No agents directory found, skipping validation');
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const files = fs.readdirSync(AGENTS_DIR).filter(f => f.endsWith('.md'));
|
|
40
|
+
let hasErrors = false;
|
|
41
|
+
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
const filePath = path.join(AGENTS_DIR, file);
|
|
44
|
+
let content;
|
|
45
|
+
try {
|
|
46
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error(`ERROR: ${file} - ${err.message}`);
|
|
49
|
+
hasErrors = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const frontmatter = extractFrontmatter(content);
|
|
53
|
+
|
|
54
|
+
if (!frontmatter) {
|
|
55
|
+
console.error(`ERROR: ${file} - Missing frontmatter`);
|
|
56
|
+
hasErrors = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const field of REQUIRED_FIELDS) {
|
|
61
|
+
if (!frontmatter[field] || (typeof frontmatter[field] === 'string' && !frontmatter[field].trim())) {
|
|
62
|
+
console.error(`ERROR: ${file} - Missing required field: ${field}`);
|
|
63
|
+
hasErrors = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Validate model is a known value
|
|
68
|
+
if (frontmatter.model && !VALID_MODELS.includes(frontmatter.model)) {
|
|
69
|
+
console.error(`ERROR: ${file} - Invalid model '${frontmatter.model}'. Must be one of: ${VALID_MODELS.join(', ')}`);
|
|
70
|
+
hasErrors = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (hasErrors) {
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(`Validated ${files.length} agent files`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
validateAgents();
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validate command markdown files are non-empty, readable,
|
|
4
|
+
* and have valid cross-references to other commands, agents, and skills.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const ROOT_DIR = path.join(__dirname, '../..');
|
|
11
|
+
const COMMANDS_DIR = path.join(ROOT_DIR, 'commands');
|
|
12
|
+
const AGENTS_DIR = path.join(ROOT_DIR, 'agents');
|
|
13
|
+
const SKILLS_DIR = path.join(ROOT_DIR, 'skills');
|
|
14
|
+
|
|
15
|
+
function validateCommands() {
|
|
16
|
+
if (!fs.existsSync(COMMANDS_DIR)) {
|
|
17
|
+
console.log('No commands directory found, skipping validation');
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const files = fs.readdirSync(COMMANDS_DIR).filter(f => f.endsWith('.md'));
|
|
22
|
+
let hasErrors = false;
|
|
23
|
+
let warnCount = 0;
|
|
24
|
+
|
|
25
|
+
// Build set of valid command names (without .md extension)
|
|
26
|
+
const validCommands = new Set(files.map(f => f.replace(/\.md$/, '')));
|
|
27
|
+
|
|
28
|
+
// Build set of valid agent names (without .md extension)
|
|
29
|
+
const validAgents = new Set();
|
|
30
|
+
if (fs.existsSync(AGENTS_DIR)) {
|
|
31
|
+
for (const f of fs.readdirSync(AGENTS_DIR)) {
|
|
32
|
+
if (f.endsWith('.md')) {
|
|
33
|
+
validAgents.add(f.replace(/\.md$/, ''));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Build set of valid skill directory names
|
|
39
|
+
const validSkills = new Set();
|
|
40
|
+
if (fs.existsSync(SKILLS_DIR)) {
|
|
41
|
+
for (const f of fs.readdirSync(SKILLS_DIR)) {
|
|
42
|
+
const skillPath = path.join(SKILLS_DIR, f);
|
|
43
|
+
try {
|
|
44
|
+
if (fs.statSync(skillPath).isDirectory()) {
|
|
45
|
+
validSkills.add(f);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// skip unreadable entries
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
const filePath = path.join(COMMANDS_DIR, file);
|
|
55
|
+
let content;
|
|
56
|
+
try {
|
|
57
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error(`ERROR: ${file} - ${err.message}`);
|
|
60
|
+
hasErrors = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Validate the file is non-empty readable markdown
|
|
65
|
+
if (content.trim().length === 0) {
|
|
66
|
+
console.error(`ERROR: ${file} - Empty command file`);
|
|
67
|
+
hasErrors = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Strip fenced code blocks before checking cross-references.
|
|
72
|
+
// Examples/templates inside ``` blocks are not real references.
|
|
73
|
+
const contentNoCodeBlocks = content.replace(/```[\s\S]*?```/g, '');
|
|
74
|
+
|
|
75
|
+
// Check cross-references to other commands (e.g., `/build-fix`)
|
|
76
|
+
// Skip lines that describe hypothetical output (e.g., "→ Creates: `/new-table`")
|
|
77
|
+
// Process line-by-line so ALL command refs per line are captured
|
|
78
|
+
// (previous anchored regex /^.*`\/...`.*$/gm only matched the last ref per line)
|
|
79
|
+
for (const line of contentNoCodeBlocks.split('\n')) {
|
|
80
|
+
if (/creates:|would create:/i.test(line)) continue;
|
|
81
|
+
const lineRefs = line.matchAll(/`\/([a-z][-a-z0-9]*)`/g);
|
|
82
|
+
for (const match of lineRefs) {
|
|
83
|
+
const refName = match[1];
|
|
84
|
+
if (!validCommands.has(refName)) {
|
|
85
|
+
console.error(`ERROR: ${file} - references non-existent command /${refName}`);
|
|
86
|
+
hasErrors = true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check agent references (e.g., "agents/planner.md" or "`planner` agent")
|
|
92
|
+
const agentPathRefs = contentNoCodeBlocks.matchAll(/agents\/([a-z][-a-z0-9]*)\.md/g);
|
|
93
|
+
for (const match of agentPathRefs) {
|
|
94
|
+
const refName = match[1];
|
|
95
|
+
if (!validAgents.has(refName)) {
|
|
96
|
+
console.error(`ERROR: ${file} - references non-existent agent agents/${refName}.md`);
|
|
97
|
+
hasErrors = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check skill directory references (e.g., "skills/tdd-workflow/")
|
|
102
|
+
const skillRefs = contentNoCodeBlocks.matchAll(/skills\/([a-z][-a-z0-9]*)\//g);
|
|
103
|
+
for (const match of skillRefs) {
|
|
104
|
+
const refName = match[1];
|
|
105
|
+
if (!validSkills.has(refName)) {
|
|
106
|
+
console.warn(`WARN: ${file} - references skill directory skills/${refName}/ (not found locally)`);
|
|
107
|
+
warnCount++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check agent name references in workflow diagrams (e.g., "planner -> tdd-guide")
|
|
112
|
+
const workflowLines = contentNoCodeBlocks.matchAll(/^([a-z][-a-z0-9]*(?:\s*->\s*[a-z][-a-z0-9]*)+)$/gm);
|
|
113
|
+
for (const match of workflowLines) {
|
|
114
|
+
const agents = match[1].split(/\s*->\s*/);
|
|
115
|
+
for (const agent of agents) {
|
|
116
|
+
if (!validAgents.has(agent)) {
|
|
117
|
+
console.error(`ERROR: ${file} - workflow references non-existent agent "${agent}"`);
|
|
118
|
+
hasErrors = true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (hasErrors) {
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let msg = `Validated ${files.length} command files`;
|
|
129
|
+
if (warnCount > 0) {
|
|
130
|
+
msg += ` (${warnCount} warnings)`;
|
|
131
|
+
}
|
|
132
|
+
console.log(msg);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
validateCommands();
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validate hooks.json schema and hook entry rules.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const vm = require('vm');
|
|
9
|
+
const Ajv = require('ajv');
|
|
10
|
+
|
|
11
|
+
const HOOKS_FILE = path.join(__dirname, '../../hooks/hooks.json');
|
|
12
|
+
const HOOKS_SCHEMA_PATH = path.join(__dirname, '../../schemas/hooks.schema.json');
|
|
13
|
+
const VALID_EVENTS = [
|
|
14
|
+
'SessionStart',
|
|
15
|
+
'UserPromptSubmit',
|
|
16
|
+
'PreToolUse',
|
|
17
|
+
'PermissionRequest',
|
|
18
|
+
'PostToolUse',
|
|
19
|
+
'PostToolUseFailure',
|
|
20
|
+
'Notification',
|
|
21
|
+
'SubagentStart',
|
|
22
|
+
'Stop',
|
|
23
|
+
'SubagentStop',
|
|
24
|
+
'PreCompact',
|
|
25
|
+
'InstructionsLoaded',
|
|
26
|
+
'TeammateIdle',
|
|
27
|
+
'TaskCompleted',
|
|
28
|
+
'ConfigChange',
|
|
29
|
+
'WorktreeCreate',
|
|
30
|
+
'WorktreeRemove',
|
|
31
|
+
'SessionEnd',
|
|
32
|
+
];
|
|
33
|
+
const VALID_HOOK_TYPES = ['command', 'http', 'prompt', 'agent'];
|
|
34
|
+
const EVENTS_WITHOUT_MATCHER = new Set(['UserPromptSubmit', 'Notification', 'Stop', 'SubagentStop']);
|
|
35
|
+
|
|
36
|
+
function isNonEmptyString(value) {
|
|
37
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isNonEmptyStringArray(value) {
|
|
41
|
+
return Array.isArray(value) && value.length > 0 && value.every(item => isNonEmptyString(item));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validate a single hook entry has required fields and valid inline JS
|
|
46
|
+
* @param {object} hook - Hook object with type and command fields
|
|
47
|
+
* @param {string} label - Label for error messages (e.g., "PreToolUse[0].hooks[1]")
|
|
48
|
+
* @returns {boolean} true if errors were found
|
|
49
|
+
*/
|
|
50
|
+
function validateHookEntry(hook, label) {
|
|
51
|
+
let hasErrors = false;
|
|
52
|
+
|
|
53
|
+
if (!hook.type || typeof hook.type !== 'string') {
|
|
54
|
+
console.error(`ERROR: ${label} missing or invalid 'type' field`);
|
|
55
|
+
hasErrors = true;
|
|
56
|
+
} else if (!VALID_HOOK_TYPES.includes(hook.type)) {
|
|
57
|
+
console.error(`ERROR: ${label} has unsupported hook type '${hook.type}'`);
|
|
58
|
+
hasErrors = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if ('timeout' in hook && (typeof hook.timeout !== 'number' || hook.timeout < 0)) {
|
|
62
|
+
console.error(`ERROR: ${label} 'timeout' must be a non-negative number`);
|
|
63
|
+
hasErrors = true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (hook.type === 'command') {
|
|
67
|
+
if ('async' in hook && typeof hook.async !== 'boolean') {
|
|
68
|
+
console.error(`ERROR: ${label} 'async' must be a boolean`);
|
|
69
|
+
hasErrors = true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!isNonEmptyString(hook.command) && !isNonEmptyStringArray(hook.command)) {
|
|
73
|
+
console.error(`ERROR: ${label} missing or invalid 'command' field`);
|
|
74
|
+
hasErrors = true;
|
|
75
|
+
} else if (typeof hook.command === 'string') {
|
|
76
|
+
const nodeEMatch = hook.command.match(/^node -e "(.*)"$/s);
|
|
77
|
+
if (nodeEMatch) {
|
|
78
|
+
try {
|
|
79
|
+
new vm.Script(nodeEMatch[1].replace(/\\\\/g, '\\').replace(/\\"/g, '"').replace(/\\n/g, '\n').replace(/\\t/g, '\t'));
|
|
80
|
+
} catch (syntaxErr) {
|
|
81
|
+
console.error(`ERROR: ${label} has invalid inline JS: ${syntaxErr.message}`);
|
|
82
|
+
hasErrors = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return hasErrors;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if ('async' in hook) {
|
|
91
|
+
console.error(`ERROR: ${label} 'async' is only supported for command hooks`);
|
|
92
|
+
hasErrors = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (hook.type === 'http') {
|
|
96
|
+
if (!isNonEmptyString(hook.url)) {
|
|
97
|
+
console.error(`ERROR: ${label} missing or invalid 'url' field`);
|
|
98
|
+
hasErrors = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if ('headers' in hook && (typeof hook.headers !== 'object' || hook.headers === null || Array.isArray(hook.headers) || !Object.values(hook.headers).every(value => typeof value === 'string'))) {
|
|
102
|
+
console.error(`ERROR: ${label} 'headers' must be an object with string values`);
|
|
103
|
+
hasErrors = true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if ('allowedEnvVars' in hook && (!Array.isArray(hook.allowedEnvVars) || !hook.allowedEnvVars.every(value => isNonEmptyString(value)))) {
|
|
107
|
+
console.error(`ERROR: ${label} 'allowedEnvVars' must be an array of strings`);
|
|
108
|
+
hasErrors = true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return hasErrors;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!isNonEmptyString(hook.prompt)) {
|
|
115
|
+
console.error(`ERROR: ${label} missing or invalid 'prompt' field`);
|
|
116
|
+
hasErrors = true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if ('model' in hook && !isNonEmptyString(hook.model)) {
|
|
120
|
+
console.error(`ERROR: ${label} 'model' must be a non-empty string`);
|
|
121
|
+
hasErrors = true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return hasErrors;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function validateHooks() {
|
|
128
|
+
if (!fs.existsSync(HOOKS_FILE)) {
|
|
129
|
+
console.log('No hooks.json found, skipping validation');
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let data;
|
|
134
|
+
try {
|
|
135
|
+
data = JSON.parse(fs.readFileSync(HOOKS_FILE, 'utf-8'));
|
|
136
|
+
} catch (e) {
|
|
137
|
+
console.error(`ERROR: Invalid JSON in hooks.json: ${e.message}`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Validate against JSON schema
|
|
142
|
+
if (fs.existsSync(HOOKS_SCHEMA_PATH)) {
|
|
143
|
+
const schema = JSON.parse(fs.readFileSync(HOOKS_SCHEMA_PATH, 'utf-8'));
|
|
144
|
+
const ajv = new Ajv({ allErrors: true });
|
|
145
|
+
const validate = ajv.compile(schema);
|
|
146
|
+
const valid = validate(data);
|
|
147
|
+
if (!valid) {
|
|
148
|
+
for (const err of validate.errors) {
|
|
149
|
+
console.error(`ERROR: hooks.json schema: ${err.instancePath || '/'} ${err.message}`);
|
|
150
|
+
}
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Support both object format { hooks: {...} } and array format
|
|
156
|
+
const hooks = data.hooks || data;
|
|
157
|
+
let hasErrors = false;
|
|
158
|
+
let totalMatchers = 0;
|
|
159
|
+
|
|
160
|
+
if (typeof hooks === 'object' && !Array.isArray(hooks)) {
|
|
161
|
+
// Object format: { EventType: [matchers] }
|
|
162
|
+
for (const [eventType, matchers] of Object.entries(hooks)) {
|
|
163
|
+
if (!VALID_EVENTS.includes(eventType)) {
|
|
164
|
+
console.error(`ERROR: Invalid event type: ${eventType}`);
|
|
165
|
+
hasErrors = true;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!Array.isArray(matchers)) {
|
|
170
|
+
console.error(`ERROR: ${eventType} must be an array`);
|
|
171
|
+
hasErrors = true;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (let i = 0; i < matchers.length; i++) {
|
|
176
|
+
const matcher = matchers[i];
|
|
177
|
+
if (typeof matcher !== 'object' || matcher === null) {
|
|
178
|
+
console.error(`ERROR: ${eventType}[${i}] is not an object`);
|
|
179
|
+
hasErrors = true;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (!('matcher' in matcher) && !EVENTS_WITHOUT_MATCHER.has(eventType)) {
|
|
183
|
+
console.error(`ERROR: ${eventType}[${i}] missing 'matcher' field`);
|
|
184
|
+
hasErrors = true;
|
|
185
|
+
} else if ('matcher' in matcher && typeof matcher.matcher !== 'string' && (typeof matcher.matcher !== 'object' || matcher.matcher === null)) {
|
|
186
|
+
console.error(`ERROR: ${eventType}[${i}] has invalid 'matcher' field`);
|
|
187
|
+
hasErrors = true;
|
|
188
|
+
}
|
|
189
|
+
if (!matcher.hooks || !Array.isArray(matcher.hooks)) {
|
|
190
|
+
console.error(`ERROR: ${eventType}[${i}] missing 'hooks' array`);
|
|
191
|
+
hasErrors = true;
|
|
192
|
+
} else {
|
|
193
|
+
// Validate each hook entry
|
|
194
|
+
for (let j = 0; j < matcher.hooks.length; j++) {
|
|
195
|
+
if (validateHookEntry(matcher.hooks[j], `${eventType}[${i}].hooks[${j}]`)) {
|
|
196
|
+
hasErrors = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
totalMatchers++;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} else if (Array.isArray(hooks)) {
|
|
204
|
+
// Array format (legacy)
|
|
205
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
206
|
+
const hook = hooks[i];
|
|
207
|
+
if (!('matcher' in hook)) {
|
|
208
|
+
console.error(`ERROR: Hook ${i} missing 'matcher' field`);
|
|
209
|
+
hasErrors = true;
|
|
210
|
+
} else if (typeof hook.matcher !== 'string' && (typeof hook.matcher !== 'object' || hook.matcher === null)) {
|
|
211
|
+
console.error(`ERROR: Hook ${i} has invalid 'matcher' field`);
|
|
212
|
+
hasErrors = true;
|
|
213
|
+
}
|
|
214
|
+
if (!hook.hooks || !Array.isArray(hook.hooks)) {
|
|
215
|
+
console.error(`ERROR: Hook ${i} missing 'hooks' array`);
|
|
216
|
+
hasErrors = true;
|
|
217
|
+
} else {
|
|
218
|
+
// Validate each hook entry
|
|
219
|
+
for (let j = 0; j < hook.hooks.length; j++) {
|
|
220
|
+
if (validateHookEntry(hook.hooks[j], `Hook ${i}.hooks[${j}]`)) {
|
|
221
|
+
hasErrors = true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
totalMatchers++;
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
console.error('ERROR: hooks.json must be an object or array');
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (hasErrors) {
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
console.log(`Validated ${totalMatchers} hook matchers`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
validateHooks();
|