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.
Files changed (204) hide show
  1. package/README.md +1248 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +16 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +19 -0
  6. package/dist/commands/branch.d.ts +45 -0
  7. package/dist/commands/branch.js +488 -0
  8. package/dist/commands/clean.d.ts +34 -0
  9. package/dist/commands/clean.js +186 -0
  10. package/dist/commands/clear.d.ts +51 -0
  11. package/dist/commands/clear.js +835 -0
  12. package/dist/commands/init/index.d.ts +107 -0
  13. package/dist/commands/init/index.js +565 -0
  14. package/dist/commands/launch.d.ts +21 -0
  15. package/dist/commands/launch.js +108 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/lib/base-command.d.ts +114 -0
  19. package/dist/lib/base-command.js +153 -0
  20. package/dist/lib/bmad-installer.d.ts +38 -0
  21. package/dist/lib/bmad-installer.js +145 -0
  22. package/dist/lib/claude-settings-types.d.ts +102 -0
  23. package/dist/lib/claude-settings-types.js +5 -0
  24. package/dist/lib/config.d.ts +25 -0
  25. package/dist/lib/config.js +46 -0
  26. package/dist/lib/debug.d.ts +39 -0
  27. package/dist/lib/debug.js +74 -0
  28. package/dist/lib/env-compat.d.ts +26 -0
  29. package/dist/lib/env-compat.js +35 -0
  30. package/dist/lib/errors.d.ts +126 -0
  31. package/dist/lib/errors.js +145 -0
  32. package/dist/lib/generic-merge.d.ts +74 -0
  33. package/dist/lib/generic-merge.js +105 -0
  34. package/dist/lib/git/branch.d.ts +67 -0
  35. package/dist/lib/git/branch.js +155 -0
  36. package/dist/lib/git/index.d.ts +11 -0
  37. package/dist/lib/git/index.js +13 -0
  38. package/dist/lib/git/safety-checks.d.ts +44 -0
  39. package/dist/lib/git/safety-checks.js +102 -0
  40. package/dist/lib/git/types.d.ts +31 -0
  41. package/dist/lib/git/types.js +6 -0
  42. package/dist/lib/git/worktree.d.ts +67 -0
  43. package/dist/lib/git/worktree.js +220 -0
  44. package/dist/lib/gitignore-manager.d.ts +10 -0
  45. package/dist/lib/gitignore-manager.js +60 -0
  46. package/dist/lib/hooks-merger.d.ts +28 -0
  47. package/dist/lib/hooks-merger.js +94 -0
  48. package/dist/lib/ide-path-resolver.d.ts +102 -0
  49. package/dist/lib/ide-path-resolver.js +129 -0
  50. package/dist/lib/index.d.ts +13 -0
  51. package/dist/lib/index.js +22 -0
  52. package/dist/lib/output.d.ts +51 -0
  53. package/dist/lib/output.js +76 -0
  54. package/dist/lib/paths.d.ts +66 -0
  55. package/dist/lib/paths.js +136 -0
  56. package/dist/lib/quiet.d.ts +12 -0
  57. package/dist/lib/quiet.js +17 -0
  58. package/dist/lib/settings-hierarchy.d.ts +42 -0
  59. package/dist/lib/settings-hierarchy.js +105 -0
  60. package/dist/lib/spawn.d.ts +105 -0
  61. package/dist/lib/spawn.js +157 -0
  62. package/dist/lib/spinner.d.ts +19 -0
  63. package/dist/lib/spinner.js +34 -0
  64. package/dist/lib/stdin.d.ts +48 -0
  65. package/dist/lib/stdin.js +60 -0
  66. package/dist/lib/template-installer.d.ts +92 -0
  67. package/dist/lib/template-installer.js +375 -0
  68. package/dist/lib/template-linter.d.ts +49 -0
  69. package/dist/lib/template-linter.js +173 -0
  70. package/dist/lib/template-merger.d.ts +47 -0
  71. package/dist/lib/template-merger.js +173 -0
  72. package/dist/lib/template-resolver.d.ts +20 -0
  73. package/dist/lib/template-resolver.js +60 -0
  74. package/dist/lib/terminal.d.ts +102 -0
  75. package/dist/lib/terminal.js +245 -0
  76. package/dist/lib/tty-detection.d.ts +62 -0
  77. package/dist/lib/tty-detection.js +83 -0
  78. package/dist/lib/user-utils.d.ts +5 -0
  79. package/dist/lib/user-utils.js +23 -0
  80. package/dist/lib/version.d.ts +99 -0
  81. package/dist/lib/version.js +144 -0
  82. package/dist/lib/watch-templates.d.ts +6 -0
  83. package/dist/lib/watch-templates.js +73 -0
  84. package/dist/lib/windsurf-hooks-hierarchy.d.ts +30 -0
  85. package/dist/lib/windsurf-hooks-hierarchy.js +66 -0
  86. package/dist/lib/windsurf-hooks-merger.d.ts +26 -0
  87. package/dist/lib/windsurf-hooks-merger.js +53 -0
  88. package/dist/lib/windsurf-hooks-types.d.ts +33 -0
  89. package/dist/lib/windsurf-hooks-types.js +5 -0
  90. package/dist/templates/CLAUDE.md +174 -0
  91. package/dist/templates/_shared/.claude/commands/handoff.md +14 -0
  92. package/dist/templates/_shared/.claude/settings.json +61 -0
  93. package/dist/templates/_shared/.codex/workflows/handoff.md +14 -0
  94. package/dist/templates/_shared/.windsurf/workflows/handoff.md +14 -0
  95. package/dist/templates/_shared/hooks/__init__.py +16 -0
  96. package/dist/templates/_shared/hooks/archive_plan.py +270 -0
  97. package/dist/templates/_shared/hooks/context_enforcer.py +621 -0
  98. package/dist/templates/_shared/hooks/context_monitor.py +322 -0
  99. package/dist/templates/_shared/hooks/file-suggestion.py +188 -0
  100. package/dist/templates/_shared/hooks/task_create_capture.py +194 -0
  101. package/dist/templates/_shared/hooks/task_update_capture.py +254 -0
  102. package/dist/templates/_shared/hooks/user_prompt_submit.py +157 -0
  103. package/dist/templates/_shared/lib/__init__.py +1 -0
  104. package/dist/templates/_shared/lib/base/__init__.py +49 -0
  105. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  106. package/dist/templates/_shared/lib/base/atomic_write.py +180 -0
  107. package/dist/templates/_shared/lib/base/constants.py +299 -0
  108. package/dist/templates/_shared/lib/base/inference.py +189 -0
  109. package/dist/templates/_shared/lib/base/utils.py +216 -0
  110. package/dist/templates/_shared/lib/context/__init__.py +119 -0
  111. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  112. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  113. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  114. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  115. package/dist/templates/_shared/lib/context/cache.py +446 -0
  116. package/dist/templates/_shared/lib/context/context_manager.py +1171 -0
  117. package/dist/templates/_shared/lib/context/discovery.py +486 -0
  118. package/dist/templates/_shared/lib/context/event_log.py +308 -0
  119. package/dist/templates/_shared/lib/context/plan_archive.py +247 -0
  120. package/dist/templates/_shared/lib/context/task_sync.py +367 -0
  121. package/dist/templates/_shared/lib/handoff/__init__.py +22 -0
  122. package/dist/templates/_shared/lib/handoff/document_generator.py +307 -0
  123. package/dist/templates/_shared/lib/templates/README.md +215 -0
  124. package/dist/templates/_shared/lib/templates/__init__.py +40 -0
  125. package/dist/templates/_shared/lib/templates/formatters.py +147 -0
  126. package/dist/templates/_shared/lib/templates/plan_context.py +119 -0
  127. package/dist/templates/_shared/scripts/save_handoff.py +99 -0
  128. package/dist/templates/_shared/workflows/handoff.md +212 -0
  129. package/dist/templates/cc-native/.claude/agents/cc-native/ACCESSIBILITY-TESTER.md +80 -0
  130. package/dist/templates/cc-native/.claude/agents/cc-native/ARCHITECT-REVIEWER.md +75 -0
  131. package/dist/templates/cc-native/.claude/agents/cc-native/ASSUMPTION-CHAIN-TRACER.md +239 -0
  132. package/dist/templates/cc-native/.claude/agents/cc-native/CLARITY-AUDITOR.md +109 -0
  133. package/dist/templates/cc-native/.claude/agents/cc-native/CODE-REVIEWER.md +71 -0
  134. package/dist/templates/cc-native/.claude/agents/cc-native/COMPLETENESS-CHECKER.md +104 -0
  135. package/dist/templates/cc-native/.claude/agents/cc-native/CONTEXT-EXTRACTOR.md +93 -0
  136. package/dist/templates/cc-native/.claude/agents/cc-native/DEVILS-ADVOCATE.md +223 -0
  137. package/dist/templates/cc-native/.claude/agents/cc-native/DOCUMENTATION-REVIEWER.md +73 -0
  138. package/dist/templates/cc-native/.claude/agents/cc-native/FEASIBILITY-ANALYST.md +93 -0
  139. package/dist/templates/cc-native/.claude/agents/cc-native/FRESH-PERSPECTIVE.md +103 -0
  140. package/dist/templates/cc-native/.claude/agents/cc-native/HANDOFF-READINESS.md +145 -0
  141. package/dist/templates/cc-native/.claude/agents/cc-native/HIDDEN-COMPLEXITY-DETECTOR.md +248 -0
  142. package/dist/templates/cc-native/.claude/agents/cc-native/INCENTIVE-MAPPER.md +235 -0
  143. package/dist/templates/cc-native/.claude/agents/cc-native/PENETRATION-TESTER.md +80 -0
  144. package/dist/templates/cc-native/.claude/agents/cc-native/PERFORMANCE-ENGINEER.md +76 -0
  145. package/dist/templates/cc-native/.claude/agents/cc-native/PLAN-ORCHESTRATOR.md +141 -0
  146. package/dist/templates/cc-native/.claude/agents/cc-native/PRECEDENT-FINDER.md +240 -0
  147. package/dist/templates/cc-native/.claude/agents/cc-native/REVERSIBILITY-ANALYST.md +211 -0
  148. package/dist/templates/cc-native/.claude/agents/cc-native/RISK-ASSESSOR.md +101 -0
  149. package/dist/templates/cc-native/.claude/agents/cc-native/SECOND-ORDER-ANALYST.md +197 -0
  150. package/dist/templates/cc-native/.claude/agents/cc-native/SIMPLICITY-GUARDIAN.md +97 -0
  151. package/dist/templates/cc-native/.claude/agents/cc-native/SKEPTIC.md +349 -0
  152. package/dist/templates/cc-native/.claude/agents/cc-native/STAKEHOLDER-ADVOCATE.md +106 -0
  153. package/dist/templates/cc-native/.claude/agents/cc-native/TRADE-OFF-ILLUMINATOR.md +205 -0
  154. package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +8 -0
  155. package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -0
  156. package/dist/templates/cc-native/.claude/settings.json +119 -0
  157. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -0
  158. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +8 -0
  159. package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -0
  160. package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -0
  161. package/dist/templates/cc-native/CC-NATIVE-README.md +192 -0
  162. package/dist/templates/cc-native/MIGRATION.md +86 -0
  163. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +331 -0
  164. package/dist/templates/cc-native/_cc-native/docs/PERMISSION_REQUEST_VERIFICATION.md +147 -0
  165. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  166. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  167. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  168. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  169. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  170. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +150 -0
  171. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +746 -0
  172. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +339 -0
  173. package/dist/templates/cc-native/_cc-native/lib/__init__.py +57 -0
  174. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  175. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  176. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  177. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  178. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +68 -0
  179. package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +98 -0
  180. package/dist/templates/cc-native/_cc-native/lib/constants.py +45 -0
  181. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +273 -0
  182. package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +28 -0
  183. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  184. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  185. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  186. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  187. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  188. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +164 -0
  189. package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +89 -0
  190. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +119 -0
  191. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +103 -0
  192. package/dist/templates/cc-native/_cc-native/lib/state.py +251 -0
  193. package/dist/templates/cc-native/_cc-native/lib/utils.py +830 -0
  194. package/dist/templates/cc-native/_cc-native/plan-review.config.json +76 -0
  195. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  196. package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +151 -0
  197. package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +134 -0
  198. package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -0
  199. package/dist/types/exit-codes.d.ts +11 -0
  200. package/dist/types/exit-codes.js +10 -0
  201. package/dist/types/index.d.ts +5 -0
  202. package/dist/types/index.js +7 -0
  203. package/oclif.manifest.json +405 -0
  204. 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
+ }