refacil-sdd-ai 4.2.4 → 4.4.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 +239 -214
- package/agents/auditor.md +189 -184
- package/agents/debugger.md +201 -204
- package/agents/implementer.md +150 -149
- package/agents/investigator.md +80 -89
- package/agents/proposer.md +219 -124
- package/agents/tester.md +140 -144
- package/agents/validator.md +153 -145
- package/bin/cli.js +158 -116
- package/lib/bus/askFulfillment.js +17 -17
- package/lib/bus/broker.js +599 -599
- package/lib/bus/ui/app.js +318 -318
- package/lib/commands/sdd.js +447 -0
- package/lib/hooks.js +236 -236
- package/lib/installer.js +58 -2
- package/lib/methodology-migration-pending.js +101 -136
- package/package.json +4 -6
- package/skills/apply/SKILL.md +139 -120
- package/skills/archive/SKILL.md +105 -107
- package/skills/ask/SKILL.md +78 -78
- package/skills/attend/SKILL.md +70 -70
- package/skills/bug/SKILL.md +121 -128
- package/skills/explore/SKILL.md +73 -63
- package/skills/guide/SKILL.md +79 -79
- package/skills/inbox/SKILL.md +43 -43
- package/skills/join/SKILL.md +82 -82
- package/skills/prereqs/BUS-CROSS-REPO.md +55 -55
- package/skills/prereqs/METHODOLOGY-CONTRACT.md +122 -115
- package/skills/prereqs/SKILL.md +30 -37
- package/skills/propose/SKILL.md +103 -102
- package/skills/reply/SKILL.md +44 -44
- package/skills/review/SKILL.md +163 -126
- package/skills/review/checklist-back.md +92 -92
- package/skills/review/checklist-front.md +72 -72
- package/skills/review/checklist.md +114 -114
- package/skills/say/SKILL.md +38 -38
- package/skills/setup/SKILL.md +85 -141
- package/skills/setup/troubleshooting.md +38 -35
- package/skills/test/SKILL.md +104 -94
- package/skills/test/testing-patterns.md +63 -63
- package/skills/up-code/SKILL.md +108 -108
- package/skills/update/SKILL.md +109 -132
- package/skills/verify/SKILL.md +159 -132
- package/templates/compact-guidance.md +45 -45
- package/templates/methodology-guide.md +46 -42
- package/config/openspec-config.yaml +0 -8
- package/skills/prereqs/OPENSPEC-DELTAS.md +0 -51
package/lib/hooks.js
CHANGED
|
@@ -1,236 +1,236 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
|
|
6
|
-
// ── Cursor: todos los hooks en .cursor/hooks.json ────────────────────────────
|
|
7
|
-
|
|
8
|
-
function readCursorHooksJson(projectRoot) {
|
|
9
|
-
const p = path.join(projectRoot, '.cursor', 'hooks.json');
|
|
10
|
-
if (!fs.existsSync(p)) return { version: 1, hooks: {} };
|
|
11
|
-
try {
|
|
12
|
-
const c = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
13
|
-
if (!c.hooks) c.hooks = {};
|
|
14
|
-
return c;
|
|
15
|
-
} catch (_) {
|
|
16
|
-
return { version: 1, hooks: {} };
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function writeCursorHooksJson(projectRoot, config) {
|
|
21
|
-
const dir = path.join(projectRoot, '.cursor');
|
|
22
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
23
|
-
fs.writeFileSync(path.join(dir, 'hooks.json'), JSON.stringify(config, null, 2) + '\n');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function installCursorHooks(projectRoot) {
|
|
27
|
-
cleanLegacySettingsHooks(projectRoot);
|
|
28
|
-
const config = readCursorHooksJson(projectRoot);
|
|
29
|
-
let changed = false;
|
|
30
|
-
|
|
31
|
-
function ensure(event, marker, entry) {
|
|
32
|
-
if (!config.hooks[event]) config.hooks[event] = [];
|
|
33
|
-
if (!config.hooks[event].some((h) => h[marker] === true)) {
|
|
34
|
-
config.hooks[event].push(entry);
|
|
35
|
-
changed = true;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
ensure('sessionStart', '_sdd', {
|
|
40
|
-
_sdd: true,
|
|
41
|
-
command: 'refacil-sdd-ai check-update',
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// compact-bash debe ir ANTES de check-review
|
|
45
|
-
if (!config.hooks.preToolUse) config.hooks.preToolUse = [];
|
|
46
|
-
if (!config.hooks.preToolUse.some((h) => h._sdd_compact === true)) {
|
|
47
|
-
config.hooks.preToolUse.unshift({ _sdd_compact: true, command: 'refacil-sdd-ai compact-bash', matcher: 'Bash' });
|
|
48
|
-
changed = true;
|
|
49
|
-
}
|
|
50
|
-
if (!config.hooks.preToolUse.some((h) => h._sdd_review === true)) {
|
|
51
|
-
config.hooks.preToolUse.push({ _sdd_review: true, command: 'refacil-sdd-ai check-review', matcher: 'Bash' });
|
|
52
|
-
changed = true;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
ensure('beforeSubmitPrompt', '_sdd_notify', {
|
|
56
|
-
_sdd_notify: true,
|
|
57
|
-
command: 'refacil-sdd-ai notify-update --cursor',
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
if (!changed) {
|
|
61
|
-
console.log(' Hooks SDD-AI ya configurados en .cursor/hooks.json.');
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
writeCursorHooksJson(projectRoot, config);
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function uninstallCursorHooks(projectRoot) {
|
|
70
|
-
const hooksJsonPath = path.join(projectRoot, '.cursor', 'hooks.json');
|
|
71
|
-
let changed = false;
|
|
72
|
-
|
|
73
|
-
if (fs.existsSync(hooksJsonPath)) {
|
|
74
|
-
let config;
|
|
75
|
-
try { config = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8')); } catch (_) { config = null; }
|
|
76
|
-
|
|
77
|
-
if (config && config.hooks) {
|
|
78
|
-
const sddMarkers = ['_sdd', '_sdd_compact', '_sdd_review', '_sdd_notify'];
|
|
79
|
-
for (const event of Object.keys(config.hooks)) {
|
|
80
|
-
if (!Array.isArray(config.hooks[event])) continue;
|
|
81
|
-
const before = config.hooks[event].length;
|
|
82
|
-
config.hooks[event] = config.hooks[event].filter(
|
|
83
|
-
(h) => !sddMarkers.some((m) => h[m] === true),
|
|
84
|
-
);
|
|
85
|
-
if (config.hooks[event].length !== before) changed = true;
|
|
86
|
-
if (config.hooks[event].length === 0) delete config.hooks[event];
|
|
87
|
-
}
|
|
88
|
-
if (changed) writeCursorHooksJson(projectRoot, config);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Limpiar vestigios en settings.json de instalaciones previas
|
|
93
|
-
const settingsPath = path.join(projectRoot, '.cursor', 'settings.json');
|
|
94
|
-
if (fs.existsSync(settingsPath)) {
|
|
95
|
-
let settings;
|
|
96
|
-
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (_) { settings = null; }
|
|
97
|
-
if (settings && settings.hooks) {
|
|
98
|
-
const evts = ['SessionStart', 'PreToolUse', 'UserPromptSubmit', 'beforeSubmitPrompt', 'Stop', 'afterAgentResponse'];
|
|
99
|
-
const sddMarkers = ['_sdd', '_sdd_compact', '_sdd_review', '_sdd_notify'];
|
|
100
|
-
for (const evt of evts) {
|
|
101
|
-
if (!Array.isArray(settings.hooks[evt])) continue;
|
|
102
|
-
const before = settings.hooks[evt].length;
|
|
103
|
-
settings.hooks[evt] = settings.hooks[evt].filter((h) => !sddMarkers.some((m) => h[m] === true));
|
|
104
|
-
if (settings.hooks[evt].length !== before) changed = true;
|
|
105
|
-
if (settings.hooks[evt].length === 0) delete settings.hooks[evt];
|
|
106
|
-
}
|
|
107
|
-
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
108
|
-
if (changed) fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return changed;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ── Claude Code: todos los hooks en .claude/settings.json ───────────────────
|
|
116
|
-
|
|
117
|
-
function installClaudeHooks(projectRoot) {
|
|
118
|
-
const settingsDir = path.join(projectRoot, '.claude');
|
|
119
|
-
const settingsPath = path.join(settingsDir, 'settings.json');
|
|
120
|
-
let settings = {};
|
|
121
|
-
|
|
122
|
-
if (fs.existsSync(settingsPath)) {
|
|
123
|
-
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (_) { settings = {}; }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (!settings.hooks) settings.hooks = {};
|
|
127
|
-
let changed = false;
|
|
128
|
-
|
|
129
|
-
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
130
|
-
if (!settings.hooks.SessionStart.some((h) => h._sdd === true)) {
|
|
131
|
-
settings.hooks.SessionStart.push({ _sdd: true, matcher: '', hooks: [{ type: 'command', command: 'refacil-sdd-ai check-update' }] });
|
|
132
|
-
changed = true;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
|
|
136
|
-
if (!settings.hooks.UserPromptSubmit.some((h) => h._sdd_notify === true)) {
|
|
137
|
-
settings.hooks.UserPromptSubmit.push({ _sdd_notify: true, matcher: '', hooks: [{ type: 'command', command: 'refacil-sdd-ai notify-update' }] });
|
|
138
|
-
changed = true;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
142
|
-
if (!settings.hooks.PreToolUse.some((h) => h._sdd_compact === true)) {
|
|
143
|
-
settings.hooks.PreToolUse.unshift({ _sdd_compact: true, matcher: 'Bash', hooks: [{ type: 'command', command: 'refacil-sdd-ai compact-bash' }] });
|
|
144
|
-
changed = true;
|
|
145
|
-
}
|
|
146
|
-
if (!settings.hooks.PreToolUse.some((h) => h._sdd_review === true)) {
|
|
147
|
-
settings.hooks.PreToolUse.push({ _sdd_review: true, matcher: 'Bash', hooks: [{ type: 'command', command: 'refacil-sdd-ai check-review' }] });
|
|
148
|
-
changed = true;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (!changed) {
|
|
152
|
-
console.log(' Hooks SDD-AI ya configurados en .claude/settings.json.');
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
fs.mkdirSync(settingsDir, { recursive: true });
|
|
157
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
158
|
-
return true;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function uninstallClaudeHooks(projectRoot) {
|
|
162
|
-
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
163
|
-
if (!fs.existsSync(settingsPath)) return false;
|
|
164
|
-
|
|
165
|
-
let settings;
|
|
166
|
-
try {
|
|
167
|
-
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
168
|
-
} catch (_) {
|
|
169
|
-
console.log(' No se pudieron remover hooks: .claude/settings.json invalido.');
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (!settings.hooks) return false;
|
|
174
|
-
|
|
175
|
-
let changed = false;
|
|
176
|
-
const sddMarkers = ['_sdd', '_sdd_compact', '_sdd_review', '_sdd_notify'];
|
|
177
|
-
|
|
178
|
-
for (const event of Object.keys(settings.hooks)) {
|
|
179
|
-
if (!Array.isArray(settings.hooks[event])) continue;
|
|
180
|
-
const before = settings.hooks[event].length;
|
|
181
|
-
settings.hooks[event] = settings.hooks[event].filter((h) => !sddMarkers.some((m) => h[m] === true));
|
|
182
|
-
if (settings.hooks[event].length !== before) changed = true;
|
|
183
|
-
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
187
|
-
if (!changed) return false;
|
|
188
|
-
|
|
189
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ── Limpieza de hooks SDD en settings.json (migracion legacy) ───────────────
|
|
194
|
-
|
|
195
|
-
function cleanLegacySettingsHooks(projectRoot) {
|
|
196
|
-
const sddMarkers = ['_sdd', '_sdd_compact', '_sdd_review', '_sdd_notify'];
|
|
197
|
-
const evts = ['SessionStart', 'PreToolUse', 'UserPromptSubmit', 'beforeSubmitPrompt', 'Stop', 'afterAgentResponse'];
|
|
198
|
-
|
|
199
|
-
for (const ideDir of ['.cursor']) {
|
|
200
|
-
const settingsPath = path.join(projectRoot, ideDir, 'settings.json');
|
|
201
|
-
if (!fs.existsSync(settingsPath)) continue;
|
|
202
|
-
|
|
203
|
-
let settings;
|
|
204
|
-
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (_) { continue; }
|
|
205
|
-
if (!settings || !settings.hooks) continue;
|
|
206
|
-
|
|
207
|
-
let changed = false;
|
|
208
|
-
for (const evt of evts) {
|
|
209
|
-
if (!Array.isArray(settings.hooks[evt])) continue;
|
|
210
|
-
const before = settings.hooks[evt].length;
|
|
211
|
-
settings.hooks[evt] = settings.hooks[evt].filter((h) => !sddMarkers.some((m) => h[m] === true));
|
|
212
|
-
if (settings.hooks[evt].length !== before) changed = true;
|
|
213
|
-
if (settings.hooks[evt].length === 0) delete settings.hooks[evt];
|
|
214
|
-
}
|
|
215
|
-
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
216
|
-
if (changed) {
|
|
217
|
-
try { fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n'); } catch (_) {}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// ── Fachada pública ──────────────────────────────────────────────────────────
|
|
223
|
-
|
|
224
|
-
function installHooks(ideDir, projectRoot) {
|
|
225
|
-
return ideDir === '.cursor'
|
|
226
|
-
? installCursorHooks(projectRoot)
|
|
227
|
-
: installClaudeHooks(projectRoot);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function uninstallHooks(ideDir, projectRoot) {
|
|
231
|
-
return ideDir === '.cursor'
|
|
232
|
-
? uninstallCursorHooks(projectRoot)
|
|
233
|
-
: uninstallClaudeHooks(projectRoot);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
module.exports = { installHooks, uninstallHooks, cleanLegacySettingsHooks };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// ── Cursor: todos los hooks en .cursor/hooks.json ────────────────────────────
|
|
7
|
+
|
|
8
|
+
function readCursorHooksJson(projectRoot) {
|
|
9
|
+
const p = path.join(projectRoot, '.cursor', 'hooks.json');
|
|
10
|
+
if (!fs.existsSync(p)) return { version: 1, hooks: {} };
|
|
11
|
+
try {
|
|
12
|
+
const c = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
13
|
+
if (!c.hooks) c.hooks = {};
|
|
14
|
+
return c;
|
|
15
|
+
} catch (_) {
|
|
16
|
+
return { version: 1, hooks: {} };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeCursorHooksJson(projectRoot, config) {
|
|
21
|
+
const dir = path.join(projectRoot, '.cursor');
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
23
|
+
fs.writeFileSync(path.join(dir, 'hooks.json'), JSON.stringify(config, null, 2) + '\n');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function installCursorHooks(projectRoot) {
|
|
27
|
+
cleanLegacySettingsHooks(projectRoot);
|
|
28
|
+
const config = readCursorHooksJson(projectRoot);
|
|
29
|
+
let changed = false;
|
|
30
|
+
|
|
31
|
+
function ensure(event, marker, entry) {
|
|
32
|
+
if (!config.hooks[event]) config.hooks[event] = [];
|
|
33
|
+
if (!config.hooks[event].some((h) => h[marker] === true)) {
|
|
34
|
+
config.hooks[event].push(entry);
|
|
35
|
+
changed = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
ensure('sessionStart', '_sdd', {
|
|
40
|
+
_sdd: true,
|
|
41
|
+
command: 'refacil-sdd-ai check-update',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// compact-bash debe ir ANTES de check-review
|
|
45
|
+
if (!config.hooks.preToolUse) config.hooks.preToolUse = [];
|
|
46
|
+
if (!config.hooks.preToolUse.some((h) => h._sdd_compact === true)) {
|
|
47
|
+
config.hooks.preToolUse.unshift({ _sdd_compact: true, command: 'refacil-sdd-ai compact-bash', matcher: 'Bash' });
|
|
48
|
+
changed = true;
|
|
49
|
+
}
|
|
50
|
+
if (!config.hooks.preToolUse.some((h) => h._sdd_review === true)) {
|
|
51
|
+
config.hooks.preToolUse.push({ _sdd_review: true, command: 'refacil-sdd-ai check-review', matcher: 'Bash' });
|
|
52
|
+
changed = true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ensure('beforeSubmitPrompt', '_sdd_notify', {
|
|
56
|
+
_sdd_notify: true,
|
|
57
|
+
command: 'refacil-sdd-ai notify-update --cursor',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!changed) {
|
|
61
|
+
console.log(' Hooks SDD-AI ya configurados en .cursor/hooks.json.');
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
writeCursorHooksJson(projectRoot, config);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function uninstallCursorHooks(projectRoot) {
|
|
70
|
+
const hooksJsonPath = path.join(projectRoot, '.cursor', 'hooks.json');
|
|
71
|
+
let changed = false;
|
|
72
|
+
|
|
73
|
+
if (fs.existsSync(hooksJsonPath)) {
|
|
74
|
+
let config;
|
|
75
|
+
try { config = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8')); } catch (_) { config = null; }
|
|
76
|
+
|
|
77
|
+
if (config && config.hooks) {
|
|
78
|
+
const sddMarkers = ['_sdd', '_sdd_compact', '_sdd_review', '_sdd_notify'];
|
|
79
|
+
for (const event of Object.keys(config.hooks)) {
|
|
80
|
+
if (!Array.isArray(config.hooks[event])) continue;
|
|
81
|
+
const before = config.hooks[event].length;
|
|
82
|
+
config.hooks[event] = config.hooks[event].filter(
|
|
83
|
+
(h) => !sddMarkers.some((m) => h[m] === true),
|
|
84
|
+
);
|
|
85
|
+
if (config.hooks[event].length !== before) changed = true;
|
|
86
|
+
if (config.hooks[event].length === 0) delete config.hooks[event];
|
|
87
|
+
}
|
|
88
|
+
if (changed) writeCursorHooksJson(projectRoot, config);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Limpiar vestigios en settings.json de instalaciones previas
|
|
93
|
+
const settingsPath = path.join(projectRoot, '.cursor', 'settings.json');
|
|
94
|
+
if (fs.existsSync(settingsPath)) {
|
|
95
|
+
let settings;
|
|
96
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (_) { settings = null; }
|
|
97
|
+
if (settings && settings.hooks) {
|
|
98
|
+
const evts = ['SessionStart', 'PreToolUse', 'UserPromptSubmit', 'beforeSubmitPrompt', 'Stop', 'afterAgentResponse'];
|
|
99
|
+
const sddMarkers = ['_sdd', '_sdd_compact', '_sdd_review', '_sdd_notify'];
|
|
100
|
+
for (const evt of evts) {
|
|
101
|
+
if (!Array.isArray(settings.hooks[evt])) continue;
|
|
102
|
+
const before = settings.hooks[evt].length;
|
|
103
|
+
settings.hooks[evt] = settings.hooks[evt].filter((h) => !sddMarkers.some((m) => h[m] === true));
|
|
104
|
+
if (settings.hooks[evt].length !== before) changed = true;
|
|
105
|
+
if (settings.hooks[evt].length === 0) delete settings.hooks[evt];
|
|
106
|
+
}
|
|
107
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
108
|
+
if (changed) fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return changed;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Claude Code: todos los hooks en .claude/settings.json ───────────────────
|
|
116
|
+
|
|
117
|
+
function installClaudeHooks(projectRoot) {
|
|
118
|
+
const settingsDir = path.join(projectRoot, '.claude');
|
|
119
|
+
const settingsPath = path.join(settingsDir, 'settings.json');
|
|
120
|
+
let settings = {};
|
|
121
|
+
|
|
122
|
+
if (fs.existsSync(settingsPath)) {
|
|
123
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (_) { settings = {}; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!settings.hooks) settings.hooks = {};
|
|
127
|
+
let changed = false;
|
|
128
|
+
|
|
129
|
+
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
130
|
+
if (!settings.hooks.SessionStart.some((h) => h._sdd === true)) {
|
|
131
|
+
settings.hooks.SessionStart.push({ _sdd: true, matcher: '', hooks: [{ type: 'command', command: 'refacil-sdd-ai check-update' }] });
|
|
132
|
+
changed = true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
|
|
136
|
+
if (!settings.hooks.UserPromptSubmit.some((h) => h._sdd_notify === true)) {
|
|
137
|
+
settings.hooks.UserPromptSubmit.push({ _sdd_notify: true, matcher: '', hooks: [{ type: 'command', command: 'refacil-sdd-ai notify-update' }] });
|
|
138
|
+
changed = true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
142
|
+
if (!settings.hooks.PreToolUse.some((h) => h._sdd_compact === true)) {
|
|
143
|
+
settings.hooks.PreToolUse.unshift({ _sdd_compact: true, matcher: 'Bash', hooks: [{ type: 'command', command: 'refacil-sdd-ai compact-bash' }] });
|
|
144
|
+
changed = true;
|
|
145
|
+
}
|
|
146
|
+
if (!settings.hooks.PreToolUse.some((h) => h._sdd_review === true)) {
|
|
147
|
+
settings.hooks.PreToolUse.push({ _sdd_review: true, matcher: 'Bash', hooks: [{ type: 'command', command: 'refacil-sdd-ai check-review' }] });
|
|
148
|
+
changed = true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!changed) {
|
|
152
|
+
console.log(' Hooks SDD-AI ya configurados en .claude/settings.json.');
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fs.mkdirSync(settingsDir, { recursive: true });
|
|
157
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function uninstallClaudeHooks(projectRoot) {
|
|
162
|
+
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
163
|
+
if (!fs.existsSync(settingsPath)) return false;
|
|
164
|
+
|
|
165
|
+
let settings;
|
|
166
|
+
try {
|
|
167
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
168
|
+
} catch (_) {
|
|
169
|
+
console.log(' No se pudieron remover hooks: .claude/settings.json invalido.');
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!settings.hooks) return false;
|
|
174
|
+
|
|
175
|
+
let changed = false;
|
|
176
|
+
const sddMarkers = ['_sdd', '_sdd_compact', '_sdd_review', '_sdd_notify'];
|
|
177
|
+
|
|
178
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
179
|
+
if (!Array.isArray(settings.hooks[event])) continue;
|
|
180
|
+
const before = settings.hooks[event].length;
|
|
181
|
+
settings.hooks[event] = settings.hooks[event].filter((h) => !sddMarkers.some((m) => h[m] === true));
|
|
182
|
+
if (settings.hooks[event].length !== before) changed = true;
|
|
183
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
187
|
+
if (!changed) return false;
|
|
188
|
+
|
|
189
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Limpieza de hooks SDD en settings.json (migracion legacy) ───────────────
|
|
194
|
+
|
|
195
|
+
function cleanLegacySettingsHooks(projectRoot) {
|
|
196
|
+
const sddMarkers = ['_sdd', '_sdd_compact', '_sdd_review', '_sdd_notify'];
|
|
197
|
+
const evts = ['SessionStart', 'PreToolUse', 'UserPromptSubmit', 'beforeSubmitPrompt', 'Stop', 'afterAgentResponse'];
|
|
198
|
+
|
|
199
|
+
for (const ideDir of ['.cursor']) {
|
|
200
|
+
const settingsPath = path.join(projectRoot, ideDir, 'settings.json');
|
|
201
|
+
if (!fs.existsSync(settingsPath)) continue;
|
|
202
|
+
|
|
203
|
+
let settings;
|
|
204
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (_) { continue; }
|
|
205
|
+
if (!settings || !settings.hooks) continue;
|
|
206
|
+
|
|
207
|
+
let changed = false;
|
|
208
|
+
for (const evt of evts) {
|
|
209
|
+
if (!Array.isArray(settings.hooks[evt])) continue;
|
|
210
|
+
const before = settings.hooks[evt].length;
|
|
211
|
+
settings.hooks[evt] = settings.hooks[evt].filter((h) => !sddMarkers.some((m) => h[m] === true));
|
|
212
|
+
if (settings.hooks[evt].length !== before) changed = true;
|
|
213
|
+
if (settings.hooks[evt].length === 0) delete settings.hooks[evt];
|
|
214
|
+
}
|
|
215
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
216
|
+
if (changed) {
|
|
217
|
+
try { fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n'); } catch (_) {}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Fachada pública ──────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
function installHooks(ideDir, projectRoot) {
|
|
225
|
+
return ideDir === '.cursor'
|
|
226
|
+
? installCursorHooks(projectRoot)
|
|
227
|
+
: installClaudeHooks(projectRoot);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function uninstallHooks(ideDir, projectRoot) {
|
|
231
|
+
return ideDir === '.cursor'
|
|
232
|
+
? uninstallCursorHooks(projectRoot)
|
|
233
|
+
: uninstallClaudeHooks(projectRoot);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = { installHooks, uninstallHooks, cleanLegacySettingsHooks };
|
package/lib/installer.js
CHANGED
|
@@ -71,10 +71,12 @@ function installSkills(packageRoot, projectRoot) {
|
|
|
71
71
|
// Claude Code: tools allowlist granular, model: sonnet|opus|haiku
|
|
72
72
|
// Cursor: readonly: true|false (booleano), model: inherit (default)
|
|
73
73
|
function transformFrontmatterForCursor(content) {
|
|
74
|
-
const
|
|
74
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
75
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
75
76
|
if (!match) return content;
|
|
76
77
|
|
|
77
78
|
const [, frontmatterRaw, body] = match;
|
|
79
|
+
// work with normalized content from here on
|
|
78
80
|
const lines = frontmatterRaw.split('\n');
|
|
79
81
|
const out = [];
|
|
80
82
|
let toolsLine = null;
|
|
@@ -214,6 +216,59 @@ function removeSkills(projectRoot) {
|
|
|
214
216
|
return removed;
|
|
215
217
|
}
|
|
216
218
|
|
|
219
|
+
function removeOpenspecLegacyAssets(projectRoot) {
|
|
220
|
+
let removed = 0;
|
|
221
|
+
|
|
222
|
+
// Remove .claude/skills/openspec-*/ and .cursor/skills/openspec-*/
|
|
223
|
+
for (const ideDir of ['.claude', '.cursor']) {
|
|
224
|
+
const skillsDir = path.join(projectRoot, ideDir, 'skills');
|
|
225
|
+
if (!fs.existsSync(skillsDir)) continue;
|
|
226
|
+
let entries;
|
|
227
|
+
try {
|
|
228
|
+
entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
229
|
+
} catch (_) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
for (const entry of entries) {
|
|
233
|
+
if (entry.isDirectory() && entry.name.startsWith('openspec-')) {
|
|
234
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
|
235
|
+
removed++;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Remove .claude/commands/opsx/ directory
|
|
241
|
+
const claudeOpsx = path.join(projectRoot, '.claude', 'commands', 'opsx');
|
|
242
|
+
if (fs.existsSync(claudeOpsx)) {
|
|
243
|
+
fs.rmSync(claudeOpsx, { recursive: true });
|
|
244
|
+
removed++;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Remove .cursor/commands/opsx/ directory and opsx-*.md loose files
|
|
248
|
+
const cursorCommandsDir = path.join(projectRoot, '.cursor', 'commands');
|
|
249
|
+
const cursorOpsx = path.join(cursorCommandsDir, 'opsx');
|
|
250
|
+
if (fs.existsSync(cursorOpsx)) {
|
|
251
|
+
fs.rmSync(cursorOpsx, { recursive: true });
|
|
252
|
+
removed++;
|
|
253
|
+
}
|
|
254
|
+
if (fs.existsSync(cursorCommandsDir)) {
|
|
255
|
+
let entries;
|
|
256
|
+
try {
|
|
257
|
+
entries = fs.readdirSync(cursorCommandsDir, { withFileTypes: true });
|
|
258
|
+
} catch (_) {
|
|
259
|
+
entries = [];
|
|
260
|
+
}
|
|
261
|
+
for (const entry of entries) {
|
|
262
|
+
if (entry.isFile() && entry.name.startsWith('opsx-') && entry.name.endsWith('.md')) {
|
|
263
|
+
fs.unlinkSync(path.join(cursorCommandsDir, entry.name));
|
|
264
|
+
removed++;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return removed;
|
|
270
|
+
}
|
|
271
|
+
|
|
217
272
|
function checkClaudeCodeVersion() {
|
|
218
273
|
const { execSync } = require('child_process');
|
|
219
274
|
try {
|
|
@@ -244,7 +299,7 @@ function checkNodeVersion() {
|
|
|
244
299
|
|
|
245
300
|
if (major < 20 || (major === 20 && minor < 19)) {
|
|
246
301
|
console.log(`\n ADVERTENCIA: Node.js ${version} detectado.`);
|
|
247
|
-
console.log('
|
|
302
|
+
console.log(' refacil-sdd-ai requiere Node.js >= 20.19.0.');
|
|
248
303
|
console.log(' Las skills se instalaran pero /refacil:setup podria fallar al instalar OpenSpec.\n');
|
|
249
304
|
return false;
|
|
250
305
|
}
|
|
@@ -264,6 +319,7 @@ module.exports = {
|
|
|
264
319
|
writeRepoVersion,
|
|
265
320
|
getPackageVersion,
|
|
266
321
|
removeSkills,
|
|
322
|
+
removeOpenspecLegacyAssets,
|
|
267
323
|
checkClaudeCodeVersion,
|
|
268
324
|
checkNodeVersion,
|
|
269
325
|
};
|