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.
- package/dist/commands/clean.d.ts +7 -0
- package/dist/commands/clean.js +17 -8
- package/dist/commands/clear.d.ts +85 -0
- package/dist/commands/clear.js +455 -347
- package/dist/commands/init/index.d.ts +15 -0
- package/dist/commands/init/index.js +79 -38
- package/dist/lib/gitignore-manager.js +12 -13
- package/dist/lib/settings-hierarchy.d.ts +13 -1
- package/dist/lib/settings-hierarchy.js +1 -1
- package/dist/lib/template-linter.d.ts +4 -0
- package/dist/lib/template-linter.js +1 -1
- package/dist/lib/tty-detection.d.ts +1 -0
- package/dist/lib/tty-detection.js +1 -0
- package/dist/templates/CLAUDE.md +1 -1
- package/dist/templates/_shared/.claude/settings.json +64 -9
- package/dist/templates/_shared/.claude/skills/handoff/SKILL.md +1 -1
- package/dist/templates/_shared/.claude/skills/handoff-resume/SKILL.md +1 -1
- package/dist/templates/_shared/.claude/skills/meta-plan/SKILL.md +43 -0
- package/dist/templates/_shared/.codex/workflows/handoff.md +1 -1
- package/dist/templates/_shared/.codex/workflows/meta-plan.md +347 -0
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +4 -221
- package/dist/templates/_shared/.windsurf/workflows/meta-plan.md +11 -0
- package/dist/templates/_shared/hooks-ts/context_monitor.ts +2 -2
- package/dist/templates/_shared/hooks-ts/lint_after_edit.ts +59 -0
- package/dist/templates/_shared/hooks-ts/session_end.ts +11 -10
- package/dist/templates/_shared/hooks-ts/session_start.ts +15 -12
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +12 -12
- package/dist/templates/_shared/lib-ts/CLAUDE.md +27 -2
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +26 -7
- package/dist/templates/_shared/lib-ts/base/inference.ts +16 -16
- package/dist/templates/_shared/lib-ts/base/lint-dispatch.ts +339 -0
- package/dist/templates/_shared/lib-ts/base/state-io.ts +4 -3
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +3 -3
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +16 -15
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +16 -16
- package/dist/templates/_shared/lib-ts/context/context-store.ts +15 -14
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +2 -2
- package/dist/templates/_shared/scripts/resolve-run.ts +62 -0
- package/dist/templates/_shared/scripts/resolve_context.ts +1 -1
- package/dist/templates/_shared/scripts/status_line.ts +74 -65
- package/dist/templates/_shared/{handoff-system → skills/handoff-system}/CLAUDE.md +14 -14
- package/dist/templates/_shared/{handoff-system → skills/handoff-system}/lib/document-generator.ts +10 -9
- package/dist/templates/_shared/{handoff-system → skills/handoff-system}/lib/handoff-reader.ts +5 -4
- package/dist/templates/_shared/{handoff-system → skills/handoff-system}/scripts/resume_handoff.ts +6 -6
- package/dist/templates/_shared/{handoff-system → skills/handoff-system}/scripts/save_handoff.ts +19 -20
- package/dist/templates/_shared/{handoff-system → skills/handoff-system}/workflows/handoff-resume.md +2 -2
- package/dist/templates/_shared/{handoff-system → skills/handoff-system}/workflows/handoff.md +5 -5
- package/dist/templates/_shared/skills/meta-plan/CLAUDE.md +46 -0
- package/dist/templates/_shared/skills/meta-plan/workflows/meta-plan.md +277 -0
- package/dist/templates/cc-native/.claude/settings.json +13 -111
- package/dist/templates/cc-native/_cc-native/artifacts/lib/format.ts +22 -20
- package/dist/templates/cc-native/_cc-native/artifacts/lib/index.ts +11 -11
- package/dist/templates/cc-native/_cc-native/artifacts/lib/tracker.ts +7 -6
- package/dist/templates/cc-native/_cc-native/artifacts/lib/write.ts +17 -16
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +32 -2
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +9 -7
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +25 -0
- package/dist/templates/cc-native/_cc-native/hooks/validate_task_prompt.ts +2 -2
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +15 -16
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +19 -19
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +3 -3
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +6 -1
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +16 -12
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +2 -3
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +31 -31
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +7 -6
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +9 -7
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +17 -14
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +41 -37
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +43 -33
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +20 -20
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +9 -8
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +4 -3
- package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +8 -9
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +20 -19
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +1 -1
- package/dist/templates/cc-native/_cc-native/plan-review/CLAUDE.md +1 -0
- package/dist/templates/cc-native/_cc-native/plan-review/CODING-STANDARDS-CHECKLIST.md +75 -0
- package/dist/templates/cc-native/_cc-native/plan-review/lib/agent-selection.ts +2 -3
- package/dist/templates/cc-native/_cc-native/plan-review/lib/corroboration.ts +69 -16
- package/dist/templates/cc-native/_cc-native/plan-review/lib/graduation.ts +1 -1
- package/dist/templates/cc-native/_cc-native/plan-review/lib/orchestrator.ts +1 -1
- package/dist/templates/cc-native/_cc-native/plan-review/lib/output-builder.ts +12 -21
- package/dist/templates/cc-native/_cc-native/plan-review/lib/plan-questions.ts +3 -4
- package/dist/templates/cc-native/_cc-native/plan-review/lib/review-pipeline.ts +35 -39
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/agent.ts +2 -3
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/codex-agent.ts +1 -1
- package/oclif.manifest.json +1 -1
- package/package.json +6 -5
package/dist/commands/clear.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import
|
|
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
|
-
//
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
//
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
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
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
}
|