code-warden 3.1.1 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONFIGURE.md +39 -39
- package/DECISIONS.md +107 -107
- package/README.md +199 -137
- package/SKILL.md +169 -169
- package/bin/code-warden.js +82 -0
- package/codewarden.json +14 -14
- package/examples/governed-session.md +132 -132
- package/install.js +399 -399
- package/install.ps1 +32 -32
- package/install.sh +33 -33
- package/package.json +45 -2
- package/references/anti-drift.md +55 -55
- package/references/architecture.md +26 -26
- package/references/cleanup.md +30 -30
- package/references/cognition.md +36 -36
- package/references/operations.md +45 -45
- package/references/planning-gates.md +83 -83
- package/references/research-and-fit.md +51 -51
- package/references/safety.md +31 -31
- package/templates/ci/github-actions.yml +83 -66
- package/tools/auto-detect.js +91 -91
- package/tools/auto-targets.js +104 -104
- package/tools/auto-windsurf-adapter.js +75 -75
- package/tools/get-context.js +50 -50
- package/tools/governance-report.js +302 -0
- package/tools/hooks/claude/install-hooks.js +112 -112
- package/tools/hooks/claude/uninstall-hooks.js +75 -75
- package/tools/hooks/claude/warden-lint-hook.js +106 -106
- package/tools/hooks/claude/warden-secrets-hook.js +73 -73
- package/tools/hooks/codex/install-hooks.js +100 -100
- package/tools/hooks/codex/uninstall-hooks.js +53 -53
- package/tools/hooks/codex/warden-apply-patch-hook.js +113 -113
- package/tools/hooks/codex/warden-bash-hook.js +51 -51
- package/tools/lib/config.js +49 -49
- package/tools/lib/file-collection.js +72 -72
- package/tools/lib/line-count.js +28 -28
- package/tools/lib/secret-patterns.js +57 -57
- package/tools/tests/fixtures/clean.js +9 -9
- package/tools/tests/run-tests.js +210 -210
- package/tools/verify-secrets.js +26 -26
- 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
|
+
});
|