claudex-setup 1.7.0 → 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/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +34 -12
- package/bin/cli.js +35 -4
- package/package.json +3 -2
- package/src/analyze.js +26 -0
- package/src/audit.js +2 -2
- package/src/benchmark.js +81 -7
- package/src/context.js +3 -2
- package/src/domain-packs.js +160 -0
- package/src/governance.js +156 -0
- package/src/index.js +8 -0
- package/src/mcp-packs.js +85 -0
- package/src/plans.js +329 -59
- package/src/setup.js +60 -36
- package/src/techniques.js +19 -6
package/src/plans.js
CHANGED
|
@@ -6,6 +6,7 @@ const { analyzeProject } = require('./analyze');
|
|
|
6
6
|
const { ProjectContext } = require('./context');
|
|
7
7
|
const { TECHNIQUES, STACKS } = require('./techniques');
|
|
8
8
|
const { TEMPLATES } = require('./setup');
|
|
9
|
+
const { buildSettingsForProfile } = require('./governance');
|
|
9
10
|
const { writeActivityArtifact, writeRollbackArtifact } = require('./activity');
|
|
10
11
|
|
|
11
12
|
const TEMPLATE_DIR_MAP = {
|
|
@@ -35,6 +36,26 @@ const TEMPLATE_MODULES = {
|
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
const IMPACT_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
|
|
39
|
+
const FALLBACK_TEMPLATE_BY_KEY = {
|
|
40
|
+
importSyntax: 'claude-md',
|
|
41
|
+
verificationLoop: 'claude-md',
|
|
42
|
+
testCommand: 'claude-md',
|
|
43
|
+
lintCommand: 'claude-md',
|
|
44
|
+
buildCommand: 'claude-md',
|
|
45
|
+
securityReview: 'claude-md',
|
|
46
|
+
compactionAwareness: 'claude-md',
|
|
47
|
+
contextManagement: 'claude-md',
|
|
48
|
+
xmlTags: 'claude-md',
|
|
49
|
+
roleDefinition: 'claude-md',
|
|
50
|
+
constraintBlocks: 'claude-md',
|
|
51
|
+
claudeMdFreshness: 'claude-md',
|
|
52
|
+
permissionDeny: 'hooks',
|
|
53
|
+
secretsProtection: 'hooks',
|
|
54
|
+
preToolUseHook: 'hooks',
|
|
55
|
+
postToolUseHook: 'hooks',
|
|
56
|
+
sessionStartHook: 'hooks',
|
|
57
|
+
agentsHaveMaxTurns: 'agents',
|
|
58
|
+
};
|
|
38
59
|
|
|
39
60
|
function previewContent(content) {
|
|
40
61
|
return content.split('\n').slice(0, 12).join('\n');
|
|
@@ -46,73 +67,242 @@ function riskFromImpact(impact) {
|
|
|
46
67
|
return 'low';
|
|
47
68
|
}
|
|
48
69
|
|
|
70
|
+
function normalizeNewlines(content) {
|
|
71
|
+
return (content || '').replace(/\r\n/g, '\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ensureTrailingNewline(content) {
|
|
75
|
+
const normalized = normalizeNewlines(content);
|
|
76
|
+
return normalized.endsWith('\n') ? normalized : `${normalized}\n`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function upsertManagedBlock(content, id, block) {
|
|
80
|
+
const start = `<!-- claudex-setup:${id}:start -->`;
|
|
81
|
+
const end = `<!-- claudex-setup:${id}:end -->`;
|
|
82
|
+
const wrapped = `${start}\n${block.trim()}\n${end}`;
|
|
83
|
+
const pattern = new RegExp(`${start}[\\s\\S]*?${end}`);
|
|
84
|
+
|
|
85
|
+
if (pattern.test(content)) {
|
|
86
|
+
return {
|
|
87
|
+
changed: true,
|
|
88
|
+
content: content.replace(pattern, wrapped),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
changed: true,
|
|
94
|
+
content: `${content.trimEnd()}\n\n${wrapped}\n`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extractGeneratedBuildSection(content) {
|
|
99
|
+
const match = content.match(/## Build & Test\n([\s\S]*?)\n\n## Working Notes/);
|
|
100
|
+
return match ? `## Build & Test\n${match[1].trim()}` : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function extractGeneratedVerificationBlock(content) {
|
|
104
|
+
const match = content.match(/<verification>\n([\s\S]*?)\n<\/verification>/);
|
|
105
|
+
return match ? `<verification>\n${match[1].trim()}\n</verification>` : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function extractGeneratedWorkingNotes(content) {
|
|
109
|
+
const match = content.match(/## Working Notes\n([\s\S]*?)\n\n<constraints>/);
|
|
110
|
+
return match ? `## Working Notes\n${match[1].trim()}` : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function extractGeneratedConstraintsBlock(content) {
|
|
114
|
+
const match = content.match(/<constraints>\n([\s\S]*?)\n<\/constraints>/);
|
|
115
|
+
return match ? `<constraints>\n${match[1].trim()}\n</constraints>` : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function extractGeneratedContextSection(content) {
|
|
119
|
+
const match = content.match(/## Context Management\n([\s\S]*?)\n\n---/);
|
|
120
|
+
return match ? `## Context Management\n${match[1].trim()}` : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
49
123
|
function getFailedTemplateGroups(ctx, only = []) {
|
|
50
124
|
const groups = new Map();
|
|
51
125
|
for (const [key, technique] of Object.entries(TECHNIQUES)) {
|
|
52
126
|
const passed = technique.check(ctx);
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
if (
|
|
56
|
-
if (!
|
|
57
|
-
|
|
127
|
+
const templateKey = technique.template || FALLBACK_TEMPLATE_BY_KEY[key];
|
|
128
|
+
if (passed !== false || !templateKey) continue;
|
|
129
|
+
if (templateKey === 'mermaid') continue;
|
|
130
|
+
if (only.length > 0 && !only.includes(key) && !only.includes(templateKey)) continue;
|
|
131
|
+
if (!groups.has(templateKey)) {
|
|
132
|
+
groups.set(templateKey, []);
|
|
58
133
|
}
|
|
59
|
-
groups.get(
|
|
134
|
+
groups.get(templateKey).push({ key, ...technique });
|
|
60
135
|
}
|
|
61
136
|
return groups;
|
|
62
137
|
}
|
|
63
138
|
|
|
64
|
-
function
|
|
139
|
+
function buildClaudeMdPatchFile(ctx, stacks) {
|
|
140
|
+
const claudePath = ctx.fileContent('CLAUDE.md') !== null
|
|
141
|
+
? 'CLAUDE.md'
|
|
142
|
+
: (ctx.fileContent('.claude/CLAUDE.md') !== null ? '.claude/CLAUDE.md' : null);
|
|
143
|
+
if (!claudePath) return null;
|
|
144
|
+
|
|
145
|
+
const existing = normalizeNewlines(ctx.fileContent(claudePath));
|
|
146
|
+
const generated = TEMPLATES['claude-md'](stacks, ctx);
|
|
147
|
+
const buildSection = extractGeneratedBuildSection(generated);
|
|
148
|
+
const verificationBlock = extractGeneratedVerificationBlock(generated);
|
|
149
|
+
const workingNotes = extractGeneratedWorkingNotes(generated);
|
|
150
|
+
const constraintsBlock = extractGeneratedConstraintsBlock(generated);
|
|
151
|
+
const contextSection = extractGeneratedContextSection(generated);
|
|
152
|
+
let merged = existing;
|
|
153
|
+
let changed = false;
|
|
154
|
+
|
|
155
|
+
const hasTest = /npm test|pytest|jest|vitest|cargo test|go test|mix test|rspec/.test(merged);
|
|
156
|
+
const hasLint = /eslint|prettier|ruff|black|clippy|golangci-lint|rubocop/.test(merged);
|
|
157
|
+
const hasBuild = /npm run build|cargo build|go build|make|tsc|gradle build|mvn compile/.test(merged);
|
|
158
|
+
const hasVerification = merged.includes('<verification>');
|
|
159
|
+
const hasSecurityWorkflow = merged.toLowerCase().includes('security') || merged.includes('/security-review');
|
|
160
|
+
const hasImportGuidance = merged.includes('@import');
|
|
161
|
+
const hasRoleDefinition = /you are|your role|act as|persona|behave as/i.test(merged);
|
|
162
|
+
const hasConstraintBlock = /<constraints|<rules|<requirements|<boundaries/i.test(merged);
|
|
163
|
+
const hasCompaction = /\/compact|compaction/i.test(merged);
|
|
164
|
+
const hasContextManagement = /context.*(manage|window|limit|budget|token)/i.test(merged);
|
|
165
|
+
const modernFeatures = ['hook', 'skill', 'agent', 'subagent', 'mcp', 'worktree'];
|
|
166
|
+
const hasFreshness = modernFeatures.filter(feature => merged.toLowerCase().includes(feature)).length >= 2;
|
|
167
|
+
|
|
168
|
+
if ((!hasTest || !hasLint || !hasBuild) && buildSection) {
|
|
169
|
+
const result = upsertManagedBlock(merged, 'build-test', buildSection);
|
|
170
|
+
merged = result.content;
|
|
171
|
+
changed = true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!hasRoleDefinition && workingNotes) {
|
|
175
|
+
const result = upsertManagedBlock(merged, 'working-style', workingNotes);
|
|
176
|
+
merged = result.content;
|
|
177
|
+
changed = true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!hasConstraintBlock && constraintsBlock) {
|
|
181
|
+
const result = upsertManagedBlock(merged, 'constraints', constraintsBlock);
|
|
182
|
+
merged = result.content;
|
|
183
|
+
changed = true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!hasVerification && verificationBlock) {
|
|
187
|
+
const result = upsertManagedBlock(merged, 'verification', verificationBlock);
|
|
188
|
+
merged = result.content;
|
|
189
|
+
changed = true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!hasSecurityWorkflow) {
|
|
193
|
+
const result = upsertManagedBlock(merged, 'security-workflow', [
|
|
194
|
+
'## Security Workflow',
|
|
195
|
+
'- Run `/security-review` when touching authentication, permissions, secrets, or customer data.',
|
|
196
|
+
'- Treat secret access, shell commands, and risky file operations as review-worthy changes.',
|
|
197
|
+
].join('\n'));
|
|
198
|
+
merged = result.content;
|
|
199
|
+
changed = true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!hasImportGuidance) {
|
|
203
|
+
const result = upsertManagedBlock(merged, 'modularity', [
|
|
204
|
+
'## Modularity',
|
|
205
|
+
'- If this file grows, split it with `@import ./docs/...` so the base instructions stay concise.',
|
|
206
|
+
].join('\n'));
|
|
207
|
+
merged = result.content;
|
|
208
|
+
changed = true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if ((!hasCompaction || !hasContextManagement || !hasFreshness) && contextSection) {
|
|
212
|
+
const result = upsertManagedBlock(merged, 'context-management', contextSection);
|
|
213
|
+
merged = result.content;
|
|
214
|
+
changed = true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!changed) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
path: claudePath,
|
|
223
|
+
action: 'patch',
|
|
224
|
+
currentState: 'existing CLAUDE.md is missing recommended verification or security sections',
|
|
225
|
+
proposedState: 'append managed sections for verification, security workflow, and modularity',
|
|
226
|
+
content: ensureTrailingNewline(merged),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function buildAgentPatchFiles(ctx) {
|
|
231
|
+
if (!ctx.hasDir('.claude/agents')) {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return ctx.dirFiles('.claude/agents')
|
|
236
|
+
.filter(file => file.endsWith('.md'))
|
|
237
|
+
.map((file) => {
|
|
238
|
+
const relativePath = `.claude/agents/${file}`;
|
|
239
|
+
const content = normalizeNewlines(ctx.fileContent(relativePath) || '');
|
|
240
|
+
if (!content.startsWith('---\n') || content.includes('\nmaxTurns:')) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const updated = content.replace(/^---\n([\s\S]*?)\n---/, (match, frontmatter) => `---\n${frontmatter}\nmaxTurns: 50\n---`);
|
|
245
|
+
if (updated === content) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
path: relativePath,
|
|
251
|
+
action: 'patch',
|
|
252
|
+
currentState: 'existing agent is missing a maxTurns safety limit',
|
|
253
|
+
proposedState: 'add maxTurns: 50 to the agent frontmatter',
|
|
254
|
+
content: ensureTrailingNewline(updated),
|
|
255
|
+
};
|
|
256
|
+
})
|
|
257
|
+
.filter(Boolean);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function buildHookSettings(ctx, plannedHookFiles, options = {}) {
|
|
65
261
|
const existing = ctx.hasDir('.claude/hooks')
|
|
66
262
|
? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh'))
|
|
67
263
|
: [];
|
|
68
264
|
const hookFiles = [...new Set([...existing, ...plannedHookFiles])].sort();
|
|
69
|
-
if (hookFiles.length === 0
|
|
265
|
+
if (hookFiles.length === 0) {
|
|
70
266
|
return null;
|
|
71
267
|
}
|
|
268
|
+
const settingsPath = '.claude/settings.json';
|
|
269
|
+
const existingSettings = ctx.jsonFile(settingsPath);
|
|
270
|
+
const settings = buildSettingsForProfile({
|
|
271
|
+
profileKey: options.profile || 'safe-write',
|
|
272
|
+
hookFiles,
|
|
273
|
+
existingSettings,
|
|
274
|
+
mcpPackKeys: options.mcpPacks || [],
|
|
275
|
+
});
|
|
276
|
+
const content = `${JSON.stringify(settings, null, 2)}\n`;
|
|
277
|
+
const existingContent = existingSettings ? `${JSON.stringify(existingSettings, null, 2)}\n` : null;
|
|
72
278
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
defaultMode: 'acceptEdits',
|
|
76
|
-
deny: [
|
|
77
|
-
'Read(./.env*)',
|
|
78
|
-
'Read(./secrets/**)',
|
|
79
|
-
'Bash(rm -rf *)',
|
|
80
|
-
'Bash(git reset --hard *)',
|
|
81
|
-
'Bash(git checkout -- *)',
|
|
82
|
-
'Bash(git clean *)',
|
|
83
|
-
'Bash(git push --force *)',
|
|
84
|
-
],
|
|
85
|
-
},
|
|
86
|
-
hooks: {
|
|
87
|
-
PostToolUse: [{
|
|
88
|
-
matcher: 'Write|Edit',
|
|
89
|
-
hooks: hookFiles.filter(file => file !== 'protect-secrets.sh').map(file => ({
|
|
90
|
-
type: 'command',
|
|
91
|
-
command: `bash .claude/hooks/${file}`,
|
|
92
|
-
timeout: 10,
|
|
93
|
-
})),
|
|
94
|
-
}],
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
if (hookFiles.includes('protect-secrets.sh')) {
|
|
99
|
-
settings.hooks.PreToolUse = [{
|
|
100
|
-
matcher: 'Read|Write|Edit',
|
|
101
|
-
hooks: [{
|
|
102
|
-
type: 'command',
|
|
103
|
-
command: 'bash .claude/hooks/protect-secrets.sh',
|
|
104
|
-
timeout: 5,
|
|
105
|
-
}],
|
|
106
|
-
}];
|
|
279
|
+
if (existingContent === content) {
|
|
280
|
+
return null;
|
|
107
281
|
}
|
|
108
282
|
|
|
109
283
|
return {
|
|
110
|
-
path:
|
|
111
|
-
|
|
284
|
+
path: settingsPath,
|
|
285
|
+
action: existingSettings ? 'patch' : 'create',
|
|
286
|
+
currentState: existingSettings
|
|
287
|
+
? 'existing settings are missing selected profile protections or hook registrations'
|
|
288
|
+
: 'settings file is missing',
|
|
289
|
+
proposedState: existingSettings
|
|
290
|
+
? `merge ${options.profile || 'safe-write'} profile protections into existing settings`
|
|
291
|
+
: `create settings for ${options.profile || 'safe-write'} profile and register hooks`,
|
|
292
|
+
content,
|
|
112
293
|
};
|
|
113
294
|
}
|
|
114
295
|
|
|
115
|
-
function buildTemplateFiles(templateKey, stacks, ctx) {
|
|
296
|
+
function buildTemplateFiles(templateKey, stacks, ctx, triggers, options = {}) {
|
|
297
|
+
const patchFiles = templateKey === 'agents' ? buildAgentPatchFiles(ctx) : [];
|
|
298
|
+
|
|
299
|
+
if (templateKey === 'claude-md') {
|
|
300
|
+
const patchFile = buildClaudeMdPatchFile(ctx, stacks);
|
|
301
|
+
if (patchFile) {
|
|
302
|
+
return [patchFile];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
116
306
|
const template = TEMPLATES[templateKey];
|
|
117
307
|
if (!template) return [];
|
|
118
308
|
|
|
@@ -124,10 +314,13 @@ function buildTemplateFiles(templateKey, stacks, ctx) {
|
|
|
124
314
|
const targetDir = TEMPLATE_DIR_MAP[templateKey];
|
|
125
315
|
if (!targetDir) return [];
|
|
126
316
|
|
|
127
|
-
|
|
317
|
+
const generatedFiles = Object.entries(result).map(([fileName, content]) => ({
|
|
128
318
|
path: path.posix.join(targetDir.replace(/\\/g, '/'), fileName),
|
|
129
319
|
content,
|
|
130
320
|
}));
|
|
321
|
+
|
|
322
|
+
const patchedPaths = new Set(patchFiles.map(file => file.path));
|
|
323
|
+
return [...patchFiles, ...generatedFiles.filter(file => !patchedPaths.has(file.path))];
|
|
131
324
|
}
|
|
132
325
|
|
|
133
326
|
function toProposal(templateKey, triggers, templateFiles, ctx) {
|
|
@@ -139,9 +332,9 @@ function toProposal(templateKey, triggers, templateFiles, ctx) {
|
|
|
139
332
|
const highestImpact = sortedTriggers[0]?.impact || 'medium';
|
|
140
333
|
const files = templateFiles.map(file => {
|
|
141
334
|
const exists = ctx.fileContent(file.path) !== null || ctx.hasDir(file.path);
|
|
142
|
-
const action = exists ? 'manual-review' : 'create';
|
|
143
|
-
const currentState = exists ? 'file already exists and will be preserved' : 'missing';
|
|
144
|
-
const proposedState = exists ? 'generated baseline available for manual merge' : 'create new file';
|
|
335
|
+
const action = file.action || (exists ? 'manual-review' : 'create');
|
|
336
|
+
const currentState = file.currentState || (exists ? 'file already exists and will be preserved' : 'missing');
|
|
337
|
+
const proposedState = file.proposedState || (exists ? 'generated baseline available for manual merge' : 'create new file');
|
|
145
338
|
const diffPreview = [
|
|
146
339
|
`--- ${exists ? file.path : 'missing'}`,
|
|
147
340
|
`+++ ${file.path}`,
|
|
@@ -173,7 +366,7 @@ function toProposal(templateKey, triggers, templateFiles, ctx) {
|
|
|
173
366
|
})),
|
|
174
367
|
rationale: sortedTriggers.map(trigger => trigger.fix),
|
|
175
368
|
files,
|
|
176
|
-
readyToApply: files.some(file => file.action
|
|
369
|
+
readyToApply: files.some(file => ['create', 'patch'].includes(file.action)),
|
|
177
370
|
};
|
|
178
371
|
}
|
|
179
372
|
|
|
@@ -185,12 +378,12 @@ async function buildProposalBundle(options) {
|
|
|
185
378
|
const proposals = [];
|
|
186
379
|
|
|
187
380
|
for (const [templateKey, triggers] of groups.entries()) {
|
|
188
|
-
const templateFiles = buildTemplateFiles(templateKey, stacks, ctx);
|
|
381
|
+
const templateFiles = buildTemplateFiles(templateKey, stacks, ctx, triggers, options);
|
|
189
382
|
if (templateKey === 'hooks') {
|
|
190
383
|
const plannedHookFiles = templateFiles
|
|
191
384
|
.map(file => path.basename(file.path))
|
|
192
385
|
.filter(file => file.endsWith('.sh'));
|
|
193
|
-
const settingsFile = buildHookSettings(ctx, plannedHookFiles);
|
|
386
|
+
const settingsFile = buildHookSettings(ctx, plannedHookFiles, options);
|
|
194
387
|
if (settingsFile) {
|
|
195
388
|
templateFiles.push(settingsFile);
|
|
196
389
|
}
|
|
@@ -256,11 +449,75 @@ function writePlanFile(bundle, outFile) {
|
|
|
256
449
|
});
|
|
257
450
|
}
|
|
258
451
|
|
|
452
|
+
function tryParseJson(content) {
|
|
453
|
+
try {
|
|
454
|
+
return JSON.parse(content);
|
|
455
|
+
} catch {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function applyRuntimeSettingsOverlays(bundle, options) {
|
|
461
|
+
if (!bundle || !Array.isArray(bundle.proposals)) {
|
|
462
|
+
return bundle;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const ctx = new ProjectContext(options.dir);
|
|
466
|
+
const existingHooks = ctx.hasDir('.claude/hooks')
|
|
467
|
+
? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh'))
|
|
468
|
+
: [];
|
|
469
|
+
|
|
470
|
+
const proposals = bundle.proposals.map((proposal) => {
|
|
471
|
+
const settingsIndex = proposal.files.findIndex(file => file.path === '.claude/settings.json');
|
|
472
|
+
if (settingsIndex === -1) {
|
|
473
|
+
return proposal;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const plannedHookFiles = proposal.files
|
|
477
|
+
.filter(file => file.path.startsWith('.claude/hooks/') && file.path.endsWith('.sh'))
|
|
478
|
+
.map(file => path.basename(file.path));
|
|
479
|
+
const hookFiles = [...new Set([...existingHooks, ...plannedHookFiles])].sort();
|
|
480
|
+
const currentSettings = tryParseJson(proposal.files[settingsIndex].content) || ctx.jsonFile('.claude/settings.json') || null;
|
|
481
|
+
const mergedSettings = buildSettingsForProfile({
|
|
482
|
+
profileKey: options.profile || 'safe-write',
|
|
483
|
+
hookFiles,
|
|
484
|
+
existingSettings: currentSettings,
|
|
485
|
+
mcpPackKeys: options.mcpPacks || [],
|
|
486
|
+
});
|
|
487
|
+
const updatedContent = `${JSON.stringify(mergedSettings, null, 2)}\n`;
|
|
488
|
+
const currentFile = proposal.files[settingsIndex];
|
|
489
|
+
|
|
490
|
+
const files = [...proposal.files];
|
|
491
|
+
files[settingsIndex] = {
|
|
492
|
+
...currentFile,
|
|
493
|
+
content: updatedContent,
|
|
494
|
+
preview: previewContent(updatedContent),
|
|
495
|
+
diffPreview: [
|
|
496
|
+
`--- ${ctx.fileContent(currentFile.path) !== null ? currentFile.path : 'missing'}`,
|
|
497
|
+
`+++ ${currentFile.path}`,
|
|
498
|
+
...previewContent(updatedContent).split('\n').map(line => `+${line}`),
|
|
499
|
+
].join('\n'),
|
|
500
|
+
currentState: currentFile.currentState || 'existing settings are missing runtime-selected protections or MCP packs',
|
|
501
|
+
proposedState: `merge ${options.profile || 'safe-write'} profile protections and requested MCP packs into settings`,
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
...proposal,
|
|
506
|
+
files,
|
|
507
|
+
};
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
return {
|
|
511
|
+
...bundle,
|
|
512
|
+
proposals,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
259
516
|
function resolvePlan(bundle, options) {
|
|
260
517
|
if (options.planFile) {
|
|
261
|
-
return JSON.parse(fs.readFileSync(options.planFile, 'utf8'));
|
|
518
|
+
return applyRuntimeSettingsOverlays(JSON.parse(fs.readFileSync(options.planFile, 'utf8')), options);
|
|
262
519
|
}
|
|
263
|
-
return bundle;
|
|
520
|
+
return applyRuntimeSettingsOverlays(bundle, options);
|
|
264
521
|
}
|
|
265
522
|
|
|
266
523
|
async function applyProposalBundle(options) {
|
|
@@ -275,38 +532,49 @@ async function applyProposalBundle(options) {
|
|
|
275
532
|
});
|
|
276
533
|
|
|
277
534
|
const createdFiles = [];
|
|
535
|
+
const patchedFiles = [];
|
|
278
536
|
const skippedFiles = [];
|
|
279
537
|
for (const proposal of selected) {
|
|
280
538
|
for (const file of proposal.files) {
|
|
281
|
-
if (file.action
|
|
539
|
+
if (!['create', 'patch'].includes(file.action)) {
|
|
282
540
|
skippedFiles.push(file.path);
|
|
283
541
|
continue;
|
|
284
542
|
}
|
|
285
543
|
const fullPath = path.join(options.dir, file.path);
|
|
286
|
-
if (fs.existsSync(fullPath)) {
|
|
544
|
+
if (file.action === 'create' && fs.existsSync(fullPath)) {
|
|
287
545
|
skippedFiles.push(file.path);
|
|
288
546
|
continue;
|
|
289
547
|
}
|
|
548
|
+
const previousContent = fs.existsSync(fullPath) ? fs.readFileSync(fullPath, 'utf8') : null;
|
|
290
549
|
if (!options.dryRun) {
|
|
291
550
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
292
551
|
fs.writeFileSync(fullPath, file.content, 'utf8');
|
|
293
552
|
}
|
|
294
|
-
|
|
553
|
+
if (file.action === 'create') {
|
|
554
|
+
createdFiles.push(file.path);
|
|
555
|
+
} else {
|
|
556
|
+
patchedFiles.push({ path: file.path, previousContent });
|
|
557
|
+
}
|
|
295
558
|
}
|
|
296
559
|
}
|
|
297
560
|
|
|
298
561
|
let rollback = null;
|
|
299
562
|
let activity = null;
|
|
300
|
-
if (!options.dryRun && createdFiles.length > 0) {
|
|
563
|
+
if (!options.dryRun && (createdFiles.length > 0 || patchedFiles.length > 0)) {
|
|
301
564
|
rollback = writeRollbackArtifact(options.dir, {
|
|
302
565
|
sourcePlan: options.planFile ? path.basename(options.planFile) : 'live-plan',
|
|
303
566
|
createdFiles,
|
|
304
|
-
|
|
567
|
+
patchedFiles,
|
|
568
|
+
rollbackInstructions: [
|
|
569
|
+
...createdFiles.map(file => `Delete ${file}`),
|
|
570
|
+
...patchedFiles.map(file => `Restore previous content for ${file.path} from this manifest`),
|
|
571
|
+
],
|
|
305
572
|
});
|
|
306
573
|
activity = writeActivityArtifact(options.dir, 'apply', {
|
|
307
574
|
sourcePlan: options.planFile ? path.basename(options.planFile) : 'live-plan',
|
|
308
575
|
appliedProposalIds: selected.map(item => item.id),
|
|
309
576
|
createdFiles,
|
|
577
|
+
patchedFiles: patchedFiles.map(file => file.path),
|
|
310
578
|
skippedFiles,
|
|
311
579
|
rollbackArtifact: rollback.relativePath,
|
|
312
580
|
});
|
|
@@ -316,6 +584,7 @@ async function applyProposalBundle(options) {
|
|
|
316
584
|
proposalCount: bundle.proposals.length,
|
|
317
585
|
appliedProposalIds: selected.map(item => item.id),
|
|
318
586
|
createdFiles,
|
|
587
|
+
patchedFiles: patchedFiles.map(file => file.path),
|
|
319
588
|
skippedFiles,
|
|
320
589
|
dryRun: options.dryRun === true,
|
|
321
590
|
rollbackArtifact: rollback ? rollback.relativePath : null,
|
|
@@ -337,6 +606,7 @@ function printApplyResult(result, options = {}) {
|
|
|
337
606
|
}
|
|
338
607
|
console.log(` Applied proposal bundles: ${result.appliedProposalIds.join(', ') || 'none'}`);
|
|
339
608
|
console.log(` Created files: ${result.createdFiles.join(', ') || 'none'}`);
|
|
609
|
+
console.log(` Patched files: ${result.patchedFiles.join(', ') || 'none'}`);
|
|
340
610
|
if (result.rollbackArtifact) {
|
|
341
611
|
console.log(` Rollback: ${result.rollbackArtifact}`);
|
|
342
612
|
}
|
package/src/setup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Setup engine - applies recommended Claude Code configuration to a project.
|
|
3
|
-
* v1.
|
|
3
|
+
* v1.8.0 - Starter-safe setup engine with reusable planning primitives.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
@@ -8,6 +8,7 @@ const path = require('path');
|
|
|
8
8
|
const { TECHNIQUES, STACKS } = require('./techniques');
|
|
9
9
|
const { ProjectContext } = require('./context');
|
|
10
10
|
const { audit } = require('./audit');
|
|
11
|
+
const { buildSettingsForProfile } = require('./governance');
|
|
11
12
|
|
|
12
13
|
// ============================================================
|
|
13
14
|
// Helper: detect project scripts from package.json
|
|
@@ -826,6 +827,19 @@ mkdir -p "$LOG_DIR"
|
|
|
826
827
|
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
|
|
827
828
|
echo "[$TIMESTAMP] $TOOL_NAME: $FILE_PATH" >> "$LOG_FILE"
|
|
828
829
|
|
|
830
|
+
exit 0
|
|
831
|
+
`,
|
|
832
|
+
'session-start.sh': `#!/bin/bash
|
|
833
|
+
# SessionStart hook - prepares logs and records session entry
|
|
834
|
+
|
|
835
|
+
LOG_DIR=".claude/logs"
|
|
836
|
+
LOG_FILE="$LOG_DIR/sessions.log"
|
|
837
|
+
|
|
838
|
+
mkdir -p "$LOG_DIR"
|
|
839
|
+
|
|
840
|
+
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
|
|
841
|
+
echo "[$TIMESTAMP] session started" >> "$LOG_FILE"
|
|
842
|
+
|
|
829
843
|
exit 0
|
|
830
844
|
`,
|
|
831
845
|
}),
|
|
@@ -877,6 +891,15 @@ exit 0
|
|
|
877
891
|
1. Run \`git diff\` to see all changes
|
|
878
892
|
2. Check for: bugs, security issues, missing tests, code style
|
|
879
893
|
3. Provide actionable feedback
|
|
894
|
+
`;
|
|
895
|
+
|
|
896
|
+
cmds['security-review.md'] = `Run a focused security review using Claude Code's built-in security workflow.
|
|
897
|
+
|
|
898
|
+
## Steps:
|
|
899
|
+
1. Review auth, permissions, secrets handling, and data access paths
|
|
900
|
+
2. Run \`/security-review\` for OWASP-focused analysis
|
|
901
|
+
3. Check for unsafe shell commands, token leakage, and risky file access
|
|
902
|
+
4. Report findings ordered by severity with concrete fixes
|
|
880
903
|
`;
|
|
881
904
|
|
|
882
905
|
// Deploy - stack-specific
|
|
@@ -980,6 +1003,17 @@ Fix the GitHub issue: $ARGUMENTS
|
|
|
980
1003
|
3. Implement the fix
|
|
981
1004
|
4. Write tests
|
|
982
1005
|
5. Create a descriptive commit
|
|
1006
|
+
`,
|
|
1007
|
+
'release-check/SKILL.md': `---
|
|
1008
|
+
name: release-check
|
|
1009
|
+
description: Prepare a release candidate and verify publish readiness
|
|
1010
|
+
---
|
|
1011
|
+
Prepare a release candidate for: $ARGUMENTS
|
|
1012
|
+
|
|
1013
|
+
1. Read CHANGELOG.md and package.json version
|
|
1014
|
+
2. Run the test suite and packaging checks
|
|
1015
|
+
3. Verify docs, tags, and release notes are aligned
|
|
1016
|
+
4. Flag anything that would make the release unsafe or misleading
|
|
983
1017
|
`,
|
|
984
1018
|
}),
|
|
985
1019
|
|
|
@@ -1027,6 +1061,12 @@ Fix the GitHub issue: $ARGUMENTS
|
|
|
1027
1061
|
- Never skip or disable tests without a tracking issue
|
|
1028
1062
|
- Mock external dependencies, not internal logic
|
|
1029
1063
|
- Include both happy path and edge case tests
|
|
1064
|
+
`;
|
|
1065
|
+
rules['repository.md'] = `When changing release, packaging, or workflow files:
|
|
1066
|
+
- Keep package.json, CHANGELOG.md, README.md, and docs in sync
|
|
1067
|
+
- Prefer tagged release references over floating branch references in public docs
|
|
1068
|
+
- Preserve backward compatibility in CLI flags where practical
|
|
1069
|
+
- Any automation that writes files must document rollback expectations
|
|
1030
1070
|
`;
|
|
1031
1071
|
return rules;
|
|
1032
1072
|
},
|
|
@@ -1037,12 +1077,26 @@ name: security-reviewer
|
|
|
1037
1077
|
description: Reviews code for security vulnerabilities
|
|
1038
1078
|
tools: [Read, Grep, Glob]
|
|
1039
1079
|
model: sonnet
|
|
1080
|
+
maxTurns: 50
|
|
1040
1081
|
---
|
|
1041
1082
|
Review code for security issues:
|
|
1042
1083
|
- Injection vulnerabilities (SQL, XSS, command injection)
|
|
1043
1084
|
- Authentication and authorization flaws
|
|
1044
1085
|
- Secrets or credentials in code
|
|
1045
1086
|
- Insecure data handling
|
|
1087
|
+
`,
|
|
1088
|
+
'release-manager.md': `---
|
|
1089
|
+
name: release-manager
|
|
1090
|
+
description: Checks release readiness and packaging consistency
|
|
1091
|
+
tools: [Read, Grep, Glob]
|
|
1092
|
+
model: sonnet
|
|
1093
|
+
maxTurns: 50
|
|
1094
|
+
---
|
|
1095
|
+
Review release readiness:
|
|
1096
|
+
- version alignment across package.json, changelog, and docs
|
|
1097
|
+
- publish safety and packaging scope
|
|
1098
|
+
- missing rollback or migration notes
|
|
1099
|
+
- documentation drift that would confuse adopters
|
|
1046
1100
|
`,
|
|
1047
1101
|
}),
|
|
1048
1102
|
|
|
@@ -1164,41 +1218,11 @@ async function setup(options) {
|
|
|
1164
1218
|
if (fs.existsSync(hooksDir) && !fs.existsSync(settingsPath)) {
|
|
1165
1219
|
const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh'));
|
|
1166
1220
|
if (hookFiles.length > 0) {
|
|
1167
|
-
const settings = {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
"Read(./secrets/**)",
|
|
1173
|
-
"Bash(rm -rf *)",
|
|
1174
|
-
"Bash(git reset --hard *)",
|
|
1175
|
-
"Bash(git checkout -- *)",
|
|
1176
|
-
"Bash(git clean *)",
|
|
1177
|
-
"Bash(git push --force *)"
|
|
1178
|
-
]
|
|
1179
|
-
},
|
|
1180
|
-
hooks: {
|
|
1181
|
-
PostToolUse: [{
|
|
1182
|
-
matcher: "Write|Edit",
|
|
1183
|
-
hooks: hookFiles.filter(f => f !== 'protect-secrets.sh').map(f => ({
|
|
1184
|
-
type: "command",
|
|
1185
|
-
command: `bash .claude/hooks/${f}`,
|
|
1186
|
-
timeout: 10
|
|
1187
|
-
}))
|
|
1188
|
-
}]
|
|
1189
|
-
}
|
|
1190
|
-
};
|
|
1191
|
-
// Add protect-secrets as PreToolUse if it exists
|
|
1192
|
-
if (hookFiles.includes('protect-secrets.sh')) {
|
|
1193
|
-
settings.hooks.PreToolUse = [{
|
|
1194
|
-
matcher: "Read|Write|Edit",
|
|
1195
|
-
hooks: [{
|
|
1196
|
-
type: "command",
|
|
1197
|
-
command: "bash .claude/hooks/protect-secrets.sh",
|
|
1198
|
-
timeout: 5
|
|
1199
|
-
}]
|
|
1200
|
-
}];
|
|
1201
|
-
}
|
|
1221
|
+
const settings = buildSettingsForProfile({
|
|
1222
|
+
profileKey: options.profile || 'safe-write',
|
|
1223
|
+
hookFiles,
|
|
1224
|
+
mcpPackKeys: options.mcpPacks || [],
|
|
1225
|
+
});
|
|
1202
1226
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
1203
1227
|
writtenFiles.push('.claude/settings.json');
|
|
1204
1228
|
log(` \x1b[32m✅\x1b[0m Created .claude/settings.json (hooks registered)`);
|