aiwcli 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1248 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +16 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +19 -0
- package/dist/commands/branch.d.ts +45 -0
- package/dist/commands/branch.js +488 -0
- package/dist/commands/clean.d.ts +34 -0
- package/dist/commands/clean.js +186 -0
- package/dist/commands/clear.d.ts +51 -0
- package/dist/commands/clear.js +835 -0
- package/dist/commands/init/index.d.ts +107 -0
- package/dist/commands/init/index.js +565 -0
- package/dist/commands/launch.d.ts +21 -0
- package/dist/commands/launch.js +108 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/base-command.d.ts +114 -0
- package/dist/lib/base-command.js +153 -0
- package/dist/lib/bmad-installer.d.ts +38 -0
- package/dist/lib/bmad-installer.js +145 -0
- package/dist/lib/claude-settings-types.d.ts +102 -0
- package/dist/lib/claude-settings-types.js +5 -0
- package/dist/lib/config.d.ts +25 -0
- package/dist/lib/config.js +46 -0
- package/dist/lib/debug.d.ts +39 -0
- package/dist/lib/debug.js +74 -0
- package/dist/lib/env-compat.d.ts +26 -0
- package/dist/lib/env-compat.js +35 -0
- package/dist/lib/errors.d.ts +126 -0
- package/dist/lib/errors.js +145 -0
- package/dist/lib/generic-merge.d.ts +74 -0
- package/dist/lib/generic-merge.js +105 -0
- package/dist/lib/git/branch.d.ts +67 -0
- package/dist/lib/git/branch.js +155 -0
- package/dist/lib/git/index.d.ts +11 -0
- package/dist/lib/git/index.js +13 -0
- package/dist/lib/git/safety-checks.d.ts +44 -0
- package/dist/lib/git/safety-checks.js +102 -0
- package/dist/lib/git/types.d.ts +31 -0
- package/dist/lib/git/types.js +6 -0
- package/dist/lib/git/worktree.d.ts +67 -0
- package/dist/lib/git/worktree.js +220 -0
- package/dist/lib/gitignore-manager.d.ts +10 -0
- package/dist/lib/gitignore-manager.js +60 -0
- package/dist/lib/hooks-merger.d.ts +28 -0
- package/dist/lib/hooks-merger.js +94 -0
- package/dist/lib/ide-path-resolver.d.ts +102 -0
- package/dist/lib/ide-path-resolver.js +129 -0
- package/dist/lib/index.d.ts +13 -0
- package/dist/lib/index.js +22 -0
- package/dist/lib/output.d.ts +51 -0
- package/dist/lib/output.js +76 -0
- package/dist/lib/paths.d.ts +66 -0
- package/dist/lib/paths.js +136 -0
- package/dist/lib/quiet.d.ts +12 -0
- package/dist/lib/quiet.js +17 -0
- package/dist/lib/settings-hierarchy.d.ts +42 -0
- package/dist/lib/settings-hierarchy.js +105 -0
- package/dist/lib/spawn.d.ts +105 -0
- package/dist/lib/spawn.js +157 -0
- package/dist/lib/spinner.d.ts +19 -0
- package/dist/lib/spinner.js +34 -0
- package/dist/lib/stdin.d.ts +48 -0
- package/dist/lib/stdin.js +60 -0
- package/dist/lib/template-installer.d.ts +92 -0
- package/dist/lib/template-installer.js +375 -0
- package/dist/lib/template-linter.d.ts +49 -0
- package/dist/lib/template-linter.js +173 -0
- package/dist/lib/template-merger.d.ts +47 -0
- package/dist/lib/template-merger.js +173 -0
- package/dist/lib/template-resolver.d.ts +20 -0
- package/dist/lib/template-resolver.js +60 -0
- package/dist/lib/terminal.d.ts +102 -0
- package/dist/lib/terminal.js +245 -0
- package/dist/lib/tty-detection.d.ts +62 -0
- package/dist/lib/tty-detection.js +83 -0
- package/dist/lib/user-utils.d.ts +5 -0
- package/dist/lib/user-utils.js +23 -0
- package/dist/lib/version.d.ts +99 -0
- package/dist/lib/version.js +144 -0
- package/dist/lib/watch-templates.d.ts +6 -0
- package/dist/lib/watch-templates.js +73 -0
- package/dist/lib/windsurf-hooks-hierarchy.d.ts +30 -0
- package/dist/lib/windsurf-hooks-hierarchy.js +66 -0
- package/dist/lib/windsurf-hooks-merger.d.ts +26 -0
- package/dist/lib/windsurf-hooks-merger.js +53 -0
- package/dist/lib/windsurf-hooks-types.d.ts +33 -0
- package/dist/lib/windsurf-hooks-types.js +5 -0
- package/dist/templates/CLAUDE.md +174 -0
- package/dist/templates/_shared/.claude/commands/handoff.md +14 -0
- package/dist/templates/_shared/.claude/settings.json +61 -0
- package/dist/templates/_shared/.codex/workflows/handoff.md +14 -0
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +14 -0
- package/dist/templates/_shared/hooks/__init__.py +16 -0
- package/dist/templates/_shared/hooks/archive_plan.py +270 -0
- package/dist/templates/_shared/hooks/context_enforcer.py +621 -0
- package/dist/templates/_shared/hooks/context_monitor.py +322 -0
- package/dist/templates/_shared/hooks/file-suggestion.py +188 -0
- package/dist/templates/_shared/hooks/task_create_capture.py +194 -0
- package/dist/templates/_shared/hooks/task_update_capture.py +254 -0
- package/dist/templates/_shared/hooks/user_prompt_submit.py +157 -0
- package/dist/templates/_shared/lib/__init__.py +1 -0
- package/dist/templates/_shared/lib/base/__init__.py +49 -0
- package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/atomic_write.py +180 -0
- package/dist/templates/_shared/lib/base/constants.py +299 -0
- package/dist/templates/_shared/lib/base/inference.py +189 -0
- package/dist/templates/_shared/lib/base/utils.py +216 -0
- package/dist/templates/_shared/lib/context/__init__.py +119 -0
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/cache.py +446 -0
- package/dist/templates/_shared/lib/context/context_manager.py +1171 -0
- package/dist/templates/_shared/lib/context/discovery.py +486 -0
- package/dist/templates/_shared/lib/context/event_log.py +308 -0
- package/dist/templates/_shared/lib/context/plan_archive.py +247 -0
- package/dist/templates/_shared/lib/context/task_sync.py +367 -0
- package/dist/templates/_shared/lib/handoff/__init__.py +22 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +307 -0
- package/dist/templates/_shared/lib/templates/README.md +215 -0
- package/dist/templates/_shared/lib/templates/__init__.py +40 -0
- package/dist/templates/_shared/lib/templates/formatters.py +147 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +119 -0
- package/dist/templates/_shared/scripts/save_handoff.py +99 -0
- package/dist/templates/_shared/workflows/handoff.md +212 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/ACCESSIBILITY-TESTER.md +80 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/ARCHITECT-REVIEWER.md +75 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/ASSUMPTION-CHAIN-TRACER.md +239 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/CLARITY-AUDITOR.md +109 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/CODE-REVIEWER.md +71 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/COMPLETENESS-CHECKER.md +104 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/CONTEXT-EXTRACTOR.md +93 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/DEVILS-ADVOCATE.md +223 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/DOCUMENTATION-REVIEWER.md +73 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/FEASIBILITY-ANALYST.md +93 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/FRESH-PERSPECTIVE.md +103 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/HANDOFF-READINESS.md +145 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/HIDDEN-COMPLEXITY-DETECTOR.md +248 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/INCENTIVE-MAPPER.md +235 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PENETRATION-TESTER.md +80 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PERFORMANCE-ENGINEER.md +76 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PLAN-ORCHESTRATOR.md +141 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PRECEDENT-FINDER.md +240 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/REVERSIBILITY-ANALYST.md +211 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/RISK-ASSESSOR.md +101 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/SECOND-ORDER-ANALYST.md +197 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/SIMPLICITY-GUARDIAN.md +97 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/SKEPTIC.md +349 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/STAKEHOLDER-ADVOCATE.md +106 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/TRADE-OFF-ILLUMINATOR.md +205 -0
- package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +8 -0
- package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -0
- package/dist/templates/cc-native/.claude/settings.json +119 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +8 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -0
- package/dist/templates/cc-native/CC-NATIVE-README.md +192 -0
- package/dist/templates/cc-native/MIGRATION.md +86 -0
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +331 -0
- package/dist/templates/cc-native/_cc-native/docs/PERMISSION_REQUEST_VERIFICATION.md +147 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -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/__pycache__/test_permission_request.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +150 -0
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +746 -0
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +339 -0
- package/dist/templates/cc-native/_cc-native/lib/__init__.py +57 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/async_archive.py +68 -0
- package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +98 -0
- package/dist/templates/cc-native/_cc-native/lib/constants.py +45 -0
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +273 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +28 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +164 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +89 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +119 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +103 -0
- package/dist/templates/cc-native/_cc-native/lib/state.py +251 -0
- package/dist/templates/cc-native/_cc-native/lib/utils.py +830 -0
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +76 -0
- package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +151 -0
- package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +134 -0
- package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -0
- package/dist/types/exit-codes.d.ts +11 -0
- package/dist/types/exit-codes.js +10 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +7 -0
- package/oclif.manifest.json +405 -0
- package/package.json +109 -0
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { confirm } from '@inquirer/prompts';
|
|
4
|
+
import { Flags } from '@oclif/core';
|
|
5
|
+
import BaseCommand from '../lib/base-command.js';
|
|
6
|
+
import { EXIT_CODES } from '../types/exit-codes.js';
|
|
7
|
+
/**
|
|
8
|
+
* Container folder for method-specific files
|
|
9
|
+
* This keeps template infrastructure separate from IDE config
|
|
10
|
+
*/
|
|
11
|
+
const AIWCLI_CONTAINER = '.aiwcli';
|
|
12
|
+
/**
|
|
13
|
+
* The output folder name that contains method subdirectories.
|
|
14
|
+
* Structure: .aiwcli/_output/{method}/ (e.g., .aiwcli/_output/bmad/, .aiwcli/_output/gsd/)
|
|
15
|
+
*/
|
|
16
|
+
const OUTPUT_FOLDER_NAME = '_output';
|
|
17
|
+
/**
|
|
18
|
+
* IDE configuration folder names and their method subfolder structure
|
|
19
|
+
*/
|
|
20
|
+
const IDE_FOLDERS = {
|
|
21
|
+
claude: {
|
|
22
|
+
root: '.claude',
|
|
23
|
+
methodSubfolders: ['commands', 'skills', 'agents'],
|
|
24
|
+
settingsFile: 'settings.json',
|
|
25
|
+
},
|
|
26
|
+
windsurf: {
|
|
27
|
+
root: '.windsurf',
|
|
28
|
+
methodSubfolders: ['workflows'],
|
|
29
|
+
settingsFile: 'hooks.json',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* AIW gitignore section header marker
|
|
34
|
+
*/
|
|
35
|
+
const AIW_GITIGNORE_HEADER = '# AIW Installation';
|
|
36
|
+
/**
|
|
37
|
+
* Check if a directory is empty.
|
|
38
|
+
*
|
|
39
|
+
* @param dir - Directory to check
|
|
40
|
+
* @returns True if directory is empty or doesn't exist
|
|
41
|
+
*/
|
|
42
|
+
async function isDirectoryEmpty(dir) {
|
|
43
|
+
try {
|
|
44
|
+
const entries = await fs.readdir(dir);
|
|
45
|
+
return entries.length === 0;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Check if a JSON settings file is empty or effectively empty.
|
|
53
|
+
* Returns true if the file doesn't exist, can't be parsed, or contains an empty object.
|
|
54
|
+
*
|
|
55
|
+
* @param filePath - Path to the JSON settings file
|
|
56
|
+
* @returns True if file is empty or doesn't exist
|
|
57
|
+
*/
|
|
58
|
+
async function isSettingsFileEmpty(filePath) {
|
|
59
|
+
try {
|
|
60
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
61
|
+
const trimmed = content.trim();
|
|
62
|
+
if (trimmed === '' || trimmed === '{}') {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
const parsed = JSON.parse(content);
|
|
66
|
+
// Check if it's an empty object
|
|
67
|
+
return typeof parsed === 'object' && parsed !== null && Object.keys(parsed).length === 0;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// File doesn't exist or can't be parsed - consider it empty
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Check if an IDE folder should be fully deleted.
|
|
76
|
+
* Returns true if:
|
|
77
|
+
* 1. The settings file is empty (or doesn't exist)
|
|
78
|
+
* 2. All method subfolders are empty (or don't exist)
|
|
79
|
+
* Backup files (e.g., settings.json.backup) are ignored.
|
|
80
|
+
*
|
|
81
|
+
* @param targetDir - Directory containing the IDE folder
|
|
82
|
+
* @param ideFolder - IDE folder configuration
|
|
83
|
+
* @param ideFolder.methodSubfolders - List of method subfolder names to check
|
|
84
|
+
* @param ideFolder.root - Root folder name (e.g., '.claude')
|
|
85
|
+
* @param ideFolder.settingsFile - Settings file name (e.g., 'settings.json')
|
|
86
|
+
* @returns True if the IDE folder should be fully deleted
|
|
87
|
+
*/
|
|
88
|
+
async function shouldDeleteIdeFolder(targetDir, ideFolder) {
|
|
89
|
+
const ideFolderPath = join(targetDir, ideFolder.root);
|
|
90
|
+
// Check if IDE folder exists at all
|
|
91
|
+
try {
|
|
92
|
+
const stat = await fs.stat(ideFolderPath);
|
|
93
|
+
if (!stat.isDirectory()) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Folder doesn't exist - nothing to delete
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
// Check if settings file is empty
|
|
102
|
+
const settingsPath = join(ideFolderPath, ideFolder.settingsFile);
|
|
103
|
+
const settingsEmpty = await isSettingsFileEmpty(settingsPath);
|
|
104
|
+
if (!settingsEmpty) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
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
|
+
// Check the IDE folder itself - ignore backup files and check for other meaningful content
|
|
116
|
+
try {
|
|
117
|
+
const entries = await fs.readdir(ideFolderPath);
|
|
118
|
+
// Filter entries to check (skip backup files, settings file, and known subfolders)
|
|
119
|
+
const entriesToCheck = entries.filter((entry) => {
|
|
120
|
+
if (entry.endsWith('.backup'))
|
|
121
|
+
return false;
|
|
122
|
+
if (entry === ideFolder.settingsFile)
|
|
123
|
+
return false;
|
|
124
|
+
if (ideFolder.methodSubfolders.includes(entry))
|
|
125
|
+
return false;
|
|
126
|
+
return true;
|
|
127
|
+
});
|
|
128
|
+
// Check all entries in parallel
|
|
129
|
+
const entryResults = await Promise.all(entriesToCheck.map(async (entry) => {
|
|
130
|
+
const entryPath = join(ideFolderPath, entry);
|
|
131
|
+
try {
|
|
132
|
+
const stat = await fs.stat(entryPath);
|
|
133
|
+
if (stat.isDirectory()) {
|
|
134
|
+
return isDirectoryEmpty(entryPath);
|
|
135
|
+
}
|
|
136
|
+
// Non-backup file exists - don't delete the folder
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Can't stat entry - be safe and don't delete
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}));
|
|
144
|
+
// If any entry is not empty (or is a non-backup file), don't delete
|
|
145
|
+
if (entryResults.some((result) => !result)) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Remove a directory recursively.
|
|
156
|
+
*
|
|
157
|
+
* @param dir - Directory to remove
|
|
158
|
+
*/
|
|
159
|
+
async function removeDirectory(dir) {
|
|
160
|
+
await fs.rm(dir, { force: true, recursive: true });
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Update .gitignore to remove patterns for cleared folders.
|
|
164
|
+
* Removes patterns from the AIW Installation section.
|
|
165
|
+
*
|
|
166
|
+
* @param targetDir - Directory containing .gitignore
|
|
167
|
+
* @param foldersToRemove - Folder patterns to remove (without trailing slash)
|
|
168
|
+
*/
|
|
169
|
+
async function updateGitignoreAfterClear(targetDir, foldersToRemove) {
|
|
170
|
+
const gitignorePath = join(targetDir, '.gitignore');
|
|
171
|
+
try {
|
|
172
|
+
const content = await fs.readFile(gitignorePath, 'utf8');
|
|
173
|
+
// Check if AIW Installation section exists
|
|
174
|
+
if (!content.includes(AIW_GITIGNORE_HEADER)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Split content into lines
|
|
178
|
+
const lines = content.split('\n');
|
|
179
|
+
const newLines = [];
|
|
180
|
+
let inAiwSection = false;
|
|
181
|
+
const aiwSectionLines = [];
|
|
182
|
+
// Parse lines and identify AIW section
|
|
183
|
+
for (const line of lines) {
|
|
184
|
+
if (line === AIW_GITIGNORE_HEADER) {
|
|
185
|
+
inAiwSection = true;
|
|
186
|
+
aiwSectionLines.push(line);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (inAiwSection) {
|
|
190
|
+
// AIW section ends at empty line or another comment header
|
|
191
|
+
if (line === '' || (line.startsWith('#') && line !== AIW_GITIGNORE_HEADER)) {
|
|
192
|
+
inAiwSection = false;
|
|
193
|
+
// Process AIW section lines now
|
|
194
|
+
const filteredAiwLines = filterAiwSection(aiwSectionLines, foldersToRemove);
|
|
195
|
+
newLines.push(...filteredAiwLines, line);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
aiwSectionLines.push(line);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
newLines.push(line);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Handle case where AIW section is at end of file
|
|
206
|
+
if (inAiwSection) {
|
|
207
|
+
const filteredAiwLines = filterAiwSection(aiwSectionLines, foldersToRemove);
|
|
208
|
+
newLines.push(...filteredAiwLines);
|
|
209
|
+
}
|
|
210
|
+
// Clean up: remove AIW section entirely if only header remains
|
|
211
|
+
const finalContent = cleanupGitignoreContent(newLines.join('\n'));
|
|
212
|
+
await fs.writeFile(gitignorePath, finalContent, 'utf8');
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// .gitignore doesn't exist or can't be read
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Filter AIW section lines to remove specified folders.
|
|
220
|
+
*
|
|
221
|
+
* @param aiwLines - Lines from AIW section (including header)
|
|
222
|
+
* @param foldersToRemove - Folder names to remove
|
|
223
|
+
* @returns Filtered lines
|
|
224
|
+
*/
|
|
225
|
+
function filterAiwSection(aiwLines, foldersToRemove) {
|
|
226
|
+
const patternsToRemove = new Set(foldersToRemove.map((f) => `${f}/`));
|
|
227
|
+
return aiwLines.filter((line) => {
|
|
228
|
+
// Always keep the header
|
|
229
|
+
if (line === AIW_GITIGNORE_HEADER) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
// Remove matching patterns
|
|
233
|
+
return !patternsToRemove.has(line);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Clean up gitignore content after filtering.
|
|
238
|
+
* Removes AIW section if empty, cleans up extra newlines.
|
|
239
|
+
*
|
|
240
|
+
* @param content - Gitignore content
|
|
241
|
+
* @returns Cleaned content
|
|
242
|
+
*/
|
|
243
|
+
function cleanupGitignoreContent(content) {
|
|
244
|
+
const lines = content.split('\n');
|
|
245
|
+
const newLines = [];
|
|
246
|
+
let i = 0;
|
|
247
|
+
while (i < lines.length) {
|
|
248
|
+
const line = lines[i];
|
|
249
|
+
// Check if this is an AIW header with nothing following
|
|
250
|
+
if (line === AIW_GITIGNORE_HEADER) {
|
|
251
|
+
// Look ahead to see if there are any patterns
|
|
252
|
+
let hasPatterns = false;
|
|
253
|
+
const nextLine = lines[i + 1];
|
|
254
|
+
if (nextLine !== undefined && nextLine !== '' && !nextLine.startsWith('#')) {
|
|
255
|
+
hasPatterns = true;
|
|
256
|
+
}
|
|
257
|
+
if (!hasPatterns) {
|
|
258
|
+
// Skip the AIW header since it has no patterns
|
|
259
|
+
i++;
|
|
260
|
+
// Also skip any trailing empty lines that were before AIW section
|
|
261
|
+
while (newLines.length > 0 && newLines.at(-1) === '') {
|
|
262
|
+
newLines.pop();
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
newLines.push(line);
|
|
268
|
+
i++;
|
|
269
|
+
}
|
|
270
|
+
// Ensure file ends with newline but not multiple
|
|
271
|
+
let result = newLines.join('\n');
|
|
272
|
+
result = result.replace(/\n+$/, '\n');
|
|
273
|
+
// Handle empty file case
|
|
274
|
+
if (result.trim() === '') {
|
|
275
|
+
return '';
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
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
|
+
/**
|
|
375
|
+
* Clear workflow folders, output folders, IDE method folders, and update configurations.
|
|
376
|
+
*/
|
|
377
|
+
export default class ClearCommand extends BaseCommand {
|
|
378
|
+
static description = 'Clear workflow folders, output folders, IDE method folders (.claude/.windsurf), and update configurations';
|
|
379
|
+
static examples = [
|
|
380
|
+
'<%= config.bin %> <%= command.id %>',
|
|
381
|
+
'<%= config.bin %> <%= command.id %> --template cc-native',
|
|
382
|
+
'<%= config.bin %> <%= command.id %> -t cc-native',
|
|
383
|
+
'<%= config.bin %> <%= command.id %> --dry-run',
|
|
384
|
+
'<%= config.bin %> <%= command.id %> --force',
|
|
385
|
+
];
|
|
386
|
+
static flags = {
|
|
387
|
+
...BaseCommand.baseFlags,
|
|
388
|
+
'dry-run': Flags.boolean({
|
|
389
|
+
char: 'n',
|
|
390
|
+
description: 'Show what would be deleted without actually deleting',
|
|
391
|
+
default: false,
|
|
392
|
+
}),
|
|
393
|
+
force: Flags.boolean({
|
|
394
|
+
char: 'f',
|
|
395
|
+
description: 'Skip confirmation prompt',
|
|
396
|
+
default: false,
|
|
397
|
+
}),
|
|
398
|
+
template: Flags.string({
|
|
399
|
+
char: 't',
|
|
400
|
+
description: 'Clear only a specific template (e.g., cc-native)',
|
|
401
|
+
}),
|
|
402
|
+
};
|
|
403
|
+
async run() {
|
|
404
|
+
const { flags } = await this.parse(ClearCommand);
|
|
405
|
+
const targetDir = process.cwd();
|
|
406
|
+
try {
|
|
407
|
+
// Find all folders to clear
|
|
408
|
+
const workflowFolders = await this.findWorkflowFolders(targetDir, flags.template);
|
|
409
|
+
const outputMethodFolders = await this.findOutputFolders(targetDir, flags.template);
|
|
410
|
+
const ideMethodFolders = await this.findIdeMethodFolders(targetDir, flags.template);
|
|
411
|
+
// Nothing to clear
|
|
412
|
+
if (workflowFolders.length === 0 && outputMethodFolders.length === 0 && ideMethodFolders.length === 0) {
|
|
413
|
+
const msg = flags.template
|
|
414
|
+
? `No folders found for template '${flags.template}'.`
|
|
415
|
+
: 'No workflow, output, or IDE method folders found.';
|
|
416
|
+
this.logInfo(msg);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
// Show what will be deleted
|
|
420
|
+
this.log('');
|
|
421
|
+
// Workflow folders (.aiwcli/_{method}/) - will be deleted entirely
|
|
422
|
+
if (workflowFolders.length > 0) {
|
|
423
|
+
this.logInfo(`Workflow folders to remove (${workflowFolders.length}):`);
|
|
424
|
+
for (const folder of workflowFolders) {
|
|
425
|
+
const folderName = folder.replace(targetDir + '\\', '').replace(targetDir + '/', '');
|
|
426
|
+
this.log(` ${folderName}/`);
|
|
427
|
+
}
|
|
428
|
+
this.log('');
|
|
429
|
+
}
|
|
430
|
+
// Output folders (_output/{method}/) - will be deleted
|
|
431
|
+
if (outputMethodFolders.length > 0) {
|
|
432
|
+
this.logInfo(`Output folders to remove (${outputMethodFolders.length}):`);
|
|
433
|
+
for (const folder of outputMethodFolders) {
|
|
434
|
+
const folderName = folder.replace(targetDir + '\\', '').replace(targetDir + '/', '');
|
|
435
|
+
this.log(` ${folderName}/`);
|
|
436
|
+
}
|
|
437
|
+
this.log('');
|
|
438
|
+
}
|
|
439
|
+
// IDE method folders (.claude/commands/{method}/, .windsurf/workflows/{method}/, etc.)
|
|
440
|
+
if (ideMethodFolders.length > 0) {
|
|
441
|
+
this.logInfo(`IDE method folders to remove (${ideMethodFolders.length}):`);
|
|
442
|
+
for (const folder of ideMethodFolders) {
|
|
443
|
+
const folderName = folder.replace(targetDir + '\\', '').replace(targetDir + '/', '');
|
|
444
|
+
this.log(` ${folderName}/`);
|
|
445
|
+
}
|
|
446
|
+
this.log('');
|
|
447
|
+
}
|
|
448
|
+
// Extract method names for settings.json updates
|
|
449
|
+
const methodsToRemove = this.extractMethodNames(workflowFolders);
|
|
450
|
+
if (methodsToRemove.length > 0) {
|
|
451
|
+
this.logInfo(`Will update settings files to remove method entries: ${methodsToRemove.join(', ')}`);
|
|
452
|
+
this.log('');
|
|
453
|
+
}
|
|
454
|
+
// Check if _output will be empty after clearing
|
|
455
|
+
const containerDir = join(targetDir, AIWCLI_CONTAINER);
|
|
456
|
+
const outputDir = join(containerDir, OUTPUT_FOLDER_NAME);
|
|
457
|
+
const allMethodFolders = await this.findOutputFolders(targetDir);
|
|
458
|
+
const willOutputBeEmpty = allMethodFolders.length > 0 && allMethodFolders.length === outputMethodFolders.length;
|
|
459
|
+
if (willOutputBeEmpty) {
|
|
460
|
+
this.logInfo(`${AIWCLI_CONTAINER}/${OUTPUT_FOLDER_NAME}/ folder will be removed (will be empty)`);
|
|
461
|
+
this.log('');
|
|
462
|
+
}
|
|
463
|
+
// Check if IDE folders might be removed after clearing
|
|
464
|
+
// This happens when settings.json becomes empty and all subfolders are empty
|
|
465
|
+
const checkIdeRemoval = async (ideFolder) => {
|
|
466
|
+
const idePath = join(targetDir, ideFolder.root);
|
|
467
|
+
// Check if IDE folder exists
|
|
468
|
+
try {
|
|
469
|
+
const stat = await fs.stat(idePath);
|
|
470
|
+
if (!stat.isDirectory())
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
// Count existing method folders vs folders being deleted (in parallel)
|
|
477
|
+
const subfolderResults = await Promise.all(ideFolder.methodSubfolders.map(async (subfolder) => {
|
|
478
|
+
const subfolderPath = join(idePath, subfolder);
|
|
479
|
+
try {
|
|
480
|
+
const entries = await fs.readdir(subfolderPath, { withFileTypes: true });
|
|
481
|
+
const methodDirs = entries.filter((e) => e.isDirectory());
|
|
482
|
+
const beingDeleted = methodDirs.filter((entry) => {
|
|
483
|
+
const fullPath = join(subfolderPath, entry.name);
|
|
484
|
+
return ideMethodFolders.includes(fullPath);
|
|
485
|
+
}).length;
|
|
486
|
+
return { beingDeleted, total: methodDirs.length };
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
// Subfolder doesn't exist
|
|
490
|
+
return { beingDeleted: 0, total: 0 };
|
|
491
|
+
}
|
|
492
|
+
}));
|
|
493
|
+
const totalMethodFolders = subfolderResults.reduce((sum, r) => sum + r.total, 0);
|
|
494
|
+
const foldersBeingDeleted = subfolderResults.reduce((sum, r) => sum + r.beingDeleted, 0);
|
|
495
|
+
// If all method folders are being deleted, check if settings would be empty
|
|
496
|
+
if (totalMethodFolders > 0 && totalMethodFolders === foldersBeingDeleted) {
|
|
497
|
+
// Check if settings file would become empty after removing methods
|
|
498
|
+
const settingsPath = join(idePath, ideFolder.settingsFile);
|
|
499
|
+
try {
|
|
500
|
+
const content = await fs.readFile(settingsPath, 'utf8');
|
|
501
|
+
const settings = JSON.parse(content);
|
|
502
|
+
// Remove method entries from methods tracking object
|
|
503
|
+
if (settings.methods && typeof settings.methods === 'object') {
|
|
504
|
+
for (const method of methodsToRemove) {
|
|
505
|
+
if (method in settings.methods) {
|
|
506
|
+
delete settings.methods[method];
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Remove methods object if empty
|
|
510
|
+
if (Object.keys(settings.methods).length === 0) {
|
|
511
|
+
delete settings.methods;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// Remove hooks that would be empty
|
|
515
|
+
if (settings.hooks && typeof settings.hooks === 'object') {
|
|
516
|
+
// Simplified check - if hooks only contains method-related entries
|
|
517
|
+
delete settings.hooks;
|
|
518
|
+
}
|
|
519
|
+
return Object.keys(settings).length === 0;
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
// Settings file doesn't exist or is invalid - would be considered empty
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return false;
|
|
527
|
+
};
|
|
528
|
+
const [willClaudeFolderBeEmpty, willWindsurfFolderBeEmpty] = await Promise.all([
|
|
529
|
+
checkIdeRemoval(IDE_FOLDERS.claude),
|
|
530
|
+
checkIdeRemoval(IDE_FOLDERS.windsurf),
|
|
531
|
+
]);
|
|
532
|
+
if (willClaudeFolderBeEmpty) {
|
|
533
|
+
this.logInfo(`${IDE_FOLDERS.claude.root}/ folder will be removed (will be empty)`);
|
|
534
|
+
this.log('');
|
|
535
|
+
}
|
|
536
|
+
if (willWindsurfFolderBeEmpty) {
|
|
537
|
+
this.logInfo(`${IDE_FOLDERS.windsurf.root}/ folder will be removed (will be empty)`);
|
|
538
|
+
this.log('');
|
|
539
|
+
}
|
|
540
|
+
// Dry run - just show what would happen
|
|
541
|
+
if (flags['dry-run']) {
|
|
542
|
+
this.logInfo('Dry run complete. No files or folders were deleted.');
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
// Calculate total items for confirmation
|
|
546
|
+
const totalFolders = workflowFolders.length + outputMethodFolders.length + ideMethodFolders.length;
|
|
547
|
+
// Confirm deletion
|
|
548
|
+
if (!flags.force) {
|
|
549
|
+
const shouldDelete = await confirm({
|
|
550
|
+
message: `Delete ${totalFolders} folder(s)?`,
|
|
551
|
+
default: false,
|
|
552
|
+
});
|
|
553
|
+
if (!shouldDelete) {
|
|
554
|
+
this.log('Operation cancelled.');
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// Delete all folders in parallel
|
|
559
|
+
const deleteFolder = async (folder, type) => {
|
|
560
|
+
try {
|
|
561
|
+
await removeDirectory(folder);
|
|
562
|
+
this.logDebug(`Removed ${type} folder: ${folder}`);
|
|
563
|
+
return { folder, success: true, type };
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
const err = error;
|
|
567
|
+
this.logWarning(`Failed to delete ${folder}: ${err.message}`);
|
|
568
|
+
return { folder, success: false, type };
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
const deleteResults = await Promise.all([
|
|
572
|
+
...workflowFolders.map((f) => deleteFolder(f, 'workflow')),
|
|
573
|
+
...outputMethodFolders.map((f) => deleteFolder(f, 'output')),
|
|
574
|
+
...ideMethodFolders.map((f) => deleteFolder(f, 'IDE method')),
|
|
575
|
+
]);
|
|
576
|
+
const deletedWorkflow = deleteResults.filter((r) => r.success && r.type === 'workflow').length;
|
|
577
|
+
const deletedOutput = deleteResults.filter((r) => r.success && r.type === 'output').length;
|
|
578
|
+
const deletedIde = deleteResults.filter((r) => r.success && r.type === 'IDE method').length;
|
|
579
|
+
// Check if _output folder is now empty and remove it
|
|
580
|
+
let removedOutputDir = false;
|
|
581
|
+
try {
|
|
582
|
+
if (await isDirectoryEmpty(outputDir)) {
|
|
583
|
+
await removeDirectory(outputDir);
|
|
584
|
+
this.logDebug(`Removed empty ${AIWCLI_CONTAINER}/${OUTPUT_FOLDER_NAME}/ folder`);
|
|
585
|
+
removedOutputDir = true;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
// _output doesn't exist or can't be accessed
|
|
590
|
+
}
|
|
591
|
+
// Check if .aiwcli container is now empty and remove it
|
|
592
|
+
let removedAiwcliContainer = false;
|
|
593
|
+
try {
|
|
594
|
+
if (await isDirectoryEmpty(containerDir)) {
|
|
595
|
+
await removeDirectory(containerDir);
|
|
596
|
+
this.logDebug(`Removed empty ${AIWCLI_CONTAINER}/ folder`);
|
|
597
|
+
removedAiwcliContainer = true;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
// .aiwcli doesn't exist or can't be accessed
|
|
602
|
+
}
|
|
603
|
+
// Update .gitignore to remove .aiwcli if the container was deleted
|
|
604
|
+
if (removedAiwcliContainer) {
|
|
605
|
+
await updateGitignoreAfterClear(targetDir, [AIWCLI_CONTAINER]);
|
|
606
|
+
this.logDebug('Updated .gitignore');
|
|
607
|
+
}
|
|
608
|
+
// Update IDE settings files to remove method-specific entries
|
|
609
|
+
let updatedClaudeSettings = false;
|
|
610
|
+
let updatedWindsurfSettings = false;
|
|
611
|
+
if (methodsToRemove.length > 0) {
|
|
612
|
+
const claudeResult = await updateIdeSettings(targetDir, IDE_FOLDERS.claude, methodsToRemove);
|
|
613
|
+
if (claudeResult.updated) {
|
|
614
|
+
this.logDebug('Updated .claude/settings.json (backup created)');
|
|
615
|
+
updatedClaudeSettings = true;
|
|
616
|
+
}
|
|
617
|
+
const windsurfResult = await updateIdeSettings(targetDir, IDE_FOLDERS.windsurf, methodsToRemove);
|
|
618
|
+
if (windsurfResult.updated) {
|
|
619
|
+
this.logDebug('Updated .windsurf/hooks.json (backup created)');
|
|
620
|
+
updatedWindsurfSettings = true;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
// Check if IDE folders should be fully deleted (empty settings + empty subfolders)
|
|
624
|
+
let removedClaudeDir = false;
|
|
625
|
+
let removedWindsurfDir = false;
|
|
626
|
+
if (await shouldDeleteIdeFolder(targetDir, IDE_FOLDERS.claude)) {
|
|
627
|
+
const claudeDirPath = join(targetDir, IDE_FOLDERS.claude.root);
|
|
628
|
+
try {
|
|
629
|
+
await removeDirectory(claudeDirPath);
|
|
630
|
+
this.logDebug(`Removed empty ${IDE_FOLDERS.claude.root}/ folder`);
|
|
631
|
+
removedClaudeDir = true;
|
|
632
|
+
// If we deleted the whole folder, the settings update message is misleading
|
|
633
|
+
updatedClaudeSettings = false;
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
// Folder can't be removed
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (await shouldDeleteIdeFolder(targetDir, IDE_FOLDERS.windsurf)) {
|
|
640
|
+
const windsurfDirPath = join(targetDir, IDE_FOLDERS.windsurf.root);
|
|
641
|
+
try {
|
|
642
|
+
await removeDirectory(windsurfDirPath);
|
|
643
|
+
this.logDebug(`Removed empty ${IDE_FOLDERS.windsurf.root}/ folder`);
|
|
644
|
+
removedWindsurfDir = true;
|
|
645
|
+
// If we deleted the whole folder, the settings update message is misleading
|
|
646
|
+
updatedWindsurfSettings = false;
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
// Folder can't be removed
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// Report results
|
|
653
|
+
this.log('');
|
|
654
|
+
const parts = [];
|
|
655
|
+
if (deletedWorkflow > 0) {
|
|
656
|
+
parts.push(`${deletedWorkflow} workflow folder(s)`);
|
|
657
|
+
}
|
|
658
|
+
if (deletedOutput > 0) {
|
|
659
|
+
parts.push(`${deletedOutput} output folder(s)`);
|
|
660
|
+
}
|
|
661
|
+
if (deletedIde > 0) {
|
|
662
|
+
parts.push(`${deletedIde} IDE method folder(s)`);
|
|
663
|
+
}
|
|
664
|
+
if (removedOutputDir) {
|
|
665
|
+
parts.push(`${AIWCLI_CONTAINER}/${OUTPUT_FOLDER_NAME}/ folder`);
|
|
666
|
+
}
|
|
667
|
+
if (removedAiwcliContainer) {
|
|
668
|
+
parts.push(`${AIWCLI_CONTAINER}/ folder`);
|
|
669
|
+
}
|
|
670
|
+
if (removedClaudeDir) {
|
|
671
|
+
parts.push(`${IDE_FOLDERS.claude.root}/ folder`);
|
|
672
|
+
}
|
|
673
|
+
if (removedWindsurfDir) {
|
|
674
|
+
parts.push(`${IDE_FOLDERS.windsurf.root}/ folder`);
|
|
675
|
+
}
|
|
676
|
+
this.logSuccess(`Cleared: ${parts.join(', ')}.`);
|
|
677
|
+
if (removedAiwcliContainer) {
|
|
678
|
+
this.logSuccess('Updated .gitignore.');
|
|
679
|
+
}
|
|
680
|
+
if (updatedClaudeSettings) {
|
|
681
|
+
this.logSuccess('Updated .claude/settings.json (backup: settings.json.backup).');
|
|
682
|
+
}
|
|
683
|
+
if (updatedWindsurfSettings) {
|
|
684
|
+
this.logSuccess('Updated .windsurf/hooks.json (backup: hooks.json.backup).');
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
catch (error) {
|
|
688
|
+
const err = error;
|
|
689
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
690
|
+
this.error(`Permission denied. ${err.message}`, {
|
|
691
|
+
exit: EXIT_CODES.ENVIRONMENT_ERROR,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
this.error(`Clear failed: ${err.message}`, {
|
|
695
|
+
exit: EXIT_CODES.GENERAL_ERROR,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Extract method names from workflow folder names (e.g., _gsd -> gsd).
|
|
701
|
+
*
|
|
702
|
+
* @param workflowFolders - Array of workflow folder paths
|
|
703
|
+
* @returns Array of method names
|
|
704
|
+
*/
|
|
705
|
+
extractMethodNames(workflowFolders) {
|
|
706
|
+
const methods = [];
|
|
707
|
+
for (const folder of workflowFolders) {
|
|
708
|
+
const folderName = folder.split(/[/\\]/).pop() || '';
|
|
709
|
+
if (folderName.startsWith('_')) {
|
|
710
|
+
methods.push(folderName.slice(1));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return methods;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Find all IDE method folders (e.g., .claude/commands/{method}/, .claude/skills/{method}/).
|
|
717
|
+
* Searches within IDE configuration folders for method-specific subfolders.
|
|
718
|
+
*
|
|
719
|
+
* @param targetDir - Directory to search in
|
|
720
|
+
* @param template - Optional template/method name to filter by
|
|
721
|
+
* @returns Array of IDE method folder paths
|
|
722
|
+
*/
|
|
723
|
+
async findIdeMethodFolders(targetDir, template) {
|
|
724
|
+
// Build list of all subfolder paths to check
|
|
725
|
+
const subfolderChecks = [];
|
|
726
|
+
for (const ide of Object.values(IDE_FOLDERS)) {
|
|
727
|
+
const ideRoot = join(targetDir, ide.root);
|
|
728
|
+
for (const subfolder of ide.methodSubfolders) {
|
|
729
|
+
subfolderChecks.push({ ideRoot, subfolder });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
// Check all subfolders in parallel
|
|
733
|
+
const subfolderResults = await Promise.all(subfolderChecks.map(async ({ ideRoot, subfolder }) => {
|
|
734
|
+
const subfolderPath = join(ideRoot, subfolder);
|
|
735
|
+
try {
|
|
736
|
+
const stat = await fs.stat(subfolderPath);
|
|
737
|
+
if (!stat.isDirectory())
|
|
738
|
+
return [];
|
|
739
|
+
const entries = await fs.readdir(subfolderPath, { withFileTypes: true });
|
|
740
|
+
return entries
|
|
741
|
+
.filter((entry) => entry.isDirectory())
|
|
742
|
+
.filter((entry) => !template || entry.name === template)
|
|
743
|
+
.map((entry) => join(subfolderPath, entry.name));
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
return [];
|
|
747
|
+
}
|
|
748
|
+
}));
|
|
749
|
+
return subfolderResults.flat();
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Find all output folders in the target directory.
|
|
753
|
+
* Looks for .aiwcli/_output/{method}/ structure.
|
|
754
|
+
*
|
|
755
|
+
* @param targetDir - Directory to search in
|
|
756
|
+
* @param template - Optional template/method name to filter by (e.g., 'bmad', 'gsd')
|
|
757
|
+
* @returns Array of output folder paths
|
|
758
|
+
*/
|
|
759
|
+
async findOutputFolders(targetDir, template) {
|
|
760
|
+
const containerDir = join(targetDir, AIWCLI_CONTAINER);
|
|
761
|
+
const outputDir = join(containerDir, OUTPUT_FOLDER_NAME);
|
|
762
|
+
// Check if _output folder exists
|
|
763
|
+
try {
|
|
764
|
+
const stat = await fs.stat(outputDir);
|
|
765
|
+
if (!stat.isDirectory()) {
|
|
766
|
+
return [];
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
// _output folder doesn't exist
|
|
771
|
+
return [];
|
|
772
|
+
}
|
|
773
|
+
// If template specified, only look for that specific method folder
|
|
774
|
+
if (template) {
|
|
775
|
+
const methodPath = join(outputDir, template);
|
|
776
|
+
try {
|
|
777
|
+
const stat = await fs.stat(methodPath);
|
|
778
|
+
if (stat.isDirectory()) {
|
|
779
|
+
return [methodPath];
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
catch {
|
|
783
|
+
// Method folder doesn't exist
|
|
784
|
+
}
|
|
785
|
+
return [];
|
|
786
|
+
}
|
|
787
|
+
// No template filter - find all method folders within _output
|
|
788
|
+
const foundFolders = [];
|
|
789
|
+
try {
|
|
790
|
+
const entries = await fs.readdir(outputDir, { withFileTypes: true });
|
|
791
|
+
for (const entry of entries) {
|
|
792
|
+
if (entry.isDirectory()) {
|
|
793
|
+
foundFolders.push(join(outputDir, entry.name));
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
catch {
|
|
798
|
+
// Directory can't be read - return empty
|
|
799
|
+
}
|
|
800
|
+
return foundFolders;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Find all workflow folders in the target directory.
|
|
804
|
+
* Looks for .aiwcli/_{method}/ structure (e.g., .aiwcli/_gsd/, .aiwcli/_bmad/).
|
|
805
|
+
*
|
|
806
|
+
* @param targetDir - Directory to search in
|
|
807
|
+
* @param template - Optional template/method name to filter by (e.g., 'bmad', 'gsd')
|
|
808
|
+
* @returns Array of workflow folder paths
|
|
809
|
+
*/
|
|
810
|
+
async findWorkflowFolders(targetDir, template) {
|
|
811
|
+
const foundFolders = [];
|
|
812
|
+
const containerDir = join(targetDir, AIWCLI_CONTAINER);
|
|
813
|
+
try {
|
|
814
|
+
const entries = await fs.readdir(containerDir, { withFileTypes: true });
|
|
815
|
+
for (const entry of entries) {
|
|
816
|
+
// Look for directories starting with underscore (workflow folders)
|
|
817
|
+
if (entry.isDirectory() && entry.name.startsWith('_') && entry.name !== OUTPUT_FOLDER_NAME) {
|
|
818
|
+
// If template specified, only include matching folder
|
|
819
|
+
if (template) {
|
|
820
|
+
if (entry.name === `_${template}`) {
|
|
821
|
+
foundFolders.push(join(containerDir, entry.name));
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
foundFolders.push(join(containerDir, entry.name));
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
catch {
|
|
831
|
+
// Directory can't be read - return empty
|
|
832
|
+
}
|
|
833
|
+
return foundFolders;
|
|
834
|
+
}
|
|
835
|
+
}
|