hail-hydra-cc 2.3.2 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -99
- package/bin/cli.js +105 -105
- package/files/SKILL.md +1172 -1217
- package/files/agents/hydra-analyst.md +1 -1
- package/files/agents/hydra-coder.md +2 -2
- package/files/agents/hydra-git.md +1 -1
- package/files/agents/hydra-guard.md +3 -3
- package/files/agents/hydra-runner.md +1 -1
- package/files/agents/hydra-scout.md +1 -1
- package/files/agents/hydra-scribe.md +1 -1
- package/files/agents/hydra-sentinel-scan.md +19 -1
- package/files/agents/hydra-sentinel.md +19 -1
- package/files/commands/hydra/config.md +37 -37
- package/files/commands/hydra/guard.md +71 -71
- package/files/commands/hydra/help.md +47 -47
- package/files/commands/hydra/quiet.md +16 -16
- package/files/commands/hydra/stats.md +31 -0
- package/files/commands/hydra/status.md +85 -85
- package/files/commands/hydra/verbose.md +29 -29
- package/files/hooks/hydra-auto-guard.js +130 -54
- package/files/hooks/hydra-check-update.js +99 -99
- package/files/hooks/hydra-statusline.js +131 -128
- package/files/references/model-capabilities.md +164 -164
- package/files/references/routing-guide.md +303 -303
- package/package.json +1 -1
- package/src/display.js +1 -1
- package/src/files.js +110 -106
- package/src/installer.js +401 -393
- package/src/prompts.js +80 -80
package/src/installer.js
CHANGED
|
@@ -1,393 +1,401 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
const chalk = require('chalk');
|
|
7
|
-
|
|
8
|
-
const { agents, skill, references, commands, hooks, binaryHooks } = require('./files');
|
|
9
|
-
const { showInstallHeader, showFileInstalled, showInstallComplete, showStatusTable, VERSION } = require('./display');
|
|
10
|
-
|
|
11
|
-
// ── Install locations ────────────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
const GLOBAL_BASE = path.join(os.homedir(), '.claude');
|
|
14
|
-
const LOCAL_BASE = path.join(process.cwd(), '.claude');
|
|
15
|
-
|
|
16
|
-
// ── File manifest ────────────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
function buildManifest(base) {
|
|
19
|
-
const agentEntries = Object.entries(agents).map(([key, info]) => ({
|
|
20
|
-
type: 'agent',
|
|
21
|
-
key,
|
|
22
|
-
display: info.display,
|
|
23
|
-
model: info.model,
|
|
24
|
-
content: info.content,
|
|
25
|
-
dest: path.join(base, 'agents', `${key}.md`),
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
const skillEntry = {
|
|
29
|
-
type: 'skill',
|
|
30
|
-
key: 'SKILL.md',
|
|
31
|
-
display: 'skills/hydra/SKILL.md',
|
|
32
|
-
content: skill,
|
|
33
|
-
dest: path.join(base, 'skills', 'hydra', 'SKILL.md'),
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
type: '
|
|
38
|
-
key,
|
|
39
|
-
display:
|
|
40
|
-
content,
|
|
41
|
-
dest: path.join(base, 'skills', '
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
type: '
|
|
46
|
-
key,
|
|
47
|
-
display: `
|
|
48
|
-
content,
|
|
49
|
-
dest: path.join(base, '
|
|
50
|
-
}));
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
type: '
|
|
54
|
-
key
|
|
55
|
-
display:
|
|
56
|
-
content
|
|
57
|
-
dest: path.join(base, '
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
settings.hooks
|
|
133
|
-
settings.hooks.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (settings.hooks?.
|
|
182
|
-
settings.hooks.
|
|
183
|
-
if (!settings.hooks.
|
|
184
|
-
}
|
|
185
|
-
if (settings.hooks
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
|
|
8
|
+
const { agents, skill, stfuAgentsSkill, references, commands, hooks, binaryHooks } = require('./files');
|
|
9
|
+
const { showInstallHeader, showFileInstalled, showInstallComplete, showStatusTable, VERSION } = require('./display');
|
|
10
|
+
|
|
11
|
+
// ── Install locations ────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const GLOBAL_BASE = path.join(os.homedir(), '.claude');
|
|
14
|
+
const LOCAL_BASE = path.join(process.cwd(), '.claude');
|
|
15
|
+
|
|
16
|
+
// ── File manifest ────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function buildManifest(base) {
|
|
19
|
+
const agentEntries = Object.entries(agents).map(([key, info]) => ({
|
|
20
|
+
type: 'agent',
|
|
21
|
+
key,
|
|
22
|
+
display: info.display,
|
|
23
|
+
model: info.model,
|
|
24
|
+
content: info.content,
|
|
25
|
+
dest: path.join(base, 'agents', `${key}.md`),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const skillEntry = {
|
|
29
|
+
type: 'skill',
|
|
30
|
+
key: 'SKILL.md',
|
|
31
|
+
display: 'skills/hydra/SKILL.md',
|
|
32
|
+
content: skill,
|
|
33
|
+
dest: path.join(base, 'skills', 'hydra', 'SKILL.md'),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const stfuAgentsSkillEntry = {
|
|
37
|
+
type: 'skill',
|
|
38
|
+
key: 'stfu-agents/SKILL.md',
|
|
39
|
+
display: 'skills/stfu-agents/SKILL.md',
|
|
40
|
+
content: stfuAgentsSkill,
|
|
41
|
+
dest: path.join(base, 'skills', 'stfu-agents', 'SKILL.md'),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const refEntries = Object.entries(references).map(([key, content]) => ({
|
|
45
|
+
type: 'reference',
|
|
46
|
+
key,
|
|
47
|
+
display: `references/${key}.md`,
|
|
48
|
+
content,
|
|
49
|
+
dest: path.join(base, 'skills', 'hydra', 'references', `${key}.md`),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
const commandEntries = Object.entries(commands).map(([key, content]) => ({
|
|
53
|
+
type: 'command',
|
|
54
|
+
key,
|
|
55
|
+
display: `commands/hydra/${key}.md`,
|
|
56
|
+
content,
|
|
57
|
+
dest: path.join(base, 'commands', 'hydra', `${key}.md`),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
const versionEntry = {
|
|
61
|
+
type: 'version',
|
|
62
|
+
key: 'VERSION',
|
|
63
|
+
display: 'skills/hydra/VERSION',
|
|
64
|
+
content: VERSION,
|
|
65
|
+
dest: path.join(base, 'skills', 'hydra', 'VERSION'),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return [...agentEntries, skillEntry, stfuAgentsSkillEntry, ...refEntries, ...commandEntries, versionEntry];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
function ensureDir(dirPath) {
|
|
74
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeFileSafe(dest, content) {
|
|
78
|
+
ensureDir(path.dirname(dest));
|
|
79
|
+
fs.writeFileSync(dest, content, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function fileExists(filePath) {
|
|
83
|
+
try {
|
|
84
|
+
fs.accessSync(filePath, fs.constants.F_OK);
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function hasAnyInstalled(base) {
|
|
92
|
+
return buildManifest(base).some((entry) => fileExists(entry.dest));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Hooks & settings ─────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
function installHooks() {
|
|
98
|
+
const hooksDir = path.join(GLOBAL_BASE, 'hooks');
|
|
99
|
+
ensureDir(hooksDir);
|
|
100
|
+
|
|
101
|
+
for (const [key, content] of Object.entries(hooks)) {
|
|
102
|
+
const dest = path.join(hooksDir, `${key}.js`);
|
|
103
|
+
try {
|
|
104
|
+
writeFileSafe(dest, content);
|
|
105
|
+
try { fs.chmodSync(dest, 0o755); } catch {}
|
|
106
|
+
showFileInstalled(`hooks/${key}.js`, true);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
showFileInstalled(`hooks/${key}.js`, false, err.message);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Copy binary hook files (e.g., .wav) that can't be read as UTF-8 text
|
|
113
|
+
for (const [filename, srcPath] of Object.entries(binaryHooks)) {
|
|
114
|
+
const dest = path.join(hooksDir, filename);
|
|
115
|
+
try {
|
|
116
|
+
fs.copyFileSync(srcPath, dest);
|
|
117
|
+
showFileInstalled(`hooks/${filename}`, true);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
showFileInstalled(`hooks/${filename}`, false, err.message);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function registerHooksInSettings() {
|
|
125
|
+
const settingsFile = path.join(GLOBAL_BASE, 'settings.json');
|
|
126
|
+
|
|
127
|
+
let settings = {};
|
|
128
|
+
try {
|
|
129
|
+
settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
130
|
+
} catch {}
|
|
131
|
+
|
|
132
|
+
if (!settings.hooks) settings.hooks = {};
|
|
133
|
+
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
134
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
135
|
+
|
|
136
|
+
const isHydraHook = (entry) =>
|
|
137
|
+
Array.isArray(entry.hooks) && entry.hooks.some(h => h.command && h.command.includes('hydra-'));
|
|
138
|
+
|
|
139
|
+
// Remove stale Hydra entries (clean reinstall)
|
|
140
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(x => !isHydraHook(x));
|
|
141
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(x => !isHydraHook(x));
|
|
142
|
+
|
|
143
|
+
settings.hooks.SessionStart.push({
|
|
144
|
+
hooks: [{ type: 'command', command: 'node ~/.claude/hooks/hydra-check-update.js' }]
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
settings.hooks.PostToolUse.push({
|
|
148
|
+
matcher: 'Write|Edit|MultiEdit',
|
|
149
|
+
hooks: [{ type: 'command', command: 'node ~/.claude/hooks/hydra-auto-guard.js' }]
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!settings.hooks.Notification) settings.hooks.Notification = [];
|
|
153
|
+
settings.hooks.Notification = settings.hooks.Notification.filter(x => !isHydraHook(x));
|
|
154
|
+
settings.hooks.Notification.push({
|
|
155
|
+
hooks: [{ type: 'command', command: 'node ~/.claude/hooks/hydra-notify.js' }]
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
let statusLineConfigured = false;
|
|
159
|
+
if (!settings.statusLine || (settings.statusLine.command && settings.statusLine.command.includes('hydra-'))) {
|
|
160
|
+
settings.statusLine = {
|
|
161
|
+
type: 'command',
|
|
162
|
+
command: 'node ~/.claude/hooks/hydra-statusline.js',
|
|
163
|
+
padding: 0,
|
|
164
|
+
};
|
|
165
|
+
statusLineConfigured = true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
writeFileSafe(settingsFile, JSON.stringify(settings, null, 2));
|
|
169
|
+
return { statusLineConfigured };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function deregisterHooks() {
|
|
173
|
+
const settingsFile = path.join(GLOBAL_BASE, 'settings.json');
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
let settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
177
|
+
|
|
178
|
+
const isHydraHook = (entry) =>
|
|
179
|
+
Array.isArray(entry.hooks) && entry.hooks.some(h => h.command && h.command.includes('hydra-'));
|
|
180
|
+
|
|
181
|
+
if (settings.hooks?.SessionStart) {
|
|
182
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(x => !isHydraHook(x));
|
|
183
|
+
if (!settings.hooks.SessionStart.length) delete settings.hooks.SessionStart;
|
|
184
|
+
}
|
|
185
|
+
if (settings.hooks?.PostToolUse) {
|
|
186
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(x => !isHydraHook(x));
|
|
187
|
+
if (!settings.hooks.PostToolUse.length) delete settings.hooks.PostToolUse;
|
|
188
|
+
}
|
|
189
|
+
if (settings.hooks?.Notification) {
|
|
190
|
+
settings.hooks.Notification = settings.hooks.Notification.filter(x => !isHydraHook(x));
|
|
191
|
+
if (!settings.hooks.Notification.length) delete settings.hooks.Notification;
|
|
192
|
+
}
|
|
193
|
+
if (settings.hooks && !Object.keys(settings.hooks).length) delete settings.hooks;
|
|
194
|
+
|
|
195
|
+
if (settings.statusLine?.command?.includes('hydra-')) delete settings.statusLine;
|
|
196
|
+
|
|
197
|
+
writeFileSafe(settingsFile, JSON.stringify(settings, null, 2));
|
|
198
|
+
console.log(chalk.green(' \u2714 Hooks deregistered from settings.json'));
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.log(chalk.yellow(` \u26a0 Could not update settings.json: ${err.message}`));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Core install ─────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
function installToBase(base, label) {
|
|
207
|
+
showInstallHeader(label);
|
|
208
|
+
|
|
209
|
+
const manifest = buildManifest(base);
|
|
210
|
+
const results = [];
|
|
211
|
+
|
|
212
|
+
for (const entry of manifest) {
|
|
213
|
+
try {
|
|
214
|
+
writeFileSafe(entry.dest, entry.content);
|
|
215
|
+
showFileInstalled(entry.display, true);
|
|
216
|
+
results.push({ name: entry.display, success: true });
|
|
217
|
+
} catch (err) {
|
|
218
|
+
showFileInstalled(entry.display, false, err.message);
|
|
219
|
+
results.push({ name: entry.display, success: false, error: err.message });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
console.log();
|
|
224
|
+
return results;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
async function runInstall(scope, { nonInteractive = false } = {}) {
|
|
230
|
+
const bases =
|
|
231
|
+
scope === 'both' ? [[GLOBAL_BASE, 'Global (~/.claude/)'], [LOCAL_BASE, 'Local (./.claude/)']] :
|
|
232
|
+
scope === 'global' ? [[GLOBAL_BASE, 'Global (~/.claude/)']] :
|
|
233
|
+
[[LOCAL_BASE, 'Local (./.claude/)']];
|
|
234
|
+
|
|
235
|
+
// Check for existing files and ask about overwrite (skip in non-interactive mode)
|
|
236
|
+
const anyExisting = bases.some(([base]) => hasAnyInstalled(base));
|
|
237
|
+
if (anyExisting && !nonInteractive) {
|
|
238
|
+
const inquirer = require('inquirer');
|
|
239
|
+
const { overwrite } = await inquirer.prompt([
|
|
240
|
+
{
|
|
241
|
+
type: 'confirm',
|
|
242
|
+
name: 'overwrite',
|
|
243
|
+
message: 'Hydra agents already installed. Overwrite?',
|
|
244
|
+
default: true,
|
|
245
|
+
},
|
|
246
|
+
]);
|
|
247
|
+
if (!overwrite) {
|
|
248
|
+
console.log(chalk.gray('\n Installation cancelled.\n'));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
console.log();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let anyFailed = false;
|
|
255
|
+
for (const [base, label] of bases) {
|
|
256
|
+
const results = installToBase(base, label);
|
|
257
|
+
if (results.some((r) => !r.success)) anyFailed = true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Hooks and settings are always global (once, not per-base)
|
|
261
|
+
installHooks();
|
|
262
|
+
const { statusLineConfigured } = registerHooksInSettings();
|
|
263
|
+
|
|
264
|
+
if (!anyFailed) {
|
|
265
|
+
showInstallComplete(statusLineConfigured);
|
|
266
|
+
} else {
|
|
267
|
+
console.log(chalk.yellow(' \u26a0 Some files failed to install. Check errors above.\n'));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function runUninstall({ interactive = true } = {}) {
|
|
272
|
+
const bases = [
|
|
273
|
+
[GLOBAL_BASE, 'Global (~/.claude/)'],
|
|
274
|
+
[LOCAL_BASE, 'Local (./.claude/)'],
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
// Collect files to remove
|
|
278
|
+
const toRemove = [];
|
|
279
|
+
for (const [base, label] of bases) {
|
|
280
|
+
for (const entry of buildManifest(base)) {
|
|
281
|
+
if (fileExists(entry.dest)) {
|
|
282
|
+
toRemove.push({ label, dest: entry.dest, display: entry.display });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (toRemove.length === 0) {
|
|
288
|
+
console.log(chalk.gray('\n No Hydra files found. Nothing to remove.\n'));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.log('\n The following Hydra files will be removed:\n');
|
|
293
|
+
for (const item of toRemove) {
|
|
294
|
+
console.log(chalk.gray(` [${item.label}] ${item.display}`));
|
|
295
|
+
}
|
|
296
|
+
console.log();
|
|
297
|
+
|
|
298
|
+
if (interactive) {
|
|
299
|
+
const inquirer = require('inquirer');
|
|
300
|
+
const { confirm } = await inquirer.prompt([
|
|
301
|
+
{
|
|
302
|
+
type: 'confirm',
|
|
303
|
+
name: 'confirm',
|
|
304
|
+
message: `Remove ${toRemove.length} Hydra file(s)?`,
|
|
305
|
+
default: false,
|
|
306
|
+
},
|
|
307
|
+
]);
|
|
308
|
+
if (!confirm) {
|
|
309
|
+
console.log(chalk.gray('\n Uninstall cancelled.\n'));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
console.log();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let removed = 0;
|
|
316
|
+
let failed = 0;
|
|
317
|
+
for (const item of toRemove) {
|
|
318
|
+
try {
|
|
319
|
+
fs.unlinkSync(item.dest);
|
|
320
|
+
console.log(chalk.green(` ✔ Removed ${item.display}`));
|
|
321
|
+
removed++;
|
|
322
|
+
} catch (err) {
|
|
323
|
+
console.log(chalk.red(` ✖ Failed to remove ${item.display}: ${err.message}`));
|
|
324
|
+
failed++;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Remove hook .js files from ~/.claude/hooks/
|
|
329
|
+
for (const key of Object.keys(hooks)) {
|
|
330
|
+
const dest = path.join(GLOBAL_BASE, 'hooks', `${key}.js`);
|
|
331
|
+
if (fileExists(dest)) {
|
|
332
|
+
try { fs.unlinkSync(dest); console.log(chalk.green(` \u2714 Removed hooks/${key}.js`)); }
|
|
333
|
+
catch (err) { console.log(chalk.red(` \u2716 Failed: hooks/${key}.js \u2014 ${err.message}`)); }
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Remove binary hook files (e.g., .wav)
|
|
338
|
+
for (const filename of Object.keys(binaryHooks)) {
|
|
339
|
+
const dest = path.join(GLOBAL_BASE, 'hooks', filename);
|
|
340
|
+
if (fileExists(dest)) {
|
|
341
|
+
try { fs.unlinkSync(dest); console.log(chalk.green(` \u2714 Removed hooks/${filename}`)); }
|
|
342
|
+
catch (err) { console.log(chalk.red(` \u2716 Failed: hooks/${filename} \u2014 ${err.message}`)); }
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Remove cache file
|
|
347
|
+
const cacheFile = path.join(GLOBAL_BASE, 'cache', 'hydra-update-check.json');
|
|
348
|
+
if (fileExists(cacheFile)) {
|
|
349
|
+
try { fs.unlinkSync(cacheFile); } catch {}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
deregisterHooks();
|
|
353
|
+
|
|
354
|
+
console.log();
|
|
355
|
+
if (failed === 0) {
|
|
356
|
+
console.log(chalk.cyan.bold(' \uD83D\uDC09 All heads severed. Hydra sleeps.\n'));
|
|
357
|
+
} else {
|
|
358
|
+
console.log(chalk.yellow(` \u26a0 ${removed} removed, ${failed} failed.\n`));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function getStatus(base) {
|
|
363
|
+
const manifest = buildManifest(base);
|
|
364
|
+
|
|
365
|
+
const agentEntries = manifest.filter((e) => e.type === 'agent').map((e) => ({
|
|
366
|
+
key: e.key,
|
|
367
|
+
display: e.display,
|
|
368
|
+
model: e.model,
|
|
369
|
+
installed: fileExists(e.dest),
|
|
370
|
+
}));
|
|
371
|
+
|
|
372
|
+
const skillEntry = manifest.find((e) => e.type === 'skill');
|
|
373
|
+
|
|
374
|
+
const refEntries = manifest.filter((e) => e.type === 'reference').map((e) => ({
|
|
375
|
+
name: e.display,
|
|
376
|
+
installed: fileExists(e.dest),
|
|
377
|
+
}));
|
|
378
|
+
|
|
379
|
+
const commandEntries = manifest.filter((e) => e.type === 'command').map((e) => ({
|
|
380
|
+
name: e.display,
|
|
381
|
+
installed: fileExists(e.dest),
|
|
382
|
+
}));
|
|
383
|
+
|
|
384
|
+
const versionEntry = manifest.find((e) => e.type === 'version');
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
agents: agentEntries,
|
|
388
|
+
skill: skillEntry ? fileExists(skillEntry.dest) : false,
|
|
389
|
+
references: refEntries,
|
|
390
|
+
commands: commandEntries,
|
|
391
|
+
version: versionEntry ? (fileExists(versionEntry.dest) ? fs.readFileSync(versionEntry.dest, 'utf8').trim() : null) : null,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function showStatus() {
|
|
396
|
+
const globalStatus = getStatus(GLOBAL_BASE);
|
|
397
|
+
const localStatus = getStatus(LOCAL_BASE);
|
|
398
|
+
showStatusTable(globalStatus, localStatus);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
module.exports = { runInstall, runUninstall, showStatus };
|