code-warden 3.3.0 → 3.3.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.
Files changed (40) hide show
  1. package/CONFIGURE.md +39 -39
  2. package/DECISIONS.md +107 -107
  3. package/README.md +6 -0
  4. package/SKILL.md +169 -169
  5. package/bin/code-warden.js +82 -82
  6. package/codewarden.json +14 -14
  7. package/examples/governed-session.md +132 -132
  8. package/install.js +399 -399
  9. package/install.ps1 +32 -32
  10. package/install.sh +33 -33
  11. package/package.json +62 -62
  12. package/references/anti-drift.md +55 -55
  13. package/references/architecture.md +26 -26
  14. package/references/cleanup.md +30 -30
  15. package/references/cognition.md +36 -36
  16. package/references/operations.md +45 -45
  17. package/references/planning-gates.md +83 -83
  18. package/references/research-and-fit.md +51 -51
  19. package/references/safety.md +31 -31
  20. package/tools/auto-detect.js +91 -91
  21. package/tools/auto-targets.js +104 -104
  22. package/tools/auto-windsurf-adapter.js +75 -75
  23. package/tools/get-context.js +50 -50
  24. package/tools/governance-report.js +302 -302
  25. package/tools/hooks/claude/install-hooks.js +112 -112
  26. package/tools/hooks/claude/uninstall-hooks.js +75 -75
  27. package/tools/hooks/claude/warden-lint-hook.js +106 -106
  28. package/tools/hooks/claude/warden-secrets-hook.js +73 -73
  29. package/tools/hooks/codex/install-hooks.js +100 -100
  30. package/tools/hooks/codex/uninstall-hooks.js +53 -53
  31. package/tools/hooks/codex/warden-apply-patch-hook.js +113 -113
  32. package/tools/hooks/codex/warden-bash-hook.js +51 -51
  33. package/tools/lib/config.js +49 -49
  34. package/tools/lib/file-collection.js +5 -2
  35. package/tools/lib/line-count.js +28 -28
  36. package/tools/lib/secret-patterns.js +57 -57
  37. package/tools/tests/fixtures/clean.js +9 -9
  38. package/tools/tests/run-tests.js +210 -210
  39. package/tools/verify-secrets.js +26 -26
  40. package/tools/warden-lint.js +27 -27
package/install.js CHANGED
@@ -1,399 +1,399 @@
1
- #!/usr/bin/env node
2
- /**
3
- * install.js — code-warden auto-installer
4
- *
5
- * Scans for installed AI apps and deploys the skill to each detected target.
6
- *
7
- * Usage:
8
- * node install.js # scan, prompt, install
9
- * node install.js --all # scan, install all without prompt
10
- * node install.js --dry-run # scan, show plan, write nothing
11
- * node install.js --list # show detection results and exit
12
- * node install.js --doctor # verify health of all detected installs
13
- * node install.js --verify-target=claude # strict health check for one target; exits nonzero if unknown or not installed
14
- * node install.js --verify-target=claude,warp # check multiple targets
15
- * node install.js --target=claude,cursor # force specific targets (warns if not detected)
16
- * node install.js --hooks=claude # install PreToolUse hooks into ~/.claude/settings.json
17
- * node install.js --uninstall-hooks=claude # remove code-warden hook entries from ~/.claude/settings.json
18
- */
19
-
20
- const fs = require('fs');
21
- const path = require('path');
22
- const readline = require('readline');
23
-
24
- const { TARGETS } = require('./tools/auto-targets');
25
- const { scanTargets } = require('./tools/auto-detect');
26
- const { installWindsurf } = require('./tools/auto-windsurf-adapter');
27
-
28
- // ---------------------------------------------------------------------------
29
- // Config
30
- // ---------------------------------------------------------------------------
31
-
32
- const SOURCE_DIR = __dirname;
33
- const SKILL_NAME = 'code-warden';
34
- const PKG = JSON.parse(fs.readFileSync(path.join(SOURCE_DIR, 'package.json'), 'utf8'));
35
- const VERSION = PKG.version;
36
- const SKIP_DIRS = new Set(['node_modules', '.git', 'target']);
37
-
38
- // ---------------------------------------------------------------------------
39
- // Logging
40
- // ---------------------------------------------------------------------------
41
-
42
- const log = msg => console.log(`[CodeWarden] ${msg}`);
43
- const ok = msg => console.log(` [PASS] ${msg}`);
44
- const skip = msg => console.log(` [SKIP] ${msg}`);
45
- const fail = msg => console.error(` [FAIL] ${msg}`);
46
-
47
- // ---------------------------------------------------------------------------
48
- // File copy (recursive, skips heavy dirs)
49
- // ---------------------------------------------------------------------------
50
-
51
- function copyRecursive(src, dest) {
52
- for (const entry of fs.readdirSync(src)) {
53
- if (SKIP_DIRS.has(entry)) continue;
54
- const srcPath = path.join(src, entry);
55
- const destPath = path.join(dest, entry);
56
- if (fs.statSync(srcPath).isDirectory()) {
57
- fs.mkdirSync(destPath, { recursive: true });
58
- copyRecursive(srcPath, destPath);
59
- } else {
60
- fs.copyFileSync(srcPath, destPath);
61
- }
62
- }
63
- }
64
-
65
- // ---------------------------------------------------------------------------
66
- // Install manifest (.code-warden-install.json written into each install dir)
67
- // ---------------------------------------------------------------------------
68
-
69
- function writeManifest(destDir, target) {
70
- const manifest = {
71
- skill: SKILL_NAME,
72
- version: VERSION,
73
- target: target.id,
74
- format: target.format,
75
- installedAt: new Date().toISOString(),
76
- installPath: destDir,
77
- };
78
- fs.writeFileSync(
79
- path.join(destDir, '.code-warden-install.json'),
80
- JSON.stringify(manifest, null, 2),
81
- 'utf8'
82
- );
83
- }
84
-
85
- // ---------------------------------------------------------------------------
86
- // Install one target (atomic: copy to .tmp, then swap)
87
- // ---------------------------------------------------------------------------
88
-
89
- function installTarget(target, dryRun) {
90
- if (target.format === 'windsurf-flat') {
91
- const destFile = path.join(target.skillsDir, `${SKILL_NAME}.md`);
92
- if (dryRun) {
93
- ok(`DRY RUN: would write Windsurf flat file -> ${destFile}`);
94
- return true;
95
- }
96
- try {
97
- installWindsurf(SOURCE_DIR, target.skillsDir);
98
- ok(`Windsurf flat file written -> ${destFile}`);
99
- return true;
100
- } catch (err) {
101
- fail(`Windsurf install failed: ${err.message}`);
102
- return false;
103
- }
104
- }
105
-
106
- // skill-md: copy full folder atomically via temp dir
107
- const destDir = path.join(target.skillsDir, SKILL_NAME);
108
- const tmpDir = `${destDir}.tmp`;
109
-
110
- if (dryRun) {
111
- ok(`DRY RUN: would install -> ${destDir}`);
112
- return true;
113
- }
114
- try {
115
- // Clean up stale temp from any previous failed install
116
- if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
117
-
118
- // Stage into temp dir first
119
- fs.mkdirSync(tmpDir, { recursive: true });
120
- copyRecursive(SOURCE_DIR, tmpDir);
121
- writeManifest(tmpDir, target);
122
-
123
- // Swap: remove old, rename temp into place
124
- if (fs.existsSync(destDir)) fs.rmSync(destDir, { recursive: true, force: true });
125
- fs.renameSync(tmpDir, destDir);
126
-
127
- ok(`Installed -> ${destDir}`);
128
- return true;
129
- } catch (err) {
130
- // Best-effort cleanup of temp on failure
131
- try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
132
- fail(`Install failed for ${target.name}: ${err.message}`);
133
- return false;
134
- }
135
- }
136
-
137
- // ---------------------------------------------------------------------------
138
- // CLI argument parsing
139
- // ---------------------------------------------------------------------------
140
-
141
- function parseArgs(argv) {
142
- const args = argv.slice(2);
143
- const pick = prefix => {
144
- const a = args.find(a => a.startsWith(prefix));
145
- return a ? a.split('=')[1].split(',').map(s => s.trim()) : null;
146
- };
147
- return {
148
- dryRun: args.includes('--dry-run'),
149
- all: args.includes('--all'),
150
- list: args.includes('--list'),
151
- doctor: args.includes('--doctor'),
152
- targetFilter: pick('--target='),
153
- verifyTarget: pick('--verify-target='),
154
- hooksTarget: pick('--hooks='),
155
- uninstallHooksTarget: pick('--uninstall-hooks='),
156
- };
157
- }
158
-
159
- // ---------------------------------------------------------------------------
160
- // Main
161
- // ---------------------------------------------------------------------------
162
-
163
- // ---------------------------------------------------------------------------
164
- // Doctor helpers — shared by --doctor and --verify-target
165
- // ---------------------------------------------------------------------------
166
-
167
- function checkSourceIntegrity(issues) {
168
- const check = (label, pass) => {
169
- if (pass) ok(label); else { fail(label); issues.push(label); }
170
- };
171
- log('Checking source integrity...\n');
172
- check('SKILL.md present', fs.existsSync(path.join(SOURCE_DIR, 'SKILL.md')));
173
- check('references/ present', fs.existsSync(path.join(SOURCE_DIR, 'references')));
174
- check('tools/get-context.js present', fs.existsSync(path.join(SOURCE_DIR, 'tools', 'get-context.js')));
175
- check('tools/warden-lint.js present', fs.existsSync(path.join(SOURCE_DIR, 'tools', 'warden-lint.js')));
176
- check('tools/verify-secrets.js present', fs.existsSync(path.join(SOURCE_DIR, 'tools', 'verify-secrets.js')));
177
- const scripts = Object.keys(PKG.scripts || {});
178
- check('package.json: install-auto script', scripts.includes('install-auto'));
179
- check('package.json: install-dry-run script', scripts.includes('install-dry-run'));
180
- }
181
-
182
- function checkTarget(t, issues) {
183
- const check = (label, pass) => {
184
- if (pass) ok(label); else { fail(label); issues.push(label); }
185
- };
186
- if (t.format === 'windsurf-flat') {
187
- const flatFile = path.join(t.skillsDir, `${SKILL_NAME}.md`);
188
- console.log(` ${t.name}`);
189
- check(` Windsurf flat file present (${flatFile})`, fs.existsSync(flatFile));
190
- } else {
191
- const installDir = path.join(t.skillsDir, SKILL_NAME);
192
- const manifestPath = path.join(installDir, '.code-warden-install.json');
193
- const skillMdPath = path.join(installDir, 'SKILL.md');
194
- console.log(` ${t.name} (${installDir})`);
195
- const hasManifest = fs.existsSync(manifestPath);
196
- check(' Manifest (.code-warden-install.json) present', hasManifest);
197
- if (hasManifest) {
198
- try {
199
- const m = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
200
- check(` Manifest version current (${m.version} == ${VERSION})`, m.version === VERSION);
201
- } catch {
202
- fail(' Manifest present but could not be parsed');
203
- issues.push(`${t.id}: manifest parse error`);
204
- }
205
- }
206
- check(' SKILL.md present in install dir', fs.existsSync(skillMdPath));
207
-
208
- // Validate hook entries if registered in runtime settings
209
- if (t.id === 'claude') {
210
- const sp = path.join(t.skillsDir, '..', 'settings.json');
211
- if (fs.existsSync(sp)) { try {
212
- const cw=(JSON.parse(fs.readFileSync(sp,'utf8'))?.hooks?.PreToolUse||[]).flatMap(m=>m.hooks||[]).filter(h=>String(h.description||'').startsWith('code-warden:'));
213
- if(cw.length>0){check(` Hooks registered (${cw.length})`,true);cw.forEach(h=>{const p=h.args&&h.args[0];check(` Hook script: ${path.basename(p||'?')}`,!!(p&&fs.existsSync(p)));});}
214
- } catch { fail(' settings.json parse error'); issues.push('claude: settings.json'); } }
215
- }
216
- if (t.id === 'codex') {
217
- const hp = path.join(t.skillsDir, '..', 'hooks.json');
218
- if (fs.existsSync(hp)) { try {
219
- const cw=(JSON.parse(fs.readFileSync(hp,'utf8'))?.PreToolUse||[]).filter(e=>String(e.description||'').startsWith('code-warden:'));
220
- if(cw.length>0){check(` Hooks registered (${cw.length})`,true);cw.forEach(e=>{const p=e.args&&e.args[0];check(` Hook script: ${path.basename(p||'?')}`,!!(p&&fs.existsSync(p)));});}
221
- } catch { fail(' hooks.json parse error'); issues.push('codex: hooks.json'); } }
222
- }
223
- }
224
- console.log('');
225
- }
226
-
227
- // ---------------------------------------------------------------------------
228
- // Doctor — verify source integrity + all detected installs
229
- // ---------------------------------------------------------------------------
230
-
231
- function runDoctor(scanned) {
232
- const issues = [];
233
- checkSourceIntegrity(issues);
234
- console.log('');
235
- log('Checking installed targets...\n');
236
- const detected = scanned.filter(t => t.detected);
237
- if (detected.length === 0) console.log(' -- No targets detected on this machine.\n');
238
- for (const t of detected) checkTarget(t, issues);
239
- const count = issues.length;
240
- log(`Doctor complete. ${count === 0 ? 'No issues found.' : `${count} issue(s) found.`}`);
241
- if (count > 0) process.exit(1);
242
- }
243
-
244
- // ---------------------------------------------------------------------------
245
- // Verify-target — strict per-target health check
246
- // ---------------------------------------------------------------------------
247
-
248
- function runVerifyTarget(ids) {
249
- const knownIds = TARGETS.map(t => t.id);
250
- const issues = [];
251
-
252
- // Validate all requested IDs before doing any checks
253
- const unknown = ids.filter(id => !knownIds.includes(id));
254
- if (unknown.length > 0) {
255
- for (const id of unknown) {
256
- console.error(`[CodeWarden] [FAIL] Unknown target ID: "${id}"`);
257
- }
258
- console.error(`[CodeWarden] Known IDs: ${knownIds.join(', ')}`);
259
- process.exit(1);
260
- }
261
-
262
- checkSourceIntegrity(issues);
263
- console.log('');
264
- log(`Checking target(s): ${ids.join(', ')}\n`);
265
-
266
- for (const id of ids) {
267
- const t = TARGETS.find(t => t.id === id);
268
- checkTarget(t, issues);
269
- }
270
-
271
- const count = issues.length;
272
- log(`Verify-target complete. ${count === 0 ? 'No issues found.' : `${count} issue(s) found.`}`);
273
- if (count > 0) process.exit(1);
274
- }
275
-
276
- // ---------------------------------------------------------------------------
277
-
278
- function destPath(target) {
279
- return target.format === 'windsurf-flat'
280
- ? path.join(target.skillsDir, `${SKILL_NAME}.md`)
281
- : path.join(target.skillsDir, SKILL_NAME);
282
- }
283
-
284
- async function main() {
285
- const { dryRun, all, list, doctor, targetFilter, verifyTarget,
286
- hooksTarget, uninstallHooksTarget } = parseArgs(process.argv);
287
-
288
- log(`Auto-Installer v${VERSION}`);
289
-
290
- // --verify-target: strict per-target check — does not need a scan
291
- if (verifyTarget) {
292
- runVerifyTarget(verifyTarget);
293
- return;
294
- }
295
-
296
- if (hooksTarget || uninstallHooksTarget) { // --hooks / --uninstall-hooks dispatch
297
- const HOOK_TARGETS = { claude: 'Claude Code', codex: 'OpenAI Codex' };
298
- const ids = hooksTarget || uninstallHooksTarget;
299
- const bad = ids.filter(id => !HOOK_TARGETS[id]);
300
- if (bad.length > 0) {
301
- console.error(`[CodeWarden] hooks support: ${Object.keys(HOOK_TARGETS).join(', ')}. Unknown: ${bad.join(', ')}`);
302
- process.exit(1);
303
- }
304
- for (const id of ids) {
305
- const skillDir = path.join(TARGETS.find(t => t.id === id).skillsDir, SKILL_NAME);
306
- const mod = require(`./tools/hooks/${id}/${hooksTarget ? 'install' : 'uninstall'}-hooks`);
307
- if (hooksTarget) {
308
- log(`Installing hooks for ${HOOK_TARGETS[id]}...`);
309
- mod.installHooks(skillDir);
310
- ok('Hook entries written');
311
- log(`Restart ${HOOK_TARGETS[id]} for hooks to take effect.`);
312
- } else {
313
- log(`Removing hooks for ${HOOK_TARGETS[id]}...`);
314
- mod.uninstallHooks();
315
- log(`Restart ${HOOK_TARGETS[id]} for changes to take effect.`);
316
- }
317
- }
318
- return;
319
- }
320
-
321
- log('Scanning for installed AI apps...\n');
322
-
323
- // Step 1: Detection — annotate all targets, never mutate detected field here
324
- const scanned = scanTargets(TARGETS);
325
-
326
- // --doctor: verify health of source + all detected installs, then exit
327
- if (doctor) {
328
- runDoctor(scanned);
329
- return;
330
- }
331
-
332
- // Step 2: Selection — separate concern from detection
333
- let selected;
334
- if (targetFilter) {
335
- // Explicit override: honour --target= regardless of detection result
336
- selected = scanned.filter(t => targetFilter.includes(t.id));
337
- for (const t of selected) {
338
- if (!t.detected) {
339
- console.warn(`[CodeWarden] WARN ${t.name} was not detected but was explicitly requested.`);
340
- }
341
- }
342
- } else {
343
- // Auto: only install what was detected
344
- selected = scanned.filter(t => t.detected);
345
- }
346
-
347
- // Print full scan table (detected + undetected)
348
- for (const r of scanned) {
349
- const isSelected = selected.some(s => s.id === r.id);
350
- const status = isSelected ? 'FOUND' : '-- ';
351
- const methodNote = r.method ? ` (${r.method})` : '';
352
- console.log(` ${status} ${r.name.padEnd(22)} -> ${destPath(r)}${list ? methodNote : ''}`);
353
- }
354
- console.log('');
355
-
356
- // --list: just show scan results and exit
357
- if (list) {
358
- log('Use --all or --target=<id> to install.');
359
- process.exit(0);
360
- }
361
-
362
- if (selected.length === 0) {
363
- log('No targets selected. Use --target=claude,cursor to force install.');
364
- process.exit(0);
365
- }
366
-
367
- if (!all && !dryRun) {
368
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
369
- const answer = await new Promise(resolve =>
370
- rl.question(`Install to ${selected.length} target(s)? [Y/n] `, resolve)
371
- );
372
- rl.close();
373
- if (answer.trim().toLowerCase() === 'n') {
374
- log('Aborted.');
375
- process.exit(0);
376
- }
377
- console.log('');
378
- }
379
-
380
- log(dryRun ? 'Dry run — no files will be written.\n' : 'Installing...\n');
381
-
382
- let success = 0;
383
- let failure = 0;
384
-
385
- for (const target of selected) {
386
- if (installTarget(target, dryRun)) success++;
387
- else failure++;
388
- }
389
-
390
- console.log('');
391
- log(`Done. ${success} installed, ${failure} failed.`);
392
- if (!dryRun) log('Restart or refresh your agent session to load the updated skill.');
393
- if (failure > 0) process.exit(1);
394
- }
395
-
396
- main().catch(err => {
397
- console.error(`[CodeWarden] Fatal: ${err.message}`);
398
- process.exit(1);
399
- });
1
+ #!/usr/bin/env node
2
+ /**
3
+ * install.js — code-warden auto-installer
4
+ *
5
+ * Scans for installed AI apps and deploys the skill to each detected target.
6
+ *
7
+ * Usage:
8
+ * node install.js # scan, prompt, install
9
+ * node install.js --all # scan, install all without prompt
10
+ * node install.js --dry-run # scan, show plan, write nothing
11
+ * node install.js --list # show detection results and exit
12
+ * node install.js --doctor # verify health of all detected installs
13
+ * node install.js --verify-target=claude # strict health check for one target; exits nonzero if unknown or not installed
14
+ * node install.js --verify-target=claude,warp # check multiple targets
15
+ * node install.js --target=claude,cursor # force specific targets (warns if not detected)
16
+ * node install.js --hooks=claude # install PreToolUse hooks into ~/.claude/settings.json
17
+ * node install.js --uninstall-hooks=claude # remove code-warden hook entries from ~/.claude/settings.json
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const readline = require('readline');
23
+
24
+ const { TARGETS } = require('./tools/auto-targets');
25
+ const { scanTargets } = require('./tools/auto-detect');
26
+ const { installWindsurf } = require('./tools/auto-windsurf-adapter');
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Config
30
+ // ---------------------------------------------------------------------------
31
+
32
+ const SOURCE_DIR = __dirname;
33
+ const SKILL_NAME = 'code-warden';
34
+ const PKG = JSON.parse(fs.readFileSync(path.join(SOURCE_DIR, 'package.json'), 'utf8'));
35
+ const VERSION = PKG.version;
36
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'target']);
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Logging
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const log = msg => console.log(`[CodeWarden] ${msg}`);
43
+ const ok = msg => console.log(` [PASS] ${msg}`);
44
+ const skip = msg => console.log(` [SKIP] ${msg}`);
45
+ const fail = msg => console.error(` [FAIL] ${msg}`);
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // File copy (recursive, skips heavy dirs)
49
+ // ---------------------------------------------------------------------------
50
+
51
+ function copyRecursive(src, dest) {
52
+ for (const entry of fs.readdirSync(src)) {
53
+ if (SKIP_DIRS.has(entry)) continue;
54
+ const srcPath = path.join(src, entry);
55
+ const destPath = path.join(dest, entry);
56
+ if (fs.statSync(srcPath).isDirectory()) {
57
+ fs.mkdirSync(destPath, { recursive: true });
58
+ copyRecursive(srcPath, destPath);
59
+ } else {
60
+ fs.copyFileSync(srcPath, destPath);
61
+ }
62
+ }
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Install manifest (.code-warden-install.json written into each install dir)
67
+ // ---------------------------------------------------------------------------
68
+
69
+ function writeManifest(destDir, target) {
70
+ const manifest = {
71
+ skill: SKILL_NAME,
72
+ version: VERSION,
73
+ target: target.id,
74
+ format: target.format,
75
+ installedAt: new Date().toISOString(),
76
+ installPath: destDir,
77
+ };
78
+ fs.writeFileSync(
79
+ path.join(destDir, '.code-warden-install.json'),
80
+ JSON.stringify(manifest, null, 2),
81
+ 'utf8'
82
+ );
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Install one target (atomic: copy to .tmp, then swap)
87
+ // ---------------------------------------------------------------------------
88
+
89
+ function installTarget(target, dryRun) {
90
+ if (target.format === 'windsurf-flat') {
91
+ const destFile = path.join(target.skillsDir, `${SKILL_NAME}.md`);
92
+ if (dryRun) {
93
+ ok(`DRY RUN: would write Windsurf flat file -> ${destFile}`);
94
+ return true;
95
+ }
96
+ try {
97
+ installWindsurf(SOURCE_DIR, target.skillsDir);
98
+ ok(`Windsurf flat file written -> ${destFile}`);
99
+ return true;
100
+ } catch (err) {
101
+ fail(`Windsurf install failed: ${err.message}`);
102
+ return false;
103
+ }
104
+ }
105
+
106
+ // skill-md: copy full folder atomically via temp dir
107
+ const destDir = path.join(target.skillsDir, SKILL_NAME);
108
+ const tmpDir = `${destDir}.tmp`;
109
+
110
+ if (dryRun) {
111
+ ok(`DRY RUN: would install -> ${destDir}`);
112
+ return true;
113
+ }
114
+ try {
115
+ // Clean up stale temp from any previous failed install
116
+ if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
117
+
118
+ // Stage into temp dir first
119
+ fs.mkdirSync(tmpDir, { recursive: true });
120
+ copyRecursive(SOURCE_DIR, tmpDir);
121
+ writeManifest(tmpDir, target);
122
+
123
+ // Swap: remove old, rename temp into place
124
+ if (fs.existsSync(destDir)) fs.rmSync(destDir, { recursive: true, force: true });
125
+ fs.renameSync(tmpDir, destDir);
126
+
127
+ ok(`Installed -> ${destDir}`);
128
+ return true;
129
+ } catch (err) {
130
+ // Best-effort cleanup of temp on failure
131
+ try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
132
+ fail(`Install failed for ${target.name}: ${err.message}`);
133
+ return false;
134
+ }
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // CLI argument parsing
139
+ // ---------------------------------------------------------------------------
140
+
141
+ function parseArgs(argv) {
142
+ const args = argv.slice(2);
143
+ const pick = prefix => {
144
+ const a = args.find(a => a.startsWith(prefix));
145
+ return a ? a.split('=')[1].split(',').map(s => s.trim()) : null;
146
+ };
147
+ return {
148
+ dryRun: args.includes('--dry-run'),
149
+ all: args.includes('--all'),
150
+ list: args.includes('--list'),
151
+ doctor: args.includes('--doctor'),
152
+ targetFilter: pick('--target='),
153
+ verifyTarget: pick('--verify-target='),
154
+ hooksTarget: pick('--hooks='),
155
+ uninstallHooksTarget: pick('--uninstall-hooks='),
156
+ };
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Main
161
+ // ---------------------------------------------------------------------------
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Doctor helpers — shared by --doctor and --verify-target
165
+ // ---------------------------------------------------------------------------
166
+
167
+ function checkSourceIntegrity(issues) {
168
+ const check = (label, pass) => {
169
+ if (pass) ok(label); else { fail(label); issues.push(label); }
170
+ };
171
+ log('Checking source integrity...\n');
172
+ check('SKILL.md present', fs.existsSync(path.join(SOURCE_DIR, 'SKILL.md')));
173
+ check('references/ present', fs.existsSync(path.join(SOURCE_DIR, 'references')));
174
+ check('tools/get-context.js present', fs.existsSync(path.join(SOURCE_DIR, 'tools', 'get-context.js')));
175
+ check('tools/warden-lint.js present', fs.existsSync(path.join(SOURCE_DIR, 'tools', 'warden-lint.js')));
176
+ check('tools/verify-secrets.js present', fs.existsSync(path.join(SOURCE_DIR, 'tools', 'verify-secrets.js')));
177
+ const scripts = Object.keys(PKG.scripts || {});
178
+ check('package.json: install-auto script', scripts.includes('install-auto'));
179
+ check('package.json: install-dry-run script', scripts.includes('install-dry-run'));
180
+ }
181
+
182
+ function checkTarget(t, issues) {
183
+ const check = (label, pass) => {
184
+ if (pass) ok(label); else { fail(label); issues.push(label); }
185
+ };
186
+ if (t.format === 'windsurf-flat') {
187
+ const flatFile = path.join(t.skillsDir, `${SKILL_NAME}.md`);
188
+ console.log(` ${t.name}`);
189
+ check(` Windsurf flat file present (${flatFile})`, fs.existsSync(flatFile));
190
+ } else {
191
+ const installDir = path.join(t.skillsDir, SKILL_NAME);
192
+ const manifestPath = path.join(installDir, '.code-warden-install.json');
193
+ const skillMdPath = path.join(installDir, 'SKILL.md');
194
+ console.log(` ${t.name} (${installDir})`);
195
+ const hasManifest = fs.existsSync(manifestPath);
196
+ check(' Manifest (.code-warden-install.json) present', hasManifest);
197
+ if (hasManifest) {
198
+ try {
199
+ const m = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
200
+ check(` Manifest version current (${m.version} == ${VERSION})`, m.version === VERSION);
201
+ } catch {
202
+ fail(' Manifest present but could not be parsed');
203
+ issues.push(`${t.id}: manifest parse error`);
204
+ }
205
+ }
206
+ check(' SKILL.md present in install dir', fs.existsSync(skillMdPath));
207
+
208
+ // Validate hook entries if registered in runtime settings
209
+ if (t.id === 'claude') {
210
+ const sp = path.join(t.skillsDir, '..', 'settings.json');
211
+ if (fs.existsSync(sp)) { try {
212
+ const cw=(JSON.parse(fs.readFileSync(sp,'utf8'))?.hooks?.PreToolUse||[]).flatMap(m=>m.hooks||[]).filter(h=>String(h.description||'').startsWith('code-warden:'));
213
+ if(cw.length>0){check(` Hooks registered (${cw.length})`,true);cw.forEach(h=>{const p=h.args&&h.args[0];check(` Hook script: ${path.basename(p||'?')}`,!!(p&&fs.existsSync(p)));});}
214
+ } catch { fail(' settings.json parse error'); issues.push('claude: settings.json'); } }
215
+ }
216
+ if (t.id === 'codex') {
217
+ const hp = path.join(t.skillsDir, '..', 'hooks.json');
218
+ if (fs.existsSync(hp)) { try {
219
+ const cw=(JSON.parse(fs.readFileSync(hp,'utf8'))?.PreToolUse||[]).filter(e=>String(e.description||'').startsWith('code-warden:'));
220
+ if(cw.length>0){check(` Hooks registered (${cw.length})`,true);cw.forEach(e=>{const p=e.args&&e.args[0];check(` Hook script: ${path.basename(p||'?')}`,!!(p&&fs.existsSync(p)));});}
221
+ } catch { fail(' hooks.json parse error'); issues.push('codex: hooks.json'); } }
222
+ }
223
+ }
224
+ console.log('');
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Doctor — verify source integrity + all detected installs
229
+ // ---------------------------------------------------------------------------
230
+
231
+ function runDoctor(scanned) {
232
+ const issues = [];
233
+ checkSourceIntegrity(issues);
234
+ console.log('');
235
+ log('Checking installed targets...\n');
236
+ const detected = scanned.filter(t => t.detected);
237
+ if (detected.length === 0) console.log(' -- No targets detected on this machine.\n');
238
+ for (const t of detected) checkTarget(t, issues);
239
+ const count = issues.length;
240
+ log(`Doctor complete. ${count === 0 ? 'No issues found.' : `${count} issue(s) found.`}`);
241
+ if (count > 0) process.exit(1);
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // Verify-target — strict per-target health check
246
+ // ---------------------------------------------------------------------------
247
+
248
+ function runVerifyTarget(ids) {
249
+ const knownIds = TARGETS.map(t => t.id);
250
+ const issues = [];
251
+
252
+ // Validate all requested IDs before doing any checks
253
+ const unknown = ids.filter(id => !knownIds.includes(id));
254
+ if (unknown.length > 0) {
255
+ for (const id of unknown) {
256
+ console.error(`[CodeWarden] [FAIL] Unknown target ID: "${id}"`);
257
+ }
258
+ console.error(`[CodeWarden] Known IDs: ${knownIds.join(', ')}`);
259
+ process.exit(1);
260
+ }
261
+
262
+ checkSourceIntegrity(issues);
263
+ console.log('');
264
+ log(`Checking target(s): ${ids.join(', ')}\n`);
265
+
266
+ for (const id of ids) {
267
+ const t = TARGETS.find(t => t.id === id);
268
+ checkTarget(t, issues);
269
+ }
270
+
271
+ const count = issues.length;
272
+ log(`Verify-target complete. ${count === 0 ? 'No issues found.' : `${count} issue(s) found.`}`);
273
+ if (count > 0) process.exit(1);
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+
278
+ function destPath(target) {
279
+ return target.format === 'windsurf-flat'
280
+ ? path.join(target.skillsDir, `${SKILL_NAME}.md`)
281
+ : path.join(target.skillsDir, SKILL_NAME);
282
+ }
283
+
284
+ async function main() {
285
+ const { dryRun, all, list, doctor, targetFilter, verifyTarget,
286
+ hooksTarget, uninstallHooksTarget } = parseArgs(process.argv);
287
+
288
+ log(`Auto-Installer v${VERSION}`);
289
+
290
+ // --verify-target: strict per-target check — does not need a scan
291
+ if (verifyTarget) {
292
+ runVerifyTarget(verifyTarget);
293
+ return;
294
+ }
295
+
296
+ if (hooksTarget || uninstallHooksTarget) { // --hooks / --uninstall-hooks dispatch
297
+ const HOOK_TARGETS = { claude: 'Claude Code', codex: 'OpenAI Codex' };
298
+ const ids = hooksTarget || uninstallHooksTarget;
299
+ const bad = ids.filter(id => !HOOK_TARGETS[id]);
300
+ if (bad.length > 0) {
301
+ console.error(`[CodeWarden] hooks support: ${Object.keys(HOOK_TARGETS).join(', ')}. Unknown: ${bad.join(', ')}`);
302
+ process.exit(1);
303
+ }
304
+ for (const id of ids) {
305
+ const skillDir = path.join(TARGETS.find(t => t.id === id).skillsDir, SKILL_NAME);
306
+ const mod = require(`./tools/hooks/${id}/${hooksTarget ? 'install' : 'uninstall'}-hooks`);
307
+ if (hooksTarget) {
308
+ log(`Installing hooks for ${HOOK_TARGETS[id]}...`);
309
+ mod.installHooks(skillDir);
310
+ ok('Hook entries written');
311
+ log(`Restart ${HOOK_TARGETS[id]} for hooks to take effect.`);
312
+ } else {
313
+ log(`Removing hooks for ${HOOK_TARGETS[id]}...`);
314
+ mod.uninstallHooks();
315
+ log(`Restart ${HOOK_TARGETS[id]} for changes to take effect.`);
316
+ }
317
+ }
318
+ return;
319
+ }
320
+
321
+ log('Scanning for installed AI apps...\n');
322
+
323
+ // Step 1: Detection — annotate all targets, never mutate detected field here
324
+ const scanned = scanTargets(TARGETS);
325
+
326
+ // --doctor: verify health of source + all detected installs, then exit
327
+ if (doctor) {
328
+ runDoctor(scanned);
329
+ return;
330
+ }
331
+
332
+ // Step 2: Selection — separate concern from detection
333
+ let selected;
334
+ if (targetFilter) {
335
+ // Explicit override: honour --target= regardless of detection result
336
+ selected = scanned.filter(t => targetFilter.includes(t.id));
337
+ for (const t of selected) {
338
+ if (!t.detected) {
339
+ console.warn(`[CodeWarden] WARN ${t.name} was not detected but was explicitly requested.`);
340
+ }
341
+ }
342
+ } else {
343
+ // Auto: only install what was detected
344
+ selected = scanned.filter(t => t.detected);
345
+ }
346
+
347
+ // Print full scan table (detected + undetected)
348
+ for (const r of scanned) {
349
+ const isSelected = selected.some(s => s.id === r.id);
350
+ const status = isSelected ? 'FOUND' : '-- ';
351
+ const methodNote = r.method ? ` (${r.method})` : '';
352
+ console.log(` ${status} ${r.name.padEnd(22)} -> ${destPath(r)}${list ? methodNote : ''}`);
353
+ }
354
+ console.log('');
355
+
356
+ // --list: just show scan results and exit
357
+ if (list) {
358
+ log('Use --all or --target=<id> to install.');
359
+ process.exit(0);
360
+ }
361
+
362
+ if (selected.length === 0) {
363
+ log('No targets selected. Use --target=claude,cursor to force install.');
364
+ process.exit(0);
365
+ }
366
+
367
+ if (!all && !dryRun) {
368
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
369
+ const answer = await new Promise(resolve =>
370
+ rl.question(`Install to ${selected.length} target(s)? [Y/n] `, resolve)
371
+ );
372
+ rl.close();
373
+ if (answer.trim().toLowerCase() === 'n') {
374
+ log('Aborted.');
375
+ process.exit(0);
376
+ }
377
+ console.log('');
378
+ }
379
+
380
+ log(dryRun ? 'Dry run — no files will be written.\n' : 'Installing...\n');
381
+
382
+ let success = 0;
383
+ let failure = 0;
384
+
385
+ for (const target of selected) {
386
+ if (installTarget(target, dryRun)) success++;
387
+ else failure++;
388
+ }
389
+
390
+ console.log('');
391
+ log(`Done. ${success} installed, ${failure} failed.`);
392
+ if (!dryRun) log('Restart or refresh your agent session to load the updated skill.');
393
+ if (failure > 0) process.exit(1);
394
+ }
395
+
396
+ main().catch(err => {
397
+ console.error(`[CodeWarden] Fatal: ${err.message}`);
398
+ process.exit(1);
399
+ });