aiwcli 0.12.8 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/commands/clean.d.ts +7 -0
  2. package/dist/commands/clean.js +17 -8
  3. package/dist/commands/clear.d.ts +85 -0
  4. package/dist/commands/clear.js +455 -347
  5. package/dist/commands/init/index.d.ts +15 -0
  6. package/dist/commands/init/index.js +79 -38
  7. package/dist/lib/gitignore-manager.js +12 -13
  8. package/dist/lib/settings-hierarchy.d.ts +13 -1
  9. package/dist/lib/settings-hierarchy.js +1 -1
  10. package/dist/lib/template-linter.d.ts +4 -0
  11. package/dist/lib/template-linter.js +1 -1
  12. package/dist/lib/tty-detection.d.ts +1 -0
  13. package/dist/lib/tty-detection.js +1 -0
  14. package/dist/templates/CLAUDE.md +1 -1
  15. package/dist/templates/_shared/.claude/settings.json +64 -9
  16. package/dist/templates/_shared/.claude/skills/handoff/SKILL.md +1 -1
  17. package/dist/templates/_shared/.claude/skills/handoff-resume/SKILL.md +1 -1
  18. package/dist/templates/_shared/.claude/skills/meta-plan/SKILL.md +43 -0
  19. package/dist/templates/_shared/.codex/workflows/handoff.md +1 -1
  20. package/dist/templates/_shared/.codex/workflows/meta-plan.md +347 -0
  21. package/dist/templates/_shared/.windsurf/workflows/handoff.md +4 -221
  22. package/dist/templates/_shared/.windsurf/workflows/meta-plan.md +11 -0
  23. package/dist/templates/_shared/hooks-ts/context_monitor.ts +2 -2
  24. package/dist/templates/_shared/hooks-ts/lint_after_edit.ts +59 -0
  25. package/dist/templates/_shared/hooks-ts/session_end.ts +11 -10
  26. package/dist/templates/_shared/hooks-ts/session_start.ts +15 -12
  27. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +12 -12
  28. package/dist/templates/_shared/lib-ts/CLAUDE.md +27 -2
  29. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +26 -7
  30. package/dist/templates/_shared/lib-ts/base/inference.ts +16 -16
  31. package/dist/templates/_shared/lib-ts/base/lint-dispatch.ts +339 -0
  32. package/dist/templates/_shared/lib-ts/base/state-io.ts +4 -3
  33. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +3 -3
  34. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +16 -15
  35. package/dist/templates/_shared/lib-ts/context/context-selector.ts +16 -16
  36. package/dist/templates/_shared/lib-ts/context/context-store.ts +15 -14
  37. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +2 -2
  38. package/dist/templates/_shared/scripts/resolve-run.ts +62 -0
  39. package/dist/templates/_shared/scripts/resolve_context.ts +1 -1
  40. package/dist/templates/_shared/scripts/status_line.ts +74 -65
  41. package/dist/templates/_shared/{handoff-system → skills/handoff-system}/CLAUDE.md +14 -14
  42. package/dist/templates/_shared/{handoff-system → skills/handoff-system}/lib/document-generator.ts +10 -9
  43. package/dist/templates/_shared/{handoff-system → skills/handoff-system}/lib/handoff-reader.ts +5 -4
  44. package/dist/templates/_shared/{handoff-system → skills/handoff-system}/scripts/resume_handoff.ts +6 -6
  45. package/dist/templates/_shared/{handoff-system → skills/handoff-system}/scripts/save_handoff.ts +19 -20
  46. package/dist/templates/_shared/{handoff-system → skills/handoff-system}/workflows/handoff-resume.md +2 -2
  47. package/dist/templates/_shared/{handoff-system → skills/handoff-system}/workflows/handoff.md +5 -5
  48. package/dist/templates/_shared/skills/meta-plan/CLAUDE.md +46 -0
  49. package/dist/templates/_shared/skills/meta-plan/workflows/meta-plan.md +277 -0
  50. package/dist/templates/cc-native/.claude/settings.json +13 -111
  51. package/dist/templates/cc-native/_cc-native/artifacts/lib/format.ts +22 -20
  52. package/dist/templates/cc-native/_cc-native/artifacts/lib/index.ts +11 -11
  53. package/dist/templates/cc-native/_cc-native/artifacts/lib/tracker.ts +7 -6
  54. package/dist/templates/cc-native/_cc-native/artifacts/lib/write.ts +17 -16
  55. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +32 -2
  56. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +9 -7
  57. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +25 -0
  58. package/dist/templates/cc-native/_cc-native/hooks/validate_task_prompt.ts +2 -2
  59. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +15 -16
  60. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +19 -19
  61. package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +3 -3
  62. package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +6 -1
  63. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +16 -12
  64. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +2 -3
  65. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +31 -31
  66. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +7 -6
  67. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +9 -7
  68. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +17 -14
  69. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +41 -37
  70. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +43 -33
  71. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +20 -20
  72. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +9 -8
  73. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +4 -3
  74. package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +8 -9
  75. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +20 -19
  76. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +1 -1
  77. package/dist/templates/cc-native/_cc-native/plan-review/CLAUDE.md +1 -0
  78. package/dist/templates/cc-native/_cc-native/plan-review/CODING-STANDARDS-CHECKLIST.md +75 -0
  79. package/dist/templates/cc-native/_cc-native/plan-review/lib/agent-selection.ts +2 -3
  80. package/dist/templates/cc-native/_cc-native/plan-review/lib/corroboration.ts +69 -16
  81. package/dist/templates/cc-native/_cc-native/plan-review/lib/graduation.ts +1 -1
  82. package/dist/templates/cc-native/_cc-native/plan-review/lib/orchestrator.ts +1 -1
  83. package/dist/templates/cc-native/_cc-native/plan-review/lib/output-builder.ts +12 -21
  84. package/dist/templates/cc-native/_cc-native/plan-review/lib/plan-questions.ts +3 -4
  85. package/dist/templates/cc-native/_cc-native/plan-review/lib/review-pipeline.ts +35 -39
  86. package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/agent.ts +2 -3
  87. package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/codex-agent.ts +1 -1
  88. package/oclif.manifest.json +1 -1
  89. package/package.json +6 -5
@@ -1,6 +1,6 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { confirm } from '@inquirer/prompts';
3
+ import confirm from '@inquirer/confirm';
4
4
  import { Flags } from '@oclif/core';
5
5
  import BaseCommand from '../lib/base-command.js';
6
6
  import { computeGitignoreRemovals, pruneGitignoreStaleEntries, removeGitignoreEntries } from '../lib/gitignore-manager.js';
@@ -188,6 +188,155 @@ async function shouldDeleteIdeFolder(targetDir, ideFolder) {
188
188
  async function removeDirectory(dir) {
189
189
  await fs.rm(dir, { force: true, recursive: true });
190
190
  }
191
+ /**
192
+ * Try to remove a directory if it is empty.
193
+ *
194
+ * @param dir - Directory to check and potentially remove
195
+ * @returns True if the directory was removed
196
+ */
197
+ async function tryRemoveEmptyDir(dir) {
198
+ try {
199
+ if (await isDirectoryEmpty(dir)) {
200
+ await removeDirectory(dir);
201
+ return true;
202
+ }
203
+ }
204
+ catch {
205
+ // Directory doesn't exist or can't be accessed
206
+ }
207
+ return false;
208
+ }
209
+ /**
210
+ * Check if an IDE folder will be empty after removing specified method folders.
211
+ * Counts method folders vs folders being deleted, then simulates settings cleanup.
212
+ *
213
+ * @param targetDir - Project root directory
214
+ * @param ideFolder - IDE folder configuration
215
+ * @param ideFolder.root - Root folder name (e.g., '.claude')
216
+ * @param ideFolder.settingsFile - Settings file name (e.g., 'settings.json')
217
+ * @param ideMethodFolders - IDE method folders being deleted
218
+ * @param methodsToRemove - Method names being removed
219
+ * @returns True if the IDE folder will be empty after removal
220
+ */
221
+ async function checkIdeRemovalEligibility(targetDir, ideFolder, ideMethodFolders, methodsToRemove) {
222
+ const idePath = join(targetDir, ideFolder.root);
223
+ try {
224
+ const stat = await fs.stat(idePath);
225
+ if (!stat.isDirectory())
226
+ return false;
227
+ }
228
+ catch {
229
+ return false;
230
+ }
231
+ // Count method folders vs folders being deleted
232
+ const counts = await countMethodFolderDeletions(idePath, ideMethodFolders);
233
+ if (counts.total === 0 || counts.total !== counts.deleted)
234
+ return false;
235
+ // Check if settings file would become empty after removing methods
236
+ return wouldSettingsBeEmpty(idePath, ideFolder.settingsFile, methodsToRemove);
237
+ }
238
+ /**
239
+ * Count total method folders and how many are being deleted in an IDE root.
240
+ *
241
+ * @param idePath - Path to IDE root folder
242
+ * @param ideMethodFolders - IDE method folders being deleted
243
+ * @returns Counts of total and deleted method folders
244
+ */
245
+ async function countMethodFolderDeletions(idePath, ideMethodFolders) {
246
+ let total = 0;
247
+ let deleted = 0;
248
+ try {
249
+ const topEntries = await fs.readdir(idePath, { withFileTypes: true });
250
+ const subdirs = topEntries.filter((e) => e.isDirectory());
251
+ const subResults = await Promise.all(subdirs.map(async (subdir) => {
252
+ const subdirPath = join(idePath, subdir.name);
253
+ try {
254
+ const entries = await fs.readdir(subdirPath, { withFileTypes: true });
255
+ const methodDirs = entries.filter((e) => e.isDirectory());
256
+ const deletedCount = methodDirs.filter((entry) => ideMethodFolders.includes(join(subdirPath, entry.name))).length;
257
+ return { deleted: deletedCount, total: methodDirs.length };
258
+ }
259
+ catch {
260
+ return { deleted: 0, total: 0 };
261
+ }
262
+ }));
263
+ for (const r of subResults) {
264
+ total += r.total;
265
+ deleted += r.deleted;
266
+ }
267
+ }
268
+ catch {
269
+ return { deleted: 0, total: 0 };
270
+ }
271
+ return { deleted, total };
272
+ }
273
+ /**
274
+ * Check if a settings file would be empty after removing specified methods and hooks.
275
+ *
276
+ * @param idePath - Path to IDE root folder
277
+ * @param settingsFile - Settings file name
278
+ * @param methodsToRemove - Method names being removed
279
+ * @returns True if settings would be empty
280
+ */
281
+ async function wouldSettingsBeEmpty(idePath, settingsFile, methodsToRemove) {
282
+ const settingsPath = join(idePath, settingsFile);
283
+ try {
284
+ const content = await fs.readFile(settingsPath, 'utf8');
285
+ const settings = JSON.parse(content);
286
+ if (settings.methods && typeof settings.methods === 'object') {
287
+ for (const method of methodsToRemove) {
288
+ delete settings.methods[method];
289
+ }
290
+ if (Object.keys(settings.methods).length === 0) {
291
+ delete settings.methods;
292
+ }
293
+ }
294
+ if (settings.hooks && typeof settings.hooks === 'object') {
295
+ delete settings.hooks;
296
+ }
297
+ return Object.keys(settings).length === 0;
298
+ }
299
+ catch {
300
+ return true;
301
+ }
302
+ }
303
+ /**
304
+ * Check if a log file exceeds 1MB and needs rotation.
305
+ *
306
+ * @param logPath - Path to the log file
307
+ * @returns Log action info if rotation needed, null otherwise
308
+ */
309
+ async function checkLogRotation(logPath) {
310
+ try {
311
+ const stat = await fs.stat(logPath);
312
+ if (stat.size > 1_048_576) {
313
+ return { path: logPath, sizeBytes: stat.size };
314
+ }
315
+ }
316
+ catch {
317
+ // Can't stat log file
318
+ }
319
+ return null;
320
+ }
321
+ /**
322
+ * Check if a contexts directory has a non-empty _archive/ subdirectory.
323
+ *
324
+ * @param contextsPath - Path to the contexts directory
325
+ * @returns Archive info if found, null otherwise
326
+ */
327
+ async function checkArchiveDir(contextsPath) {
328
+ const archivePath = join(contextsPath, '_archive');
329
+ try {
330
+ const entries = await fs.readdir(archivePath);
331
+ if (entries.length > 0) {
332
+ return { path: archivePath, count: entries.length };
333
+ }
334
+ }
335
+ catch {
336
+ // No archive directory
337
+ }
338
+ return null;
339
+ }
191
340
  /**
192
341
  * Recursively remove files from targetDir that match files in sourceDir.
193
342
  * Only removes files that exist in the source template — user-created files are preserved.
@@ -294,158 +443,18 @@ export default class ClearCommand extends BaseCommand {
294
443
  this.logInfo(msg);
295
444
  return;
296
445
  }
297
- // Show what will be deleted
298
- this.log('');
299
- // Workflow folders (.aiwcli/_{method}/) - will be deleted entirely
300
- if (workflowFolders.length > 0) {
301
- this.logInfo(`Workflow folders to remove (${workflowFolders.length}):`);
302
- for (const folder of workflowFolders) {
303
- const folderName = folder.replace(targetDir + '\\', '').replace(targetDir + '/', '');
304
- this.log(` ${folderName}/`);
305
- }
306
- this.log('');
307
- }
308
- // Output folders (_output/{method}/) - will be deleted
309
- if (outputMethodFolders.length > 0) {
310
- this.logInfo(`Output folders to remove (${outputMethodFolders.length}):`);
311
- for (const folder of outputMethodFolders) {
312
- const folderName = folder.replace(targetDir + '\\', '').replace(targetDir + '/', '');
313
- this.log(` ${folderName}/`);
314
- }
315
- this.log('');
316
- }
317
- // IDE method folders (.claude/commands/{method}/, .windsurf/workflows/{method}/, etc.)
318
- if (ideMethodFolders.length > 0) {
319
- this.logInfo(`IDE method folders to remove (${ideMethodFolders.length}):`);
320
- for (const folder of ideMethodFolders) {
321
- const folderName = folder.replace(targetDir + '\\', '').replace(targetDir + '/', '');
322
- this.log(` ${folderName}/`);
323
- }
324
- this.log('');
325
- }
326
- // Extract method names for settings.json updates
446
+ // Display pending changes
327
447
  const methodsToRemove = this.extractMethodNames(workflowFolders);
328
- if (methodsToRemove.length > 0) {
329
- this.logInfo(`Will update settings files to remove method entries: ${methodsToRemove.join(', ')}`);
330
- this.log('');
331
- }
332
- // Check if _output will be empty after clearing
333
- const containerDir = join(targetDir, AIWCLI_CONTAINER);
334
- const outputDir = join(containerDir, OUTPUT_FOLDER_NAME);
335
- const allMethodFolders = await this.findOutputFolders(targetDir);
336
- const willOutputBeEmpty = allMethodFolders.length > 0 && allMethodFolders.length === outputMethodFolders.length;
337
- if (willOutputBeEmpty) {
338
- this.logInfo(`${AIWCLI_CONTAINER}/${OUTPUT_FOLDER_NAME}/ folder will be removed (will be empty)`);
339
- this.log('');
340
- }
341
- // Check if IDE folders might be removed after clearing
342
- // This happens when settings.json becomes empty and all subfolders are empty
343
- const checkIdeRemoval = async (ideFolder) => {
344
- const idePath = join(targetDir, ideFolder.root);
345
- // Check if IDE folder exists
346
- try {
347
- const stat = await fs.stat(idePath);
348
- if (!stat.isDirectory())
349
- return false;
350
- }
351
- catch {
352
- return false;
353
- }
354
- // Scan all subdirectories to count method folders vs folders being deleted
355
- let totalMethodFolders = 0;
356
- let foldersBeingDeleted = 0;
357
- try {
358
- const topEntries = await fs.readdir(idePath, { withFileTypes: true });
359
- const subdirs = topEntries.filter((e) => e.isDirectory());
360
- // Check each subdirectory for method folders
361
- const subResults = await Promise.all(subdirs.map(async (subdir) => {
362
- const subdirPath = join(idePath, subdir.name);
363
- try {
364
- const entries = await fs.readdir(subdirPath, { withFileTypes: true });
365
- const methodDirs = entries.filter((e) => e.isDirectory());
366
- const deleted = methodDirs.filter((entry) => {
367
- const fullPath = join(subdirPath, entry.name);
368
- return ideMethodFolders.includes(fullPath);
369
- }).length;
370
- return { deleted, total: methodDirs.length };
371
- }
372
- catch {
373
- return { deleted: 0, total: 0 };
374
- }
375
- }));
376
- for (const r of subResults) {
377
- totalMethodFolders += r.total;
378
- foldersBeingDeleted += r.deleted;
379
- }
380
- }
381
- catch {
382
- return false;
383
- }
384
- // If all method folders are being deleted, check if settings would be empty
385
- if (totalMethodFolders > 0 && totalMethodFolders === foldersBeingDeleted) {
386
- // Check if settings file would become empty after removing methods
387
- const settingsPath = join(idePath, ideFolder.settingsFile);
388
- try {
389
- const content = await fs.readFile(settingsPath, 'utf8');
390
- const settings = JSON.parse(content);
391
- // Remove method entries from methods tracking object
392
- if (settings.methods && typeof settings.methods === 'object') {
393
- for (const method of methodsToRemove) {
394
- if (method in settings.methods) {
395
- delete settings.methods[method];
396
- }
397
- }
398
- // Remove methods object if empty
399
- if (Object.keys(settings.methods).length === 0) {
400
- delete settings.methods;
401
- }
402
- }
403
- // Remove hooks that would be empty
404
- if (settings.hooks && typeof settings.hooks === 'object') {
405
- // Simplified check - if hooks only contains method-related entries
406
- delete settings.hooks;
407
- }
408
- return Object.keys(settings).length === 0;
409
- }
410
- catch {
411
- // Settings file doesn't exist or is invalid - would be considered empty
412
- return true;
413
- }
414
- }
415
- return false;
416
- };
417
- const [willClaudeFolderBeEmpty, willWindsurfFolderBeEmpty] = await Promise.all([
418
- checkIdeRemoval(IDE_FOLDERS.claude),
419
- checkIdeRemoval(IDE_FOLDERS.windsurf),
420
- ]);
421
- if (willClaudeFolderBeEmpty) {
422
- this.logInfo(`${IDE_FOLDERS.claude.root}/ folder will be removed (will be empty)`);
423
- this.log('');
424
- }
425
- if (willWindsurfFolderBeEmpty) {
426
- this.logInfo(`${IDE_FOLDERS.windsurf.root}/ folder will be removed (will be empty)`);
427
- this.log('');
428
- }
429
- // Compute gitignore changes for dry-run display
430
- const gitignoreSimulation = await computeGitignoreRemovals(targetDir);
431
- if (gitignoreSimulation.toRemove.length > 0 || gitignoreSimulation.toKeep.length > 0) {
432
- this.logInfo('Gitignore changes:');
433
- for (const { entry, reason } of gitignoreSimulation.toKeep) {
434
- this.log(` keep ${entry}/ (${reason})`);
435
- }
436
- for (const entry of gitignoreSimulation.toRemove) {
437
- this.log(` remove ${entry}/`);
438
- }
439
- this.log('');
440
- }
448
+ await this.displayPendingChanges(targetDir, {
449
+ workflowFolders, outputMethodFolders, ideMethodFolders, methodsToRemove,
450
+ });
441
451
  // Dry run - just show what would happen
442
452
  if (flags['dry-run']) {
443
453
  this.logInfo('Dry run complete. No files or folders were deleted.');
444
454
  return;
445
455
  }
446
- // Calculate total items for confirmation
447
- const totalFolders = workflowFolders.length + outputMethodFolders.length + ideMethodFolders.length;
448
456
  // Confirm deletion
457
+ const totalFolders = workflowFolders.length + outputMethodFolders.length + ideMethodFolders.length;
449
458
  if (!flags.force) {
450
459
  const shouldDelete = await confirm({
451
460
  message: `Delete ${totalFolders} folder(s)?`,
@@ -456,177 +465,10 @@ export default class ClearCommand extends BaseCommand {
456
465
  return;
457
466
  }
458
467
  }
459
- // Delete all folders in parallel
460
- const deleteFolder = async (folder, type) => {
461
- try {
462
- await removeDirectory(folder);
463
- this.logDebug(`Removed ${type} folder: ${folder}`);
464
- return { folder, success: true, type };
465
- }
466
- catch (error) {
467
- const err = error;
468
- this.logWarning(`Failed to delete ${folder}: ${err.message}`);
469
- return { folder, success: false, type };
470
- }
471
- };
472
- const deleteResults = await Promise.all([
473
- ...workflowFolders.map((f) => deleteFolder(f, 'workflow')),
474
- ...outputMethodFolders.map((f) => deleteFolder(f, 'output')),
475
- ...ideMethodFolders.map((f) => deleteFolder(f, 'IDE method')),
476
- ]);
477
- const deletedWorkflow = deleteResults.filter((r) => r.success && r.type === 'workflow').length;
478
- const deletedOutput = deleteResults.filter((r) => r.success && r.type === 'output').length;
479
- const deletedIde = deleteResults.filter((r) => r.success && r.type === 'IDE method').length;
480
- // Check if _output folder is now empty and remove it
481
- let removedOutputDir = false;
482
- try {
483
- if (await isDirectoryEmpty(outputDir)) {
484
- await removeDirectory(outputDir);
485
- this.logDebug(`Removed empty ${AIWCLI_CONTAINER}/${OUTPUT_FOLDER_NAME}/ folder`);
486
- removedOutputDir = true;
487
- }
488
- }
489
- catch {
490
- // _output doesn't exist or can't be accessed
491
- }
492
- // Check if .aiwcli container is now empty and remove it
493
- let removedAiwcliContainer = false;
494
- try {
495
- if (await isDirectoryEmpty(containerDir)) {
496
- await removeDirectory(containerDir);
497
- this.logDebug(`Removed empty ${AIWCLI_CONTAINER}/ folder`);
498
- removedAiwcliContainer = true;
499
- }
500
- }
501
- catch {
502
- // .aiwcli doesn't exist or can't be accessed
503
- }
504
- // Smart gitignore removal: compute what should be removed based on disk state
505
- const { toRemove, toKeep } = await computeGitignoreRemovals(targetDir);
506
- for (const { entry, reason } of toKeep) {
507
- this.logDebug(`Keeping ${entry}/ in .gitignore (${reason})`);
508
- }
509
- if (toRemove.length > 0) {
510
- await removeGitignoreEntries(targetDir, toRemove);
511
- this.logDebug(`Removed from .gitignore: ${toRemove.join(', ')}`);
512
- }
513
- // Prune stale gitignore entries as safety net
514
- const pruned = await pruneGitignoreStaleEntries(targetDir);
515
- if (pruned) {
516
- this.logDebug('Pruned stale .gitignore entries');
517
- }
518
- // Reconstruct IDE settings from remaining templates
519
- let updatedClaudeSettings = false;
520
- let updatedWindsurfSettings = false;
521
- if (methodsToRemove.length > 0) {
522
- // Remove method entries from settings files first
523
- await this.removeMethodEntries(targetDir, methodsToRemove);
524
- // Get remaining installed methods
525
- const allMethods = await getInstalledMethodNames(targetDir);
526
- // Filter out methods being removed (in case disk scan still finds them)
527
- const remainingTemplates = [...allMethods].filter(m => !methodsToRemove.includes(m));
528
- // Determine which IDEs need reconstruction
529
- const ides = [];
530
- if (await pathExists(join(targetDir, IDE_FOLDERS.claude.root))) {
531
- ides.push('claude');
532
- }
533
- if (await pathExists(join(targetDir, IDE_FOLDERS.windsurf.root))) {
534
- ides.push('windsurf');
535
- }
536
- if (ides.length > 0) {
537
- await reconstructIdeSettings(targetDir, remainingTemplates, ides);
538
- if (ides.includes('claude')) {
539
- this.logDebug('Reconstructed .claude/settings.json (backup created)');
540
- updatedClaudeSettings = true;
541
- }
542
- if (ides.includes('windsurf')) {
543
- this.logDebug('Reconstructed .windsurf/hooks.json (backup created)');
544
- updatedWindsurfSettings = true;
545
- }
546
- }
547
- }
548
- // Remove shared IDE content when no templates remain
549
- if (methodsToRemove.length > 0) {
550
- const allMethodsAfterRemove = await getInstalledMethodNames(targetDir);
551
- const remainingAfterRemove = [...allMethodsAfterRemove].filter(m => !methodsToRemove.includes(m));
552
- if (remainingAfterRemove.length === 0) {
553
- await this.removeSharedIdeContent(targetDir);
554
- }
555
- }
556
- // Clean up backup files created during reconstruction
557
- const backupCleanups = Object.values(IDE_FOLDERS).map(async (ide) => {
558
- const backupPath = join(targetDir, ide.root, `${ide.settingsFile}.backup`);
559
- try {
560
- await fs.rm(backupPath, { force: true });
561
- }
562
- catch {
563
- // Backup doesn't exist or can't be removed
564
- }
565
- });
566
- await Promise.all(backupCleanups);
567
- // Check if IDE folders should be fully deleted (empty settings + empty subfolders)
568
- let removedClaudeDir = false;
569
- let removedWindsurfDir = false;
570
- if (await shouldDeleteIdeFolder(targetDir, IDE_FOLDERS.claude)) {
571
- const claudeDirPath = join(targetDir, IDE_FOLDERS.claude.root);
572
- try {
573
- await removeDirectory(claudeDirPath);
574
- this.logDebug(`Removed empty ${IDE_FOLDERS.claude.root}/ folder`);
575
- removedClaudeDir = true;
576
- // If we deleted the whole folder, the settings update message is misleading
577
- updatedClaudeSettings = false;
578
- }
579
- catch {
580
- // Folder can't be removed
581
- }
582
- }
583
- if (await shouldDeleteIdeFolder(targetDir, IDE_FOLDERS.windsurf)) {
584
- const windsurfDirPath = join(targetDir, IDE_FOLDERS.windsurf.root);
585
- try {
586
- await removeDirectory(windsurfDirPath);
587
- this.logDebug(`Removed empty ${IDE_FOLDERS.windsurf.root}/ folder`);
588
- removedWindsurfDir = true;
589
- // If we deleted the whole folder, the settings update message is misleading
590
- updatedWindsurfSettings = false;
591
- }
592
- catch {
593
- // Folder can't be removed
594
- }
595
- }
596
- // Report results
597
- this.log('');
598
- const parts = [];
599
- if (deletedWorkflow > 0) {
600
- parts.push(`${deletedWorkflow} workflow folder(s)`);
601
- }
602
- if (deletedOutput > 0) {
603
- parts.push(`${deletedOutput} output folder(s)`);
604
- }
605
- if (deletedIde > 0) {
606
- parts.push(`${deletedIde} IDE method folder(s)`);
607
- }
608
- if (removedOutputDir) {
609
- parts.push(`${AIWCLI_CONTAINER}/${OUTPUT_FOLDER_NAME}/ folder`);
610
- }
611
- if (removedAiwcliContainer) {
612
- parts.push(`${AIWCLI_CONTAINER}/ folder`);
613
- }
614
- if (removedClaudeDir) {
615
- parts.push(`${IDE_FOLDERS.claude.root}/ folder`);
616
- }
617
- if (removedWindsurfDir) {
618
- parts.push(`${IDE_FOLDERS.windsurf.root}/ folder`);
619
- }
620
- this.logSuccess(`Cleared: ${parts.join(', ')}.`);
621
- if (toRemove.length > 0 || pruned) {
622
- this.logSuccess('Updated .gitignore.');
623
- }
624
- if (updatedClaudeSettings) {
625
- this.logSuccess('Updated .claude/settings.json (backup: settings.json.backup).');
626
- }
627
- if (updatedWindsurfSettings) {
628
- this.logSuccess('Updated .windsurf/hooks.json (backup: hooks.json.backup).');
629
- }
468
+ // Execute deletion and cleanup
469
+ const deleteCounts = await this.executeFolderDeletion(workflowFolders, outputMethodFolders, ideMethodFolders);
470
+ const cleanupResult = await this.performPostDeleteCleanup(targetDir, methodsToRemove);
471
+ this.reportClearResults(deleteCounts, cleanupResult);
630
472
  }
631
473
  catch (error) {
632
474
  const err = error;
@@ -646,6 +488,7 @@ export default class ClearCommand extends BaseCommand {
646
488
  *
647
489
  * @param targetDir - Project root directory
648
490
  * @param flags - Command flags (dry-run, force)
491
+ * @param flags.force - Skip confirmation prompt
649
492
  */
650
493
  // eslint-disable-next-line complexity
651
494
  async cleanRuntimeOutput(targetDir, flags) {
@@ -674,29 +517,15 @@ export default class ClearCommand extends BaseCommand {
674
517
  }
675
518
  // Log rotation: hook-log.jsonl > 1MB
676
519
  if (entry.isFile() && entry.name === 'hook-log.jsonl') {
677
- try {
678
- const stat = await fs.stat(entryPath); // eslint-disable-line no-await-in-loop
679
- if (stat.size > 1_048_576) {
680
- logAction = { path: entryPath, sizeBytes: stat.size };
681
- }
682
- }
683
- catch {
684
- // Can't stat log file
685
- }
520
+ logAction = await checkLogRotation(entryPath); // eslint-disable-line no-await-in-loop
686
521
  continue;
687
522
  }
688
523
  // Archive cleanup: contexts/_archive/
689
524
  if (entry.isDirectory() && entry.name === 'contexts') {
690
- const archivePath = join(entryPath, '_archive');
691
- try {
692
- const archiveEntries = await fs.readdir(archivePath); // eslint-disable-line no-await-in-loop
693
- if (archiveEntries.length > 0) {
694
- archiveDir = archivePath;
695
- archiveCount = archiveEntries.length;
696
- }
697
- }
698
- catch {
699
- // No archive directory
525
+ const result = await checkArchiveDir(entryPath); // eslint-disable-line no-await-in-loop
526
+ if (result) {
527
+ archiveDir = result.path;
528
+ archiveCount = result.count;
700
529
  }
701
530
  }
702
531
  }
@@ -813,6 +642,145 @@ export default class ClearCommand extends BaseCommand {
813
642
  this.logInfo('No changes made.');
814
643
  }
815
644
  }
645
+ /**
646
+ * Clean up backup files created during settings reconstruction.
647
+ *
648
+ * @param targetDir - Project root directory
649
+ */
650
+ async cleanupBackupFiles(targetDir) {
651
+ const cleanups = Object.values(IDE_FOLDERS).map(async (ide) => {
652
+ const backupPath = join(targetDir, ide.root, `${ide.settingsFile}.backup`);
653
+ try {
654
+ await fs.rm(backupPath, { force: true });
655
+ }
656
+ catch {
657
+ // Backup doesn't exist or can't be removed
658
+ }
659
+ });
660
+ await Promise.all(cleanups);
661
+ }
662
+ /**
663
+ * Clean up gitignore entries and prune stale entries.
664
+ *
665
+ * @param targetDir - Project root directory
666
+ * @returns True if gitignore was updated
667
+ */
668
+ async cleanupGitignore(targetDir) {
669
+ const { toRemove, toKeep } = await computeGitignoreRemovals(targetDir);
670
+ for (const { entry, reason } of toKeep) {
671
+ this.logDebug(`Keeping ${entry}/ in .gitignore (${reason})`);
672
+ }
673
+ if (toRemove.length > 0) {
674
+ await removeGitignoreEntries(targetDir, toRemove);
675
+ this.logDebug(`Removed from .gitignore: ${toRemove.join(', ')}`);
676
+ }
677
+ const pruned = await pruneGitignoreStaleEntries(targetDir);
678
+ if (pruned) {
679
+ this.logDebug('Pruned stale .gitignore entries');
680
+ }
681
+ return toRemove.length > 0 || pruned;
682
+ }
683
+ /**
684
+ * Display a list of folders to remove.
685
+ *
686
+ * @param targetDir - Base directory for relative path display
687
+ * @param folders - Array of folder paths
688
+ * @param label - Label for the folder type
689
+ */
690
+ displayFolderList(targetDir, folders, label) {
691
+ if (folders.length === 0)
692
+ return;
693
+ this.logInfo(`${label} to remove (${folders.length}):`);
694
+ for (const folder of folders) {
695
+ const folderName = folder.replace(targetDir + '\\', '').replace(targetDir + '/', '');
696
+ this.log(` ${folderName}/`);
697
+ }
698
+ this.log('');
699
+ }
700
+ /**
701
+ * Display all pending changes before confirmation.
702
+ *
703
+ * @param targetDir - Project root directory
704
+ * @param folders - Discovered folders and methods to remove
705
+ * @param folders.workflowFolders - Workflow folders to remove
706
+ * @param folders.outputMethodFolders - Output method folders to remove
707
+ * @param folders.ideMethodFolders - IDE method folders to remove
708
+ * @param folders.methodsToRemove - Method names being removed
709
+ */
710
+ async displayPendingChanges(targetDir, folders) {
711
+ const { workflowFolders, outputMethodFolders, ideMethodFolders, methodsToRemove } = folders;
712
+ this.log('');
713
+ this.displayFolderList(targetDir, workflowFolders, 'Workflow folders');
714
+ this.displayFolderList(targetDir, outputMethodFolders, 'Output folders');
715
+ this.displayFolderList(targetDir, ideMethodFolders, 'IDE method folders');
716
+ if (methodsToRemove.length > 0) {
717
+ this.logInfo(`Will update settings files to remove method entries: ${methodsToRemove.join(', ')}`);
718
+ this.log('');
719
+ }
720
+ // Check if _output will be empty after clearing
721
+ const allMethodFolders = await this.findOutputFolders(targetDir);
722
+ if (allMethodFolders.length > 0 && allMethodFolders.length === outputMethodFolders.length) {
723
+ this.logInfo(`${AIWCLI_CONTAINER}/${OUTPUT_FOLDER_NAME}/ folder will be removed (will be empty)`);
724
+ this.log('');
725
+ }
726
+ // Check if IDE folders might be removed after clearing
727
+ const [willClaudeFolderBeEmpty, willWindsurfFolderBeEmpty] = await Promise.all([
728
+ checkIdeRemovalEligibility(targetDir, IDE_FOLDERS.claude, ideMethodFolders, methodsToRemove),
729
+ checkIdeRemovalEligibility(targetDir, IDE_FOLDERS.windsurf, ideMethodFolders, methodsToRemove),
730
+ ]);
731
+ if (willClaudeFolderBeEmpty) {
732
+ this.logInfo(`${IDE_FOLDERS.claude.root}/ folder will be removed (will be empty)`);
733
+ this.log('');
734
+ }
735
+ if (willWindsurfFolderBeEmpty) {
736
+ this.logInfo(`${IDE_FOLDERS.windsurf.root}/ folder will be removed (will be empty)`);
737
+ this.log('');
738
+ }
739
+ // Compute gitignore changes for dry-run display
740
+ const gitignoreSimulation = await computeGitignoreRemovals(targetDir);
741
+ if (gitignoreSimulation.toRemove.length > 0 || gitignoreSimulation.toKeep.length > 0) {
742
+ this.logInfo('Gitignore changes:');
743
+ for (const { entry, reason } of gitignoreSimulation.toKeep) {
744
+ this.log(` keep ${entry}/ (${reason})`);
745
+ }
746
+ for (const entry of gitignoreSimulation.toRemove) {
747
+ this.log(` remove ${entry}/`);
748
+ }
749
+ this.log('');
750
+ }
751
+ }
752
+ /**
753
+ * Delete all discovered folders in parallel.
754
+ *
755
+ * @param workflowFolders - Workflow folders to delete
756
+ * @param outputMethodFolders - Output method folders to delete
757
+ * @param ideMethodFolders - IDE method folders to delete
758
+ * @returns Count of successfully deleted folders by type
759
+ */
760
+ async executeFolderDeletion(workflowFolders, outputMethodFolders, ideMethodFolders) {
761
+ const deleteFolder = async (folder, type) => {
762
+ try {
763
+ await removeDirectory(folder);
764
+ this.logDebug(`Removed ${type} folder: ${folder}`);
765
+ return { success: true, type };
766
+ }
767
+ catch (error) {
768
+ const err = error;
769
+ this.logWarning(`Failed to delete ${folder}: ${err.message}`);
770
+ return { success: false, type };
771
+ }
772
+ };
773
+ const deleteResults = await Promise.all([
774
+ ...workflowFolders.map((f) => deleteFolder(f, 'workflow')),
775
+ ...outputMethodFolders.map((f) => deleteFolder(f, 'output')),
776
+ ...ideMethodFolders.map((f) => deleteFolder(f, 'IDE method')),
777
+ ]);
778
+ return {
779
+ deletedWorkflow: deleteResults.filter((r) => r.success && r.type === 'workflow').length,
780
+ deletedOutput: deleteResults.filter((r) => r.success && r.type === 'output').length,
781
+ deletedIde: deleteResults.filter((r) => r.success && r.type === 'IDE method').length,
782
+ };
783
+ }
816
784
  /**
817
785
  * Extract method names from workflow folder names (e.g., _gsd -> gsd).
818
786
  *
@@ -939,18 +907,14 @@ export default class ClearCommand extends BaseCommand {
939
907
  try {
940
908
  const entries = await fs.readdir(containerDir, { withFileTypes: true });
941
909
  for (const entry of entries) {
942
- // Look for directories starting with underscore (workflow folders)
943
- if (entry.isDirectory() && entry.name.startsWith('_') && entry.name !== OUTPUT_FOLDER_NAME) {
944
- // If template specified, only include matching folder
945
- if (template) {
946
- if (entry.name === `_${template}`) {
947
- foundFolders.push(join(containerDir, entry.name));
948
- }
949
- }
950
- else {
951
- foundFolders.push(join(containerDir, entry.name));
952
- }
910
+ if (!entry.isDirectory() || !entry.name.startsWith('_') || entry.name === OUTPUT_FOLDER_NAME) {
911
+ continue;
912
+ }
913
+ // If template specified, only include matching folder
914
+ if (template && entry.name !== `_${template}`) {
915
+ continue;
953
916
  }
917
+ foundFolders.push(join(containerDir, entry.name));
954
918
  }
955
919
  }
956
920
  catch {
@@ -958,6 +922,84 @@ export default class ClearCommand extends BaseCommand {
958
922
  }
959
923
  return foundFolders;
960
924
  }
925
+ /**
926
+ * Perform all post-deletion cleanup: empty dir removal, gitignore, settings, IDE folders.
927
+ *
928
+ * @param targetDir - Project root directory
929
+ * @param methodsToRemove - Method names being removed
930
+ * @returns Cleanup result state
931
+ */
932
+ async performPostDeleteCleanup(targetDir, methodsToRemove) {
933
+ const containerDir = join(targetDir, AIWCLI_CONTAINER);
934
+ const outputDir = join(containerDir, OUTPUT_FOLDER_NAME);
935
+ // Check if _output folder is now empty and remove it
936
+ const removedOutputDir = await tryRemoveEmptyDir(outputDir);
937
+ if (removedOutputDir) {
938
+ this.logDebug(`Removed empty ${AIWCLI_CONTAINER}/${OUTPUT_FOLDER_NAME}/ folder`);
939
+ }
940
+ // Check if .aiwcli container is now empty and remove it
941
+ const removedAiwcliContainer = await tryRemoveEmptyDir(containerDir);
942
+ if (removedAiwcliContainer) {
943
+ this.logDebug(`Removed empty ${AIWCLI_CONTAINER}/ folder`);
944
+ }
945
+ // Smart gitignore removal
946
+ const gitignoreUpdated = await this.cleanupGitignore(targetDir);
947
+ // Reconstruct IDE settings
948
+ let { updatedClaudeSettings, updatedWindsurfSettings } = await this.reconstructSettingsAfterRemoval(targetDir, methodsToRemove);
949
+ // Clean up backup files
950
+ await this.cleanupBackupFiles(targetDir);
951
+ // Check if IDE folders should be fully deleted
952
+ const removedClaudeDir = await this.tryRemoveIdeFolder(targetDir, IDE_FOLDERS.claude);
953
+ if (removedClaudeDir)
954
+ updatedClaudeSettings = false;
955
+ const removedWindsurfDir = await this.tryRemoveIdeFolder(targetDir, IDE_FOLDERS.windsurf);
956
+ if (removedWindsurfDir)
957
+ updatedWindsurfSettings = false;
958
+ return {
959
+ removedOutputDir, removedAiwcliContainer, removedClaudeDir, removedWindsurfDir,
960
+ updatedClaudeSettings, updatedWindsurfSettings, gitignoreUpdated,
961
+ };
962
+ }
963
+ /**
964
+ * Reconstruct IDE settings after method removal.
965
+ *
966
+ * @param targetDir - Project root directory
967
+ * @param methodsToRemove - Methods being removed
968
+ * @returns Which IDE settings were updated
969
+ */
970
+ async reconstructSettingsAfterRemoval(targetDir, methodsToRemove) {
971
+ let updatedClaudeSettings = false;
972
+ let updatedWindsurfSettings = false;
973
+ if (methodsToRemove.length === 0) {
974
+ return { updatedClaudeSettings, updatedWindsurfSettings };
975
+ }
976
+ await this.removeMethodEntries(targetDir, methodsToRemove);
977
+ const allMethods = await getInstalledMethodNames(targetDir);
978
+ const remainingTemplates = [...allMethods].filter(m => !methodsToRemove.includes(m));
979
+ const ides = [];
980
+ if (await pathExists(join(targetDir, IDE_FOLDERS.claude.root)))
981
+ ides.push('claude');
982
+ if (await pathExists(join(targetDir, IDE_FOLDERS.windsurf.root)))
983
+ ides.push('windsurf');
984
+ if (ides.length > 0) {
985
+ await reconstructIdeSettings(targetDir, remainingTemplates, ides);
986
+ if (ides.includes('claude')) {
987
+ this.logDebug('Reconstructed .claude/settings.json (backup created)');
988
+ updatedClaudeSettings = true;
989
+ }
990
+ if (ides.includes('windsurf')) {
991
+ this.logDebug('Reconstructed .windsurf/hooks.json (backup created)');
992
+ updatedWindsurfSettings = true;
993
+ }
994
+ }
995
+ // Remove shared IDE content when no templates remain
996
+ const allMethodsAfterRemove = await getInstalledMethodNames(targetDir);
997
+ const remainingAfterRemove = [...allMethodsAfterRemove].filter(m => !methodsToRemove.includes(m));
998
+ if (remainingAfterRemove.length === 0) {
999
+ await this.removeSharedIdeContent(targetDir);
1000
+ }
1001
+ return { updatedClaudeSettings, updatedWindsurfSettings };
1002
+ }
961
1003
  /**
962
1004
  * Remove method entries from IDE settings files (methods tracking only).
963
1005
  * Settings reconstruction handles hooks/permissions; this only strips the methods object.
@@ -1006,4 +1048,70 @@ export default class ClearCommand extends BaseCommand {
1006
1048
  await removeMatchingFiles(sharedIdeFolder, targetIdeFolder); // eslint-disable-line no-await-in-loop
1007
1049
  }
1008
1050
  }
1051
+ /**
1052
+ * Report the results of a clear operation.
1053
+ *
1054
+ * @param deleteCounts - Counts of deleted folders by type
1055
+ * @param deleteCounts.deletedWorkflow - Number of workflow folders deleted
1056
+ * @param deleteCounts.deletedOutput - Number of output folders deleted
1057
+ * @param deleteCounts.deletedIde - Number of IDE method folders deleted
1058
+ * @param cleanup - Cleanup operation results
1059
+ * @param cleanup.gitignoreUpdated - Whether gitignore was updated
1060
+ * @param cleanup.removedOutputDir - Whether _output dir was removed
1061
+ * @param cleanup.removedAiwcliContainer - Whether .aiwcli dir was removed
1062
+ * @param cleanup.removedClaudeDir - Whether .claude dir was removed
1063
+ * @param cleanup.removedWindsurfDir - Whether .windsurf dir was removed
1064
+ * @param cleanup.updatedClaudeSettings - Whether Claude settings were updated
1065
+ * @param cleanup.updatedWindsurfSettings - Whether Windsurf settings were updated
1066
+ */
1067
+ reportClearResults(deleteCounts, cleanup) {
1068
+ this.log('');
1069
+ const parts = [];
1070
+ if (deleteCounts.deletedWorkflow > 0)
1071
+ parts.push(`${deleteCounts.deletedWorkflow} workflow folder(s)`);
1072
+ if (deleteCounts.deletedOutput > 0)
1073
+ parts.push(`${deleteCounts.deletedOutput} output folder(s)`);
1074
+ if (deleteCounts.deletedIde > 0)
1075
+ parts.push(`${deleteCounts.deletedIde} IDE method folder(s)`);
1076
+ if (cleanup.removedOutputDir)
1077
+ parts.push(`${AIWCLI_CONTAINER}/${OUTPUT_FOLDER_NAME}/ folder`);
1078
+ if (cleanup.removedAiwcliContainer)
1079
+ parts.push(`${AIWCLI_CONTAINER}/ folder`);
1080
+ if (cleanup.removedClaudeDir)
1081
+ parts.push(`${IDE_FOLDERS.claude.root}/ folder`);
1082
+ if (cleanup.removedWindsurfDir)
1083
+ parts.push(`${IDE_FOLDERS.windsurf.root}/ folder`);
1084
+ this.logSuccess(`Cleared: ${parts.join(', ')}.`);
1085
+ if (cleanup.gitignoreUpdated) {
1086
+ this.logSuccess('Updated .gitignore.');
1087
+ }
1088
+ if (cleanup.updatedClaudeSettings) {
1089
+ this.logSuccess('Updated .claude/settings.json (backup: settings.json.backup).');
1090
+ }
1091
+ if (cleanup.updatedWindsurfSettings) {
1092
+ this.logSuccess('Updated .windsurf/hooks.json (backup: hooks.json.backup).');
1093
+ }
1094
+ }
1095
+ /**
1096
+ * Try to remove an IDE folder if it should be deleted (empty settings + empty subfolders).
1097
+ *
1098
+ * @param targetDir - Project root directory
1099
+ * @param ideFolder - IDE folder configuration
1100
+ * @param ideFolder.root - Root folder name (e.g., '.claude')
1101
+ * @param ideFolder.settingsFile - Settings file name (e.g., 'settings.json')
1102
+ * @returns True if the folder was removed
1103
+ */
1104
+ async tryRemoveIdeFolder(targetDir, ideFolder) {
1105
+ if (!(await shouldDeleteIdeFolder(targetDir, ideFolder)))
1106
+ return false;
1107
+ const dirPath = join(targetDir, ideFolder.root);
1108
+ try {
1109
+ await removeDirectory(dirPath);
1110
+ this.logDebug(`Removed empty ${ideFolder.root}/ folder`);
1111
+ return true;
1112
+ }
1113
+ catch {
1114
+ return false;
1115
+ }
1116
+ }
1009
1117
  }