@whitehatd/crag 0.0.1 → 0.2.1
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 +838 -15
- package/bin/crag.js +7 -0
- package/package.json +18 -4
- package/src/cli.js +102 -0
- package/src/commands/analyze.js +513 -0
- package/src/commands/check.js +55 -0
- package/src/commands/compile.js +104 -0
- package/src/commands/diff.js +289 -0
- package/src/commands/init.js +112 -0
- package/src/commands/upgrade.js +64 -0
- package/src/commands/workspace.js +94 -0
- package/src/compile/agents-md.js +58 -0
- package/src/compile/atomic-write.js +32 -0
- package/src/compile/cline.js +83 -0
- package/src/compile/cody.js +82 -0
- package/src/compile/continue.js +78 -0
- package/src/compile/copilot.js +70 -0
- package/src/compile/cursor-rules.js +66 -0
- package/src/compile/gemini-md.js +58 -0
- package/src/compile/github-actions.js +165 -0
- package/src/compile/husky.js +66 -0
- package/src/compile/pre-commit.js +50 -0
- package/src/compile/windsurf.js +76 -0
- package/src/compile/zed.js +86 -0
- package/src/crag-agent.md +254 -0
- package/src/governance/gate-to-shell.js +28 -0
- package/src/governance/parse.js +182 -0
- package/src/skills/post-start-validation.md +297 -0
- package/src/skills/pre-start-context.md +506 -0
- package/src/update/integrity.js +131 -0
- package/src/update/skill-sync.js +116 -0
- package/src/update/version-check.js +156 -0
- package/src/workspace/detect.js +190 -0
- package/src/workspace/enumerate.js +270 -0
- package/src/workspace/governance.js +119 -0
- package/cli.js +0 -15
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function check() {
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
const checks = [
|
|
9
|
+
['.claude/skills/pre-start-context/SKILL.md', 'Pre-start skill (universal)'],
|
|
10
|
+
['.claude/skills/post-start-validation/SKILL.md', 'Post-start skill (universal)'],
|
|
11
|
+
['.claude/governance.md', 'Governance rules'],
|
|
12
|
+
['.claude/hooks/drift-detector.sh', 'Drift detector hook'],
|
|
13
|
+
['.claude/hooks/circuit-breaker.sh', 'Circuit breaker hook'],
|
|
14
|
+
['.claude/agents/test-runner.md', 'Test runner agent'],
|
|
15
|
+
['.claude/agents/security-reviewer.md', 'Security reviewer agent'],
|
|
16
|
+
['.claude/ci-playbook.md', 'CI playbook'],
|
|
17
|
+
['.claude/settings.local.json', 'Settings with hooks'],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const optional = [
|
|
21
|
+
['.claude/.session-name', 'Session name (remote access)'],
|
|
22
|
+
['.claude/hooks/pre-compact-snapshot.sh', 'Pre-compact hook (MemStack)'],
|
|
23
|
+
['.claude/hooks/post-compact-recovery.sh', 'Post-compact hook (MemStack)'],
|
|
24
|
+
['.claude/rules/knowledge.md', 'MemStack knowledge rule'],
|
|
25
|
+
['.claude/rules/diary.md', 'MemStack diary rule'],
|
|
26
|
+
['.claude/rules/echo.md', 'MemStack echo rule'],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
console.log(`\n Checking crag infrastructure in ${cwd}\n`);
|
|
30
|
+
|
|
31
|
+
console.log(` Core:`);
|
|
32
|
+
let missing = 0;
|
|
33
|
+
for (const [file, name] of checks) {
|
|
34
|
+
const exists = fs.existsSync(path.join(cwd, file));
|
|
35
|
+
const icon = exists ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
|
|
36
|
+
console.log(` ${icon} ${name}`);
|
|
37
|
+
if (!exists) missing++;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(`\n Optional:`);
|
|
41
|
+
for (const [file, name] of optional) {
|
|
42
|
+
const exists = fs.existsSync(path.join(cwd, file));
|
|
43
|
+
const icon = exists ? '\x1b[32m✓\x1b[0m' : '\x1b[90m○\x1b[0m';
|
|
44
|
+
console.log(` ${icon} ${name}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(`\n ${checks.length - missing}/${checks.length} core files present.`);
|
|
48
|
+
if (missing > 0) {
|
|
49
|
+
console.log(` Run 'crag init' to generate missing files.\n`);
|
|
50
|
+
} else {
|
|
51
|
+
console.log(` Infrastructure complete.\n`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { check };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { parseGovernance, flattenGates } = require('../governance/parse');
|
|
6
|
+
const { generateGitHubActions } = require('../compile/github-actions');
|
|
7
|
+
const { generateHusky } = require('../compile/husky');
|
|
8
|
+
const { generatePreCommitConfig } = require('../compile/pre-commit');
|
|
9
|
+
const { generateAgentsMd } = require('../compile/agents-md');
|
|
10
|
+
const { generateCursorRules } = require('../compile/cursor-rules');
|
|
11
|
+
const { generateGeminiMd } = require('../compile/gemini-md');
|
|
12
|
+
const { generateCopilot } = require('../compile/copilot');
|
|
13
|
+
const { generateCline } = require('../compile/cline');
|
|
14
|
+
const { generateContinue } = require('../compile/continue');
|
|
15
|
+
const { generateWindsurf } = require('../compile/windsurf');
|
|
16
|
+
const { generateZed } = require('../compile/zed');
|
|
17
|
+
const { generateCody } = require('../compile/cody');
|
|
18
|
+
|
|
19
|
+
// All supported compile targets in dispatch order.
|
|
20
|
+
// Grouped: CI (3) + AI agent native (3) + AI agent extras (6)
|
|
21
|
+
const ALL_TARGETS = [
|
|
22
|
+
'github',
|
|
23
|
+
'husky',
|
|
24
|
+
'pre-commit',
|
|
25
|
+
'agents-md',
|
|
26
|
+
'cursor',
|
|
27
|
+
'gemini',
|
|
28
|
+
'copilot',
|
|
29
|
+
'cline',
|
|
30
|
+
'continue',
|
|
31
|
+
'windsurf',
|
|
32
|
+
'zed',
|
|
33
|
+
'cody',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function compile(args) {
|
|
37
|
+
const targetIdx = args.indexOf('--target');
|
|
38
|
+
const target = targetIdx !== -1 ? args[targetIdx + 1] : args[1];
|
|
39
|
+
const cwd = process.cwd();
|
|
40
|
+
const govPath = path.join(cwd, '.claude', 'governance.md');
|
|
41
|
+
|
|
42
|
+
if (!fs.existsSync(govPath)) {
|
|
43
|
+
console.error(' Error: No .claude/governance.md found. Run crag init first.');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const content = fs.readFileSync(govPath, 'utf-8');
|
|
48
|
+
const parsed = parseGovernance(content);
|
|
49
|
+
const flat = flattenGates(parsed.gates);
|
|
50
|
+
const gateCount = Object.values(flat).flat().length;
|
|
51
|
+
|
|
52
|
+
if (!target || target === 'list') {
|
|
53
|
+
console.log(`\n crag compile — ${parsed.name || 'unnamed project'}`);
|
|
54
|
+
console.log(` ${gateCount} gate(s) in ${Object.keys(parsed.gates).length} section(s), ${parsed.runtimes.length} runtime(s) detected\n`);
|
|
55
|
+
console.log(' CI / git hooks:');
|
|
56
|
+
console.log(' crag compile --target github .github/workflows/gates.yml');
|
|
57
|
+
console.log(' crag compile --target husky .husky/pre-commit');
|
|
58
|
+
console.log(' crag compile --target pre-commit .pre-commit-config.yaml\n');
|
|
59
|
+
console.log(' AI coding agents — native formats:');
|
|
60
|
+
console.log(' crag compile --target agents-md AGENTS.md (Codex, Aider, Factory)');
|
|
61
|
+
console.log(' crag compile --target cursor .cursor/rules/governance.mdc');
|
|
62
|
+
console.log(' crag compile --target gemini GEMINI.md\n');
|
|
63
|
+
console.log(' AI coding agents — additional formats:');
|
|
64
|
+
console.log(' crag compile --target copilot .github/copilot-instructions.md');
|
|
65
|
+
console.log(' crag compile --target cline .clinerules');
|
|
66
|
+
console.log(' crag compile --target continue .continuerules');
|
|
67
|
+
console.log(' crag compile --target windsurf .windsurfrules');
|
|
68
|
+
console.log(' crag compile --target zed .zed/rules.md');
|
|
69
|
+
console.log(' crag compile --target cody .sourcegraph/cody-instructions.md\n');
|
|
70
|
+
console.log(' Combined:');
|
|
71
|
+
console.log(' crag compile --target all All 12 targets at once\n');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const targets = target === 'all' ? ALL_TARGETS : [target];
|
|
76
|
+
|
|
77
|
+
console.log(`\n Compiling governance.md → ${targets.join(', ')}`);
|
|
78
|
+
console.log(` ${gateCount} gates, ${parsed.runtimes.length} runtimes detected\n`);
|
|
79
|
+
|
|
80
|
+
for (const t of targets) {
|
|
81
|
+
switch (t) {
|
|
82
|
+
case 'github': generateGitHubActions(cwd, parsed); break;
|
|
83
|
+
case 'husky': generateHusky(cwd, parsed); break;
|
|
84
|
+
case 'pre-commit': generatePreCommitConfig(cwd, parsed); break;
|
|
85
|
+
case 'agents-md': generateAgentsMd(cwd, parsed); break;
|
|
86
|
+
case 'cursor': generateCursorRules(cwd, parsed); break;
|
|
87
|
+
case 'gemini': generateGeminiMd(cwd, parsed); break;
|
|
88
|
+
case 'copilot': generateCopilot(cwd, parsed); break;
|
|
89
|
+
case 'cline': generateCline(cwd, parsed); break;
|
|
90
|
+
case 'continue': generateContinue(cwd, parsed); break;
|
|
91
|
+
case 'windsurf': generateWindsurf(cwd, parsed); break;
|
|
92
|
+
case 'zed': generateZed(cwd, parsed); break;
|
|
93
|
+
case 'cody': generateCody(cwd, parsed); break;
|
|
94
|
+
default:
|
|
95
|
+
console.error(` Unknown target: ${t}`);
|
|
96
|
+
console.error(` Valid targets: ${ALL_TARGETS.join(', ')}, all, list`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log('\n Done. Governance is now executable infrastructure.\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { compile, ALL_TARGETS };
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { parseGovernance, flattenGates } = require('../governance/parse');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* crag diff — compare governance.md against codebase reality.
|
|
10
|
+
*/
|
|
11
|
+
function diff(args) {
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const govPath = path.join(cwd, '.claude', 'governance.md');
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(govPath)) {
|
|
16
|
+
console.error(' Error: No .claude/governance.md found. Run crag init or crag analyze first.');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const content = fs.readFileSync(govPath, 'utf-8');
|
|
21
|
+
const parsed = parseGovernance(content);
|
|
22
|
+
const flat = flattenGates(parsed.gates);
|
|
23
|
+
|
|
24
|
+
console.log(`\n Governance vs Reality — ${parsed.name || 'project'}\n`);
|
|
25
|
+
|
|
26
|
+
const results = { match: 0, drift: 0, missing: 0, extra: 0 };
|
|
27
|
+
|
|
28
|
+
// Check each gate command
|
|
29
|
+
for (const [section, cmds] of Object.entries(flat)) {
|
|
30
|
+
for (const cmd of cmds) {
|
|
31
|
+
const check = checkGateReality(cwd, cmd);
|
|
32
|
+
const icon = check.status === 'match' ? '\x1b[32mMATCH\x1b[0m'
|
|
33
|
+
: check.status === 'drift' ? '\x1b[33mDRIFT\x1b[0m'
|
|
34
|
+
: '\x1b[31mMISSING\x1b[0m';
|
|
35
|
+
console.log(` ${icon} ${cmd}`);
|
|
36
|
+
if (check.detail) console.log(` ${check.detail}`);
|
|
37
|
+
results[check.status]++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check for CI gates not in governance
|
|
42
|
+
const ciGates = extractCIGateCommands(cwd);
|
|
43
|
+
const govCommands = Object.values(flat).flat();
|
|
44
|
+
for (const ciGate of ciGates) {
|
|
45
|
+
if (!govCommands.some(g => normalizeCmd(g) === normalizeCmd(ciGate))) {
|
|
46
|
+
console.log(` \x1b[36mEXTRA\x1b[0m ${ciGate}`);
|
|
47
|
+
console.log(` In CI workflow but not in governance`);
|
|
48
|
+
results.extra++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check branch strategy
|
|
53
|
+
checkBranchStrategy(cwd, content, results);
|
|
54
|
+
|
|
55
|
+
// Check commit convention
|
|
56
|
+
checkCommitConvention(cwd, content, results);
|
|
57
|
+
|
|
58
|
+
// Summary
|
|
59
|
+
const total = results.match + results.drift + results.missing + results.extra;
|
|
60
|
+
console.log(`\n ${results.match} match, ${results.drift} drift, ${results.missing} missing, ${results.extra} extra (${total} total)\n`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function checkGateReality(cwd, cmd) {
|
|
64
|
+
// Check if the tool referenced in the command actually exists
|
|
65
|
+
const toolChecks = [
|
|
66
|
+
{ pattern: /^npx\s+(\w+)/, check: (tool) => hasNpmDep(cwd, tool) || hasNpmBin(cwd, tool) },
|
|
67
|
+
{ pattern: /^npm\s+run\s+(\w+)/, check: (script) => hasNpmScript(cwd, script) },
|
|
68
|
+
{ pattern: /^node\s+--check\s+(.+)/, check: (file) => fs.existsSync(path.join(cwd, file.trim())) },
|
|
69
|
+
{ pattern: /^node\s+(\S+)/, check: (file) => fs.existsSync(path.join(cwd, file.trim())) },
|
|
70
|
+
{ pattern: /^cargo\s+/, check: () => fs.existsSync(path.join(cwd, 'Cargo.toml')) },
|
|
71
|
+
{ pattern: /^go\s+/, check: () => fs.existsSync(path.join(cwd, 'go.mod')) },
|
|
72
|
+
{ pattern: /^(\.\/)?gradlew\s+/, check: () => fs.existsSync(path.join(cwd, 'gradlew')) || fs.existsSync(path.join(cwd, 'gradlew.bat')) },
|
|
73
|
+
{ pattern: /^pytest/, check: () => fs.existsSync(path.join(cwd, 'pyproject.toml')) || fs.existsSync(path.join(cwd, 'setup.py')) },
|
|
74
|
+
{ pattern: /^ruff\s+/, check: () => fs.existsSync(path.join(cwd, 'ruff.toml')) || fs.existsSync(path.join(cwd, '.ruff.toml')) || fs.existsSync(path.join(cwd, 'pyproject.toml')) },
|
|
75
|
+
{ pattern: /^docker\s+/, check: () => fs.existsSync(path.join(cwd, 'Dockerfile')) || fs.existsSync(path.join(cwd, 'docker-compose.yml')) },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
// Verify commands
|
|
79
|
+
const verifyMatch = cmd.match(/^Verify\s+(\S+)\s+contains\s+/i);
|
|
80
|
+
if (verifyMatch) {
|
|
81
|
+
const file = verifyMatch[1];
|
|
82
|
+
if (!fs.existsSync(path.join(cwd, file))) {
|
|
83
|
+
return { status: 'missing', detail: `File ${file} does not exist` };
|
|
84
|
+
}
|
|
85
|
+
return { status: 'match', detail: null };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const { pattern, check } of toolChecks) {
|
|
89
|
+
const m = cmd.match(pattern);
|
|
90
|
+
if (m) {
|
|
91
|
+
if (check(m[1])) return { status: 'match', detail: null };
|
|
92
|
+
return { status: 'drift', detail: `Tool or dependency not found for: ${cmd}` };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Unknown command — assume match (can't verify)
|
|
97
|
+
return { status: 'match', detail: null };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function checkBranchStrategy(cwd, content, results) {
|
|
101
|
+
const govStrategy = content.includes('Feature branches') || content.includes('feature branches')
|
|
102
|
+
? 'feature-branches' : content.includes('Trunk-based') || content.includes('trunk-based')
|
|
103
|
+
? 'trunk-based' : null;
|
|
104
|
+
|
|
105
|
+
if (!govStrategy) return;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const branches = execSync('git branch -a --format="%(refname:short)"', { cwd, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
109
|
+
const list = branches.trim().split('\n');
|
|
110
|
+
const featureBranches = list.filter(b => /^(feat|fix|docs|chore|feature|hotfix)\//.test(b));
|
|
111
|
+
const actual = featureBranches.length > 2 ? 'feature-branches' : 'trunk-based';
|
|
112
|
+
|
|
113
|
+
if (actual !== govStrategy) {
|
|
114
|
+
console.log(` \x1b[33mDRIFT\x1b[0m Branch strategy: governance says ${govStrategy}, git shows ${actual}`);
|
|
115
|
+
results.drift++;
|
|
116
|
+
} else {
|
|
117
|
+
results.match++;
|
|
118
|
+
}
|
|
119
|
+
} catch { /* skip */ }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function checkCommitConvention(cwd, content, results) {
|
|
123
|
+
const govConvention = content.includes('Conventional commits') || content.includes('conventional commits')
|
|
124
|
+
? 'conventional' : content.includes('Free-form') || content.includes('free-form')
|
|
125
|
+
? 'free-form' : null;
|
|
126
|
+
|
|
127
|
+
if (!govConvention) return;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const log = execSync('git log --oneline -20', { cwd, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
131
|
+
const lines = log.trim().split('\n');
|
|
132
|
+
const conventional = lines.filter(l => /\b(feat|fix|docs|chore|style|refactor|test|build|ci|perf|revert)[\(:!]/.test(l));
|
|
133
|
+
const actual = conventional.length > lines.length * 0.3 ? 'conventional' : 'free-form';
|
|
134
|
+
|
|
135
|
+
if (actual !== govConvention) {
|
|
136
|
+
console.log(` \x1b[33mDRIFT\x1b[0m Commit convention: governance says ${govConvention}, git shows ${actual}`);
|
|
137
|
+
results.drift++;
|
|
138
|
+
} else {
|
|
139
|
+
results.match++;
|
|
140
|
+
}
|
|
141
|
+
} catch { /* skip */ }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function extractCIGateCommands(cwd) {
|
|
145
|
+
const gates = [];
|
|
146
|
+
const workflowDir = path.join(cwd, '.github', 'workflows');
|
|
147
|
+
if (!fs.existsSync(workflowDir)) return gates;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
// Walk recursively to catch nested workflow files
|
|
151
|
+
const walk = (d) => {
|
|
152
|
+
const out = [];
|
|
153
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
154
|
+
const full = path.join(d, entry.name);
|
|
155
|
+
if (entry.isDirectory()) out.push(...walk(full));
|
|
156
|
+
else if (entry.name.endsWith('.yml') || entry.name.endsWith('.yaml')) out.push(full);
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
for (const file of walk(workflowDir)) {
|
|
162
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
163
|
+
for (const cmd of extractRunCommands(content)) {
|
|
164
|
+
if (isGateCommand(cmd)) gates.push(cmd);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch { /* skip */ }
|
|
168
|
+
|
|
169
|
+
return gates;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Extract commands from YAML `run:` steps, handling block scalars.
|
|
174
|
+
*/
|
|
175
|
+
function extractRunCommands(content) {
|
|
176
|
+
const commands = [];
|
|
177
|
+
const lines = content.split(/\r?\n/);
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < lines.length; i++) {
|
|
180
|
+
const line = lines[i];
|
|
181
|
+
const m = line.match(/^(\s*)-?\s*run:\s*(.*)$/);
|
|
182
|
+
if (!m) continue;
|
|
183
|
+
|
|
184
|
+
const baseIndent = m[1].length;
|
|
185
|
+
const rest = m[2].trim();
|
|
186
|
+
|
|
187
|
+
if (/^[|>][+-]?\s*$/.test(rest)) {
|
|
188
|
+
// Block scalar
|
|
189
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
190
|
+
const ln = lines[j];
|
|
191
|
+
if (ln.trim() === '') continue;
|
|
192
|
+
const indentMatch = ln.match(/^(\s*)/);
|
|
193
|
+
if (indentMatch[1].length <= baseIndent) break;
|
|
194
|
+
const trimmed = ln.trim();
|
|
195
|
+
if (trimmed && !trimmed.startsWith('#')) commands.push(trimmed);
|
|
196
|
+
}
|
|
197
|
+
} else if (rest && !rest.startsWith('#')) {
|
|
198
|
+
commands.push(rest.replace(/^["']|["']$/g, ''));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return commands;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isGateCommand(cmd) {
|
|
206
|
+
const patterns = [
|
|
207
|
+
/npm (run|test|ci)/, /npx /, /node /, /cargo /, /go (test|build|vet)/,
|
|
208
|
+
/pytest/, /ruff/, /mypy/, /eslint/, /biome/, /prettier/, /tsc/, /gradle/,
|
|
209
|
+
];
|
|
210
|
+
return patterns.some(p => p.test(cmd));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Normalize commands to a canonical form for equality comparison.
|
|
215
|
+
* Handles common aliases:
|
|
216
|
+
* - `npm test` ⇔ `npm run test`
|
|
217
|
+
* - `npm start` ⇔ `npm run start`
|
|
218
|
+
* - `./gradlew x` ⇔ `gradlew x`
|
|
219
|
+
* - `./mvnw x` ⇔ `mvnw x`
|
|
220
|
+
* - quoted arguments ⇔ unquoted (lexical equality only for simple cases)
|
|
221
|
+
*/
|
|
222
|
+
function normalizeCmd(cmd) {
|
|
223
|
+
let c = String(cmd).replace(/\s+/g, ' ').trim().toLowerCase();
|
|
224
|
+
|
|
225
|
+
// npm lifecycle aliases: `npm <script>` → `npm run <script>` (except reserved verbs)
|
|
226
|
+
const npmReserved = new Set([
|
|
227
|
+
'test', 'start', 'stop', 'restart', 'install', 'uninstall', 'ci', 'publish',
|
|
228
|
+
'pack', 'audit', 'ls', 'list', 'run', 'exec', 'init', 'update', 'outdated',
|
|
229
|
+
'version', 'view', 'whoami', 'login', 'logout', 'link', 'unlink', 'config',
|
|
230
|
+
'cache', 'prune', 'dedupe', 'rebuild', 'shrinkwrap', 'help',
|
|
231
|
+
]);
|
|
232
|
+
const npmMatch = c.match(/^npm\s+(\S+)(\s+.*)?$/);
|
|
233
|
+
if (npmMatch && (npmMatch[1] === 'test' || npmMatch[1] === 'start' || npmMatch[1] === 'stop' || npmMatch[1] === 'restart')) {
|
|
234
|
+
// npm test == npm run test
|
|
235
|
+
c = `npm run ${npmMatch[1]}${npmMatch[2] || ''}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// gradlew aliases: ./gradlew == gradlew
|
|
239
|
+
c = c.replace(/^\.\/gradlew/, 'gradlew');
|
|
240
|
+
c = c.replace(/^\.\/mvnw/, 'mvnw');
|
|
241
|
+
c = c.replace(/^\.\/(\S+)/, '$1'); // any other ./x
|
|
242
|
+
|
|
243
|
+
return c;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Memoize package.json reads per cwd — diff runs many checks per call.
|
|
247
|
+
const _pkgCache = new Map();
|
|
248
|
+
function getPkg(cwd) {
|
|
249
|
+
if (_pkgCache.has(cwd)) return _pkgCache.get(cwd);
|
|
250
|
+
let pkg = null;
|
|
251
|
+
try {
|
|
252
|
+
pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8'));
|
|
253
|
+
} catch { /* missing or malformed */ }
|
|
254
|
+
_pkgCache.set(cwd, pkg);
|
|
255
|
+
return pkg;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Memoize bin directory entries per cwd.
|
|
259
|
+
const _binCache = new Map();
|
|
260
|
+
function getBins(cwd) {
|
|
261
|
+
if (_binCache.has(cwd)) return _binCache.get(cwd);
|
|
262
|
+
const binDir = path.join(cwd, 'node_modules', '.bin');
|
|
263
|
+
let set;
|
|
264
|
+
try {
|
|
265
|
+
set = new Set(fs.readdirSync(binDir));
|
|
266
|
+
} catch {
|
|
267
|
+
set = new Set();
|
|
268
|
+
}
|
|
269
|
+
_binCache.set(cwd, set);
|
|
270
|
+
return set;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function hasNpmDep(cwd, dep) {
|
|
274
|
+
const pkg = getPkg(cwd);
|
|
275
|
+
if (!pkg) return false;
|
|
276
|
+
return !!(pkg.dependencies?.[dep] || pkg.devDependencies?.[dep] || pkg.peerDependencies?.[dep] || pkg.optionalDependencies?.[dep]);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function hasNpmBin(cwd, bin) {
|
|
280
|
+
const bins = getBins(cwd);
|
|
281
|
+
return bins.has(bin) || bins.has(bin + '.cmd') || bins.has(bin + '.exe');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function hasNpmScript(cwd, script) {
|
|
285
|
+
const pkg = getPkg(cwd);
|
|
286
|
+
return !!(pkg && pkg.scripts?.[script]);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = { diff, normalizeCmd, extractRunCommands, isGateCommand };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync, spawn } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const SRC = path.join(__dirname, '..');
|
|
8
|
+
const AGENT_SRC = path.join(SRC, 'crag-agent.md');
|
|
9
|
+
const PRE_START_SRC = path.join(SRC, 'skills', 'pre-start-context.md');
|
|
10
|
+
const POST_START_SRC = path.join(SRC, 'skills', 'post-start-validation.md');
|
|
11
|
+
const GLOBAL_AGENT_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.claude', 'agents');
|
|
12
|
+
const GLOBAL_AGENT_PATH = path.join(GLOBAL_AGENT_DIR, 'crag-project.md');
|
|
13
|
+
|
|
14
|
+
function install() {
|
|
15
|
+
if (!fs.existsSync(GLOBAL_AGENT_DIR)) {
|
|
16
|
+
fs.mkdirSync(GLOBAL_AGENT_DIR, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
fs.copyFileSync(AGENT_SRC, GLOBAL_AGENT_PATH);
|
|
19
|
+
console.log(` Installed crag-project agent to ${GLOBAL_AGENT_PATH}`);
|
|
20
|
+
console.log(` Run /crag-project from any Claude Code session.`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function installSkills(targetDir) {
|
|
24
|
+
const skillDirs = [
|
|
25
|
+
path.join(targetDir, '.claude', 'skills', 'pre-start-context'),
|
|
26
|
+
path.join(targetDir, '.claude', 'skills', 'post-start-validation'),
|
|
27
|
+
path.join(targetDir, '.agents', 'workflows'),
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
for (const dir of skillDirs) {
|
|
31
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Copy skills
|
|
35
|
+
fs.copyFileSync(PRE_START_SRC, path.join(targetDir, '.claude', 'skills', 'pre-start-context', 'SKILL.md'));
|
|
36
|
+
fs.copyFileSync(POST_START_SRC, path.join(targetDir, '.claude', 'skills', 'post-start-validation', 'SKILL.md'));
|
|
37
|
+
|
|
38
|
+
// Workflow copies (remove name: line)
|
|
39
|
+
for (const [src, name] of [[PRE_START_SRC, 'pre-start-context.md'], [POST_START_SRC, 'post-start-validation.md']]) {
|
|
40
|
+
const content = fs.readFileSync(src, 'utf-8').replace(/^name:.*\n/m, '');
|
|
41
|
+
fs.writeFileSync(path.join(targetDir, '.agents', 'workflows', name), content);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(` Installed universal skills to ${targetDir}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function init() {
|
|
48
|
+
const cwd = process.cwd();
|
|
49
|
+
|
|
50
|
+
// Pre-flight: check claude CLI
|
|
51
|
+
try {
|
|
52
|
+
execSync('claude --version', { stdio: 'ignore', timeout: 5000 });
|
|
53
|
+
} catch {
|
|
54
|
+
console.error(' Error: Claude Code CLI not found (or did not respond in 5s).');
|
|
55
|
+
console.error(' Install: https://claude.com/claude-code');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Pre-flight: warn if not a git repo (non-blocking, just informative)
|
|
60
|
+
if (!fs.existsSync(path.join(cwd, '.git'))) {
|
|
61
|
+
console.warn(' \x1b[33m!\x1b[0m Warning: not a git repository.');
|
|
62
|
+
console.warn(' crag works best in git repos (branch inference, commit conventions,');
|
|
63
|
+
console.warn(' discovery cache keyed by commit hash). Run: git init');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Pre-flight: warn if existing governance.md would be overwritten by interview
|
|
67
|
+
const existingGov = path.join(cwd, '.claude', 'governance.md');
|
|
68
|
+
if (fs.existsSync(existingGov)) {
|
|
69
|
+
console.warn(' \x1b[33m!\x1b[0m Warning: .claude/governance.md already exists.');
|
|
70
|
+
console.warn(' The interview will suggest changes; review before saving.');
|
|
71
|
+
console.warn(' To update skills only without interview, use: crag upgrade');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Install universal skills first
|
|
75
|
+
console.log(`\n Installing universal skills...`);
|
|
76
|
+
installSkills(cwd);
|
|
77
|
+
|
|
78
|
+
// Install agent globally if needed
|
|
79
|
+
if (!fs.existsSync(GLOBAL_AGENT_PATH)) {
|
|
80
|
+
install();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(`\n Starting crag interview...\n`);
|
|
84
|
+
console.log(` Claude Code will ask about your project.`);
|
|
85
|
+
console.log(` It generates: governance.md, hooks, agents, settings.`);
|
|
86
|
+
console.log(` The universal skills are already installed.\n`);
|
|
87
|
+
console.log(` >>> Type "go" and press Enter to start the interview <<<\n`);
|
|
88
|
+
|
|
89
|
+
const claude = spawn('claude', ['--agent', 'crag-project'], {
|
|
90
|
+
stdio: 'inherit',
|
|
91
|
+
shell: true,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
claude.on('error', (err) => {
|
|
95
|
+
console.error(`\n Error launching claude: ${err.message}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
claude.on('exit', (code, signal) => {
|
|
100
|
+
if (code === 0) {
|
|
101
|
+
console.log(`\n crag setup complete. Run 'crag check' to verify.`);
|
|
102
|
+
} else if (signal) {
|
|
103
|
+
console.error(`\n Interview terminated by signal: ${signal}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
} else if (code !== null) {
|
|
106
|
+
console.error(`\n Interview exited with code ${code}`);
|
|
107
|
+
process.exit(code);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { init, install, installSkills };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { syncSkills } = require('../update/skill-sync');
|
|
5
|
+
const { detectWorkspace } = require('../workspace/detect');
|
|
6
|
+
const { enumerateMembers } = require('../workspace/enumerate');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* crag upgrade — update universal skills to latest version.
|
|
10
|
+
*/
|
|
11
|
+
function upgrade(args) {
|
|
12
|
+
const checkOnly = args.includes('--check');
|
|
13
|
+
const workspace = args.includes('--workspace');
|
|
14
|
+
const force = args.includes('--force');
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
|
|
17
|
+
console.log(`\n crag upgrade${checkOnly ? ' --check' : ''}${force ? ' --force' : ''}\n`);
|
|
18
|
+
|
|
19
|
+
// Upgrade current project
|
|
20
|
+
const result = syncSkills(cwd, { force, dryRun: checkOnly });
|
|
21
|
+
printResult(cwd, result, checkOnly);
|
|
22
|
+
|
|
23
|
+
// Upgrade workspace members if requested
|
|
24
|
+
if (workspace) {
|
|
25
|
+
const ws = detectWorkspace(cwd);
|
|
26
|
+
if (ws.type !== 'none') {
|
|
27
|
+
const members = enumerateMembers(ws);
|
|
28
|
+
console.log(`\n Workspace: ${ws.type} (${members.length} members)\n`);
|
|
29
|
+
|
|
30
|
+
for (const member of members) {
|
|
31
|
+
if (member.hasClaude) {
|
|
32
|
+
const memberResult = syncSkills(member.path, { force, dryRun: checkOnly });
|
|
33
|
+
printResult(member.path, memberResult, checkOnly);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
console.log(' No workspace detected.\n');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function printResult(dir, result, checkOnly) {
|
|
43
|
+
const label = path.basename(dir);
|
|
44
|
+
const prefix = checkOnly ? '(dry run) ' : '';
|
|
45
|
+
|
|
46
|
+
for (const item of result.updated) {
|
|
47
|
+
console.log(` \x1b[32m✓\x1b[0m ${prefix}${label}/${item.name}: ${item.from} → ${item.to}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const item of result.skipped) {
|
|
51
|
+
console.log(` \x1b[90m○\x1b[0m ${label}/${item.name}: ${item.version} (${item.reason})`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const item of result.conflicted) {
|
|
55
|
+
console.log(` \x1b[33m!\x1b[0m ${label}/${item.name}: ${item.installed} → ${item.available} (${item.reason})`);
|
|
56
|
+
console.log(` Run with --force to overwrite (backup will be created)`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (result.updated.length === 0 && result.conflicted.length === 0) {
|
|
60
|
+
console.log(` \x1b[32m✓\x1b[0m ${label}: all skills current`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { upgrade };
|