ctx-cc 3.5.0 → 4.1.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 +375 -676
- package/agents/ctx-arch-mapper.md +5 -3
- package/agents/ctx-auditor.md +5 -3
- package/agents/ctx-codex-reviewer.md +214 -0
- package/agents/ctx-concerns-mapper.md +5 -3
- package/agents/ctx-criteria-suggester.md +6 -4
- package/agents/ctx-debugger.md +5 -3
- package/agents/ctx-designer.md +488 -114
- package/agents/ctx-discusser.md +5 -3
- package/agents/ctx-executor.md +5 -3
- package/agents/ctx-handoff.md +6 -4
- package/agents/ctx-learner.md +5 -3
- package/agents/ctx-mapper.md +4 -3
- package/agents/ctx-ml-analyst.md +600 -0
- package/agents/ctx-ml-engineer.md +933 -0
- package/agents/ctx-ml-reviewer.md +485 -0
- package/agents/ctx-ml-scientist.md +626 -0
- package/agents/ctx-parallelizer.md +4 -3
- package/agents/ctx-planner.md +5 -3
- package/agents/ctx-predictor.md +4 -3
- package/agents/ctx-qa.md +5 -3
- package/agents/ctx-quality-mapper.md +5 -3
- package/agents/ctx-researcher.md +5 -3
- package/agents/ctx-reviewer.md +6 -4
- package/agents/ctx-team-coordinator.md +5 -3
- package/agents/ctx-tech-mapper.md +5 -3
- package/agents/ctx-verifier.md +5 -3
- package/bin/ctx.js +199 -27
- package/commands/brand.md +309 -0
- package/commands/ctx.md +10 -10
- package/commands/design.md +304 -0
- package/commands/experiment.md +251 -0
- package/commands/help.md +57 -7
- package/commands/init.md +25 -0
- package/commands/metrics.md +1 -1
- package/commands/milestone.md +1 -1
- package/commands/ml-status.md +197 -0
- package/commands/monitor.md +1 -1
- package/commands/train.md +266 -0
- package/commands/visual-qa.md +559 -0
- package/commands/voice.md +1 -1
- package/hooks/post-tool-use.js +39 -0
- package/hooks/pre-tool-use.js +94 -0
- package/hooks/subagent-stop.js +32 -0
- package/package.json +9 -3
- package/plugin.json +46 -0
- package/skills/ctx-design-system/SKILL.md +572 -0
- package/skills/ctx-ml-experiment/SKILL.md +334 -0
- package/skills/ctx-ml-pipeline/SKILL.md +437 -0
- package/skills/ctx-orchestrator/SKILL.md +91 -0
- package/skills/ctx-review-gate/SKILL.md +147 -0
- package/skills/ctx-state/SKILL.md +100 -0
- package/skills/ctx-visual-qa/SKILL.md +587 -0
- package/src/agents.js +109 -0
- package/src/auto.js +287 -0
- package/src/capabilities.js +226 -0
- package/src/commits.js +94 -0
- package/src/config.js +112 -0
- package/src/context.js +241 -0
- package/src/handoff.js +156 -0
- package/src/hooks.js +218 -0
- package/src/install.js +125 -50
- package/src/lifecycle.js +194 -0
- package/src/metrics.js +198 -0
- package/src/pipeline.js +269 -0
- package/src/review-gate.js +338 -0
- package/src/runner.js +120 -0
- package/src/skills.js +143 -0
- package/src/state.js +267 -0
- package/src/worktree.js +244 -0
- package/templates/PRD.json +1 -1
- package/templates/config.json +4 -237
- package/workflows/ctx-router.md +0 -485
- package/workflows/map-codebase.md +0 -329
package/src/hooks.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { readState, writeState } from './state.js';
|
|
4
|
+
|
|
5
|
+
const CTX_HOOK_MARKER = '// CTX_HOOKS';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook definitions that CTX generates for Claude Code settings.json.
|
|
9
|
+
* Each hook has: event, matcher (optional), command, description.
|
|
10
|
+
*/
|
|
11
|
+
const HOOK_DEFINITIONS = {
|
|
12
|
+
// Record agent completion in STATE.json
|
|
13
|
+
subagentCompletion: {
|
|
14
|
+
event: 'SubagentStop',
|
|
15
|
+
command: `node -e "
|
|
16
|
+
const fs=require('fs'),p=require('path');
|
|
17
|
+
const d=p.join(process.cwd(),'.ctx','STATE.json');
|
|
18
|
+
if(!fs.existsSync(d))process.exit(0);
|
|
19
|
+
const s=JSON.parse(fs.readFileSync(d,'utf-8'));
|
|
20
|
+
const h=s.agentHistory||[];
|
|
21
|
+
for(let i=h.length-1;i>=0;i--){
|
|
22
|
+
if(!h[i].completedAt){h[i].completedAt=new Date().toISOString();break;}
|
|
23
|
+
}
|
|
24
|
+
s.session={...s.session,lastActivity:new Date().toISOString()};
|
|
25
|
+
fs.writeFileSync(d,JSON.stringify(s,null,2)+'\\n');
|
|
26
|
+
"`,
|
|
27
|
+
description: 'Record agent completion in STATE.json',
|
|
28
|
+
configKey: 'hooks.subagentCompletion',
|
|
29
|
+
default: true,
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// Block commits without test file changes
|
|
33
|
+
blockCommitWithoutTests: {
|
|
34
|
+
event: 'PreToolUse',
|
|
35
|
+
matcher: 'Bash',
|
|
36
|
+
command: `node -e "
|
|
37
|
+
const cmd=process.env.TOOL_INPUT||'';
|
|
38
|
+
if(!/git commit/.test(cmd))process.exit(0);
|
|
39
|
+
const {execSync}=require('child_process');
|
|
40
|
+
try{
|
|
41
|
+
const diff=execSync('git diff --cached --name-only',{encoding:'utf-8'});
|
|
42
|
+
const files=diff.trim().split('\\n');
|
|
43
|
+
const hasCode=files.some(f=>/\\.(js|ts|jsx|tsx|py|go|rs)$/.test(f));
|
|
44
|
+
const hasTest=files.some(f=>/\\.(test|spec)\\.|__tests__/.test(f));
|
|
45
|
+
if(hasCode&&!hasTest){
|
|
46
|
+
console.error('CTX: Commit blocked — code changes without tests.');
|
|
47
|
+
console.error('Files: '+files.filter(f=>/\\.(js|ts|jsx|tsx|py|go|rs)$/.test(f)).join(', '));
|
|
48
|
+
process.exit(2);
|
|
49
|
+
}
|
|
50
|
+
}catch{}
|
|
51
|
+
"`,
|
|
52
|
+
description: 'Block commits without corresponding test changes',
|
|
53
|
+
configKey: 'hooks.blockCommitWithoutTests',
|
|
54
|
+
default: false,
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Enforce TDD mode (strict/warn/off)
|
|
58
|
+
tddEnforcement: {
|
|
59
|
+
event: 'PreToolUse',
|
|
60
|
+
matcher: 'Bash',
|
|
61
|
+
command: `node -e "
|
|
62
|
+
const cmd=process.env.TOOL_INPUT||'';
|
|
63
|
+
if(!/git commit/.test(cmd))process.exit(0);
|
|
64
|
+
const fs=require('fs'),p=require('path');
|
|
65
|
+
const cfgPath=p.join(process.cwd(),'.ctx','config.json');
|
|
66
|
+
let mode='off';
|
|
67
|
+
try{const c=JSON.parse(fs.readFileSync(cfgPath,'utf-8'));mode=c.hooks?.tddMode||'off';}catch{}
|
|
68
|
+
if(mode==='off')process.exit(0);
|
|
69
|
+
const {execSync}=require('child_process');
|
|
70
|
+
try{
|
|
71
|
+
const diff=execSync('git diff --cached --name-only',{encoding:'utf-8'});
|
|
72
|
+
const files=diff.trim().split('\\n');
|
|
73
|
+
const hasCode=files.some(f=>/\\.(js|ts|jsx|tsx|py|go|rs)$/.test(f)&&!/\\.(test|spec)\\.|__tests__/.test(f));
|
|
74
|
+
const hasTest=files.some(f=>/\\.(test|spec)\\.|__tests__/.test(f));
|
|
75
|
+
if(hasCode&&!hasTest){
|
|
76
|
+
if(mode==='strict'){
|
|
77
|
+
console.error('CTX TDD: Commit blocked — write tests first.');
|
|
78
|
+
process.exit(2);
|
|
79
|
+
}else if(mode==='warn'){
|
|
80
|
+
console.error('CTX TDD Warning: Code changes without tests.');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}catch{}
|
|
84
|
+
"`,
|
|
85
|
+
description: 'TDD enforcement (strict/warn/off)',
|
|
86
|
+
configKey: 'hooks.tddMode',
|
|
87
|
+
default: 'off',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Generate Claude Code hooks section for settings.json.
|
|
93
|
+
* Reads current config to determine which hooks are enabled.
|
|
94
|
+
*
|
|
95
|
+
* Returns an array of hook objects ready for settings.json.
|
|
96
|
+
*/
|
|
97
|
+
export function generateHooks(config = {}) {
|
|
98
|
+
const hooks = [];
|
|
99
|
+
const hooksConfig = config.hooks || {};
|
|
100
|
+
|
|
101
|
+
for (const [key, def] of Object.entries(HOOK_DEFINITIONS)) {
|
|
102
|
+
const configValue = getNestedValue(hooksConfig, key);
|
|
103
|
+
const enabled = configValue !== undefined ? configValue : def.default;
|
|
104
|
+
|
|
105
|
+
// Skip disabled hooks
|
|
106
|
+
if (enabled === false || enabled === 'off') continue;
|
|
107
|
+
|
|
108
|
+
const hook = {
|
|
109
|
+
type: def.event,
|
|
110
|
+
command: `${CTX_HOOK_MARKER} ${def.command.replace(/\n\s*/g, ' ').trim()}`,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (def.matcher) {
|
|
114
|
+
hook.matcher = def.matcher;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
hooks.push(hook);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return hooks;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Write hooks to .claude/settings.json, preserving user's existing hooks.
|
|
125
|
+
*/
|
|
126
|
+
export function syncHooks(settingsDir, config = {}) {
|
|
127
|
+
const settingsPath = path.join(settingsDir, 'settings.json');
|
|
128
|
+
let settings = {};
|
|
129
|
+
|
|
130
|
+
// Read existing settings
|
|
131
|
+
try {
|
|
132
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
133
|
+
} catch {}
|
|
134
|
+
|
|
135
|
+
// Preserve user hooks (non-CTX)
|
|
136
|
+
const existingHooks = settings.hooks || {};
|
|
137
|
+
const userHooks = {};
|
|
138
|
+
|
|
139
|
+
for (const [event, hookList] of Object.entries(existingHooks)) {
|
|
140
|
+
if (Array.isArray(hookList)) {
|
|
141
|
+
userHooks[event] = hookList.filter(h => !h.command?.includes(CTX_HOOK_MARKER));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Generate CTX hooks
|
|
146
|
+
const ctxHooks = generateHooks(config);
|
|
147
|
+
|
|
148
|
+
// Merge: group by event type
|
|
149
|
+
const merged = { ...userHooks };
|
|
150
|
+
for (const hook of ctxHooks) {
|
|
151
|
+
const event = hook.type;
|
|
152
|
+
if (!merged[event]) merged[event] = [];
|
|
153
|
+
merged[event].push({
|
|
154
|
+
matcher: hook.matcher || undefined,
|
|
155
|
+
command: hook.command,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Clean up empty arrays
|
|
160
|
+
for (const [key, val] of Object.entries(merged)) {
|
|
161
|
+
if (Array.isArray(val) && val.length === 0) delete merged[key];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
settings.hooks = merged;
|
|
165
|
+
|
|
166
|
+
// Write back
|
|
167
|
+
if (!fs.existsSync(settingsDir)) fs.mkdirSync(settingsDir, { recursive: true });
|
|
168
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
169
|
+
|
|
170
|
+
return { hooksGenerated: ctxHooks.length, settingsPath };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* List all available hooks with their current status.
|
|
175
|
+
*/
|
|
176
|
+
export function listHooks(config = {}) {
|
|
177
|
+
const hooksConfig = config.hooks || {};
|
|
178
|
+
const result = [];
|
|
179
|
+
|
|
180
|
+
for (const [key, def] of Object.entries(HOOK_DEFINITIONS)) {
|
|
181
|
+
const configValue = getNestedValue(hooksConfig, key);
|
|
182
|
+
const status = configValue !== undefined ? configValue : def.default;
|
|
183
|
+
|
|
184
|
+
result.push({
|
|
185
|
+
key,
|
|
186
|
+
event: def.event,
|
|
187
|
+
description: def.description,
|
|
188
|
+
configKey: def.configKey,
|
|
189
|
+
status: status === false ? 'disabled' : status === true ? 'enabled' : String(status),
|
|
190
|
+
default: def.default,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Format hooks list for display.
|
|
199
|
+
*/
|
|
200
|
+
export function formatHooksList(hooks) {
|
|
201
|
+
const lines = [];
|
|
202
|
+
const maxKey = Math.max(20, ...hooks.map(h => h.configKey.length));
|
|
203
|
+
|
|
204
|
+
for (const h of hooks) {
|
|
205
|
+
const icon = h.status === 'disabled' || h.status === 'off' ? '○' : '●';
|
|
206
|
+
lines.push(` ${icon} ${h.configKey.padEnd(maxKey)} ${h.status.padEnd(10)} ${h.description}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return lines.join('\n');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// --- internal ---
|
|
213
|
+
|
|
214
|
+
function getNestedValue(obj, key) {
|
|
215
|
+
return key.split('.').reduce((o, k) => o?.[k], obj);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export { HOOK_DEFINITIONS, CTX_HOOK_MARKER };
|
package/src/install.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
import
|
|
4
|
+
import { saveCapabilityManifest } from './capabilities.js';
|
|
5
5
|
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = path.dirname(__filename);
|
|
@@ -15,7 +15,6 @@ const VERSION = JSON.parse(
|
|
|
15
15
|
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
16
16
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
17
17
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
18
|
-
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
19
18
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
20
19
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
21
20
|
|
|
@@ -28,23 +27,18 @@ function printBanner() {
|
|
|
28
27
|
╚██████╗ ██║ ██╔╝ ██╗
|
|
29
28
|
╚═════╝ ╚═╝ ╚═╝ ╚═╝
|
|
30
29
|
`));
|
|
31
|
-
console.log(` ${bold('CTX
|
|
30
|
+
console.log(` ${bold('CTX 4.0')} ${dim(`v${VERSION}`)}`);
|
|
32
31
|
console.log(' Intelligent workflow orchestration for Claude Code.');
|
|
33
|
-
console.log('
|
|
32
|
+
console.log(' 26 agents. 7 skills. Hooks. Phase-based lifecycle.\n');
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
function copyDir(src, dest) {
|
|
37
|
-
if (!fs.existsSync(dest)) {
|
|
38
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
39
|
-
}
|
|
40
|
-
|
|
36
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
41
37
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
42
38
|
let count = 0;
|
|
43
|
-
|
|
44
39
|
for (const entry of entries) {
|
|
45
40
|
const srcPath = path.join(src, entry.name);
|
|
46
41
|
const destPath = path.join(dest, entry.name);
|
|
47
|
-
|
|
48
42
|
if (entry.isDirectory()) {
|
|
49
43
|
count += copyDir(srcPath, destPath);
|
|
50
44
|
} else {
|
|
@@ -52,20 +46,16 @@ function copyDir(src, dest) {
|
|
|
52
46
|
count++;
|
|
53
47
|
}
|
|
54
48
|
}
|
|
55
|
-
|
|
56
49
|
return count;
|
|
57
50
|
}
|
|
58
51
|
|
|
59
52
|
function removeDir(dir) {
|
|
60
|
-
if (fs.existsSync(dir)) {
|
|
61
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
62
|
-
}
|
|
53
|
+
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
|
|
63
54
|
}
|
|
64
55
|
|
|
65
56
|
export async function install(options) {
|
|
66
57
|
printBanner();
|
|
67
58
|
|
|
68
|
-
// Determine target directory
|
|
69
59
|
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
70
60
|
let targetBase;
|
|
71
61
|
|
|
@@ -77,19 +67,18 @@ export async function install(options) {
|
|
|
77
67
|
console.log(` Installing ${cyan('globally')} to ${cyan('~/.claude')}\n`);
|
|
78
68
|
}
|
|
79
69
|
|
|
80
|
-
// Check for existing installation
|
|
81
70
|
const commandsDir = path.join(targetBase, 'commands', 'ctx');
|
|
82
|
-
const agentsDir =
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
const agentsDir = path.join(targetBase, 'agents');
|
|
72
|
+
const skillsDir = path.join(targetBase, 'skills');
|
|
73
|
+
const hooksDir = path.join(targetBase, 'hooks');
|
|
85
74
|
const ctxDir = path.join(targetBase, 'ctx');
|
|
86
75
|
|
|
76
|
+
// Check existing installation
|
|
87
77
|
if (fs.existsSync(commandsDir) && !options.force) {
|
|
88
78
|
const existingVersion = getExistingVersion(ctxDir);
|
|
89
79
|
if (existingVersion) {
|
|
90
80
|
console.log(yellow(` ⚠ CTX ${existingVersion} is already installed.`));
|
|
91
81
|
console.log(` Use ${cyan('--force')} to reinstall.\n`);
|
|
92
|
-
|
|
93
82
|
if (existingVersion === VERSION) {
|
|
94
83
|
console.log(green(' ✓ Already up to date.\n'));
|
|
95
84
|
process.exit(0);
|
|
@@ -97,56 +86,77 @@ export async function install(options) {
|
|
|
97
86
|
}
|
|
98
87
|
}
|
|
99
88
|
|
|
100
|
-
// Clean
|
|
89
|
+
// Clean previous installation
|
|
101
90
|
if (options.force || !fs.existsSync(commandsDir)) {
|
|
102
91
|
console.log(' Cleaning previous installation...');
|
|
103
92
|
removeDir(commandsDir);
|
|
104
93
|
removeDir(ctxDir);
|
|
105
|
-
// Only remove ctx-* agents
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (agent.startsWith('ctx-')) {
|
|
110
|
-
fs.unlinkSync(path.join(agentsDir, agent));
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
94
|
+
// Only remove ctx-* agents and ctx-* skills
|
|
95
|
+
cleanupPrefixed(agentsDir, 'ctx-');
|
|
96
|
+
cleanupPrefixed(skillsDir, 'ctx-');
|
|
97
|
+
cleanupPrefixed(hooksDir, 'ctx-');
|
|
114
98
|
}
|
|
115
99
|
|
|
116
100
|
// Create directories
|
|
117
101
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
118
102
|
fs.mkdirSync(ctxDir, { recursive: true });
|
|
119
103
|
fs.mkdirSync(agentsDir, { recursive: true });
|
|
104
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
105
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
120
106
|
|
|
121
|
-
//
|
|
107
|
+
// 1. Install commands (slash commands)
|
|
122
108
|
const srcCommands = path.join(packageRoot, 'commands');
|
|
123
109
|
if (fs.existsSync(srcCommands)) {
|
|
124
110
|
const count = copyDir(srcCommands, commandsDir);
|
|
125
111
|
console.log(green(` ✓`) + ` Installed commands/ctx (${count} files)`);
|
|
126
112
|
|
|
127
|
-
//
|
|
113
|
+
// Copy ctx.md to commands root so /ctx works directly
|
|
128
114
|
const mainRouter = path.join(commandsDir, 'ctx.md');
|
|
129
115
|
const rootRouter = path.join(targetBase, 'commands', 'ctx.md');
|
|
130
116
|
if (fs.existsSync(mainRouter)) {
|
|
131
117
|
fs.copyFileSync(mainRouter, rootRouter);
|
|
132
|
-
console.log(green(` ✓`) + ` Installed /ctx command`);
|
|
118
|
+
console.log(green(` ✓`) + ` Installed /ctx router command`);
|
|
133
119
|
}
|
|
134
120
|
}
|
|
135
121
|
|
|
136
|
-
//
|
|
122
|
+
// 2. Install agents (subagents with native frontmatter)
|
|
137
123
|
const srcAgents = path.join(packageRoot, 'agents');
|
|
138
124
|
if (fs.existsSync(srcAgents)) {
|
|
139
|
-
const agents = fs.readdirSync(srcAgents);
|
|
125
|
+
const agents = fs.readdirSync(srcAgents).filter(f => f.endsWith('.md'));
|
|
140
126
|
for (const agent of agents) {
|
|
141
|
-
fs.copyFileSync(
|
|
142
|
-
path.join(srcAgents, agent),
|
|
143
|
-
path.join(agentsDir, agent)
|
|
144
|
-
);
|
|
127
|
+
fs.copyFileSync(path.join(srcAgents, agent), path.join(agentsDir, agent));
|
|
145
128
|
}
|
|
146
|
-
console.log(green(` ✓`) + ` Installed agents (${agents.length}
|
|
129
|
+
console.log(green(` ✓`) + ` Installed agents (${agents.length} subagents)`);
|
|
147
130
|
}
|
|
148
131
|
|
|
149
|
-
//
|
|
132
|
+
// 3. Install skills (progressive disclosure)
|
|
133
|
+
const srcSkills = path.join(packageRoot, 'skills');
|
|
134
|
+
if (fs.existsSync(srcSkills)) {
|
|
135
|
+
const skillDirs = fs.readdirSync(srcSkills, { withFileTypes: true })
|
|
136
|
+
.filter(d => d.isDirectory() && d.name.startsWith('ctx-'));
|
|
137
|
+
for (const dir of skillDirs) {
|
|
138
|
+
const destSkillDir = path.join(skillsDir, dir.name);
|
|
139
|
+
copyDir(path.join(srcSkills, dir.name), destSkillDir);
|
|
140
|
+
}
|
|
141
|
+
console.log(green(` ✓`) + ` Installed skills (${skillDirs.length} skills)`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 4. Install hooks (deterministic enforcement scripts)
|
|
145
|
+
const srcHooks = path.join(packageRoot, 'hooks');
|
|
146
|
+
if (fs.existsSync(srcHooks)) {
|
|
147
|
+
const hookFiles = fs.readdirSync(srcHooks).filter(f => f.endsWith('.js'));
|
|
148
|
+
for (const hookFile of hookFiles) {
|
|
149
|
+
const destFile = path.join(hooksDir, `ctx-${hookFile}`);
|
|
150
|
+
fs.copyFileSync(path.join(srcHooks, hookFile), destFile);
|
|
151
|
+
}
|
|
152
|
+
console.log(green(` ✓`) + ` Installed hooks (${hookFiles.length} scripts)`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 5. Generate/merge settings.json with hooks
|
|
156
|
+
mergeHooksIntoSettings(targetBase, hooksDir);
|
|
157
|
+
console.log(green(` ✓`) + ` Configured hooks in settings.json`);
|
|
158
|
+
|
|
159
|
+
// 6. Install references and templates
|
|
150
160
|
const srcRefs = path.join(packageRoot, 'references');
|
|
151
161
|
const destRefs = path.join(ctxDir, 'references');
|
|
152
162
|
if (fs.existsSync(srcRefs)) {
|
|
@@ -154,7 +164,6 @@ export async function install(options) {
|
|
|
154
164
|
console.log(green(` ✓`) + ` Installed references (${count} files)`);
|
|
155
165
|
}
|
|
156
166
|
|
|
157
|
-
// Copy templates
|
|
158
167
|
const srcTemplates = path.join(packageRoot, 'templates');
|
|
159
168
|
const destTemplates = path.join(ctxDir, 'templates');
|
|
160
169
|
if (fs.existsSync(srcTemplates)) {
|
|
@@ -162,13 +171,11 @@ export async function install(options) {
|
|
|
162
171
|
console.log(green(` ✓`) + ` Installed templates (${count} files)`);
|
|
163
172
|
}
|
|
164
173
|
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
console.log(green(` ✓`) + ` Installed workflows (${count} files)`);
|
|
171
|
-
}
|
|
174
|
+
// Generate capability-manifest.json template from DEFAULT_CAPABILITIES.
|
|
175
|
+
// /ctx:init copies this into each project's .ctx/ so the PreToolUse hook
|
|
176
|
+
// (hooks/pre-tool-use.js) has a manifest to enforce against.
|
|
177
|
+
saveCapabilityManifest(destTemplates);
|
|
178
|
+
console.log(green(` ✓`) + ` Generated capability-manifest.json template`);
|
|
172
179
|
|
|
173
180
|
// Write VERSION file
|
|
174
181
|
fs.writeFileSync(path.join(ctxDir, 'VERSION'), VERSION);
|
|
@@ -176,10 +183,78 @@ export async function install(options) {
|
|
|
176
183
|
|
|
177
184
|
// Success message
|
|
178
185
|
console.log(`\n ${green('Done!')} Launch Claude Code and run ${cyan('/ctx:help')}.`);
|
|
179
|
-
console.log(
|
|
186
|
+
console.log(`
|
|
187
|
+
${bold('What was installed:')}
|
|
188
|
+
${dim('Agents:')} ~/.claude/agents/ctx-*.md (26 subagents)
|
|
189
|
+
${dim('Skills:')} ~/.claude/skills/ctx-*/ (7 skills)
|
|
190
|
+
${dim('Commands:')} ~/.claude/commands/ctx/ (slash commands)
|
|
191
|
+
${dim('Hooks:')} ~/.claude/hooks/ctx-*.js (3 hook scripts)
|
|
192
|
+
${dim('Config:')} ~/.claude/settings.json (hooks registered)
|
|
193
|
+
`);
|
|
194
|
+
console.log(` ${dim('GitHub:')} https://github.com/jufjuf/CTX`);
|
|
180
195
|
console.log(` ${dim('Report issues:')} https://github.com/jufjuf/CTX/issues\n`);
|
|
181
196
|
}
|
|
182
197
|
|
|
198
|
+
// --- Helpers ---
|
|
199
|
+
|
|
200
|
+
function cleanupPrefixed(dir, prefix) {
|
|
201
|
+
if (!fs.existsSync(dir)) return;
|
|
202
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
if (entry.name.startsWith(prefix)) {
|
|
205
|
+
const fullPath = path.join(dir, entry.name);
|
|
206
|
+
if (entry.isDirectory()) {
|
|
207
|
+
removeDir(fullPath);
|
|
208
|
+
} else {
|
|
209
|
+
fs.unlinkSync(fullPath);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function mergeHooksIntoSettings(targetBase, hooksDir) {
|
|
216
|
+
const settingsPath = path.join(targetBase, 'settings.json');
|
|
217
|
+
let settings = {};
|
|
218
|
+
|
|
219
|
+
// Read existing settings
|
|
220
|
+
try {
|
|
221
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
222
|
+
} catch {}
|
|
223
|
+
|
|
224
|
+
// Preserve non-CTX hooks
|
|
225
|
+
const existingHooks = settings.hooks || {};
|
|
226
|
+
const userHooks = {};
|
|
227
|
+
for (const [event, hookList] of Object.entries(existingHooks)) {
|
|
228
|
+
if (Array.isArray(hookList)) {
|
|
229
|
+
userHooks[event] = hookList.filter(h => !h.command?.includes('ctx-'));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Add CTX hooks (referencing installed script files)
|
|
234
|
+
const ctxHooks = {
|
|
235
|
+
SubagentStop: [
|
|
236
|
+
{ command: `node ${path.join(hooksDir, 'ctx-subagent-stop.js')}` }
|
|
237
|
+
],
|
|
238
|
+
PreToolUse: [
|
|
239
|
+
{ command: `node ${path.join(hooksDir, 'ctx-pre-tool-use.js')}` }
|
|
240
|
+
],
|
|
241
|
+
PostToolUse: [
|
|
242
|
+
{ command: `node ${path.join(hooksDir, 'ctx-post-tool-use.js')}` }
|
|
243
|
+
],
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Merge
|
|
247
|
+
const merged = {};
|
|
248
|
+
for (const [event, hooks] of Object.entries({ ...userHooks, ...ctxHooks })) {
|
|
249
|
+
const userList = userHooks[event] || [];
|
|
250
|
+
const ctxList = ctxHooks[event] || [];
|
|
251
|
+
merged[event] = [...userList, ...ctxList];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
settings.hooks = merged;
|
|
255
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
256
|
+
}
|
|
257
|
+
|
|
183
258
|
function getExistingVersion(ctxDir) {
|
|
184
259
|
const versionFile = path.join(ctxDir, 'VERSION');
|
|
185
260
|
if (fs.existsSync(versionFile)) {
|