harness-everything 0.2.0 → 0.2.2
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 +7 -0
- package/README.md +1 -1
- package/docs/architecture.md +2 -2
- package/hooks/scripts/lib/harness-state.js +29 -3
- package/hooks/scripts/lib/platforms/claude.js +100 -10
- package/hooks/scripts/lib/platforms/codex.js +50 -0
- package/hooks/scripts/lib/platforms/continue.js +32 -0
- package/hooks/scripts/lib/platforms/copilot.js +50 -0
- package/hooks/scripts/lib/platforms/cursor.js +30 -0
- package/hooks/scripts/lib/platforms/hermes.js +25 -0
- package/package.json +1 -1
- package/scripts/installer.js +506 -649
- package/scripts/lib/gitignore.js +80 -58
- package/scripts/lib/skills.js +2 -0
package/scripts/installer.js
CHANGED
|
@@ -1,649 +1,506 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Lightweight, non-intrusive installer for Harness OS.
|
|
4
|
-
* Detects the local project layout (Claude Code, Cursor, etc.)
|
|
5
|
-
* and merges the Harness hook definitions safely without destroying user configs.
|
|
6
|
-
*
|
|
7
|
-
* Placement rule: a platform's own native file/folder (`.claude/settings.json`,
|
|
8
|
-
* `.cursorrules`, `.cursor/skills/`, etc.) is never moved - it lives exactly
|
|
9
|
-
* where that platform expects it. Anything Harness itself needs that has no
|
|
10
|
-
* native home (install manifest, runtime hook state, and - only for Claude,
|
|
11
|
-
* which has no native project-skill directory - the installed skill copies)
|
|
12
|
-
* converges into one exclusively-owned subfolder per platform:
|
|
13
|
-
* `<platform-dir>/harness-everything/`. Nothing else ever creates a directory
|
|
14
|
-
* literally named "harness-everything", so it can be added or removed as a
|
|
15
|
-
* unit without touching anything else in that platform's directory.
|
|
16
|
-
*
|
|
17
|
-
* This file only orchestrates the install/uninstall flow (CLI parsing,
|
|
18
|
-
* interactive prompts, sequencing). Each concern it delegates to lives in
|
|
19
|
-
* scripts/lib/: skill discovery & manifest-tracked install/removal
|
|
20
|
-
* (lib/skills.js, lib/manifest.js), Claude Code hook merge/removal
|
|
21
|
-
* (lib/claude-hooks.js), advisory-file injection for hookless platforms
|
|
22
|
-
* (lib/advisory-text.js), .gitignore/empty-dir housekeeping (lib/gitignore.js),
|
|
23
|
-
* and the terminal picker widgets (lib/prompts.js).
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
const fs = require('fs');
|
|
27
|
-
const path = require('path');
|
|
28
|
-
|
|
29
|
-
const { getWorkspaceRoot, getUserPromptsDir } = require('./lib/workspace');
|
|
30
|
-
const { interactiveSelect, interactiveSingleSelect, askQuestion } = require('./lib/prompts');
|
|
31
|
-
const advisory = require('./lib/advisory-text');
|
|
32
|
-
const claudeHooks = require('./lib/claude-hooks');
|
|
33
|
-
const manifest = require('./lib/manifest');
|
|
34
|
-
const skills = require('./lib/skills');
|
|
35
|
-
const { ensureWorkspaceGitignorePatterns, cleanEmptyDirs } = require('./lib/gitignore');
|
|
36
|
-
|
|
37
|
-
const userHome = require('os').homedir();
|
|
38
|
-
const workspaceRoot = getWorkspaceRoot();
|
|
39
|
-
const harnessSourceDir = path.resolve(__dirname, '..');
|
|
40
|
-
const packageVersion = require(path.join(harnessSourceDir, 'package.json')).version;
|
|
41
|
-
|
|
42
|
-
let platformModules = [];
|
|
43
|
-
try {
|
|
44
|
-
platformModules = require(path.join(__dirname, '../hooks/scripts/lib/platforms'));
|
|
45
|
-
} catch (e) {
|
|
46
|
-
platformModules = []; // Best-effort - gitignore hints and state cleanup degrade gracefully without it.
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function main() {
|
|
50
|
-
const args = process.argv;
|
|
51
|
-
const command = args[2] || 'install'; // 'install', 'add', 'skills', etc.
|
|
52
|
-
|
|
53
|
-
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
54
|
-
const hasClaudeFlag = args.includes('--claude');
|
|
55
|
-
const hasCursorFlag = args.includes('--cursor');
|
|
56
|
-
const hasCopilotFlag = args.includes('--copilot');
|
|
57
|
-
const hasCodexFlag = args.includes('--codex');
|
|
58
|
-
const hasContinueFlag = args.includes('--continue');
|
|
59
|
-
const hasHermesFlag = args.includes('--hermes');
|
|
60
|
-
const hasAllFlag = args.includes('--all');
|
|
61
|
-
const hasGlobalFlag = args.includes('--global') || args.includes('-g');
|
|
62
|
-
const hasYesFlag = args.includes('-y') || args.includes('--yes');
|
|
63
|
-
const hasAnyPlatformFlag = hasClaudeFlag || hasCursorFlag || hasCopilotFlag || hasCodexFlag || hasContinueFlag || hasHermesFlag || hasAllFlag;
|
|
64
|
-
const requestedSkills = ['add', 'skill', 'skills'].includes(command)
|
|
65
|
-
? args.slice(3).filter(arg => !arg.startsWith('-'))
|
|
66
|
-
: [];
|
|
67
|
-
|
|
68
|
-
const claudeExists = fs.existsSync(path.join(workspaceRoot, '.claude'));
|
|
69
|
-
const cursorExists = fs.existsSync(path.join(workspaceRoot, '.cursorrules'));
|
|
70
|
-
const copilotExists = fs.existsSync(path.join(workspaceRoot, '.github', 'copilot-instructions.md')) || fs.existsSync(path.join(workspaceRoot, '.github'));
|
|
71
|
-
const codexExists = fs.existsSync(path.join(workspaceRoot, 'AGENTS.md'));
|
|
72
|
-
const continueExists = fs.existsSync(path.join(workspaceRoot, '.continue'));
|
|
73
|
-
const hermesExists = fs.existsSync(path.join(workspaceRoot, '.hermes.md'));
|
|
74
|
-
const detectedAny = claudeExists || cursorExists || copilotExists || codexExists || continueExists || hermesExists;
|
|
75
|
-
|
|
76
|
-
if (command === 'uninstall') {
|
|
77
|
-
await runUninstall({ hasYesFlag, args, isInteractive });
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
let targets = {
|
|
82
|
-
claude: false,
|
|
83
|
-
cursor: false,
|
|
84
|
-
copilot: false,
|
|
85
|
-
codex: false,
|
|
86
|
-
continue: false,
|
|
87
|
-
hermes: false
|
|
88
|
-
};
|
|
89
|
-
let chosenSkills = [];
|
|
90
|
-
let isGlobal = hasGlobalFlag;
|
|
91
|
-
|
|
92
|
-
const runInteractively = isInteractive && !hasYesFlag && !hasAnyPlatformFlag && (['install', 'add', 'skill', 'skills'].includes(command) && requestedSkills.length === 0);
|
|
93
|
-
|
|
94
|
-
if (runInteractively) {
|
|
95
|
-
console.log("=================================================");
|
|
96
|
-
console.log(` Harness OS - Interactive Setup `);
|
|
97
|
-
console.log("=================================================");
|
|
98
|
-
|
|
99
|
-
// Step 1: Choose platforms
|
|
100
|
-
console.log("\n[Step 1/3] Select target platforms to configure:");
|
|
101
|
-
const platformItems = [
|
|
102
|
-
{ id: 'claude', name: `Claude Code (hook-enforced) ${claudeExists ? '(Detected)' : ''}`, checked: claudeExists || !detectedAny },
|
|
103
|
-
{ id: 'cursor', name: `Cursor (.cursorrules) ${cursorExists ? '(Detected)' : ''}`, checked: cursorExists },
|
|
104
|
-
{ id: 'copilot', name: `Copilot (.github/copilot-...) ${copilotExists ? '(Detected)' : ''}`, checked: copilotExists },
|
|
105
|
-
{ id: 'codex', name: `Codex (AGENTS.md) ${codexExists ? '(Detected)' : ''}`, checked: codexExists },
|
|
106
|
-
{ id: 'continue', name: `Continue.dev (.continue/rules) ${continueExists ? '(Detected)' : ''}`, checked: continueExists },
|
|
107
|
-
{ id: 'hermes', name: `Hermes Agent (.hermes.md) ${hermesExists ? '(Detected)' : ''}`, checked: hermesExists }
|
|
108
|
-
];
|
|
109
|
-
const selectedPlatforms = await interactiveSelect(platformItems);
|
|
110
|
-
selectedPlatforms.forEach(p => {
|
|
111
|
-
targets[p.id] = p.checked;
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// Step 2: Choose skills
|
|
115
|
-
const availableSkills = skills.getAvailableSkills(harnessSourceDir);
|
|
116
|
-
console.log("\n[Step 2/3] Select skills to install:");
|
|
117
|
-
const skillItems = availableSkills.map(s => {
|
|
118
|
-
const info = skills.getSkillInfo(harnessSourceDir, s);
|
|
119
|
-
// Pre-check essential skills
|
|
120
|
-
const isEssential = ['tdd', 'git-commit', 'todo-driven-workflow', 'verification-loop'].includes(s);
|
|
121
|
-
const isHarnessEverything = s === 'harness-everything';
|
|
122
|
-
return {
|
|
123
|
-
id: s,
|
|
124
|
-
name: isHarnessEverything ? `${s} (Required)` : s,
|
|
125
|
-
description: info ? info.description : '',
|
|
126
|
-
checked: isEssential || isHarnessEverything
|
|
127
|
-
};
|
|
128
|
-
});
|
|
129
|
-
const selectedSkills = await interactiveSelect(skillItems, 8);
|
|
130
|
-
chosenSkills = selectedSkills.filter(s => s.checked).map(s => s.id);
|
|
131
|
-
|
|
132
|
-
// Step 3: Choose global vs local location
|
|
133
|
-
console.log("\n[Step 3/3] Select installation scope / location:");
|
|
134
|
-
const locationItems = [
|
|
135
|
-
{ id: 'local', name: "Local Workspace (Install to current project directories)", checked: true },
|
|
136
|
-
{ id: 'global', name: "Global (~/.agents - applies across all projects)", checked: false }
|
|
137
|
-
];
|
|
138
|
-
const chosenLocationId = await interactiveSingleSelect(locationItems);
|
|
139
|
-
isGlobal = chosenLocationId === 'global';
|
|
140
|
-
} else {
|
|
141
|
-
// Non-interactive/bypass mode
|
|
142
|
-
isGlobal = hasGlobalFlag;
|
|
143
|
-
|
|
144
|
-
if (['add', 'skill', 'skills'].includes(command)) {
|
|
145
|
-
if (requestedSkills.length > 0) {
|
|
146
|
-
const availableSkills = skills.getAvailableSkills(harnessSourceDir);
|
|
147
|
-
for (const skillName of requestedSkills) {
|
|
148
|
-
if (availableSkills.includes(skillName)) {
|
|
149
|
-
chosenSkills.push(skillName);
|
|
150
|
-
} else {
|
|
151
|
-
console.error(`⚠️ Skill "${skillName}" not found. Skipping.`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
} else {
|
|
155
|
-
chosenSkills = ['tdd', 'git-commit', 'todo-driven-workflow', 'verification-loop', 'zoom-out', 'install-cognitive-os'];
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (!isGlobal) {
|
|
159
|
-
targets.claude = claudeExists;
|
|
160
|
-
targets.cursor = cursorExists;
|
|
161
|
-
targets.copilot = copilotExists;
|
|
162
|
-
targets.codex = codexExists;
|
|
163
|
-
targets.continue = continueExists;
|
|
164
|
-
targets.hermes = hermesExists;
|
|
165
|
-
if (!detectedAny) {
|
|
166
|
-
targets.claude = true;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
} else {
|
|
170
|
-
if (hasAnyPlatformFlag) {
|
|
171
|
-
if (hasAllFlag) {
|
|
172
|
-
targets.claude = true;
|
|
173
|
-
targets.cursor = true;
|
|
174
|
-
targets.copilot = true;
|
|
175
|
-
targets.codex = true;
|
|
176
|
-
targets.continue = true;
|
|
177
|
-
targets.hermes = true;
|
|
178
|
-
} else {
|
|
179
|
-
targets.claude = hasClaudeFlag;
|
|
180
|
-
targets.cursor = hasCursorFlag;
|
|
181
|
-
targets.copilot = hasCopilotFlag;
|
|
182
|
-
targets.codex = hasCodexFlag;
|
|
183
|
-
targets.continue = hasContinueFlag;
|
|
184
|
-
targets.hermes = hasHermesFlag;
|
|
185
|
-
}
|
|
186
|
-
} else {
|
|
187
|
-
if (detectedAny) {
|
|
188
|
-
targets.claude = claudeExists;
|
|
189
|
-
targets.cursor = cursorExists;
|
|
190
|
-
targets.copilot = copilotExists;
|
|
191
|
-
targets.codex = codexExists;
|
|
192
|
-
targets.continue = continueExists;
|
|
193
|
-
targets.hermes = hermesExists;
|
|
194
|
-
} else {
|
|
195
|
-
targets.claude = true;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
chosenSkills = ['tdd', 'git-commit', 'todo-driven-workflow', 'verification-loop', 'zoom-out', 'install-cognitive-os'];
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Ensure harness-everything is ALWAYS included in chosenSkills
|
|
203
|
-
if (!chosenSkills.includes('harness-everything')) {
|
|
204
|
-
chosenSkills.push('harness-everything');
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// If no targets selected at all, exit
|
|
208
|
-
if (!targets.claude && !targets.cursor && !targets.copilot && !targets.codex && !targets.continue && !targets.hermes && !isGlobal) {
|
|
209
|
-
console.log("\n❌ No platforms selected. Exiting.");
|
|
210
|
-
process.exit(0);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const targetWorkspaceRoot = isGlobal ? path.join(userHome, '.agents') : workspaceRoot;
|
|
214
|
-
|
|
215
|
-
console.log("\n-------------------------------------------------");
|
|
216
|
-
console.log(`Installing Harness OS to ${isGlobal ? 'Global Home' : 'Local Workspace'}`);
|
|
217
|
-
console.log("-------------------------------------------------");
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
if (hasYesFlag) {
|
|
508
|
-
// -y auto-confirms local (cwd-scoped, obviously "this project") but
|
|
509
|
-
// must NEVER auto-confirm global (~/.agents, ~/.claude, etc.) just
|
|
510
|
-
// because something global happens to exist - home-directory state
|
|
511
|
-
// isn't implied by "yes" to the workspace you're standing in, and a
|
|
512
|
-
// bare `-y` run from any random project must not be able to wipe it.
|
|
513
|
-
removeLocal = localInstalled;
|
|
514
|
-
removeGlobal = globalInstalled && hasGlobalUninstallFlag;
|
|
515
|
-
removeSkills = installedSkills.length > 0;
|
|
516
|
-
} else {
|
|
517
|
-
removeLocal = hasLocalFlag;
|
|
518
|
-
removeGlobal = hasGlobalUninstallFlag;
|
|
519
|
-
removeSkills = hasSkillsUninstallFlag;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
if (removeLocal) {
|
|
524
|
-
console.log("\n-------------------------------------------------");
|
|
525
|
-
console.log("Uninstalling Harness OS from Local Workspace...");
|
|
526
|
-
console.log("-------------------------------------------------");
|
|
527
|
-
|
|
528
|
-
const localSettingsFile = path.join(workspaceRoot, '.claude', 'settings.json');
|
|
529
|
-
claudeHooks.removeHarnessHooks(localSettingsFile);
|
|
530
|
-
|
|
531
|
-
// Clear each platform's own runtime hook-state. Manifest + any remaining
|
|
532
|
-
// skills are a separate, opt-in concern handled by the skills-removal
|
|
533
|
-
// step below, so the harness-everything/ dir itself isn't swept here yet
|
|
534
|
-
// - manifest.json (deleted only once its skills list empties out) may
|
|
535
|
-
// still be sitting in it at this point. The final sweep near the end of
|
|
536
|
-
// this function catches it once skill removal has actually run.
|
|
537
|
-
for (const platform of platformModules) {
|
|
538
|
-
if (typeof platform.getStateDir !== 'function') continue;
|
|
539
|
-
const stateDir = platform.getStateDir(workspaceRoot);
|
|
540
|
-
if (fs.existsSync(stateDir)) {
|
|
541
|
-
fs.rmSync(stateDir, { recursive: true, force: true });
|
|
542
|
-
console.log(` ✅ Removed local ${path.relative(workspaceRoot, stateDir).replace(/\\/g, '/')}/ directory`);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
advisory.removeAdvisoryText(path.join(workspaceRoot, '.cursorrules'));
|
|
547
|
-
advisory.removeAdvisoryText(path.join(workspaceRoot, '.github', 'copilot-instructions.md'));
|
|
548
|
-
advisory.removeAdvisoryText(path.join(workspaceRoot, 'AGENTS.md'));
|
|
549
|
-
advisory.removeAdvisoryText(path.join(workspaceRoot, '.hermes.md'));
|
|
550
|
-
|
|
551
|
-
advisory.removeContinueRule(path.join(workspaceRoot, '.continue', 'rules', 'harness.md'));
|
|
552
|
-
cleanEmptyDirs(path.join(workspaceRoot, '.continue', 'rules'), [workspaceRoot, userHome]);
|
|
553
|
-
|
|
554
|
-
// One-time migration cleanup: earlier versions used a self-invented
|
|
555
|
-
// top-level .harness/ directory for Claude's manifest + skill copies.
|
|
556
|
-
// It was never shared with anything else in the workspace, so - unlike
|
|
557
|
-
// every other removal in this file - it's always safe to wipe wholesale.
|
|
558
|
-
const legacyHarnessDir = path.join(workspaceRoot, '.harness');
|
|
559
|
-
if (fs.existsSync(legacyHarnessDir)) {
|
|
560
|
-
fs.rmSync(legacyHarnessDir, { recursive: true, force: true });
|
|
561
|
-
console.log(` ✅ Removed legacy .harness/ directory (superseded by .claude/harness-everything/)`);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (removeGlobal) {
|
|
566
|
-
console.log("\n-------------------------------------------------");
|
|
567
|
-
console.log("Uninstalling Harness OS from Global Home...");
|
|
568
|
-
console.log("-------------------------------------------------");
|
|
569
|
-
|
|
570
|
-
const globalSettingsFile = path.join(userHome, '.claude', 'settings.json');
|
|
571
|
-
claudeHooks.removeHarnessHooks(globalSettingsFile);
|
|
572
|
-
const globalClaudeDir = path.join(userHome, '.claude');
|
|
573
|
-
cleanEmptyDirs(globalClaudeDir, [userHome]);
|
|
574
|
-
|
|
575
|
-
advisory.removeAdvisoryText(path.join(userHome, '.cursorrules'));
|
|
576
|
-
|
|
577
|
-
advisory.removeContinueRule(path.join(userHome, '.continue', 'rules', 'harness.md'));
|
|
578
|
-
cleanEmptyDirs(path.join(userHome, '.continue', 'rules'), [userHome]);
|
|
579
|
-
|
|
580
|
-
const promptsDir = getUserPromptsDir();
|
|
581
|
-
const vscodeInstFile = path.join(promptsDir, 'harness.instructions.md');
|
|
582
|
-
if (fs.existsSync(vscodeInstFile)) {
|
|
583
|
-
fs.unlinkSync(vscodeInstFile);
|
|
584
|
-
console.log(` ✅ Removed global Copilot instructions: ${vscodeInstFile}`);
|
|
585
|
-
}
|
|
586
|
-
const vscodeAgentFile = path.join(promptsDir, 'harness.agent.md');
|
|
587
|
-
if (fs.existsSync(vscodeAgentFile)) {
|
|
588
|
-
fs.unlinkSync(vscodeAgentFile);
|
|
589
|
-
console.log(` ✅ Removed global Codex agent: ${vscodeAgentFile}`);
|
|
590
|
-
}
|
|
591
|
-
cleanEmptyDirs(promptsDir, [userHome]);
|
|
592
|
-
|
|
593
|
-
// ~/.agents is a shared directory - other tools or the user's own files
|
|
594
|
-
// may live there. Never touch it directly; only clean up the
|
|
595
|
-
// harness-everything/ subfolder this package exclusively owns, and only
|
|
596
|
-
// once it's actually empty (manifest.json is removed automatically by
|
|
597
|
-
// the skills-removal step below once no skills remain in it).
|
|
598
|
-
cleanEmptyDirs(path.join(globalHarnessDir, 'skills'), [userHome]);
|
|
599
|
-
cleanEmptyDirs(globalHarnessDir, [userHome]);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
if (removeSkills && installedSkills.length > 0) {
|
|
603
|
-
// This block is only reached via a bulk "remove everything detected"
|
|
604
|
-
// path (interactive "all", or the non-interactive -y/--skills bypass) -
|
|
605
|
-
// never via the interactive per-item picker, which already removes
|
|
606
|
-
// exactly what the user checked. A bulk sweep must not reach into the
|
|
607
|
-
// real global home directory unless the user separately opted into
|
|
608
|
-
// global scope (same rule as the config removal above). Every entry in
|
|
609
|
-
// `installedSkills` is already manifest-tracked (or, for legacy
|
|
610
|
-
// installs, author-marker-verified) - see lib/skills.js - so this never
|
|
611
|
-
// touches a skill Harness didn't install.
|
|
612
|
-
const skillsToRemove = installedSkills.filter(s => s.scope !== 'global' || removeGlobal);
|
|
613
|
-
const skippedGlobalCount = installedSkills.length - skillsToRemove.length;
|
|
614
|
-
|
|
615
|
-
console.log("\n-------------------------------------------------");
|
|
616
|
-
console.log("Uninstalling all detected skills...");
|
|
617
|
-
console.log("-------------------------------------------------");
|
|
618
|
-
for (const skill of skillsToRemove) {
|
|
619
|
-
if (fs.existsSync(skill.dirPath)) {
|
|
620
|
-
skills.removeSkill(skill);
|
|
621
|
-
console.log(` ✅ Removed skill: ${skill.id} (${skill.scope})`);
|
|
622
|
-
cleanEmptyDirs(skill.parentPath, [workspaceRoot, userHome]);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
if (skippedGlobalCount > 0) {
|
|
626
|
-
console.log(` ℹ️ Skipped ${skippedGlobalCount} global-scoped skill(s) - pass --global (or select "Uninstall ... Global Home") to remove those too.`);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Final sweep: a platform's harness-everything/ dir (manifest + Claude's
|
|
631
|
-
// skill copies) may have only just become empty above - either through the
|
|
632
|
-
// bulk removeSkills block or the interactive per-item picker, both of
|
|
633
|
-
// which clean up the skill's own parent dir but not necessarily this one.
|
|
634
|
-
// Always safe to run unconditionally: cleanEmptyDirs only ever removes a
|
|
635
|
-
// directory that's genuinely empty.
|
|
636
|
-
for (const platform of platformModules) {
|
|
637
|
-
if (typeof platform.getHarnessDir !== 'function') continue;
|
|
638
|
-
cleanEmptyDirs(platform.getHarnessDir(workspaceRoot), [workspaceRoot, userHome]);
|
|
639
|
-
}
|
|
640
|
-
cleanEmptyDirs(path.join(workspaceRoot, '.claude'), [workspaceRoot, userHome]);
|
|
641
|
-
|
|
642
|
-
console.log("\n🎉 UNINSTALL COMPLETE! Harness OS has been removed.");
|
|
643
|
-
console.log("=================================================");
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
main().catch(err => {
|
|
647
|
-
console.error(`\n❌ Install failed: ${err.message}`);
|
|
648
|
-
process.exit(1);
|
|
649
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Lightweight, non-intrusive installer for Harness OS.
|
|
4
|
+
* Detects the local project layout (Claude Code, Cursor, etc.)
|
|
5
|
+
* and merges the Harness hook definitions safely without destroying user configs.
|
|
6
|
+
*
|
|
7
|
+
* Placement rule: a platform's own native file/folder (`.claude/settings.json`,
|
|
8
|
+
* `.cursorrules`, `.cursor/skills/`, etc.) is never moved - it lives exactly
|
|
9
|
+
* where that platform expects it. Anything Harness itself needs that has no
|
|
10
|
+
* native home (install manifest, runtime hook state, and - only for Claude,
|
|
11
|
+
* which has no native project-skill directory - the installed skill copies)
|
|
12
|
+
* converges into one exclusively-owned subfolder per platform:
|
|
13
|
+
* `<platform-dir>/harness-everything/`. Nothing else ever creates a directory
|
|
14
|
+
* literally named "harness-everything", so it can be added or removed as a
|
|
15
|
+
* unit without touching anything else in that platform's directory.
|
|
16
|
+
*
|
|
17
|
+
* This file only orchestrates the install/uninstall flow (CLI parsing,
|
|
18
|
+
* interactive prompts, sequencing). Each concern it delegates to lives in
|
|
19
|
+
* scripts/lib/: skill discovery & manifest-tracked install/removal
|
|
20
|
+
* (lib/skills.js, lib/manifest.js), Claude Code hook merge/removal
|
|
21
|
+
* (lib/claude-hooks.js), advisory-file injection for hookless platforms
|
|
22
|
+
* (lib/advisory-text.js), .gitignore/empty-dir housekeeping (lib/gitignore.js),
|
|
23
|
+
* and the terminal picker widgets (lib/prompts.js).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
|
|
29
|
+
const { getWorkspaceRoot, getUserPromptsDir } = require('./lib/workspace');
|
|
30
|
+
const { interactiveSelect, interactiveSingleSelect, askQuestion } = require('./lib/prompts');
|
|
31
|
+
const advisory = require('./lib/advisory-text');
|
|
32
|
+
const claudeHooks = require('./lib/claude-hooks');
|
|
33
|
+
const manifest = require('./lib/manifest');
|
|
34
|
+
const skills = require('./lib/skills');
|
|
35
|
+
const { ensureWorkspaceGitignorePatterns, cleanEmptyDirs } = require('./lib/gitignore');
|
|
36
|
+
|
|
37
|
+
const userHome = require('os').homedir();
|
|
38
|
+
const workspaceRoot = getWorkspaceRoot();
|
|
39
|
+
const harnessSourceDir = path.resolve(__dirname, '..');
|
|
40
|
+
const packageVersion = require(path.join(harnessSourceDir, 'package.json')).version;
|
|
41
|
+
|
|
42
|
+
let platformModules = [];
|
|
43
|
+
try {
|
|
44
|
+
platformModules = require(path.join(__dirname, '../hooks/scripts/lib/platforms'));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
platformModules = []; // Best-effort - gitignore hints and state cleanup degrade gracefully without it.
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function main() {
|
|
50
|
+
const args = process.argv;
|
|
51
|
+
const command = args[2] || 'install'; // 'install', 'add', 'skills', etc.
|
|
52
|
+
|
|
53
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
54
|
+
const hasClaudeFlag = args.includes('--claude');
|
|
55
|
+
const hasCursorFlag = args.includes('--cursor');
|
|
56
|
+
const hasCopilotFlag = args.includes('--copilot');
|
|
57
|
+
const hasCodexFlag = args.includes('--codex');
|
|
58
|
+
const hasContinueFlag = args.includes('--continue');
|
|
59
|
+
const hasHermesFlag = args.includes('--hermes');
|
|
60
|
+
const hasAllFlag = args.includes('--all');
|
|
61
|
+
const hasGlobalFlag = args.includes('--global') || args.includes('-g');
|
|
62
|
+
const hasYesFlag = args.includes('-y') || args.includes('--yes');
|
|
63
|
+
const hasAnyPlatformFlag = hasClaudeFlag || hasCursorFlag || hasCopilotFlag || hasCodexFlag || hasContinueFlag || hasHermesFlag || hasAllFlag;
|
|
64
|
+
const requestedSkills = ['add', 'skill', 'skills'].includes(command)
|
|
65
|
+
? args.slice(3).filter(arg => !arg.startsWith('-'))
|
|
66
|
+
: [];
|
|
67
|
+
|
|
68
|
+
const claudeExists = fs.existsSync(path.join(workspaceRoot, '.claude'));
|
|
69
|
+
const cursorExists = fs.existsSync(path.join(workspaceRoot, '.cursorrules'));
|
|
70
|
+
const copilotExists = fs.existsSync(path.join(workspaceRoot, '.github', 'copilot-instructions.md')) || fs.existsSync(path.join(workspaceRoot, '.github'));
|
|
71
|
+
const codexExists = fs.existsSync(path.join(workspaceRoot, 'AGENTS.md'));
|
|
72
|
+
const continueExists = fs.existsSync(path.join(workspaceRoot, '.continue'));
|
|
73
|
+
const hermesExists = fs.existsSync(path.join(workspaceRoot, '.hermes.md'));
|
|
74
|
+
const detectedAny = claudeExists || cursorExists || copilotExists || codexExists || continueExists || hermesExists;
|
|
75
|
+
|
|
76
|
+
if (command === 'uninstall') {
|
|
77
|
+
await runUninstall({ hasYesFlag, args, isInteractive });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let targets = {
|
|
82
|
+
claude: false,
|
|
83
|
+
cursor: false,
|
|
84
|
+
copilot: false,
|
|
85
|
+
codex: false,
|
|
86
|
+
continue: false,
|
|
87
|
+
hermes: false
|
|
88
|
+
};
|
|
89
|
+
let chosenSkills = [];
|
|
90
|
+
let isGlobal = hasGlobalFlag;
|
|
91
|
+
|
|
92
|
+
const runInteractively = isInteractive && !hasYesFlag && !hasAnyPlatformFlag && (['install', 'add', 'skill', 'skills'].includes(command) && requestedSkills.length === 0);
|
|
93
|
+
|
|
94
|
+
if (runInteractively) {
|
|
95
|
+
console.log("=================================================");
|
|
96
|
+
console.log(` Harness OS - Interactive Setup `);
|
|
97
|
+
console.log("=================================================");
|
|
98
|
+
|
|
99
|
+
// Step 1: Choose platforms
|
|
100
|
+
console.log("\n[Step 1/3] Select target platforms to configure:");
|
|
101
|
+
const platformItems = [
|
|
102
|
+
{ id: 'claude', name: `Claude Code (hook-enforced) ${claudeExists ? '(Detected)' : ''}`, checked: claudeExists || !detectedAny },
|
|
103
|
+
{ id: 'cursor', name: `Cursor (.cursorrules) ${cursorExists ? '(Detected)' : ''}`, checked: cursorExists },
|
|
104
|
+
{ id: 'copilot', name: `Copilot (.github/copilot-...) ${copilotExists ? '(Detected)' : ''}`, checked: copilotExists },
|
|
105
|
+
{ id: 'codex', name: `Codex (AGENTS.md) ${codexExists ? '(Detected)' : ''}`, checked: codexExists },
|
|
106
|
+
{ id: 'continue', name: `Continue.dev (.continue/rules) ${continueExists ? '(Detected)' : ''}`, checked: continueExists },
|
|
107
|
+
{ id: 'hermes', name: `Hermes Agent (.hermes.md) ${hermesExists ? '(Detected)' : ''}`, checked: hermesExists }
|
|
108
|
+
];
|
|
109
|
+
const selectedPlatforms = await interactiveSelect(platformItems);
|
|
110
|
+
selectedPlatforms.forEach(p => {
|
|
111
|
+
targets[p.id] = p.checked;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Step 2: Choose skills
|
|
115
|
+
const availableSkills = skills.getAvailableSkills(harnessSourceDir);
|
|
116
|
+
console.log("\n[Step 2/3] Select skills to install:");
|
|
117
|
+
const skillItems = availableSkills.map(s => {
|
|
118
|
+
const info = skills.getSkillInfo(harnessSourceDir, s);
|
|
119
|
+
// Pre-check essential skills
|
|
120
|
+
const isEssential = ['tdd', 'git-commit', 'todo-driven-workflow', 'verification-loop'].includes(s);
|
|
121
|
+
const isHarnessEverything = s === 'harness-everything';
|
|
122
|
+
return {
|
|
123
|
+
id: s,
|
|
124
|
+
name: isHarnessEverything ? `${s} (Required)` : s,
|
|
125
|
+
description: info ? info.description : '',
|
|
126
|
+
checked: isEssential || isHarnessEverything
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
const selectedSkills = await interactiveSelect(skillItems, 8);
|
|
130
|
+
chosenSkills = selectedSkills.filter(s => s.checked).map(s => s.id);
|
|
131
|
+
|
|
132
|
+
// Step 3: Choose global vs local location
|
|
133
|
+
console.log("\n[Step 3/3] Select installation scope / location:");
|
|
134
|
+
const locationItems = [
|
|
135
|
+
{ id: 'local', name: "Local Workspace (Install to current project directories)", checked: true },
|
|
136
|
+
{ id: 'global', name: "Global (~/.agents - applies across all projects)", checked: false }
|
|
137
|
+
];
|
|
138
|
+
const chosenLocationId = await interactiveSingleSelect(locationItems);
|
|
139
|
+
isGlobal = chosenLocationId === 'global';
|
|
140
|
+
} else {
|
|
141
|
+
// Non-interactive/bypass mode
|
|
142
|
+
isGlobal = hasGlobalFlag;
|
|
143
|
+
|
|
144
|
+
if (['add', 'skill', 'skills'].includes(command)) {
|
|
145
|
+
if (requestedSkills.length > 0) {
|
|
146
|
+
const availableSkills = skills.getAvailableSkills(harnessSourceDir);
|
|
147
|
+
for (const skillName of requestedSkills) {
|
|
148
|
+
if (availableSkills.includes(skillName)) {
|
|
149
|
+
chosenSkills.push(skillName);
|
|
150
|
+
} else {
|
|
151
|
+
console.error(`⚠️ Skill "${skillName}" not found. Skipping.`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
chosenSkills = ['tdd', 'git-commit', 'todo-driven-workflow', 'verification-loop', 'zoom-out', 'install-cognitive-os'];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!isGlobal) {
|
|
159
|
+
targets.claude = claudeExists;
|
|
160
|
+
targets.cursor = cursorExists;
|
|
161
|
+
targets.copilot = copilotExists;
|
|
162
|
+
targets.codex = codexExists;
|
|
163
|
+
targets.continue = continueExists;
|
|
164
|
+
targets.hermes = hermesExists;
|
|
165
|
+
if (!detectedAny) {
|
|
166
|
+
targets.claude = true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
if (hasAnyPlatformFlag) {
|
|
171
|
+
if (hasAllFlag) {
|
|
172
|
+
targets.claude = true;
|
|
173
|
+
targets.cursor = true;
|
|
174
|
+
targets.copilot = true;
|
|
175
|
+
targets.codex = true;
|
|
176
|
+
targets.continue = true;
|
|
177
|
+
targets.hermes = true;
|
|
178
|
+
} else {
|
|
179
|
+
targets.claude = hasClaudeFlag;
|
|
180
|
+
targets.cursor = hasCursorFlag;
|
|
181
|
+
targets.copilot = hasCopilotFlag;
|
|
182
|
+
targets.codex = hasCodexFlag;
|
|
183
|
+
targets.continue = hasContinueFlag;
|
|
184
|
+
targets.hermes = hasHermesFlag;
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
if (detectedAny) {
|
|
188
|
+
targets.claude = claudeExists;
|
|
189
|
+
targets.cursor = cursorExists;
|
|
190
|
+
targets.copilot = copilotExists;
|
|
191
|
+
targets.codex = codexExists;
|
|
192
|
+
targets.continue = continueExists;
|
|
193
|
+
targets.hermes = hermesExists;
|
|
194
|
+
} else {
|
|
195
|
+
targets.claude = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
chosenSkills = ['tdd', 'git-commit', 'todo-driven-workflow', 'verification-loop', 'zoom-out', 'install-cognitive-os'];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Ensure harness-everything is ALWAYS included in chosenSkills
|
|
203
|
+
if (!chosenSkills.includes('harness-everything')) {
|
|
204
|
+
chosenSkills.push('harness-everything');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// If no targets selected at all, exit
|
|
208
|
+
if (!targets.claude && !targets.cursor && !targets.copilot && !targets.codex && !targets.continue && !targets.hermes && !isGlobal) {
|
|
209
|
+
console.log("\n❌ No platforms selected. Exiting.");
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const targetWorkspaceRoot = isGlobal ? path.join(userHome, '.agents') : workspaceRoot;
|
|
214
|
+
|
|
215
|
+
console.log("\n-------------------------------------------------");
|
|
216
|
+
console.log(`Installing Harness OS to ${isGlobal ? 'Global Home' : 'Local Workspace'}`);
|
|
217
|
+
console.log("-------------------------------------------------");
|
|
218
|
+
|
|
219
|
+
for (const platform of platformModules) {
|
|
220
|
+
if (targets[platform.name] && typeof platform.install === 'function') {
|
|
221
|
+
platform.install({
|
|
222
|
+
isGlobal,
|
|
223
|
+
targetWorkspaceRoot,
|
|
224
|
+
harnessSourceDir,
|
|
225
|
+
packageVersion,
|
|
226
|
+
getUserPromptsDir,
|
|
227
|
+
advisory,
|
|
228
|
+
claudeHooks,
|
|
229
|
+
manifest,
|
|
230
|
+
userHome,
|
|
231
|
+
workspaceRoot
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Install chosen skills. Every platform's skills stay at that platform's
|
|
237
|
+
// own native location (unchanged) EXCEPT Claude, which has its copy under
|
|
238
|
+
// .claude/skills/. Each platform's manifest.json tracks its own.
|
|
239
|
+
if (chosenSkills.length > 0) {
|
|
240
|
+
const targetDirs = [];
|
|
241
|
+
const addedPaths = new Set();
|
|
242
|
+
for (const platform of platformModules) {
|
|
243
|
+
// If it's a global install, gather all configured/selected platforms or defaults
|
|
244
|
+
if (targets[platform.name] || (isGlobal && (targets.claude || targets.cursor || targets.copilot || targets.codex || targets.continue || targets.hermes))) {
|
|
245
|
+
if (typeof platform.getSkillsTarget === 'function') {
|
|
246
|
+
const tgt = platform.getSkillsTarget({ workspaceRoot, userHome, isGlobal, manifest });
|
|
247
|
+
if (tgt && tgt.path && !addedPaths.has(tgt.path)) {
|
|
248
|
+
targetDirs.push(tgt);
|
|
249
|
+
addedPaths.add(tgt.path);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
console.log(`\nInstalling Harness skills:`);
|
|
256
|
+
skills.installSkillsToTargets({ chosenSkills, targetDirs, harnessSourceDir, packageVersion });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!isGlobal) {
|
|
260
|
+
const patternsToIgnore = [];
|
|
261
|
+
for (const platform of platformModules) {
|
|
262
|
+
if (targets[platform.name] && typeof platform.getIgnorePatterns === 'function') {
|
|
263
|
+
for (const pattern of platform.getIgnorePatterns(workspaceRoot)) {
|
|
264
|
+
if (!patternsToIgnore.includes(pattern)) {
|
|
265
|
+
patternsToIgnore.push(pattern);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (patternsToIgnore.length === 0) {
|
|
271
|
+
// Fallback if platform modules failed to load entirely.
|
|
272
|
+
if (targets.claude) patternsToIgnore.push('.claude/harness-everything/');
|
|
273
|
+
if (targets.cursor) patternsToIgnore.push('.cursor/harness-everything/', '.cursor/skills/');
|
|
274
|
+
if (targets.copilot) patternsToIgnore.push('.github/harness-everything/', '.github/skills/');
|
|
275
|
+
if (targets.codex) patternsToIgnore.push('.codex/harness-everything/', '.codex/skills/');
|
|
276
|
+
if (targets.continue) patternsToIgnore.push('.continue/harness-everything/', '.continue/skills/');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
ensureWorkspaceGitignorePatterns(workspaceRoot, patternsToIgnore);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
console.log("\n🎉 INSTALL SUCCESS! Harness OS is now protecting this project.");
|
|
283
|
+
console.log("Try your first AI interaction! The bootstrapped OS will auto-route your tier.");
|
|
284
|
+
console.log("=================================================");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function runUninstall({ hasYesFlag, args, isInteractive }) {
|
|
288
|
+
const localHarnessDirs = platformModules
|
|
289
|
+
.filter(p => typeof p.getHarnessDir === 'function')
|
|
290
|
+
.map(p => p.getHarnessDir(workspaceRoot));
|
|
291
|
+
|
|
292
|
+
const localInstalled = fs.existsSync(path.join(workspaceRoot, '.harness')) || // legacy pre-reorg root
|
|
293
|
+
localHarnessDirs.some(d => fs.existsSync(d)) ||
|
|
294
|
+
fs.existsSync(path.join(workspaceRoot, '.claude', 'settings.json')) ||
|
|
295
|
+
fs.existsSync(path.join(workspaceRoot, '.cursorrules')) ||
|
|
296
|
+
fs.existsSync(path.join(workspaceRoot, '.github', 'copilot-instructions.md')) ||
|
|
297
|
+
fs.existsSync(path.join(workspaceRoot, 'AGENTS.md')) ||
|
|
298
|
+
fs.existsSync(path.join(workspaceRoot, '.continue', 'rules', 'harness.md')) ||
|
|
299
|
+
fs.existsSync(path.join(workspaceRoot, '.hermes.md'));
|
|
300
|
+
|
|
301
|
+
const globalAgentsDir = path.join(userHome, '.agents');
|
|
302
|
+
const globalHarnessDir = manifest.getHarnessDir(globalAgentsDir);
|
|
303
|
+
const globalInstalled = fs.existsSync(globalHarnessDir) ||
|
|
304
|
+
fs.existsSync(path.join(userHome, '.claude', 'settings.json')) ||
|
|
305
|
+
fs.existsSync(path.join(userHome, '.cursorrules')) ||
|
|
306
|
+
fs.existsSync(path.join(userHome, '.continue', 'rules', 'harness.md')) ||
|
|
307
|
+
fs.existsSync(path.join(getUserPromptsDir(), 'harness.instructions.md')) ||
|
|
308
|
+
fs.existsSync(path.join(getUserPromptsDir(), 'harness.agent.md'));
|
|
309
|
+
|
|
310
|
+
const installedSkills = skills.getInstalledSkills(workspaceRoot, userHome);
|
|
311
|
+
|
|
312
|
+
console.log("=================================================");
|
|
313
|
+
console.log(` Harness OS - Uninstall Utility `);
|
|
314
|
+
console.log("=================================================");
|
|
315
|
+
|
|
316
|
+
let removeLocal = false;
|
|
317
|
+
let removeGlobal = false;
|
|
318
|
+
let removeSkills = false;
|
|
319
|
+
|
|
320
|
+
const runInteractively = isInteractive && !hasYesFlag;
|
|
321
|
+
|
|
322
|
+
if (runInteractively) {
|
|
323
|
+
const choices = [];
|
|
324
|
+
if (localInstalled) {
|
|
325
|
+
choices.push({ id: 'local', name: 'Uninstall Harness OS from Local Workspace (Hooks & Advisory files)', checked: true });
|
|
326
|
+
}
|
|
327
|
+
if (globalInstalled) {
|
|
328
|
+
choices.push({ id: 'global', name: 'Uninstall Harness OS from Global Home (~/.agents, VS Code Prompts, etc.)', checked: false });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (choices.length > 0) {
|
|
332
|
+
console.log("\nSelect installation scopes to uninstall:");
|
|
333
|
+
const selectedScopes = await interactiveSelect(choices);
|
|
334
|
+
removeLocal = selectedScopes.some(s => s.id === 'local' && s.checked);
|
|
335
|
+
removeGlobal = selectedScopes.some(s => s.id === 'global' && s.checked);
|
|
336
|
+
} else {
|
|
337
|
+
console.log("\nNo Harness OS installation detected in local workspace or global home.");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (installedSkills.length > 0) {
|
|
341
|
+
console.log(`\nDetected ${installedSkills.length} installed skill(s):`);
|
|
342
|
+
installedSkills.forEach(s => {
|
|
343
|
+
console.log(` - ${s.id} (${s.scope})`);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
console.log("\nWould you like to uninstall these skills?");
|
|
347
|
+
const skillChoiceItems = [
|
|
348
|
+
{ id: 'all', name: 'Remove ALL detected skills', checked: true },
|
|
349
|
+
{ id: 'select', name: 'Select specific skills to remove', checked: false },
|
|
350
|
+
{ id: 'none', name: 'Keep all skills', checked: false }
|
|
351
|
+
];
|
|
352
|
+
const skillAction = await interactiveSingleSelect(skillChoiceItems);
|
|
353
|
+
|
|
354
|
+
if (skillAction === 'all') {
|
|
355
|
+
removeSkills = true;
|
|
356
|
+
} else if (skillAction === 'select') {
|
|
357
|
+
const selectSkillItems = installedSkills.map(s => ({
|
|
358
|
+
id: s.dirPath,
|
|
359
|
+
name: `${s.id} (${s.scope})`,
|
|
360
|
+
checked: true,
|
|
361
|
+
_entry: s
|
|
362
|
+
}));
|
|
363
|
+
const chosenSkillsToRemove = await interactiveSelect(selectSkillItems);
|
|
364
|
+
for (const item of chosenSkillsToRemove) {
|
|
365
|
+
if (item.checked) {
|
|
366
|
+
skills.removeSkill(item._entry);
|
|
367
|
+
console.log(` ✅ Removed skill: ${item.name}`);
|
|
368
|
+
cleanEmptyDirs(item._entry.parentPath, [workspaceRoot, userHome]);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
const hasLocalFlag = args.includes('--local');
|
|
375
|
+
const hasGlobalUninstallFlag = args.includes('--global') || args.includes('-g');
|
|
376
|
+
const hasSkillsUninstallFlag = args.includes('--skills');
|
|
377
|
+
|
|
378
|
+
if (hasYesFlag) {
|
|
379
|
+
// -y auto-confirms local (cwd-scoped, obviously "this project") but
|
|
380
|
+
// must NEVER auto-confirm global (~/.agents, ~/.claude, etc.) just
|
|
381
|
+
// because something global happens to exist - home-directory state
|
|
382
|
+
// isn't implied by "yes" to the workspace you're standing in, and a
|
|
383
|
+
// bare `-y` run from any random project must not be able to wipe it.
|
|
384
|
+
removeLocal = localInstalled;
|
|
385
|
+
removeGlobal = globalInstalled && hasGlobalUninstallFlag;
|
|
386
|
+
removeSkills = installedSkills.length > 0;
|
|
387
|
+
} else {
|
|
388
|
+
removeLocal = hasLocalFlag;
|
|
389
|
+
removeGlobal = hasGlobalUninstallFlag;
|
|
390
|
+
removeSkills = hasSkillsUninstallFlag;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (removeLocal) {
|
|
395
|
+
console.log("\n-------------------------------------------------");
|
|
396
|
+
console.log("Uninstalling Harness OS from Local Workspace...");
|
|
397
|
+
console.log("-------------------------------------------------");
|
|
398
|
+
|
|
399
|
+
for (const platform of platformModules) {
|
|
400
|
+
if (typeof platform.uninstall === 'function') {
|
|
401
|
+
platform.uninstall({
|
|
402
|
+
removeLocal: true,
|
|
403
|
+
removeGlobal: false,
|
|
404
|
+
workspaceRoot,
|
|
405
|
+
userHome,
|
|
406
|
+
getUserPromptsDir,
|
|
407
|
+
cleanEmptyDirs,
|
|
408
|
+
claudeHooks
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (typeof platform.getStateDir === 'function') {
|
|
413
|
+
const stateDir = platform.getStateDir(workspaceRoot);
|
|
414
|
+
if (fs.existsSync(stateDir)) {
|
|
415
|
+
fs.rmSync(stateDir, { recursive: true, force: true });
|
|
416
|
+
console.log(` ✅ Removed local ${path.relative(workspaceRoot, stateDir).replace(/\\/g, '/')}/ directory`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// One-time migration cleanup: earlier versions used a self-invented
|
|
422
|
+
// top-level .harness/ directory for Claude's manifest + skill copies.
|
|
423
|
+
const legacyHarnessDir = path.join(workspaceRoot, '.harness');
|
|
424
|
+
if (fs.existsSync(legacyHarnessDir)) {
|
|
425
|
+
fs.rmSync(legacyHarnessDir, { recursive: true, force: true });
|
|
426
|
+
console.log(` ✅ Removed legacy .harness/ directory (superseded by .claude/harness-everything/)`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (removeGlobal) {
|
|
431
|
+
console.log("\n-------------------------------------------------");
|
|
432
|
+
console.log("Uninstalling Harness OS from Global Home...");
|
|
433
|
+
console.log("-------------------------------------------------");
|
|
434
|
+
|
|
435
|
+
for (const platform of platformModules) {
|
|
436
|
+
if (typeof platform.uninstall === 'function') {
|
|
437
|
+
platform.uninstall({
|
|
438
|
+
removeLocal: false,
|
|
439
|
+
removeGlobal: true,
|
|
440
|
+
workspaceRoot,
|
|
441
|
+
userHome,
|
|
442
|
+
getUserPromptsDir,
|
|
443
|
+
cleanEmptyDirs,
|
|
444
|
+
claudeHooks
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ~/.agents is a shared directory - other tools or the user's own files
|
|
450
|
+
// may live there. Never touch it directly; only clean up the
|
|
451
|
+
// harness-everything/ subfolder (manifest bookkeeping) and the skills/
|
|
452
|
+
// subfolder this package exclusively owns, and only once each is
|
|
453
|
+
// actually empty (manifest.json is removed automatically by the
|
|
454
|
+
// skills-removal step below once no skills remain in it).
|
|
455
|
+
cleanEmptyDirs(path.join(globalAgentsDir, 'skills'), [userHome]);
|
|
456
|
+
cleanEmptyDirs(globalHarnessDir, [userHome]);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (removeSkills && installedSkills.length > 0) {
|
|
460
|
+
// This block is only reached via a bulk "remove everything detected"
|
|
461
|
+
// path (interactive "all", or the non-interactive -y/--skills bypass) -
|
|
462
|
+
// never via the interactive per-item picker, which already removes
|
|
463
|
+
// exactly what the user checked. A bulk sweep must not reach into the
|
|
464
|
+
// real global home directory unless the user separately opted into
|
|
465
|
+
// global scope (same rule as the config removal above). Every entry in
|
|
466
|
+
// `installedSkills` is already manifest-tracked (or, for legacy
|
|
467
|
+
// installs, author-marker-verified) - see lib/skills.js - so this never
|
|
468
|
+
// touches a skill Harness didn't install.
|
|
469
|
+
const skillsToRemove = installedSkills.filter(s => s.scope !== 'global' || removeGlobal);
|
|
470
|
+
const skippedGlobalCount = installedSkills.length - skillsToRemove.length;
|
|
471
|
+
|
|
472
|
+
console.log("\n-------------------------------------------------");
|
|
473
|
+
console.log("Uninstalling all detected skills...");
|
|
474
|
+
console.log("-------------------------------------------------");
|
|
475
|
+
for (const skill of skillsToRemove) {
|
|
476
|
+
if (fs.existsSync(skill.dirPath)) {
|
|
477
|
+
skills.removeSkill(skill);
|
|
478
|
+
console.log(` ✅ Removed skill: ${skill.id} (${skill.scope})`);
|
|
479
|
+
cleanEmptyDirs(skill.parentPath, [workspaceRoot, userHome]);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (skippedGlobalCount > 0) {
|
|
483
|
+
console.log(` ℹ️ Skipped ${skippedGlobalCount} global-scoped skill(s) - pass --global (or select "Uninstall ... Global Home") to remove those too.`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Final sweep: a platform's harness-everything/ dir (manifest + Claude's
|
|
488
|
+
// skill copies) may have only just become empty above - either through the
|
|
489
|
+
// bulk removeSkills block or the interactive per-item picker, both of
|
|
490
|
+
// which clean up the skill's own parent dir but not necessarily this one.
|
|
491
|
+
// Always safe to run unconditionally: cleanEmptyDirs only ever removes a
|
|
492
|
+
// directory that's genuinely empty.
|
|
493
|
+
for (const platform of platformModules) {
|
|
494
|
+
if (typeof platform.getHarnessDir !== 'function') continue;
|
|
495
|
+
cleanEmptyDirs(platform.getHarnessDir(workspaceRoot), [workspaceRoot, userHome]);
|
|
496
|
+
}
|
|
497
|
+
cleanEmptyDirs(path.join(workspaceRoot, '.claude'), [workspaceRoot, userHome]);
|
|
498
|
+
|
|
499
|
+
console.log("\n🎉 UNINSTALL COMPLETE! Harness OS has been removed.");
|
|
500
|
+
console.log("=================================================");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
main().catch(err => {
|
|
504
|
+
console.error(`\n❌ Install failed: ${err.message}`);
|
|
505
|
+
process.exit(1);
|
|
506
|
+
});
|