claudex-setup 1.6.0 → 1.7.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 +16 -0
- package/README.md +90 -7
- package/bin/cli.js +214 -11
- package/package.json +1 -1
- package/src/activity.js +60 -0
- package/src/analyze.js +397 -0
- package/src/audit.js +25 -20
- package/src/benchmark.js +176 -0
- package/src/governance.js +192 -0
- package/src/index.js +13 -1
- package/src/interactive.js +2 -2
- package/src/plans.js +355 -0
- package/src/setup.js +154 -52
- package/src/techniques.js +8 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
const PERMISSION_PROFILES = [
|
|
2
|
+
{
|
|
3
|
+
key: 'read-only',
|
|
4
|
+
label: 'Read-Only',
|
|
5
|
+
risk: 'low',
|
|
6
|
+
defaultMode: 'plan',
|
|
7
|
+
useWhen: 'Security review, discovery, and first contact with a mature repo.',
|
|
8
|
+
behavior: 'No file writes. Safe for audits, workshops, and approval flows.',
|
|
9
|
+
deny: ['Write(**)', 'Edit(**)', 'MultiEdit(**)', 'Bash(rm -rf *)', 'Bash(git reset --hard *)'],
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
key: 'suggest-only',
|
|
13
|
+
label: 'Suggest-Only',
|
|
14
|
+
risk: 'low',
|
|
15
|
+
defaultMode: 'acceptEdits',
|
|
16
|
+
useWhen: 'Teams want structured proposals and exported plans without automatic apply.',
|
|
17
|
+
behavior: 'Generates plans and proposal bundles, but no source changes are applied.',
|
|
18
|
+
deny: ['Bash(rm -rf *)', 'Bash(git reset --hard *)', 'Bash(git clean *)', 'Read(./.env*)'],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
key: 'safe-write',
|
|
22
|
+
label: 'Safe-Write',
|
|
23
|
+
risk: 'medium',
|
|
24
|
+
defaultMode: 'acceptEdits',
|
|
25
|
+
useWhen: 'Starter repos or tightly scoped apply flows with visible rollback.',
|
|
26
|
+
behavior: 'Allows creation of missing Claude artifacts while preserving existing files.',
|
|
27
|
+
deny: ['Read(./.env*)', 'Read(./secrets/**)', 'Bash(rm -rf *)', 'Bash(git push --force *)'],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: 'power-user',
|
|
31
|
+
label: 'Power-User',
|
|
32
|
+
risk: 'medium',
|
|
33
|
+
defaultMode: 'acceptEdits',
|
|
34
|
+
useWhen: 'Experienced maintainers who understand the repo and want faster iteration.',
|
|
35
|
+
behavior: 'Broader local automation with fewer prompts, still without bypass defaults.',
|
|
36
|
+
deny: ['Read(./.env*)', 'Bash(rm -rf *)'],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: 'internal-research',
|
|
40
|
+
label: 'Internal-Research',
|
|
41
|
+
risk: 'high',
|
|
42
|
+
defaultMode: 'bypassPermissions',
|
|
43
|
+
useWhen: 'Internal experiments only, never as a product-facing default.',
|
|
44
|
+
behavior: 'Maximum autonomy for research workflows, suitable only with explicit human oversight.',
|
|
45
|
+
deny: [],
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const HOOK_REGISTRY = [
|
|
50
|
+
{
|
|
51
|
+
key: 'protect-secrets',
|
|
52
|
+
file: '.claude/hooks/protect-secrets.sh',
|
|
53
|
+
triggerPoint: 'PreToolUse',
|
|
54
|
+
matcher: 'Read|Write|Edit',
|
|
55
|
+
purpose: 'Blocks direct access to secret or credential files before a tool runs.',
|
|
56
|
+
filesTouched: [],
|
|
57
|
+
sideEffects: ['Stops the action and returns a block decision when a secret path is targeted.'],
|
|
58
|
+
risk: 'low',
|
|
59
|
+
dryRunExample: 'Attempt to read `.env` and confirm the hook blocks the request.',
|
|
60
|
+
rollbackPath: 'Remove the PreToolUse registration from settings.json.',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
key: 'on-edit-lint',
|
|
64
|
+
file: '.claude/hooks/on-edit-lint.sh',
|
|
65
|
+
triggerPoint: 'PostToolUse',
|
|
66
|
+
matcher: 'Write|Edit',
|
|
67
|
+
purpose: 'Runs the repo linter or formatter after file edits when tooling is available.',
|
|
68
|
+
filesTouched: ['Working tree files targeted by eslint/ruff fixes'],
|
|
69
|
+
sideEffects: ['May auto-fix formatting or lint issues.', 'Can modify the same files that were just edited.'],
|
|
70
|
+
risk: 'medium',
|
|
71
|
+
dryRunExample: 'Edit a JS or Python file and inspect whether eslint or ruff would run.',
|
|
72
|
+
rollbackPath: 'Remove the PostToolUse hook entry or delete the script.',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: 'log-changes',
|
|
76
|
+
file: '.claude/hooks/log-changes.sh',
|
|
77
|
+
triggerPoint: 'PostToolUse',
|
|
78
|
+
matcher: 'Write|Edit',
|
|
79
|
+
purpose: 'Appends a durable file-change log under `.claude/logs/` for later review.',
|
|
80
|
+
filesTouched: ['.claude/logs/file-changes.log'],
|
|
81
|
+
sideEffects: ['Creates the logs directory on first use.', 'Adds a timestamped audit line per file change.'],
|
|
82
|
+
risk: 'low',
|
|
83
|
+
dryRunExample: 'Edit one file and verify the log entry is appended.',
|
|
84
|
+
rollbackPath: 'Remove the PostToolUse hook entry and delete the log file if desired.',
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const POLICY_PACKS = [
|
|
89
|
+
{
|
|
90
|
+
key: 'baseline-engineering',
|
|
91
|
+
label: 'Baseline Engineering',
|
|
92
|
+
modules: ['CLAUDE.md baseline', 'commands', 'rules', 'safe-write profile'],
|
|
93
|
+
useWhen: 'General product teams that want a pragmatic default.',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
key: 'security-sensitive',
|
|
97
|
+
label: 'Security-Sensitive',
|
|
98
|
+
modules: ['read-only profile', 'suggest-only mode', 'protect-secrets hook', 'approval checklist'],
|
|
99
|
+
useWhen: 'Auth, payments, customer data, or regulated surfaces.',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
key: 'oss-friendly',
|
|
103
|
+
label: 'OSS-Friendly',
|
|
104
|
+
modules: ['suggest-only profile', 'minimal commands', 'light rules', 'manual merge expectations'],
|
|
105
|
+
useWhen: 'Open-source repos with many external contributors.',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: 'regulated-lite',
|
|
109
|
+
label: 'Regulated-Lite',
|
|
110
|
+
modules: ['suggest-only or safe-write profile', 'activity artifacts', 'rollback manifests', 'benchmark evidence'],
|
|
111
|
+
useWhen: 'Teams that need auditable change paths before broader adoption.',
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const PILOT_ROLLOUT_KIT = {
|
|
116
|
+
recommendedScope: [
|
|
117
|
+
'Pick 1-2 repos with active maintainers and low blast radius.',
|
|
118
|
+
'Run discover and suggest-only first; avoid direct writes on mature repos.',
|
|
119
|
+
'Choose one permission profile before any pilot starts.',
|
|
120
|
+
'Define success metrics before the first benchmark run.',
|
|
121
|
+
],
|
|
122
|
+
approvals: [
|
|
123
|
+
'Engineering owner approves scope and rollback expectations.',
|
|
124
|
+
'Security owner approves the selected permission profile and hooks.',
|
|
125
|
+
'Pilot owner records the benchmark baseline and acceptance criteria.',
|
|
126
|
+
],
|
|
127
|
+
successMetrics: [
|
|
128
|
+
'Score delta and organic score delta',
|
|
129
|
+
'Number of recommendations accepted',
|
|
130
|
+
'Time to first useful Claude workflow',
|
|
131
|
+
'Rollback-free apply rate',
|
|
132
|
+
],
|
|
133
|
+
rollbackExpectations: [
|
|
134
|
+
'Every apply batch must emit a rollback artifact.',
|
|
135
|
+
'If a created artifact is rejected, delete the files listed in the rollback manifest.',
|
|
136
|
+
'Record the rollback event in the activity log for auditability.',
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
function getGovernanceSummary() {
|
|
141
|
+
return {
|
|
142
|
+
permissionProfiles: PERMISSION_PROFILES,
|
|
143
|
+
hookRegistry: HOOK_REGISTRY,
|
|
144
|
+
policyPacks: POLICY_PACKS,
|
|
145
|
+
pilotRolloutKit: PILOT_ROLLOUT_KIT,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function printGovernanceSummary(summary, options = {}) {
|
|
150
|
+
if (options.json) {
|
|
151
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log('');
|
|
156
|
+
console.log(' claudex-setup governance');
|
|
157
|
+
console.log(' ═══════════════════════════════════════');
|
|
158
|
+
console.log(' Safe defaults, hook transparency, and pilot guidance.');
|
|
159
|
+
console.log('');
|
|
160
|
+
|
|
161
|
+
console.log(' Permission Profiles');
|
|
162
|
+
for (const profile of summary.permissionProfiles) {
|
|
163
|
+
console.log(` - ${profile.label} [${profile.risk}]`);
|
|
164
|
+
console.log(` ${profile.useWhen}`);
|
|
165
|
+
console.log(` defaultMode=${profile.defaultMode}`);
|
|
166
|
+
}
|
|
167
|
+
console.log('');
|
|
168
|
+
|
|
169
|
+
console.log(' Hook Registry');
|
|
170
|
+
for (const hook of summary.hookRegistry) {
|
|
171
|
+
console.log(` - ${hook.file}`);
|
|
172
|
+
console.log(` ${hook.triggerPoint} ${hook.matcher} -> ${hook.purpose}`);
|
|
173
|
+
}
|
|
174
|
+
console.log('');
|
|
175
|
+
|
|
176
|
+
console.log(' Policy Packs');
|
|
177
|
+
for (const pack of summary.policyPacks) {
|
|
178
|
+
console.log(` - ${pack.label}: ${pack.modules.join(', ')}`);
|
|
179
|
+
}
|
|
180
|
+
console.log('');
|
|
181
|
+
|
|
182
|
+
console.log(' Pilot Rollout Kit');
|
|
183
|
+
for (const item of summary.pilotRolloutKit.recommendedScope) {
|
|
184
|
+
console.log(` - ${item}`);
|
|
185
|
+
}
|
|
186
|
+
console.log('');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
getGovernanceSummary,
|
|
191
|
+
printGovernanceSummary,
|
|
192
|
+
};
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
const { audit } = require('./audit');
|
|
2
2
|
const { setup } = require('./setup');
|
|
3
|
+
const { analyzeProject } = require('./analyze');
|
|
4
|
+
const { buildProposalBundle, applyProposalBundle } = require('./plans');
|
|
5
|
+
const { getGovernanceSummary } = require('./governance');
|
|
6
|
+
const { runBenchmark } = require('./benchmark');
|
|
3
7
|
|
|
4
|
-
module.exports = {
|
|
8
|
+
module.exports = {
|
|
9
|
+
audit,
|
|
10
|
+
setup,
|
|
11
|
+
analyzeProject,
|
|
12
|
+
buildProposalBundle,
|
|
13
|
+
applyProposalBundle,
|
|
14
|
+
getGovernanceSummary,
|
|
15
|
+
runBenchmark,
|
|
16
|
+
};
|
package/src/interactive.js
CHANGED
|
@@ -41,8 +41,8 @@ async function interactive(options) {
|
|
|
41
41
|
for (const [key, technique] of Object.entries(TECHNIQUES)) {
|
|
42
42
|
results.push({ key, ...technique, passed: technique.check(ctx) });
|
|
43
43
|
}
|
|
44
|
-
const failed = results.filter(r =>
|
|
45
|
-
const passed = results.filter(r => r.passed);
|
|
44
|
+
const failed = results.filter(r => r.passed === false);
|
|
45
|
+
const passed = results.filter(r => r.passed === true);
|
|
46
46
|
|
|
47
47
|
console.log(` ${c(`${passed.length}/${results.length}`, 'bold')} checks already passing.`);
|
|
48
48
|
console.log(` ${c(String(failed.length), 'yellow')} improvements available.`);
|
package/src/plans.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const { version } = require('../package.json');
|
|
5
|
+
const { analyzeProject } = require('./analyze');
|
|
6
|
+
const { ProjectContext } = require('./context');
|
|
7
|
+
const { TECHNIQUES, STACKS } = require('./techniques');
|
|
8
|
+
const { TEMPLATES } = require('./setup');
|
|
9
|
+
const { writeActivityArtifact, writeRollbackArtifact } = require('./activity');
|
|
10
|
+
|
|
11
|
+
const TEMPLATE_DIR_MAP = {
|
|
12
|
+
hooks: '.claude/hooks',
|
|
13
|
+
commands: '.claude/commands',
|
|
14
|
+
skills: '.claude/skills',
|
|
15
|
+
rules: '.claude/rules',
|
|
16
|
+
agents: '.claude/agents',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const TEMPLATE_LABELS = {
|
|
20
|
+
'claude-md': 'CLAUDE.md baseline',
|
|
21
|
+
hooks: 'Hooks bundle',
|
|
22
|
+
commands: 'Slash commands',
|
|
23
|
+
skills: 'Skills pack',
|
|
24
|
+
rules: 'Rules pack',
|
|
25
|
+
agents: 'Specialized agents',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const TEMPLATE_MODULES = {
|
|
29
|
+
'claude-md': 'CLAUDE.md',
|
|
30
|
+
hooks: 'hooks',
|
|
31
|
+
commands: 'commands',
|
|
32
|
+
skills: 'skills',
|
|
33
|
+
rules: 'rules',
|
|
34
|
+
agents: 'agents',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const IMPACT_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
|
|
38
|
+
|
|
39
|
+
function previewContent(content) {
|
|
40
|
+
return content.split('\n').slice(0, 12).join('\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function riskFromImpact(impact) {
|
|
44
|
+
if (impact === 'critical') return 'medium';
|
|
45
|
+
if (impact === 'high') return 'medium';
|
|
46
|
+
return 'low';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getFailedTemplateGroups(ctx, only = []) {
|
|
50
|
+
const groups = new Map();
|
|
51
|
+
for (const [key, technique] of Object.entries(TECHNIQUES)) {
|
|
52
|
+
const passed = technique.check(ctx);
|
|
53
|
+
if (passed !== false || !technique.template) continue;
|
|
54
|
+
if (technique.template === 'mermaid') continue;
|
|
55
|
+
if (only.length > 0 && !only.includes(key) && !only.includes(technique.template)) continue;
|
|
56
|
+
if (!groups.has(technique.template)) {
|
|
57
|
+
groups.set(technique.template, []);
|
|
58
|
+
}
|
|
59
|
+
groups.get(technique.template).push({ key, ...technique });
|
|
60
|
+
}
|
|
61
|
+
return groups;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildHookSettings(ctx, plannedHookFiles) {
|
|
65
|
+
const existing = ctx.hasDir('.claude/hooks')
|
|
66
|
+
? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh'))
|
|
67
|
+
: [];
|
|
68
|
+
const hookFiles = [...new Set([...existing, ...plannedHookFiles])].sort();
|
|
69
|
+
if (hookFiles.length === 0 || ctx.fileContent('.claude/settings.json')) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const settings = {
|
|
74
|
+
permissions: {
|
|
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
|
+
}];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
path: '.claude/settings.json',
|
|
111
|
+
content: `${JSON.stringify(settings, null, 2)}\n`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildTemplateFiles(templateKey, stacks, ctx) {
|
|
116
|
+
const template = TEMPLATES[templateKey];
|
|
117
|
+
if (!template) return [];
|
|
118
|
+
|
|
119
|
+
const result = template(stacks, ctx);
|
|
120
|
+
if (typeof result === 'string') {
|
|
121
|
+
return [{ path: 'CLAUDE.md', content: result }];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const targetDir = TEMPLATE_DIR_MAP[templateKey];
|
|
125
|
+
if (!targetDir) return [];
|
|
126
|
+
|
|
127
|
+
return Object.entries(result).map(([fileName, content]) => ({
|
|
128
|
+
path: path.posix.join(targetDir.replace(/\\/g, '/'), fileName),
|
|
129
|
+
content,
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function toProposal(templateKey, triggers, templateFiles, ctx) {
|
|
134
|
+
const sortedTriggers = [...triggers].sort((a, b) => {
|
|
135
|
+
const impactA = IMPACT_ORDER[a.impact] ?? 0;
|
|
136
|
+
const impactB = IMPACT_ORDER[b.impact] ?? 0;
|
|
137
|
+
return impactB - impactA;
|
|
138
|
+
});
|
|
139
|
+
const highestImpact = sortedTriggers[0]?.impact || 'medium';
|
|
140
|
+
const files = templateFiles.map(file => {
|
|
141
|
+
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';
|
|
145
|
+
const diffPreview = [
|
|
146
|
+
`--- ${exists ? file.path : 'missing'}`,
|
|
147
|
+
`+++ ${file.path}`,
|
|
148
|
+
...previewContent(file.content).split('\n').map(line => `+${line}`),
|
|
149
|
+
].join('\n');
|
|
150
|
+
return {
|
|
151
|
+
path: file.path,
|
|
152
|
+
action,
|
|
153
|
+
currentState,
|
|
154
|
+
proposedState,
|
|
155
|
+
bytes: Buffer.byteLength(file.content, 'utf8'),
|
|
156
|
+
content: file.content,
|
|
157
|
+
preview: previewContent(file.content),
|
|
158
|
+
diffPreview,
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
id: templateKey,
|
|
164
|
+
title: TEMPLATE_LABELS[templateKey] || templateKey,
|
|
165
|
+
module: TEMPLATE_MODULES[templateKey] || templateKey,
|
|
166
|
+
risk: riskFromImpact(highestImpact),
|
|
167
|
+
confidence: sortedTriggers.length >= 2 ? 'high' : 'medium',
|
|
168
|
+
triggers: sortedTriggers.map(trigger => ({
|
|
169
|
+
key: trigger.key,
|
|
170
|
+
name: trigger.name,
|
|
171
|
+
impact: trigger.impact,
|
|
172
|
+
fix: trigger.fix,
|
|
173
|
+
})),
|
|
174
|
+
rationale: sortedTriggers.map(trigger => trigger.fix),
|
|
175
|
+
files,
|
|
176
|
+
readyToApply: files.some(file => file.action === 'create'),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function buildProposalBundle(options) {
|
|
181
|
+
const ctx = new ProjectContext(options.dir);
|
|
182
|
+
const stacks = ctx.detectStacks(STACKS);
|
|
183
|
+
const report = await analyzeProject({ ...options, mode: 'augment' });
|
|
184
|
+
const groups = getFailedTemplateGroups(ctx, options.only || []);
|
|
185
|
+
const proposals = [];
|
|
186
|
+
|
|
187
|
+
for (const [templateKey, triggers] of groups.entries()) {
|
|
188
|
+
const templateFiles = buildTemplateFiles(templateKey, stacks, ctx);
|
|
189
|
+
if (templateKey === 'hooks') {
|
|
190
|
+
const plannedHookFiles = templateFiles
|
|
191
|
+
.map(file => path.basename(file.path))
|
|
192
|
+
.filter(file => file.endsWith('.sh'));
|
|
193
|
+
const settingsFile = buildHookSettings(ctx, plannedHookFiles);
|
|
194
|
+
if (settingsFile) {
|
|
195
|
+
templateFiles.push(settingsFile);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
proposals.push(toProposal(templateKey, triggers, templateFiles, ctx));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
proposals.sort((a, b) => {
|
|
202
|
+
const impactA = IMPACT_ORDER[a.triggers[0]?.impact] ?? 0;
|
|
203
|
+
const impactB = IMPACT_ORDER[b.triggers[0]?.impact] ?? 0;
|
|
204
|
+
return impactB - impactA;
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
schemaVersion: 1,
|
|
209
|
+
generatedBy: `claudex-setup@${version}`,
|
|
210
|
+
createdAt: new Date().toISOString(),
|
|
211
|
+
directory: options.dir,
|
|
212
|
+
projectSummary: report.projectSummary,
|
|
213
|
+
strengthsPreserved: report.strengthsPreserved,
|
|
214
|
+
topNextActions: report.topNextActions,
|
|
215
|
+
riskNotes: report.riskNotes,
|
|
216
|
+
proposals,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function printProposalBundle(bundle, options = {}) {
|
|
221
|
+
if (options.json) {
|
|
222
|
+
console.log(JSON.stringify(bundle, null, 2));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log('');
|
|
227
|
+
console.log(' claudex-setup plan');
|
|
228
|
+
console.log(' ═══════════════════════════════════════');
|
|
229
|
+
console.log(` ${bundle.projectSummary.name} | maturity=${bundle.projectSummary.maturity} | score=${bundle.projectSummary.score}/100`);
|
|
230
|
+
console.log('');
|
|
231
|
+
|
|
232
|
+
if (bundle.proposals.length === 0) {
|
|
233
|
+
console.log(' No templated proposals are needed right now.');
|
|
234
|
+
console.log('');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log(' Proposal Bundles');
|
|
239
|
+
for (const proposal of bundle.proposals) {
|
|
240
|
+
const applyState = proposal.readyToApply ? 'ready' : 'manual-review';
|
|
241
|
+
console.log(` - ${proposal.id} [${applyState}]`);
|
|
242
|
+
console.log(` ${proposal.title} | risk=${proposal.risk} | confidence=${proposal.confidence}`);
|
|
243
|
+
console.log(` triggers: ${proposal.triggers.map(item => item.name).join(', ')}`);
|
|
244
|
+
console.log(` files: ${proposal.files.map(file => `${file.path} (${file.action})`).join(', ')}`);
|
|
245
|
+
}
|
|
246
|
+
console.log('');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function writePlanFile(bundle, outFile) {
|
|
250
|
+
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
|
251
|
+
fs.writeFileSync(outFile, JSON.stringify(bundle, null, 2), 'utf8');
|
|
252
|
+
return writeActivityArtifact(bundle.directory, 'plan-export', {
|
|
253
|
+
exportedPlan: outFile,
|
|
254
|
+
proposalIds: bundle.proposals.map(proposal => proposal.id),
|
|
255
|
+
proposalCount: bundle.proposals.length,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function resolvePlan(bundle, options) {
|
|
260
|
+
if (options.planFile) {
|
|
261
|
+
return JSON.parse(fs.readFileSync(options.planFile, 'utf8'));
|
|
262
|
+
}
|
|
263
|
+
return bundle;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function applyProposalBundle(options) {
|
|
267
|
+
const liveBundle = options.planFile ? null : await buildProposalBundle(options);
|
|
268
|
+
const bundle = resolvePlan(liveBundle, options);
|
|
269
|
+
const selectedIds = options.only && options.only.length > 0
|
|
270
|
+
? new Set(options.only)
|
|
271
|
+
: null;
|
|
272
|
+
const selected = bundle.proposals.filter(proposal => {
|
|
273
|
+
if (selectedIds && !selectedIds.has(proposal.id)) return false;
|
|
274
|
+
return proposal.readyToApply;
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const createdFiles = [];
|
|
278
|
+
const skippedFiles = [];
|
|
279
|
+
for (const proposal of selected) {
|
|
280
|
+
for (const file of proposal.files) {
|
|
281
|
+
if (file.action !== 'create') {
|
|
282
|
+
skippedFiles.push(file.path);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const fullPath = path.join(options.dir, file.path);
|
|
286
|
+
if (fs.existsSync(fullPath)) {
|
|
287
|
+
skippedFiles.push(file.path);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (!options.dryRun) {
|
|
291
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
292
|
+
fs.writeFileSync(fullPath, file.content, 'utf8');
|
|
293
|
+
}
|
|
294
|
+
createdFiles.push(file.path);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let rollback = null;
|
|
299
|
+
let activity = null;
|
|
300
|
+
if (!options.dryRun && createdFiles.length > 0) {
|
|
301
|
+
rollback = writeRollbackArtifact(options.dir, {
|
|
302
|
+
sourcePlan: options.planFile ? path.basename(options.planFile) : 'live-plan',
|
|
303
|
+
createdFiles,
|
|
304
|
+
rollbackInstructions: createdFiles.map(file => `Delete ${file}`),
|
|
305
|
+
});
|
|
306
|
+
activity = writeActivityArtifact(options.dir, 'apply', {
|
|
307
|
+
sourcePlan: options.planFile ? path.basename(options.planFile) : 'live-plan',
|
|
308
|
+
appliedProposalIds: selected.map(item => item.id),
|
|
309
|
+
createdFiles,
|
|
310
|
+
skippedFiles,
|
|
311
|
+
rollbackArtifact: rollback.relativePath,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
proposalCount: bundle.proposals.length,
|
|
317
|
+
appliedProposalIds: selected.map(item => item.id),
|
|
318
|
+
createdFiles,
|
|
319
|
+
skippedFiles,
|
|
320
|
+
dryRun: options.dryRun === true,
|
|
321
|
+
rollbackArtifact: rollback ? rollback.relativePath : null,
|
|
322
|
+
activityArtifact: activity ? activity.relativePath : null,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function printApplyResult(result, options = {}) {
|
|
327
|
+
if (options.json) {
|
|
328
|
+
console.log(JSON.stringify(result, null, 2));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
console.log('');
|
|
333
|
+
console.log(' claudex-setup apply');
|
|
334
|
+
console.log(' ═══════════════════════════════════════');
|
|
335
|
+
if (result.dryRun) {
|
|
336
|
+
console.log(' Dry-run only. No files were written.');
|
|
337
|
+
}
|
|
338
|
+
console.log(` Applied proposal bundles: ${result.appliedProposalIds.join(', ') || 'none'}`);
|
|
339
|
+
console.log(` Created files: ${result.createdFiles.join(', ') || 'none'}`);
|
|
340
|
+
if (result.rollbackArtifact) {
|
|
341
|
+
console.log(` Rollback: ${result.rollbackArtifact}`);
|
|
342
|
+
}
|
|
343
|
+
if (result.activityArtifact) {
|
|
344
|
+
console.log(` Activity log: ${result.activityArtifact}`);
|
|
345
|
+
}
|
|
346
|
+
console.log('');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
module.exports = {
|
|
350
|
+
buildProposalBundle,
|
|
351
|
+
printProposalBundle,
|
|
352
|
+
writePlanFile,
|
|
353
|
+
applyProposalBundle,
|
|
354
|
+
printApplyResult,
|
|
355
|
+
};
|