@nirnex/cli 3.0.0 → 4.1.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.
Files changed (52) hide show
  1. package/dist/commands/remove.d.ts +14 -0
  2. package/dist/commands/remove.d.ts.map +1 -0
  3. package/dist/commands/remove.js +678 -0
  4. package/dist/commands/remove.js.map +1 -0
  5. package/dist/commands/runtime.d.ts +2 -0
  6. package/dist/commands/runtime.d.ts.map +1 -0
  7. package/dist/commands/runtime.js +60 -0
  8. package/dist/commands/runtime.js.map +1 -0
  9. package/dist/commands/setup.d.ts.map +1 -1
  10. package/dist/commands/setup.js +99 -0
  11. package/dist/commands/setup.js.map +1 -1
  12. package/dist/commands/status.d.ts.map +1 -1
  13. package/dist/commands/status.js +69 -1
  14. package/dist/commands/status.js.map +1 -1
  15. package/dist/commands/trace.d.ts.map +1 -1
  16. package/dist/commands/trace.js +74 -1
  17. package/dist/commands/trace.js.map +1 -1
  18. package/dist/index.js +17 -7
  19. package/dist/index.js.map +1 -1
  20. package/dist/runtime/bootstrap.d.ts +2 -0
  21. package/dist/runtime/bootstrap.d.ts.map +1 -0
  22. package/dist/runtime/bootstrap.js +90 -0
  23. package/dist/runtime/bootstrap.js.map +1 -0
  24. package/dist/runtime/entry.d.ts +2 -0
  25. package/dist/runtime/entry.d.ts.map +1 -0
  26. package/dist/runtime/entry.js +86 -0
  27. package/dist/runtime/entry.js.map +1 -0
  28. package/dist/runtime/envelope.d.ts +4 -0
  29. package/dist/runtime/envelope.d.ts.map +1 -0
  30. package/dist/runtime/envelope.js +113 -0
  31. package/dist/runtime/envelope.js.map +1 -0
  32. package/dist/runtime/guard.d.ts +2 -0
  33. package/dist/runtime/guard.d.ts.map +1 -0
  34. package/dist/runtime/guard.js +114 -0
  35. package/dist/runtime/guard.js.map +1 -0
  36. package/dist/runtime/session.d.ts +13 -0
  37. package/dist/runtime/session.d.ts.map +1 -0
  38. package/dist/runtime/session.js +111 -0
  39. package/dist/runtime/session.js.map +1 -0
  40. package/dist/runtime/trace-hook.d.ts +2 -0
  41. package/dist/runtime/trace-hook.d.ts.map +1 -0
  42. package/dist/runtime/trace-hook.js +102 -0
  43. package/dist/runtime/trace-hook.js.map +1 -0
  44. package/dist/runtime/types.d.ts +108 -0
  45. package/dist/runtime/types.d.ts.map +1 -0
  46. package/dist/runtime/types.js +3 -0
  47. package/dist/runtime/types.js.map +1 -0
  48. package/dist/runtime/validate.d.ts +2 -0
  49. package/dist/runtime/validate.d.ts.map +1 -0
  50. package/dist/runtime/validate.js +91 -0
  51. package/dist/runtime/validate.js.map +1 -0
  52. package/package.json +1 -1
@@ -0,0 +1,14 @@
1
+ interface RemoveOpts {
2
+ yes: boolean;
3
+ dryRun: boolean;
4
+ force: boolean;
5
+ keepData: boolean;
6
+ keepSpecs: boolean;
7
+ keepClaude: boolean;
8
+ purgeData: boolean;
9
+ json: boolean;
10
+ }
11
+ declare function runRemove(cwd: string, opts: RemoveOpts): Promise<void>;
12
+ export { runRemove };
13
+ export declare function removeCommand(args: string[]): Promise<void>;
14
+ //# sourceMappingURL=remove.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remove.d.ts","sourceRoot":"","sources":["../../src/commands/remove.ts"],"names":[],"mappings":"AA4DA,UAAU,UAAU;IAClB,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;CACf;AA2iBD,iBAAe,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAyHrE;AAID,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAcjE"}
@@ -0,0 +1,678 @@
1
+ // Command: nirnex remove
2
+ // Safely detach Nirnex from a repository without damaging user files,
3
+ // hooks, settings, or source code that Nirnex did not create.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import readline from 'node:readline';
7
+ // ─── Known default templates (from setup.ts) ───────────────────────────────
8
+ const NIRNEX_HOOK_COMMANDS = new Set([
9
+ '.claude/hooks/nirnex-bootstrap.sh',
10
+ '.claude/hooks/nirnex-entry.sh',
11
+ '.claude/hooks/nirnex-guard.sh',
12
+ '.claude/hooks/nirnex-trace.sh',
13
+ '.claude/hooks/nirnex-validate.sh',
14
+ ]);
15
+ const NIRNEX_HOOK_SCRIPTS = {
16
+ 'nirnex-bootstrap.sh': '#!/bin/sh\nexec nirnex runtime bootstrap\n',
17
+ 'nirnex-entry.sh': '#!/bin/sh\nexec nirnex runtime entry\n',
18
+ 'nirnex-guard.sh': '#!/bin/sh\nexec nirnex runtime guard\n',
19
+ 'nirnex-trace.sh': '#!/bin/sh\nexec nirnex runtime trace\n',
20
+ 'nirnex-validate.sh': '#!/bin/sh\nexec nirnex runtime validate\n',
21
+ };
22
+ const POST_COMMIT_EXACT = '#!/bin/sh\nnirnex index\n';
23
+ const POST_COMMIT_LINE = 'nirnex index';
24
+ const DEFAULT_CRITICAL_PATHS_PREFIX = '# Critical Paths\n# List architecturally critical files';
25
+ const DEFAULT_ANALYST_PREFIX = '# Analyst Persona\n\n## Role\nYou are the Analyst agent in the Nirnex pipeline.';
26
+ const DEFAULT_IMPLEMENTER_PREFIX = '# Implementer Persona\n\n## Role\nYou are the Implementer agent in the Nirnex pipeline.';
27
+ const DEFAULT_CALIBRATION_PREFIX = '# Calibration\n\nProject-specific calibration files for Nirnex.';
28
+ // ─── Helpers ──────────────────────────────────────────────────────────────
29
+ function cross(msg) {
30
+ process.stdout.write(` \x1b[31m✖\x1b[0m ${msg}\n`);
31
+ }
32
+ function tick(msg) {
33
+ process.stdout.write(` \x1b[32m✔\x1b[0m ${msg}\n`);
34
+ }
35
+ function skip(msg) {
36
+ process.stdout.write(` \x1b[90m·\x1b[0m ${msg}\n`);
37
+ }
38
+ function warn(msg) {
39
+ process.stdout.write(` \x1b[33m!\x1b[0m ${msg}\n`);
40
+ }
41
+ function promptYesNo(rl, question, defaultYes = true) {
42
+ const hint = defaultYes ? '[Y/n]' : '[y/N]';
43
+ return new Promise(resolve => rl.question(` ${question} ${hint}: `, answer => {
44
+ const t = answer.trim().toLowerCase();
45
+ resolve(!t ? defaultYes : t === 'y' || t === 'yes');
46
+ }));
47
+ }
48
+ function readJsonSafe(p) {
49
+ try {
50
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ function isNirnexConfig(obj) {
57
+ // Recognise by shape: must have at least 3 of these nirnex-specific keys
58
+ const keys = ['specDirectory', 'criticalPathsFile', 'prompts', 'index', 'git', 'llm', 'hooks', 'sourceRoots', 'projectName'];
59
+ const present = keys.filter(k => k in obj).length;
60
+ return present >= 4;
61
+ }
62
+ function isDirEmpty(p) {
63
+ try {
64
+ return fs.readdirSync(p).length === 0;
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ function startsWith(content, prefix) {
71
+ return content.trimStart().startsWith(prefix);
72
+ }
73
+ // ─── Scanner ──────────────────────────────────────────────────────────────
74
+ function scanRemovalTargets(cwd, opts) {
75
+ const actions = [];
76
+ const manualReview = [];
77
+ // 1. nirnex.config.json
78
+ const configPath = path.join(cwd, 'nirnex.config.json');
79
+ if (fs.existsSync(configPath)) {
80
+ const obj = readJsonSafe(configPath);
81
+ if (obj && isNirnexConfig(obj)) {
82
+ actions.push({
83
+ path: configPath,
84
+ type: 'delete_file',
85
+ confidence: 'high',
86
+ reason: 'Matches Nirnex config shape (created by nirnex setup)',
87
+ requiresConfirmation: false,
88
+ });
89
+ }
90
+ else {
91
+ manualReview.push(`${configPath} — exists but does not look like a Nirnex config; inspect manually`);
92
+ }
93
+ }
94
+ // 2. .aidos.db
95
+ const dbPath = path.join(cwd, '.aidos.db');
96
+ if (fs.existsSync(dbPath) && !opts.keepData) {
97
+ actions.push({
98
+ path: dbPath,
99
+ type: 'delete_file',
100
+ confidence: 'high',
101
+ reason: 'SQLite database created by nirnex index',
102
+ requiresConfirmation: false,
103
+ });
104
+ }
105
+ else if (fs.existsSync(dbPath) && opts.keepData) {
106
+ skip(`.aidos.db preserved (--keep-data)`);
107
+ }
108
+ // 3. .ai-index/
109
+ const aiIndexDir = path.join(cwd, '.ai-index');
110
+ if (fs.existsSync(aiIndexDir) && !opts.keepData) {
111
+ actions.push({
112
+ path: aiIndexDir,
113
+ type: 'delete_dir',
114
+ confidence: 'high',
115
+ reason: 'Runtime index directory created by nirnex setup',
116
+ requiresConfirmation: false,
117
+ });
118
+ }
119
+ else if (fs.existsSync(aiIndexDir) && opts.keepData) {
120
+ skip(`.ai-index/ preserved (--keep-data)`);
121
+ }
122
+ // 4. .ai/ — selective removal
123
+ const aiDir = path.join(cwd, '.ai');
124
+ if (fs.existsSync(aiDir)) {
125
+ if (opts.purgeData) {
126
+ // Full removal requested
127
+ actions.push({
128
+ path: aiDir,
129
+ type: 'delete_dir',
130
+ confidence: 'medium',
131
+ reason: 'Full .ai/ purge requested (--purge-data)',
132
+ requiresConfirmation: true,
133
+ preview: 'This will delete all files in .ai/ including any user-authored specs and calibration files.',
134
+ });
135
+ }
136
+ else if (!opts.keepData && !opts.keepSpecs) {
137
+ // Remove only default template files, preserve user content
138
+ scanAiDir(cwd, aiDir, actions, manualReview);
139
+ }
140
+ else {
141
+ skip(`.ai/ preserved (--keep-data or --keep-specs)`);
142
+ }
143
+ }
144
+ // 5. Git post-commit hook
145
+ if (!opts.keepData) {
146
+ const hookPath = path.join(cwd, '.git', 'hooks', 'post-commit');
147
+ if (fs.existsSync(hookPath)) {
148
+ try {
149
+ const content = fs.readFileSync(hookPath, 'utf8');
150
+ if (content === POST_COMMIT_EXACT) {
151
+ actions.push({
152
+ path: hookPath,
153
+ type: 'delete_file',
154
+ confidence: 'high',
155
+ reason: 'Exact match to Nirnex post-commit hook template',
156
+ requiresConfirmation: false,
157
+ });
158
+ }
159
+ else if (content.includes(POST_COMMIT_LINE)) {
160
+ // Mixed hook — patch only if manageable
161
+ actions.push({
162
+ path: hookPath,
163
+ type: 'patch_hook',
164
+ confidence: 'medium',
165
+ reason: `Contains "nirnex index" alongside other commands — will remove only the Nirnex line`,
166
+ preview: `Will remove the line: ${POST_COMMIT_LINE}`,
167
+ requiresConfirmation: true,
168
+ });
169
+ }
170
+ // else: hook exists but has no nirnex content — leave it alone
171
+ }
172
+ catch {
173
+ manualReview.push(`${hookPath} — could not read; inspect manually`);
174
+ }
175
+ }
176
+ }
177
+ // 6. Claude hook scripts
178
+ if (!opts.keepClaude) {
179
+ const claudeHooksDir = path.join(cwd, '.claude', 'hooks');
180
+ for (const [name, expectedContent] of Object.entries(NIRNEX_HOOK_SCRIPTS)) {
181
+ const p = path.join(claudeHooksDir, name);
182
+ if (fs.existsSync(p)) {
183
+ try {
184
+ const content = fs.readFileSync(p, 'utf8');
185
+ if (content === expectedContent) {
186
+ actions.push({
187
+ path: p,
188
+ type: 'delete_file',
189
+ confidence: 'high',
190
+ reason: `Exact match to Nirnex hook template for ${name}`,
191
+ requiresConfirmation: false,
192
+ });
193
+ }
194
+ else {
195
+ manualReview.push(`${p} — exists but content differs from Nirnex template; inspect manually`);
196
+ }
197
+ }
198
+ catch {
199
+ manualReview.push(`${p} — could not read; inspect manually`);
200
+ }
201
+ }
202
+ }
203
+ }
204
+ // 7. .claude/settings.json — surgical patch
205
+ if (!opts.keepClaude) {
206
+ const settingsPath = path.join(cwd, '.claude', 'settings.json');
207
+ if (fs.existsSync(settingsPath)) {
208
+ const obj = readJsonSafe(settingsPath);
209
+ if (obj && obj.hooks && hasNirnexHooks(obj.hooks)) {
210
+ actions.push({
211
+ path: settingsPath,
212
+ type: 'patch_json',
213
+ confidence: 'low',
214
+ reason: 'Contains Nirnex hook bindings — will remove only Nirnex entries, preserve all other settings',
215
+ preview: buildSettingsPatchPreview(obj.hooks),
216
+ requiresConfirmation: true,
217
+ });
218
+ }
219
+ }
220
+ }
221
+ // 8. .claude/ and .claude/hooks/ — remove if empty after file removal
222
+ // These are added dynamically during execution; see executeRemovalPlan.
223
+ return { cwd, actions, manualReview };
224
+ }
225
+ function scanAiDir(cwd, aiDir, actions, manualReview) {
226
+ const toCheck = [
227
+ { rel: 'critical-paths.txt', defaultPrefix: DEFAULT_CRITICAL_PATHS_PREFIX },
228
+ { rel: path.join('prompts', 'analyst.md'), defaultPrefix: DEFAULT_ANALYST_PREFIX },
229
+ { rel: path.join('prompts', 'implementer.md'), defaultPrefix: DEFAULT_IMPLEMENTER_PREFIX },
230
+ { rel: path.join('calibration', 'README.md'), defaultPrefix: DEFAULT_CALIBRATION_PREFIX },
231
+ ];
232
+ for (const { rel, defaultPrefix } of toCheck) {
233
+ const full = path.join(aiDir, rel);
234
+ if (!fs.existsSync(full))
235
+ continue;
236
+ try {
237
+ const content = fs.readFileSync(full, 'utf8');
238
+ if (startsWith(content, defaultPrefix)) {
239
+ actions.push({
240
+ path: full,
241
+ type: 'delete_file',
242
+ confidence: 'medium',
243
+ reason: `Default Nirnex template file (content matches setup default)`,
244
+ requiresConfirmation: false,
245
+ });
246
+ }
247
+ else {
248
+ manualReview.push(`${full} — appears user-modified; not removed. Delete manually if not needed.`);
249
+ }
250
+ }
251
+ catch {
252
+ manualReview.push(`${full} — could not read; skipping`);
253
+ }
254
+ }
255
+ // Warn about specs/ — never auto-remove
256
+ const specsDir = path.join(aiDir, 'specs');
257
+ if (fs.existsSync(specsDir)) {
258
+ const entries = fs.readdirSync(specsDir).filter(e => e !== '.gitkeep');
259
+ if (entries.length > 0) {
260
+ manualReview.push(`.ai/specs/ contains ${entries.length} file(s) — preserved. Delete manually with: rm -rf .ai/specs`);
261
+ }
262
+ }
263
+ }
264
+ // ─── Settings JSON helpers ────────────────────────────────────────────────
265
+ function isNirnexHookCommand(cmd) {
266
+ return typeof cmd === 'string' && NIRNEX_HOOK_COMMANDS.has(cmd);
267
+ }
268
+ function hasNirnexHooks(hooks) {
269
+ for (const stage of Object.values(hooks)) {
270
+ if (!Array.isArray(stage))
271
+ continue;
272
+ for (const entry of stage) {
273
+ if (!entry || typeof entry !== 'object')
274
+ continue;
275
+ const entryHooks = entry.hooks;
276
+ if (!Array.isArray(entryHooks))
277
+ continue;
278
+ for (const h of entryHooks) {
279
+ if (h && typeof h === 'object' && isNirnexHookCommand(h.command)) {
280
+ return true;
281
+ }
282
+ }
283
+ }
284
+ }
285
+ return false;
286
+ }
287
+ function removeNirnexHooks(hooks) {
288
+ const cleaned = {};
289
+ for (const [stage, entries] of Object.entries(hooks)) {
290
+ if (!Array.isArray(entries)) {
291
+ cleaned[stage] = entries;
292
+ continue;
293
+ }
294
+ const patchedEntries = [];
295
+ for (const entry of entries) {
296
+ if (!entry || typeof entry !== 'object') {
297
+ patchedEntries.push(entry);
298
+ continue;
299
+ }
300
+ const e = entry;
301
+ const entryHooks = Array.isArray(e.hooks) ? e.hooks : [];
302
+ const filteredHooks = entryHooks.filter((h) => !(h && typeof h === 'object' && isNirnexHookCommand(h.command)));
303
+ if (filteredHooks.length > 0) {
304
+ patchedEntries.push({ ...e, hooks: filteredHooks });
305
+ }
306
+ // If no hooks remain in this entry, drop the entry entirely
307
+ }
308
+ if (patchedEntries.length > 0) {
309
+ cleaned[stage] = patchedEntries;
310
+ }
311
+ // If no entries remain for this stage, drop the stage entirely
312
+ }
313
+ return cleaned;
314
+ }
315
+ function buildSettingsPatchPreview(hooks) {
316
+ const removed = [];
317
+ for (const [stage, entries] of Object.entries(hooks)) {
318
+ if (!Array.isArray(entries))
319
+ continue;
320
+ for (const entry of entries) {
321
+ if (!entry || typeof entry !== 'object')
322
+ continue;
323
+ const entryHooks = entry.hooks;
324
+ if (!Array.isArray(entryHooks))
325
+ continue;
326
+ for (const h of entryHooks) {
327
+ if (h && typeof h === 'object' && isNirnexHookCommand(h.command)) {
328
+ removed.push(` ${stage} → ${h.command}`);
329
+ }
330
+ }
331
+ }
332
+ }
333
+ return `Will remove Nirnex hook bindings:\n${removed.join('\n')}`;
334
+ }
335
+ // ─── Executor ─────────────────────────────────────────────────────────────
336
+ function executeAction(action) {
337
+ switch (action.type) {
338
+ case 'delete_file':
339
+ try {
340
+ fs.unlinkSync(action.path);
341
+ tick(`Removed ${path.relative(process.cwd(), action.path)}`);
342
+ }
343
+ catch (e) {
344
+ warn(`Failed to remove ${action.path}: ${e instanceof Error ? e.message : String(e)}`);
345
+ }
346
+ break;
347
+ case 'delete_dir':
348
+ try {
349
+ fs.rmSync(action.path, { recursive: true, force: true });
350
+ tick(`Removed ${path.relative(process.cwd(), action.path)}/`);
351
+ }
352
+ catch (e) {
353
+ warn(`Failed to remove ${action.path}: ${e instanceof Error ? e.message : String(e)}`);
354
+ }
355
+ break;
356
+ case 'patch_json': {
357
+ const backupPath = action.path + '.nirnex-backup';
358
+ try {
359
+ fs.copyFileSync(action.path, backupPath);
360
+ const obj = readJsonSafe(action.path);
361
+ if (!obj) {
362
+ warn(`Could not parse ${action.path} — skipping patch`);
363
+ fs.unlinkSync(backupPath);
364
+ break;
365
+ }
366
+ const cleaned = { ...obj };
367
+ if (cleaned.hooks && typeof cleaned.hooks === 'object') {
368
+ const patchedHooks = removeNirnexHooks(cleaned.hooks);
369
+ if (Object.keys(patchedHooks).length === 0) {
370
+ delete cleaned.hooks;
371
+ }
372
+ else {
373
+ cleaned.hooks = patchedHooks;
374
+ }
375
+ }
376
+ const result = JSON.stringify(cleaned, null, 2) + '\n';
377
+ fs.writeFileSync(action.path, result, 'utf8');
378
+ fs.unlinkSync(backupPath);
379
+ tick(`Patched ${path.relative(process.cwd(), action.path)} — Nirnex hooks removed`);
380
+ }
381
+ catch (e) {
382
+ warn(`Failed to patch ${action.path}: ${e instanceof Error ? e.message : String(e)}`);
383
+ // Restore backup if it exists
384
+ if (fs.existsSync(backupPath)) {
385
+ try {
386
+ fs.copyFileSync(backupPath, action.path);
387
+ fs.unlinkSync(backupPath);
388
+ warn(` → Restored backup`);
389
+ }
390
+ catch { }
391
+ }
392
+ }
393
+ break;
394
+ }
395
+ case 'patch_hook': {
396
+ const backupPath = action.path + '.nirnex-backup';
397
+ try {
398
+ const content = fs.readFileSync(action.path, 'utf8');
399
+ fs.copyFileSync(action.path, backupPath);
400
+ const lines = content.split('\n');
401
+ const filtered = lines.filter(l => l.trim() !== POST_COMMIT_LINE);
402
+ // Clean up trailing blank lines left behind
403
+ while (filtered.length > 1 && filtered[filtered.length - 1].trim() === '') {
404
+ filtered.pop();
405
+ }
406
+ filtered.push(''); // Ensure trailing newline
407
+ fs.writeFileSync(action.path, filtered.join('\n'), 'utf8');
408
+ fs.unlinkSync(backupPath);
409
+ tick(`Patched ${path.relative(process.cwd(), action.path)} — removed "nirnex index" line`);
410
+ }
411
+ catch (e) {
412
+ warn(`Failed to patch ${action.path}: ${e instanceof Error ? e.message : String(e)}`);
413
+ if (fs.existsSync(backupPath)) {
414
+ try {
415
+ fs.copyFileSync(backupPath, action.path);
416
+ fs.unlinkSync(backupPath);
417
+ warn(` → Restored backup`);
418
+ }
419
+ catch { }
420
+ }
421
+ }
422
+ break;
423
+ }
424
+ case 'skip':
425
+ skip(`Skipped ${path.relative(process.cwd(), action.path)}`);
426
+ break;
427
+ }
428
+ }
429
+ function cleanupEmptyDirs(cwd) {
430
+ // Remove .claude/hooks/ if empty
431
+ const claudeHooksDir = path.join(cwd, '.claude', 'hooks');
432
+ if (fs.existsSync(claudeHooksDir) && isDirEmpty(claudeHooksDir)) {
433
+ try {
434
+ fs.rmdirSync(claudeHooksDir);
435
+ tick('Removed .claude/hooks/ (empty)');
436
+ }
437
+ catch { }
438
+ }
439
+ // Remove .claude/ if empty
440
+ const claudeDir = path.join(cwd, '.claude');
441
+ if (fs.existsSync(claudeDir) && isDirEmpty(claudeDir)) {
442
+ try {
443
+ fs.rmdirSync(claudeDir);
444
+ tick('Removed .claude/ (empty)');
445
+ }
446
+ catch { }
447
+ }
448
+ // Remove .ai/prompts/ if empty
449
+ const promptsDir = path.join(cwd, '.ai', 'prompts');
450
+ if (fs.existsSync(promptsDir) && isDirEmpty(promptsDir)) {
451
+ try {
452
+ fs.rmdirSync(promptsDir);
453
+ tick('Removed .ai/prompts/ (empty)');
454
+ }
455
+ catch { }
456
+ }
457
+ // Remove .ai/calibration/ if empty
458
+ const calibrationDir = path.join(cwd, '.ai', 'calibration');
459
+ if (fs.existsSync(calibrationDir) && isDirEmpty(calibrationDir)) {
460
+ try {
461
+ fs.rmdirSync(calibrationDir);
462
+ tick('Removed .ai/calibration/ (empty)');
463
+ }
464
+ catch { }
465
+ }
466
+ // Remove .ai/specs/ if empty
467
+ const specsDir = path.join(cwd, '.ai', 'specs');
468
+ if (fs.existsSync(specsDir) && isDirEmpty(specsDir)) {
469
+ try {
470
+ fs.rmdirSync(specsDir);
471
+ tick('Removed .ai/specs/ (empty)');
472
+ }
473
+ catch { }
474
+ }
475
+ // Remove .ai/ if empty
476
+ const aiDir = path.join(cwd, '.ai');
477
+ if (fs.existsSync(aiDir) && isDirEmpty(aiDir)) {
478
+ try {
479
+ fs.rmdirSync(aiDir);
480
+ tick('Removed .ai/ (empty)');
481
+ }
482
+ catch { }
483
+ }
484
+ }
485
+ // ─── Plan display ─────────────────────────────────────────────────────────
486
+ function printPlan(plan, opts) {
487
+ const { actions, manualReview } = plan;
488
+ const cwd = plan.cwd;
489
+ if (actions.length === 0 && manualReview.length === 0) {
490
+ console.log('\n No Nirnex artifacts found in this repository.\n');
491
+ return;
492
+ }
493
+ console.log('\n\x1b[1mRemoval plan:\x1b[0m\n');
494
+ const autoActions = actions.filter(a => !a.requiresConfirmation || opts.yes || opts.force);
495
+ const confirmActions = actions.filter(a => a.requiresConfirmation && !opts.yes && !opts.force);
496
+ if (autoActions.length > 0) {
497
+ console.log(' \x1b[1mWill remove:\x1b[0m');
498
+ for (const a of autoActions) {
499
+ const rel = path.relative(cwd, a.path);
500
+ const suffix = a.type === 'delete_dir' ? '/' : a.type === 'patch_json' || a.type === 'patch_hook' ? ' (patch)' : '';
501
+ console.log(` \x1b[31m✖\x1b[0m ${rel}${suffix}`);
502
+ console.log(` ${a.reason}`);
503
+ }
504
+ console.log('');
505
+ }
506
+ if (confirmActions.length > 0) {
507
+ console.log(' \x1b[1mRequires confirmation:\x1b[0m');
508
+ for (const a of confirmActions) {
509
+ const rel = path.relative(cwd, a.path);
510
+ const suffix = a.type === 'delete_dir' ? '/' : a.type === 'patch_json' || a.type === 'patch_hook' ? ' (patch)' : '';
511
+ console.log(` \x1b[33m?\x1b[0m ${rel}${suffix}`);
512
+ console.log(` ${a.reason}`);
513
+ if (a.preview) {
514
+ for (const line of a.preview.split('\n')) {
515
+ console.log(` \x1b[90m${line}\x1b[0m`);
516
+ }
517
+ }
518
+ }
519
+ console.log('');
520
+ }
521
+ if (manualReview.length > 0) {
522
+ console.log(' \x1b[1mPreserved (manual review):\x1b[0m');
523
+ for (const item of manualReview) {
524
+ const rel = item.replace(cwd + path.sep, '');
525
+ console.log(` \x1b[90m·\x1b[0m ${rel}`);
526
+ }
527
+ console.log('');
528
+ }
529
+ }
530
+ function printJsonPlan(plan, _opts) {
531
+ const output = {
532
+ cwd: plan.cwd,
533
+ actions: plan.actions.map(a => ({
534
+ path: path.relative(plan.cwd, a.path),
535
+ type: a.type,
536
+ confidence: a.confidence,
537
+ reason: a.reason,
538
+ preview: a.preview,
539
+ requiresConfirmation: a.requiresConfirmation,
540
+ })),
541
+ manualReview: plan.manualReview.map(item => {
542
+ // Replace absolute path prefix with relative path
543
+ return item.replace(plan.cwd + path.sep, '');
544
+ }),
545
+ };
546
+ console.log(JSON.stringify(output, null, 2));
547
+ }
548
+ // ─── Main remove logic ────────────────────────────────────────────────────
549
+ async function runRemove(cwd, opts) {
550
+ if (!opts.json) {
551
+ console.log('\n\x1b[1mNirnex Remove\x1b[0m\n');
552
+ }
553
+ // Quick check: is this even a Nirnex-enabled repo?
554
+ const configPath = path.join(cwd, 'nirnex.config.json');
555
+ const hasAiDir = fs.existsSync(path.join(cwd, '.ai'));
556
+ const hasClaudeHooks = fs.existsSync(path.join(cwd, '.claude', 'hooks', 'nirnex-bootstrap.sh'));
557
+ if (!fs.existsSync(configPath) && !hasAiDir && !hasClaudeHooks) {
558
+ if (!opts.json) {
559
+ console.log(' No Nirnex artifacts found in this directory.\n');
560
+ }
561
+ else {
562
+ console.log(JSON.stringify({ cwd, actions: [], manualReview: [], message: 'No Nirnex artifacts found' }));
563
+ }
564
+ return;
565
+ }
566
+ const plan = scanRemovalTargets(cwd, opts);
567
+ if (opts.dryRun) {
568
+ if (opts.json) {
569
+ printJsonPlan(plan, opts);
570
+ }
571
+ else {
572
+ printPlan(plan, opts);
573
+ console.log(' \x1b[33m[dry-run] No changes made.\x1b[0m\n');
574
+ }
575
+ return;
576
+ }
577
+ if (opts.json) {
578
+ printJsonPlan(plan, opts);
579
+ return;
580
+ }
581
+ if (plan.actions.length === 0) {
582
+ console.log(' No Nirnex artifacts to remove.\n');
583
+ if (plan.manualReview.length > 0) {
584
+ console.log(' Items for manual review:');
585
+ for (const item of plan.manualReview) {
586
+ console.log(` · ${item}`);
587
+ }
588
+ console.log('');
589
+ }
590
+ return;
591
+ }
592
+ printPlan(plan, opts);
593
+ // Separate actions needing per-action confirmation
594
+ const immediateActions = plan.actions.filter(a => !a.requiresConfirmation);
595
+ const confirmActions = plan.actions.filter(a => a.requiresConfirmation);
596
+ // Global confirmation (unless --yes)
597
+ if (!opts.yes && !opts.force && immediateActions.length > 0) {
598
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
599
+ try {
600
+ const ok = await promptYesNo(rl, 'Proceed with removal?', true);
601
+ if (!ok) {
602
+ console.log('\n Aborted. No changes made.\n');
603
+ return;
604
+ }
605
+ }
606
+ finally {
607
+ rl.close();
608
+ }
609
+ console.log('');
610
+ }
611
+ // Execute immediate (high-confidence, no per-action confirmation) actions
612
+ for (const action of immediateActions) {
613
+ executeAction(action);
614
+ }
615
+ // Handle actions needing individual confirmation
616
+ if (confirmActions.length > 0) {
617
+ if (opts.yes || opts.force) {
618
+ // Both --yes and --force auto-approve confirmation-requiring actions
619
+ for (const action of confirmActions) {
620
+ executeAction(action);
621
+ }
622
+ }
623
+ else {
624
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
625
+ try {
626
+ for (const action of confirmActions) {
627
+ const rel = path.relative(cwd, action.path);
628
+ console.log(`\n \x1b[33m?\x1b[0m ${rel}`);
629
+ console.log(` ${action.reason}`);
630
+ if (action.preview) {
631
+ for (const line of action.preview.split('\n')) {
632
+ console.log(` \x1b[90m${line}\x1b[0m`);
633
+ }
634
+ }
635
+ const ok = await promptYesNo(rl, ' Apply?', false);
636
+ if (ok) {
637
+ executeAction(action);
638
+ }
639
+ else {
640
+ skip(`Skipped ${rel}`);
641
+ }
642
+ }
643
+ }
644
+ finally {
645
+ rl.close();
646
+ }
647
+ }
648
+ }
649
+ // Clean up empty directories
650
+ cleanupEmptyDirs(cwd);
651
+ // Summary
652
+ console.log('\n\x1b[32m\x1b[1mDone.\x1b[0m');
653
+ if (plan.manualReview.length > 0) {
654
+ console.log('\n Items preserved — review manually if needed:');
655
+ for (const item of plan.manualReview) {
656
+ const rel = item.replace(cwd + path.sep, '');
657
+ console.log(` \x1b[90m·\x1b[0m ${rel}`);
658
+ }
659
+ }
660
+ console.log('');
661
+ }
662
+ // ─── Exported command ─────────────────────────────────────────────────────
663
+ export { runRemove };
664
+ export async function removeCommand(args) {
665
+ const opts = {
666
+ yes: args.includes('--yes') || args.includes('-y'),
667
+ dryRun: args.includes('--dry-run'),
668
+ force: args.includes('--force'),
669
+ keepData: args.includes('--keep-data'),
670
+ keepSpecs: args.includes('--keep-specs'),
671
+ keepClaude: args.includes('--keep-claude'),
672
+ purgeData: args.includes('--purge-data'),
673
+ json: args.includes('--json'),
674
+ };
675
+ const cwd = process.cwd();
676
+ await runRemove(cwd, opts);
677
+ }
678
+ //# sourceMappingURL=remove.js.map