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.
- package/dist/commands/clean.js +1 -0
- package/dist/commands/clear.d.ts +19 -2
- package/dist/commands/clear.js +351 -160
- package/dist/commands/init/index.d.ts +1 -17
- package/dist/commands/init/index.js +19 -104
- package/dist/lib/gitignore-manager.d.ts +9 -0
- package/dist/lib/gitignore-manager.js +121 -0
- package/dist/lib/template-installer.d.ts +7 -12
- package/dist/lib/template-installer.js +69 -193
- package/dist/lib/template-settings-reconstructor.d.ts +35 -0
- package/dist/lib/template-settings-reconstructor.js +130 -0
- package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/archive_plan.py +10 -2
- package/dist/templates/_shared/hooks/session_end.py +37 -29
- package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/hook_utils.py +8 -10
- package/dist/templates/_shared/lib/base/inference.py +51 -62
- package/dist/templates/_shared/lib/base/logger.py +35 -21
- package/dist/templates/_shared/lib/base/stop_words.py +8 -0
- package/dist/templates/_shared/lib/base/utils.py +29 -8
- package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/plan_manager.py +101 -2
- package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -0
- package/dist/templates/_shared/lib-ts/base/constants.ts +299 -0
- package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -0
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +360 -0
- package/dist/templates/_shared/lib-ts/base/inference.ts +245 -0
- package/dist/templates/_shared/lib-ts/base/logger.ts +234 -0
- package/dist/templates/_shared/lib-ts/base/state-io.ts +114 -0
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -0
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +23 -0
- package/dist/templates/_shared/lib-ts/base/utils.ts +184 -0
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +432 -0
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +497 -0
- package/dist/templates/_shared/lib-ts/context/context-store.ts +679 -0
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +292 -0
- package/dist/templates/_shared/lib-ts/context/task-tracker.ts +181 -0
- package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +215 -0
- package/dist/templates/_shared/lib-ts/package.json +21 -0
- package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -0
- package/dist/templates/_shared/lib-ts/templates/plan-context.ts +65 -0
- package/dist/templates/_shared/lib-ts/tsconfig.json +13 -0
- package/dist/templates/_shared/lib-ts/types.ts +151 -0
- package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/save_handoff.ts +359 -0
- package/dist/templates/_shared/scripts/status_line.py +17 -2
- package/dist/templates/cc-native/_cc-native/agents/ARCH-EVOLUTION.md +63 -0
- package/dist/templates/cc-native/_cc-native/agents/ARCH-PATTERNS.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/ARCH-STRUCTURE.md +63 -0
- package/dist/templates/cc-native/_cc-native/agents/{ASSUMPTION-CHAIN-TRACER.md → ASSUMPTION-TRACER.md} +6 -10
- package/dist/templates/cc-native/_cc-native/agents/CLARITY-AUDITOR.md +6 -10
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +74 -1
- package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-FEASIBILITY.md +67 -0
- package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-GAPS.md +71 -0
- package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-ORDERING.md +63 -0
- package/dist/templates/cc-native/_cc-native/agents/CONSTRAINT-VALIDATOR.md +73 -0
- package/dist/templates/cc-native/_cc-native/agents/DESIGN-ADR-VALIDATOR.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/DESIGN-SCALE-MATCHER.md +65 -0
- package/dist/templates/cc-native/_cc-native/agents/DEVILS-ADVOCATE.md +6 -9
- package/dist/templates/cc-native/_cc-native/agents/DOCUMENTATION-PHILOSOPHY.md +87 -0
- package/dist/templates/cc-native/_cc-native/agents/HANDOFF-READINESS.md +5 -9
- package/dist/templates/cc-native/_cc-native/agents/{HIDDEN-COMPLEXITY-DETECTOR.md → HIDDEN-COMPLEXITY.md} +6 -10
- package/dist/templates/cc-native/_cc-native/agents/INCREMENTAL-DELIVERY.md +67 -0
- package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +91 -18
- package/dist/templates/cc-native/_cc-native/agents/RISK-DEPENDENCY.md +63 -0
- package/dist/templates/cc-native/_cc-native/agents/RISK-FMEA.md +67 -0
- package/dist/templates/cc-native/_cc-native/agents/RISK-PREMORTEM.md +72 -0
- package/dist/templates/cc-native/_cc-native/agents/RISK-REVERSIBILITY.md +75 -0
- package/dist/templates/cc-native/_cc-native/agents/SCOPE-BOUNDARY.md +78 -0
- package/dist/templates/cc-native/_cc-native/agents/SIMPLICITY-GUARDIAN.md +5 -9
- package/dist/templates/cc-native/_cc-native/agents/SKEPTIC.md +16 -12
- package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-BEHAVIOR-AUDITOR.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-CHARACTERIZATION.md +72 -0
- package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-FIRST-VALIDATOR.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-PYRAMID-ANALYZER.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/TRADEOFF-COSTS.md +68 -0
- package/dist/templates/cc-native/_cc-native/agents/TRADEOFF-STAKEHOLDERS.md +66 -0
- package/dist/templates/cc-native/_cc-native/agents/VERIFY-COVERAGE.md +75 -0
- package/dist/templates/cc-native/_cc-native/agents/VERIFY-STRENGTH.md +70 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +125 -40
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/utils.py +57 -13
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +11 -7
- package/oclif.manifest.json +17 -2
- package/package.json +1 -1
- package/dist/lib/template-merger.d.ts +0 -47
- package/dist/lib/template-merger.js +0 -162
- package/dist/templates/cc-native/_cc-native/agents/ACCESSIBILITY-TESTER.md +0 -79
- package/dist/templates/cc-native/_cc-native/agents/ARCHITECT-REVIEWER.md +0 -48
- package/dist/templates/cc-native/_cc-native/agents/CODE-REVIEWER.md +0 -70
- package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-CHECKER.md +0 -59
- package/dist/templates/cc-native/_cc-native/agents/CONTEXT-EXTRACTOR.md +0 -92
- package/dist/templates/cc-native/_cc-native/agents/DOCUMENTATION-REVIEWER.md +0 -51
- package/dist/templates/cc-native/_cc-native/agents/FEASIBILITY-ANALYST.md +0 -57
- package/dist/templates/cc-native/_cc-native/agents/FRESH-PERSPECTIVE.md +0 -54
- package/dist/templates/cc-native/_cc-native/agents/INCENTIVE-MAPPER.md +0 -61
- package/dist/templates/cc-native/_cc-native/agents/PENETRATION-TESTER.md +0 -79
- package/dist/templates/cc-native/_cc-native/agents/PERFORMANCE-ENGINEER.md +0 -75
- package/dist/templates/cc-native/_cc-native/agents/PRECEDENT-FINDER.md +0 -70
- package/dist/templates/cc-native/_cc-native/agents/REVERSIBILITY-ANALYST.md +0 -61
- package/dist/templates/cc-native/_cc-native/agents/RISK-ASSESSOR.md +0 -58
- package/dist/templates/cc-native/_cc-native/agents/SECOND-ORDER-ANALYST.md +0 -61
- package/dist/templates/cc-native/_cc-native/agents/STAKEHOLDER-ADVOCATE.md +0 -55
- package/dist/templates/cc-native/_cc-native/agents/TRADE-OFF-ILLUMINATOR.md +0 -204
package/dist/commands/clear.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
494
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
|
717
|
-
*
|
|
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
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
//
|
|
733
|
-
const
|
|
734
|
-
|
|
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
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
|
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
|
|
37
|
+
* - Settings reconstruction from all active templates
|
|
54
38
|
* - .gitignore updates
|
|
55
39
|
*
|
|
56
40
|
* @param config - Post-install configuration
|