aiwcli 0.10.1 → 0.10.3

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 (110) hide show
  1. package/dist/commands/clean.js +1 -0
  2. package/dist/commands/clear.d.ts +19 -2
  3. package/dist/commands/clear.js +351 -160
  4. package/dist/commands/init/index.d.ts +1 -17
  5. package/dist/commands/init/index.js +19 -104
  6. package/dist/lib/gitignore-manager.d.ts +9 -0
  7. package/dist/lib/gitignore-manager.js +121 -0
  8. package/dist/lib/template-installer.d.ts +7 -12
  9. package/dist/lib/template-installer.js +69 -193
  10. package/dist/lib/template-settings-reconstructor.d.ts +35 -0
  11. package/dist/lib/template-settings-reconstructor.js +130 -0
  12. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  13. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/archive_plan.py +10 -2
  15. package/dist/templates/_shared/hooks/session_end.py +37 -29
  16. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  17. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  18. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  19. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  20. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  21. package/dist/templates/_shared/lib/base/hook_utils.py +8 -10
  22. package/dist/templates/_shared/lib/base/inference.py +51 -62
  23. package/dist/templates/_shared/lib/base/logger.py +35 -21
  24. package/dist/templates/_shared/lib/base/stop_words.py +8 -0
  25. package/dist/templates/_shared/lib/base/utils.py +29 -8
  26. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  27. package/dist/templates/_shared/lib/context/plan_manager.py +101 -2
  28. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -0
  29. package/dist/templates/_shared/lib-ts/base/constants.ts +299 -0
  30. package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -0
  31. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +360 -0
  32. package/dist/templates/_shared/lib-ts/base/inference.ts +245 -0
  33. package/dist/templates/_shared/lib-ts/base/logger.ts +234 -0
  34. package/dist/templates/_shared/lib-ts/base/state-io.ts +114 -0
  35. package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -0
  36. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +23 -0
  37. package/dist/templates/_shared/lib-ts/base/utils.ts +184 -0
  38. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +432 -0
  39. package/dist/templates/_shared/lib-ts/context/context-selector.ts +497 -0
  40. package/dist/templates/_shared/lib-ts/context/context-store.ts +679 -0
  41. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +292 -0
  42. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +181 -0
  43. package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +215 -0
  44. package/dist/templates/_shared/lib-ts/package.json +21 -0
  45. package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -0
  46. package/dist/templates/_shared/lib-ts/templates/plan-context.ts +65 -0
  47. package/dist/templates/_shared/lib-ts/tsconfig.json +13 -0
  48. package/dist/templates/_shared/lib-ts/types.ts +151 -0
  49. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  50. package/dist/templates/_shared/scripts/save_handoff.ts +359 -0
  51. package/dist/templates/_shared/scripts/status_line.py +17 -2
  52. package/dist/templates/cc-native/_cc-native/agents/ARCH-EVOLUTION.md +63 -0
  53. package/dist/templates/cc-native/_cc-native/agents/ARCH-PATTERNS.md +62 -0
  54. package/dist/templates/cc-native/_cc-native/agents/ARCH-STRUCTURE.md +63 -0
  55. package/dist/templates/cc-native/_cc-native/agents/{ASSUMPTION-CHAIN-TRACER.md → ASSUMPTION-TRACER.md} +6 -10
  56. package/dist/templates/cc-native/_cc-native/agents/CLARITY-AUDITOR.md +6 -10
  57. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +74 -1
  58. package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-FEASIBILITY.md +67 -0
  59. package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-GAPS.md +71 -0
  60. package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-ORDERING.md +63 -0
  61. package/dist/templates/cc-native/_cc-native/agents/CONSTRAINT-VALIDATOR.md +73 -0
  62. package/dist/templates/cc-native/_cc-native/agents/DESIGN-ADR-VALIDATOR.md +62 -0
  63. package/dist/templates/cc-native/_cc-native/agents/DESIGN-SCALE-MATCHER.md +65 -0
  64. package/dist/templates/cc-native/_cc-native/agents/DEVILS-ADVOCATE.md +6 -9
  65. package/dist/templates/cc-native/_cc-native/agents/DOCUMENTATION-PHILOSOPHY.md +87 -0
  66. package/dist/templates/cc-native/_cc-native/agents/HANDOFF-READINESS.md +5 -9
  67. package/dist/templates/cc-native/_cc-native/agents/{HIDDEN-COMPLEXITY-DETECTOR.md → HIDDEN-COMPLEXITY.md} +6 -10
  68. package/dist/templates/cc-native/_cc-native/agents/INCREMENTAL-DELIVERY.md +67 -0
  69. package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +91 -18
  70. package/dist/templates/cc-native/_cc-native/agents/RISK-DEPENDENCY.md +63 -0
  71. package/dist/templates/cc-native/_cc-native/agents/RISK-FMEA.md +67 -0
  72. package/dist/templates/cc-native/_cc-native/agents/RISK-PREMORTEM.md +72 -0
  73. package/dist/templates/cc-native/_cc-native/agents/RISK-REVERSIBILITY.md +75 -0
  74. package/dist/templates/cc-native/_cc-native/agents/SCOPE-BOUNDARY.md +78 -0
  75. package/dist/templates/cc-native/_cc-native/agents/SIMPLICITY-GUARDIAN.md +5 -9
  76. package/dist/templates/cc-native/_cc-native/agents/SKEPTIC.md +16 -12
  77. package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-BEHAVIOR-AUDITOR.md +62 -0
  78. package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-CHARACTERIZATION.md +72 -0
  79. package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-FIRST-VALIDATOR.md +62 -0
  80. package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-PYRAMID-ANALYZER.md +62 -0
  81. package/dist/templates/cc-native/_cc-native/agents/TRADEOFF-COSTS.md +68 -0
  82. package/dist/templates/cc-native/_cc-native/agents/TRADEOFF-STAKEHOLDERS.md +66 -0
  83. package/dist/templates/cc-native/_cc-native/agents/VERIFY-COVERAGE.md +75 -0
  84. package/dist/templates/cc-native/_cc-native/agents/VERIFY-STRENGTH.md +70 -0
  85. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  86. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +125 -40
  87. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  88. package/dist/templates/cc-native/_cc-native/lib/utils.py +57 -13
  89. package/dist/templates/cc-native/_cc-native/plan-review.config.json +11 -7
  90. package/oclif.manifest.json +17 -2
  91. package/package.json +1 -1
  92. package/dist/lib/template-merger.d.ts +0 -47
  93. package/dist/lib/template-merger.js +0 -162
  94. package/dist/templates/cc-native/_cc-native/agents/ACCESSIBILITY-TESTER.md +0 -79
  95. package/dist/templates/cc-native/_cc-native/agents/ARCHITECT-REVIEWER.md +0 -48
  96. package/dist/templates/cc-native/_cc-native/agents/CODE-REVIEWER.md +0 -70
  97. package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-CHECKER.md +0 -59
  98. package/dist/templates/cc-native/_cc-native/agents/CONTEXT-EXTRACTOR.md +0 -92
  99. package/dist/templates/cc-native/_cc-native/agents/DOCUMENTATION-REVIEWER.md +0 -51
  100. package/dist/templates/cc-native/_cc-native/agents/FEASIBILITY-ANALYST.md +0 -57
  101. package/dist/templates/cc-native/_cc-native/agents/FRESH-PERSPECTIVE.md +0 -54
  102. package/dist/templates/cc-native/_cc-native/agents/INCENTIVE-MAPPER.md +0 -61
  103. package/dist/templates/cc-native/_cc-native/agents/PENETRATION-TESTER.md +0 -79
  104. package/dist/templates/cc-native/_cc-native/agents/PERFORMANCE-ENGINEER.md +0 -75
  105. package/dist/templates/cc-native/_cc-native/agents/PRECEDENT-FINDER.md +0 -70
  106. package/dist/templates/cc-native/_cc-native/agents/REVERSIBILITY-ANALYST.md +0 -61
  107. package/dist/templates/cc-native/_cc-native/agents/RISK-ASSESSOR.md +0 -58
  108. package/dist/templates/cc-native/_cc-native/agents/SECOND-ORDER-ANALYST.md +0 -61
  109. package/dist/templates/cc-native/_cc-native/agents/STAKEHOLDER-ADVOCATE.md +0 -55
  110. package/dist/templates/cc-native/_cc-native/agents/TRADE-OFF-ILLUMINATOR.md +0 -204
@@ -3,6 +3,9 @@ import { join } from 'node:path';
3
3
  import { confirm } from '@inquirer/prompts';
4
4
  import { Flags } from '@oclif/core';
5
5
  import BaseCommand from '../lib/base-command.js';
6
+ import { pruneGitignoreStaleEntries } from '../lib/gitignore-manager.js';
7
+ import { pathExists } from '../lib/paths.js';
8
+ import { reconstructIdeSettings } from '../lib/template-settings-reconstructor.js';
6
9
  import { EXIT_CODES } from '../types/exit-codes.js';
7
10
  /**
8
11
  * Container folder for method-specific files
@@ -15,20 +18,59 @@ const AIWCLI_CONTAINER = '.aiwcli';
15
18
  */
16
19
  const OUTPUT_FOLDER_NAME = '_output';
17
20
  /**
18
- * IDE configuration folder names and their method subfolder structure
21
+ * IDE configuration folder names and settings file locations.
22
+ * Method subfolders are discovered dynamically via disk scanning.
19
23
  */
20
24
  const IDE_FOLDERS = {
21
25
  claude: {
22
26
  root: '.claude',
23
- methodSubfolders: ['commands', 'skills', 'agents'],
24
27
  settingsFile: 'settings.json',
25
28
  },
26
29
  windsurf: {
27
30
  root: '.windsurf',
28
- methodSubfolders: ['workflows'],
29
31
  settingsFile: 'hooks.json',
30
32
  },
31
33
  };
34
+ /**
35
+ * Get the set of installed method names by combining the settings.json registry
36
+ * with disk scan of .aiwcli/_* directories.
37
+ *
38
+ * @param targetDir - Directory containing the .aiwcli container
39
+ * @returns Set of method names (e.g., 'cc-native', 'bmad')
40
+ */
41
+ async function getInstalledMethodNames(targetDir) {
42
+ const methods = new Set();
43
+ // Source 1: settings.json methods registry
44
+ for (const ide of Object.values(IDE_FOLDERS)) {
45
+ const settingsPath = join(targetDir, ide.root, ide.settingsFile);
46
+ try {
47
+ const content = await fs.readFile(settingsPath, 'utf8');
48
+ const settings = JSON.parse(content);
49
+ if (settings.methods && typeof settings.methods === 'object') {
50
+ for (const method of Object.keys(settings.methods)) {
51
+ methods.add(method);
52
+ }
53
+ }
54
+ }
55
+ catch {
56
+ // Settings file doesn't exist or can't be parsed
57
+ }
58
+ }
59
+ // Source 2: disk scan of .aiwcli/_* directories
60
+ const containerDir = join(targetDir, AIWCLI_CONTAINER);
61
+ try {
62
+ const entries = await fs.readdir(containerDir, { withFileTypes: true });
63
+ for (const entry of entries) {
64
+ if (entry.isDirectory() && entry.name.startsWith('_') && entry.name !== OUTPUT_FOLDER_NAME) {
65
+ methods.add(entry.name.slice(1)); // strip leading underscore
66
+ }
67
+ }
68
+ }
69
+ catch {
70
+ // Container doesn't exist
71
+ }
72
+ return methods;
73
+ }
32
74
  /**
33
75
  * AIW gitignore section header marker
34
76
  */
@@ -75,12 +117,11 @@ async function isSettingsFileEmpty(filePath) {
75
117
  * Check if an IDE folder should be fully deleted.
76
118
  * Returns true if:
77
119
  * 1. The settings file is empty (or doesn't exist)
78
- * 2. All method subfolders are empty (or don't exist)
120
+ * 2. All subdirectories are empty (or don't exist)
79
121
  * Backup files (e.g., settings.json.backup) are ignored.
80
122
  *
81
123
  * @param targetDir - Directory containing the IDE folder
82
124
  * @param ideFolder - IDE folder configuration
83
- * @param ideFolder.methodSubfolders - List of method subfolder names to check
84
125
  * @param ideFolder.root - Root folder name (e.g., '.claude')
85
126
  * @param ideFolder.settingsFile - Settings file name (e.g., 'settings.json')
86
127
  * @returns True if the IDE folder should be fully deleted
@@ -104,25 +145,15 @@ async function shouldDeleteIdeFolder(targetDir, ideFolder) {
104
145
  if (!settingsEmpty) {
105
146
  return false;
106
147
  }
107
- // Check if all method subfolders are empty (in parallel)
108
- const subfolderChecks = await Promise.all(ideFolder.methodSubfolders.map(async (subfolder) => {
109
- const subfolderPath = join(ideFolderPath, subfolder);
110
- return isDirectoryEmpty(subfolderPath);
111
- }));
112
- if (subfolderChecks.some((isEmpty) => !isEmpty)) {
113
- return false;
114
- }
115
148
  // Check the IDE folder itself - ignore backup files and check for other meaningful content
116
149
  try {
117
150
  const entries = await fs.readdir(ideFolderPath);
118
- // Filter entries to check (skip backup files, settings file, and known subfolders)
151
+ // Filter entries to check (skip backup files and settings file)
119
152
  const entriesToCheck = entries.filter((entry) => {
120
153
  if (entry.endsWith('.backup'))
121
154
  return false;
122
155
  if (entry === ideFolder.settingsFile)
123
156
  return false;
124
- if (ideFolder.methodSubfolders.includes(entry))
125
- return false;
126
157
  return true;
127
158
  });
128
159
  // Check all entries in parallel
@@ -276,101 +307,6 @@ function cleanupGitignoreContent(content) {
276
307
  }
277
308
  return result;
278
309
  }
279
- /**
280
- * Update IDE settings file to remove method-specific entries.
281
- * Creates a backup before modifying.
282
- *
283
- * @param targetDir - Directory containing the IDE folder
284
- * @param ideFolder - IDE folder configuration (e.g., IDE_FOLDERS.claude)
285
- * @param ideFolder.root - Root folder name (e.g., '.claude')
286
- * @param ideFolder.settingsFile - Settings file name (e.g., 'settings.json')
287
- * @param methodsToRemove - Method names to remove from settings
288
- */
289
- async function updateIdeSettings(targetDir, ideFolder, methodsToRemove) {
290
- const settingsPath = join(targetDir, ideFolder.root, ideFolder.settingsFile);
291
- const result = { backedUp: false, updated: false };
292
- try {
293
- const content = await fs.readFile(settingsPath, 'utf8');
294
- const settings = JSON.parse(content);
295
- // Create backup before modifying
296
- const backupPath = `${settingsPath}.backup`;
297
- await fs.writeFile(backupPath, content, 'utf8');
298
- result.backedUp = true;
299
- // Remove method-specific entries from methods tracking object
300
- let modified = false;
301
- if (settings.methods && typeof settings.methods === 'object') {
302
- for (const method of methodsToRemove) {
303
- if (method in settings.methods) {
304
- delete settings.methods[method];
305
- modified = true;
306
- }
307
- }
308
- // Remove methods object if empty
309
- if (Object.keys(settings.methods).length === 0) {
310
- delete settings.methods;
311
- modified = true;
312
- }
313
- }
314
- // Remove method-specific hooks from hooks array
315
- if (settings.hooks && typeof settings.hooks === 'object') {
316
- for (const hookType of Object.keys(settings.hooks)) {
317
- const hookArray = settings.hooks[hookType];
318
- if (Array.isArray(hookArray)) {
319
- const filtered = hookArray.filter((hook) => {
320
- // Check if hook command references any of the methods to remove
321
- if (hook.hooks && Array.isArray(hook.hooks)) {
322
- const filteredInner = hook.hooks.filter((innerHook) => {
323
- if (typeof innerHook.command === 'string') {
324
- return !methodsToRemove.some((method) => innerHook.command?.toString().includes(`_${method}/`) ||
325
- innerHook.command?.toString().includes(`/${method}-`));
326
- }
327
- return true;
328
- });
329
- if (filteredInner.length !== hook.hooks.length) {
330
- hook.hooks = filteredInner;
331
- modified = true;
332
- }
333
- // Remove hook entry if all inner hooks were removed
334
- if (filteredInner.length === 0) {
335
- return false;
336
- }
337
- }
338
- return true;
339
- });
340
- if (filtered.length !== hookArray.length) {
341
- settings.hooks[hookType] = filtered;
342
- modified = true;
343
- }
344
- // Remove hook type if empty
345
- if (filtered.length === 0) {
346
- delete settings.hooks[hookType];
347
- modified = true;
348
- }
349
- }
350
- }
351
- // Remove hooks object if empty
352
- if (Object.keys(settings.hooks).length === 0) {
353
- delete settings.hooks;
354
- modified = true;
355
- }
356
- }
357
- if (modified) {
358
- // Write updated settings
359
- const newContent = JSON.stringify(settings, null, 2) + '\n';
360
- await fs.writeFile(settingsPath, newContent, 'utf8');
361
- result.updated = true;
362
- }
363
- else {
364
- // No changes needed, remove backup
365
- await fs.unlink(backupPath);
366
- result.backedUp = false;
367
- }
368
- }
369
- catch {
370
- // Settings file doesn't exist or can't be read
371
- }
372
- return result;
373
- }
374
310
  /**
375
311
  * Clear workflow folders, output folders, IDE method folders, and update configurations.
376
312
  */
@@ -382,6 +318,8 @@ export default class ClearCommand extends BaseCommand {
382
318
  '<%= config.bin %> <%= command.id %> -t cc-native',
383
319
  '<%= config.bin %> <%= command.id %> --dry-run',
384
320
  '<%= config.bin %> <%= command.id %> --force',
321
+ '<%= config.bin %> <%= command.id %> --output',
322
+ '<%= config.bin %> <%= command.id %> --output --dry-run',
385
323
  ];
386
324
  static flags = {
387
325
  ...BaseCommand.baseFlags,
@@ -395,14 +333,26 @@ export default class ClearCommand extends BaseCommand {
395
333
  description: 'Skip confirmation prompt',
396
334
  default: false,
397
335
  }),
336
+ output: Flags.boolean({
337
+ char: 'o',
338
+ description: 'Clean runtime output artifacts (temp files, caches, log rotation, archives)',
339
+ default: false,
340
+ exclusive: ['template'],
341
+ }),
398
342
  template: Flags.string({
399
343
  char: 't',
400
344
  description: 'Clear only a specific template (e.g., cc-native)',
345
+ exclusive: ['output'],
401
346
  }),
402
347
  };
403
348
  async run() {
404
349
  const { flags } = await this.parse(ClearCommand);
405
350
  const targetDir = process.cwd();
351
+ // Handle --output flag separately (mutually exclusive with --template)
352
+ if (flags.output) {
353
+ await this.cleanRuntimeOutput(targetDir, flags);
354
+ return;
355
+ }
406
356
  try {
407
357
  // Find all folders to clear
408
358
  const workflowFolders = await this.findWorkflowFolders(targetDir, flags.template);
@@ -473,25 +423,36 @@ export default class ClearCommand extends BaseCommand {
473
423
  catch {
474
424
  return false;
475
425
  }
476
- // Count existing method folders vs folders being deleted (in parallel)
477
- const subfolderResults = await Promise.all(ideFolder.methodSubfolders.map(async (subfolder) => {
478
- const subfolderPath = join(idePath, subfolder);
479
- try {
480
- const entries = await fs.readdir(subfolderPath, { withFileTypes: true });
481
- const methodDirs = entries.filter((e) => e.isDirectory());
482
- const beingDeleted = methodDirs.filter((entry) => {
483
- const fullPath = join(subfolderPath, entry.name);
484
- return ideMethodFolders.includes(fullPath);
485
- }).length;
486
- return { beingDeleted, total: methodDirs.length };
487
- }
488
- catch {
489
- // Subfolder doesn't exist
490
- return { beingDeleted: 0, total: 0 };
426
+ // Scan all subdirectories to count method folders vs folders being deleted
427
+ let totalMethodFolders = 0;
428
+ let foldersBeingDeleted = 0;
429
+ try {
430
+ const topEntries = await fs.readdir(idePath, { withFileTypes: true });
431
+ const subdirs = topEntries.filter((e) => e.isDirectory());
432
+ // Check each subdirectory for method folders
433
+ const subResults = await Promise.all(subdirs.map(async (subdir) => {
434
+ const subdirPath = join(idePath, subdir.name);
435
+ try {
436
+ const entries = await fs.readdir(subdirPath, { withFileTypes: true });
437
+ const methodDirs = entries.filter((e) => e.isDirectory());
438
+ const deleted = methodDirs.filter((entry) => {
439
+ const fullPath = join(subdirPath, entry.name);
440
+ return ideMethodFolders.includes(fullPath);
441
+ }).length;
442
+ return { deleted, total: methodDirs.length };
443
+ }
444
+ catch {
445
+ return { deleted: 0, total: 0 };
446
+ }
447
+ }));
448
+ for (const r of subResults) {
449
+ totalMethodFolders += r.total;
450
+ foldersBeingDeleted += r.deleted;
491
451
  }
492
- }));
493
- const totalMethodFolders = subfolderResults.reduce((sum, r) => sum + r.total, 0);
494
- const foldersBeingDeleted = subfolderResults.reduce((sum, r) => sum + r.beingDeleted, 0);
452
+ }
453
+ catch {
454
+ return false;
455
+ }
495
456
  // If all method folders are being deleted, check if settings would be empty
496
457
  if (totalMethodFolders > 0 && totalMethodFolders === foldersBeingDeleted) {
497
458
  // Check if settings file would become empty after removing methods
@@ -605,19 +566,39 @@ export default class ClearCommand extends BaseCommand {
605
566
  await updateGitignoreAfterClear(targetDir, [AIWCLI_CONTAINER]);
606
567
  this.logDebug('Updated .gitignore');
607
568
  }
608
- // Update IDE settings files to remove method-specific entries
569
+ // Prune stale gitignore entries (paths that no longer exist on disk)
570
+ const pruned = await pruneGitignoreStaleEntries(targetDir);
571
+ if (pruned) {
572
+ this.logDebug('Pruned stale .gitignore entries');
573
+ }
574
+ // Reconstruct IDE settings from remaining templates
609
575
  let updatedClaudeSettings = false;
610
576
  let updatedWindsurfSettings = false;
611
577
  if (methodsToRemove.length > 0) {
612
- const claudeResult = await updateIdeSettings(targetDir, IDE_FOLDERS.claude, methodsToRemove);
613
- if (claudeResult.updated) {
614
- this.logDebug('Updated .claude/settings.json (backup created)');
615
- updatedClaudeSettings = true;
578
+ // Remove method entries from settings files first
579
+ await this.removeMethodEntries(targetDir, methodsToRemove);
580
+ // Get remaining installed methods
581
+ const allMethods = await getInstalledMethodNames(targetDir);
582
+ // Filter out methods being removed (in case disk scan still finds them)
583
+ const remainingTemplates = [...allMethods].filter(m => !methodsToRemove.includes(m));
584
+ // Determine which IDEs need reconstruction
585
+ const ides = [];
586
+ if (await pathExists(join(targetDir, IDE_FOLDERS.claude.root))) {
587
+ ides.push('claude');
616
588
  }
617
- const windsurfResult = await updateIdeSettings(targetDir, IDE_FOLDERS.windsurf, methodsToRemove);
618
- if (windsurfResult.updated) {
619
- this.logDebug('Updated .windsurf/hooks.json (backup created)');
620
- updatedWindsurfSettings = true;
589
+ if (await pathExists(join(targetDir, IDE_FOLDERS.windsurf.root))) {
590
+ ides.push('windsurf');
591
+ }
592
+ if (ides.length > 0) {
593
+ await reconstructIdeSettings(targetDir, remainingTemplates, ides);
594
+ if (ides.includes('claude')) {
595
+ this.logDebug('Reconstructed .claude/settings.json (backup created)');
596
+ updatedClaudeSettings = true;
597
+ }
598
+ if (ides.includes('windsurf')) {
599
+ this.logDebug('Reconstructed .windsurf/hooks.json (backup created)');
600
+ updatedWindsurfSettings = true;
601
+ }
621
602
  }
622
603
  }
623
604
  // Check if IDE folders should be fully deleted (empty settings + empty subfolders)
@@ -674,7 +655,7 @@ export default class ClearCommand extends BaseCommand {
674
655
  parts.push(`${IDE_FOLDERS.windsurf.root}/ folder`);
675
656
  }
676
657
  this.logSuccess(`Cleared: ${parts.join(', ')}.`);
677
- if (removedAiwcliContainer) {
658
+ if (removedAiwcliContainer || pruned) {
678
659
  this.logSuccess('Updated .gitignore.');
679
660
  }
680
661
  if (updatedClaudeSettings) {
@@ -696,6 +677,178 @@ export default class ClearCommand extends BaseCommand {
696
677
  });
697
678
  }
698
679
  }
680
+ /**
681
+ * Clean runtime output artifacts from _output/ at project root.
682
+ * Handles temp files, cache files, log rotation, and archive cleanup.
683
+ *
684
+ * @param targetDir - Project root directory
685
+ * @param flags - Command flags (dry-run, force)
686
+ */
687
+ async cleanRuntimeOutput(targetDir, flags) {
688
+ const outputDir = join(targetDir, '_output');
689
+ if (!(await pathExists(outputDir))) {
690
+ this.logInfo('No _output/ directory found.');
691
+ return;
692
+ }
693
+ const toDelete = [];
694
+ let logAction = null;
695
+ let archiveDir = null;
696
+ let archiveCount = 0;
697
+ try {
698
+ const entries = await fs.readdir(outputDir, { withFileTypes: true });
699
+ for (const entry of entries) {
700
+ const entryPath = join(outputDir, entry.name);
701
+ // Temp files: .index_*.tmp (orphaned atomic write files)
702
+ if (entry.isFile() && entry.name.startsWith('.index_') && entry.name.endsWith('.tmp')) {
703
+ toDelete.push({ path: entryPath, reason: 'temp file' });
704
+ continue;
705
+ }
706
+ // Cache files: .*-cache.json
707
+ if (entry.isFile() && entry.name.startsWith('.') && entry.name.endsWith('-cache.json')) {
708
+ toDelete.push({ path: entryPath, reason: 'cache file' });
709
+ continue;
710
+ }
711
+ // Log rotation: hook-log.jsonl > 1MB
712
+ if (entry.isFile() && entry.name === 'hook-log.jsonl') {
713
+ try {
714
+ const stat = await fs.stat(entryPath);
715
+ if (stat.size > 1_048_576) {
716
+ logAction = { path: entryPath, sizeBytes: stat.size };
717
+ }
718
+ }
719
+ catch {
720
+ // Can't stat log file
721
+ }
722
+ continue;
723
+ }
724
+ // Archive cleanup: contexts/_archive/
725
+ if (entry.isDirectory() && entry.name === 'contexts') {
726
+ const archivePath = join(entryPath, '_archive');
727
+ try {
728
+ const archiveEntries = await fs.readdir(archivePath);
729
+ if (archiveEntries.length > 0) {
730
+ archiveDir = archivePath;
731
+ archiveCount = archiveEntries.length;
732
+ }
733
+ }
734
+ catch {
735
+ // No archive directory
736
+ }
737
+ }
738
+ }
739
+ }
740
+ catch (error) {
741
+ const err = error;
742
+ this.error(`Cannot read _output/: ${err.message}`, {
743
+ exit: EXIT_CODES.GENERAL_ERROR,
744
+ });
745
+ }
746
+ // Nothing to clean
747
+ if (toDelete.length === 0 && !logAction && !archiveDir) {
748
+ this.logInfo('No runtime output artifacts to clean.');
749
+ return;
750
+ }
751
+ // Show what will be cleaned
752
+ this.log('');
753
+ this.logInfo('Runtime output cleanup:');
754
+ if (toDelete.length > 0) {
755
+ for (const item of toDelete) {
756
+ const relativePath = item.path.replace(targetDir + '\\', '').replace(targetDir + '/', '');
757
+ this.log(` ${relativePath} (${item.reason})`);
758
+ }
759
+ }
760
+ if (logAction) {
761
+ const sizeMB = (logAction.sizeBytes / 1_048_576).toFixed(1);
762
+ this.log(` _output/hook-log.jsonl (${sizeMB}MB → truncate to ~512KB)`);
763
+ }
764
+ if (archiveDir) {
765
+ this.log(` _output/contexts/_archive/ (${archiveCount} archived context(s))`);
766
+ }
767
+ this.log('');
768
+ // Dry run
769
+ if (flags['dry-run']) {
770
+ this.logInfo('Dry run complete. No files were modified.');
771
+ return;
772
+ }
773
+ // Confirm archive deletion (unless --force)
774
+ if (archiveDir && !flags.force) {
775
+ const shouldDelete = await confirm({
776
+ message: `Delete ${archiveCount} archived context(s)?`,
777
+ default: false,
778
+ });
779
+ if (!shouldDelete) {
780
+ archiveDir = null;
781
+ archiveCount = 0;
782
+ }
783
+ }
784
+ // Execute deletions
785
+ let deletedCount = 0;
786
+ for (const item of toDelete) {
787
+ try {
788
+ await fs.unlink(item.path);
789
+ deletedCount++;
790
+ }
791
+ catch (error) {
792
+ const err = error;
793
+ this.logWarning(`Failed to delete ${item.path}: ${err.message}`);
794
+ }
795
+ }
796
+ // Log rotation
797
+ if (logAction) {
798
+ try {
799
+ const content = await fs.readFile(logAction.path, 'utf8');
800
+ // Keep the most recent 512KB
801
+ const truncated = content.slice(-524_288);
802
+ // Find the first complete line
803
+ const firstNewline = truncated.indexOf('\n');
804
+ const cleaned = firstNewline === -1 ? truncated : truncated.slice(firstNewline + 1);
805
+ await fs.writeFile(logAction.path, cleaned, 'utf8');
806
+ this.logDebug('Rotated hook-log.jsonl');
807
+ }
808
+ catch (error) {
809
+ const err = error;
810
+ this.logWarning(`Failed to rotate log: ${err.message}`);
811
+ }
812
+ }
813
+ // Archive cleanup
814
+ let archivedCleaned = 0;
815
+ if (archiveDir) {
816
+ try {
817
+ const archiveEntries = await fs.readdir(archiveDir);
818
+ await Promise.all(archiveEntries.map(async (entry) => {
819
+ try {
820
+ await fs.rm(join(archiveDir, entry), { force: true, recursive: true });
821
+ archivedCleaned++;
822
+ }
823
+ catch {
824
+ // Individual entry failed
825
+ }
826
+ }));
827
+ }
828
+ catch (error) {
829
+ const err = error;
830
+ this.logWarning(`Failed to clean archive: ${err.message}`);
831
+ }
832
+ }
833
+ // Summary
834
+ this.log('');
835
+ const parts = [];
836
+ if (deletedCount > 0) {
837
+ parts.push(`${deletedCount} file(s) removed`);
838
+ }
839
+ if (logAction) {
840
+ parts.push('log rotated');
841
+ }
842
+ if (archivedCleaned > 0) {
843
+ parts.push(`${archivedCleaned} archived context(s) removed`);
844
+ }
845
+ if (parts.length > 0) {
846
+ this.logSuccess(`Output cleanup: ${parts.join(', ')}.`);
847
+ }
848
+ else {
849
+ this.logInfo('No changes made.');
850
+ }
851
+ }
699
852
  /**
700
853
  * Extract method names from workflow folder names (e.g., _gsd -> gsd).
701
854
  *
@@ -713,40 +866,49 @@ export default class ClearCommand extends BaseCommand {
713
866
  return methods;
714
867
  }
715
868
  /**
716
- * Find all IDE method folders (e.g., .claude/commands/{method}/, .claude/skills/{method}/).
717
- * Searches within IDE configuration folders for method-specific subfolders.
869
+ * Find all IDE method folders by scanning subdirectories of each IDE root
870
+ * for children matching installed method names.
871
+ *
872
+ * For example, finds .claude/commands/cc-native/, .claude/skills/cc-native/,
873
+ * .windsurf/workflows/cc-native/ — without hardcoding which subdirectories exist.
718
874
  *
719
875
  * @param targetDir - Directory to search in
720
876
  * @param template - Optional template/method name to filter by
721
877
  * @returns Array of IDE method folder paths
722
878
  */
723
879
  async findIdeMethodFolders(targetDir, template) {
724
- // Build list of all subfolder paths to check
725
- const subfolderChecks = [];
726
- for (const ide of Object.values(IDE_FOLDERS)) {
727
- const ideRoot = join(targetDir, ide.root);
728
- for (const subfolder of ide.methodSubfolders) {
729
- subfolderChecks.push({ ideRoot, subfolder });
730
- }
880
+ // Build method set: from --template flag, or from installed methods
881
+ const methodNames = template ? new Set([template]) : await getInstalledMethodNames(targetDir);
882
+ if (methodNames.size === 0) {
883
+ return [];
731
884
  }
732
- // Check all subfolders in parallel
733
- const subfolderResults = await Promise.all(subfolderChecks.map(async ({ ideRoot, subfolder }) => {
734
- const subfolderPath = join(ideRoot, subfolder);
885
+ // For each IDE root, scan all subdirectories for children matching method names
886
+ const ideRoots = Object.values(IDE_FOLDERS).map((ide) => join(targetDir, ide.root));
887
+ const ideResults = await Promise.all(ideRoots.map(async (ideRoot) => {
888
+ // Get all subdirectories of IDE root (e.g., .claude/commands/, .claude/skills/)
735
889
  try {
736
- const stat = await fs.stat(subfolderPath);
737
- if (!stat.isDirectory())
738
- return [];
739
- const entries = await fs.readdir(subfolderPath, { withFileTypes: true });
740
- return entries
741
- .filter((entry) => entry.isDirectory())
742
- .filter((entry) => !template || entry.name === template)
743
- .map((entry) => join(subfolderPath, entry.name));
890
+ const topEntries = await fs.readdir(ideRoot, { withFileTypes: true });
891
+ const subdirs = topEntries.filter((e) => e.isDirectory());
892
+ // For each subdirectory, check for method-named children
893
+ const subResults = await Promise.all(subdirs.map(async (subdir) => {
894
+ const subdirPath = join(ideRoot, subdir.name);
895
+ try {
896
+ const entries = await fs.readdir(subdirPath, { withFileTypes: true });
897
+ return entries
898
+ .filter((entry) => entry.isDirectory() && methodNames.has(entry.name))
899
+ .map((entry) => join(subdirPath, entry.name));
900
+ }
901
+ catch {
902
+ return [];
903
+ }
904
+ }));
905
+ return subResults.flat();
744
906
  }
745
907
  catch {
746
908
  return [];
747
909
  }
748
910
  }));
749
- return subfolderResults.flat();
911
+ return ideResults.flat();
750
912
  }
751
913
  /**
752
914
  * Find all output folders in the target directory.
@@ -832,4 +994,33 @@ export default class ClearCommand extends BaseCommand {
832
994
  }
833
995
  return foundFolders;
834
996
  }
997
+ /**
998
+ * Remove method entries from IDE settings files (methods tracking only).
999
+ * Settings reconstruction handles hooks/permissions; this only strips the methods object.
1000
+ */
1001
+ async removeMethodEntries(targetDir, methodsToRemove) {
1002
+ const ops = Object.values(IDE_FOLDERS).map(async (ide) => {
1003
+ const settingsPath = join(targetDir, ide.root, ide.settingsFile);
1004
+ try {
1005
+ const content = await fs.readFile(settingsPath, 'utf8');
1006
+ const settings = JSON.parse(content);
1007
+ if (settings.methods && typeof settings.methods === 'object') {
1008
+ for (const method of methodsToRemove) {
1009
+ if (method in settings.methods) {
1010
+ delete settings.methods[method];
1011
+ }
1012
+ }
1013
+ if (Object.keys(settings.methods).length === 0) {
1014
+ delete settings.methods;
1015
+ }
1016
+ // Write back with methods removed (backup created by reconstructor)
1017
+ await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
1018
+ }
1019
+ }
1020
+ catch {
1021
+ // Settings file doesn't exist or can't be read
1022
+ }
1023
+ });
1024
+ await Promise.all(ops);
1025
+ }
835
1026
  }
@@ -29,28 +29,12 @@ export default class Init extends BaseCommand {
29
29
  * @returns Template description
30
30
  */
31
31
  private getTemplateDescription;
32
- /**
33
- * Merge settings from multiple method templates into project settings.
34
- * Processes methods in order, allowing later methods to override earlier ones.
35
- *
36
- * @param targetDir - Project directory
37
- * @param methods - Array of method names to merge (e.g., ['_shared', 'cc-native'])
38
- * @param ides - IDEs being configured (for IDE-specific merging)
39
- */
40
- private mergeMethodsSettings;
41
- /**
42
- * Merge Windsurf template hooks into project hooks
43
- *
44
- * @param targetDir - Project directory
45
- * @param templatePath - Template source path
46
- */
47
- private mergeWindsurfTemplateHooks;
48
32
  /**
49
33
  * Perform post-installation actions.
50
34
  *
51
35
  * Handles:
52
36
  * - Method tracking in settings.json
53
- * - Settings/hooks merging for all methods
37
+ * - Settings reconstruction from all active templates
54
38
  * - .gitignore updates
55
39
  *
56
40
  * @param config - Post-install configuration