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,375 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { IdePathResolver } from './ide-path-resolver.js';
4
+ import { mergeTemplateContent } from './template-merger.js';
5
+ /**
6
+ * Deep merge two settings objects, combining hook arrays.
7
+ * Used to merge _shared/settings.json into .claude/settings.json
8
+ */
9
+ function deepMergeSettings(target, source) {
10
+ const result = { ...target };
11
+ for (const key of Object.keys(source)) {
12
+ // Skip comment fields
13
+ if (key.startsWith('$') || key.startsWith('_')) {
14
+ continue;
15
+ }
16
+ const sourceValue = source[key];
17
+ const targetValue = result[key];
18
+ if (key === 'hooks' && typeof sourceValue === 'object' && sourceValue !== null) {
19
+ // Special handling for hooks - merge by event type
20
+ result[key] = mergeHooks(targetValue || {}, sourceValue);
21
+ }
22
+ else if (Array.isArray(sourceValue) && Array.isArray(targetValue)) {
23
+ // Concatenate arrays
24
+ result[key] = [...targetValue, ...sourceValue];
25
+ }
26
+ else if (typeof sourceValue === 'object' && sourceValue !== null && typeof targetValue === 'object' && targetValue !== null) {
27
+ // Recursively merge objects
28
+ result[key] = deepMergeSettings(targetValue, sourceValue);
29
+ }
30
+ else {
31
+ // Override with source value
32
+ result[key] = sourceValue;
33
+ }
34
+ }
35
+ return result;
36
+ }
37
+ /**
38
+ * Merge hook configurations, combining arrays for each event type.
39
+ */
40
+ function mergeHooks(target, source) {
41
+ const result = { ...target };
42
+ for (const eventType of Object.keys(source)) {
43
+ const targetHooks = result[eventType];
44
+ const sourceHooks = source[eventType];
45
+ if (targetHooks && sourceHooks) {
46
+ // Append source hooks to existing event type
47
+ result[eventType] = [...targetHooks, ...sourceHooks];
48
+ }
49
+ else if (sourceHooks) {
50
+ // New event type
51
+ result[eventType] = sourceHooks;
52
+ }
53
+ }
54
+ return result;
55
+ }
56
+ /**
57
+ * Merge settings from a source settings.json file into the IDE settings file.
58
+ * Reads from the provided source path and merges into .claude/settings.json at project root.
59
+ *
60
+ * @param targetDir - Project root directory
61
+ * @param sourceSettingsPath - Absolute path to source settings.json file
62
+ * @returns true if merge successful, false otherwise
63
+ */
64
+ async function mergeSharedSettingsFromSource(targetDir, sourceSettingsPath) {
65
+ const resolver = new IdePathResolver(targetDir);
66
+ const ideSettingsPath = resolver.getClaudeSettings();
67
+ // Check if source settings exists
68
+ if (!(await pathExists(sourceSettingsPath))) {
69
+ return false;
70
+ }
71
+ try {
72
+ // Read source settings
73
+ const sourceContent = await fs.readFile(sourceSettingsPath, 'utf8');
74
+ const sourceSettings = JSON.parse(sourceContent);
75
+ // Read IDE settings (create empty object if doesn't exist)
76
+ let ideSettings = {};
77
+ if (await pathExists(ideSettingsPath)) {
78
+ const ideContent = await fs.readFile(ideSettingsPath, 'utf8');
79
+ ideSettings = JSON.parse(ideContent);
80
+ }
81
+ else {
82
+ // Create .claude directory if it doesn't exist
83
+ await fs.mkdir(dirname(ideSettingsPath), { recursive: true });
84
+ }
85
+ // Merge source settings into IDE settings
86
+ const mergedSettings = deepMergeSettings(ideSettings, sourceSettings);
87
+ // Write merged settings back
88
+ await fs.writeFile(ideSettingsPath, JSON.stringify(mergedSettings, null, 4) + '\n', 'utf8');
89
+ return true;
90
+ }
91
+ catch {
92
+ // Silently fail on parse/write errors
93
+ return false;
94
+ }
95
+ }
96
+ /**
97
+ * Check if a path exists
98
+ */
99
+ async function pathExists(path) {
100
+ try {
101
+ await fs.access(path);
102
+ return true;
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ }
108
+ /**
109
+ * Check template installation status for a method.
110
+ * Returns which items exist and which are missing.
111
+ *
112
+ * @param templatePath - Path to the template directory
113
+ * @param targetDir - Target directory to check
114
+ * @param ides - List of IDEs to check (for dot folders)
115
+ * @param templateName - Name of the template (for identifying workflow folder)
116
+ * @returns Status of template items
117
+ */
118
+ export async function checkTemplateStatus(templatePath, targetDir, ides, templateName) {
119
+ const existing = [];
120
+ const missing = [];
121
+ // Scan template directory
122
+ const entries = await fs.readdir(templatePath, { withFileTypes: true });
123
+ // Identify workflow folder based on template name
124
+ // Convention: _templatename (e.g., _gsd, _bmad)
125
+ const workflowFolderName = `_${templateName}`;
126
+ let workflowFolder = null;
127
+ let workflowFolderExists = false;
128
+ // Filter entries to only include relevant items (skip non-selected IDE folders and excluded patterns)
129
+ const relevantEntries = entries.filter((entry) => {
130
+ // Skip excluded patterns (test files, cache, etc.)
131
+ if (shouldExclude(entry.name)) {
132
+ return false;
133
+ }
134
+ if (entry.name.startsWith('.') && entry.isDirectory()) {
135
+ const ideName = entry.name.slice(1);
136
+ return ides.includes(ideName);
137
+ }
138
+ return true;
139
+ });
140
+ // Check all entries in parallel
141
+ // Non-dot folders go into .aiwcli/, dot folders stay at project root
142
+ const resolver = new IdePathResolver(targetDir);
143
+ const containerDir = resolver.getAiwcliContainer();
144
+ const statusChecks = relevantEntries.map(async (entry) => {
145
+ // Dot folders (IDE folders) are at project root, non-dot folders are in .aiwcli/
146
+ const targetPath = entry.name.startsWith('.')
147
+ ? resolver.getIdeDir(entry.name.slice(1))
148
+ : join(containerDir, entry.name);
149
+ const exists = await pathExists(targetPath);
150
+ return {
151
+ name: entry.name,
152
+ isDirectory: entry.isDirectory(),
153
+ exists,
154
+ };
155
+ });
156
+ const statuses = await Promise.all(statusChecks);
157
+ for (const status of statuses) {
158
+ if (status.exists) {
159
+ existing.push(status);
160
+ }
161
+ else {
162
+ missing.push(status);
163
+ }
164
+ // Track workflow folder
165
+ if (status.name === workflowFolderName) {
166
+ workflowFolder = workflowFolderName;
167
+ workflowFolderExists = status.exists;
168
+ }
169
+ }
170
+ return {
171
+ existing,
172
+ missing,
173
+ workflowFolder,
174
+ workflowFolderExists,
175
+ };
176
+ }
177
+ /**
178
+ * Patterns to exclude when copying template directories.
179
+ * These are development/test artifacts that shouldn't be packaged.
180
+ */
181
+ const EXCLUDED_PATTERNS = [
182
+ '_output',
183
+ '__pycache__',
184
+ '.pytest_cache',
185
+ 'conftest.py',
186
+ /^test_.*\.py$/,
187
+ /.*\.pyc$/,
188
+ ];
189
+ /**
190
+ * Check if a filename should be excluded from copying
191
+ */
192
+ function shouldExclude(name) {
193
+ return EXCLUDED_PATTERNS.some((pattern) => {
194
+ if (typeof pattern === 'string') {
195
+ return name === pattern;
196
+ }
197
+ return pattern.test(name);
198
+ });
199
+ }
200
+ /**
201
+ * Copy directory recursively with proper error handling.
202
+ * Excludes test files, cache directories, and output folders.
203
+ *
204
+ * @param src - Source directory path
205
+ * @param dest - Destination directory path
206
+ * @param excludeIdeFolders - If true, exclude IDE config folders (.claude, .windsurf, etc.)
207
+ */
208
+ export async function copyDir(src, dest, excludeIdeFolders = false) {
209
+ await fs.mkdir(dest, { recursive: true });
210
+ const entries = await fs.readdir(src, { withFileTypes: true });
211
+ const operations = entries
212
+ .filter((entry) => {
213
+ // Standard exclusions (test files, cache, etc.)
214
+ if (shouldExclude(entry.name)) {
215
+ return false;
216
+ }
217
+ // Exclude IDE config folders if requested (used for _shared folder)
218
+ // These folders are used for settings merging, not direct installation
219
+ if (excludeIdeFolders && entry.isDirectory() && entry.name.startsWith('.')) {
220
+ return false;
221
+ }
222
+ return true;
223
+ })
224
+ .map(async (entry) => {
225
+ const srcPath = join(src, entry.name);
226
+ const destPath = join(dest, entry.name);
227
+ try {
228
+ return entry.isDirectory() ? await copyDir(srcPath, destPath, excludeIdeFolders) : await fs.copyFile(srcPath, destPath);
229
+ }
230
+ catch (error) {
231
+ const err = error;
232
+ throw new Error(`Failed to copy ${srcPath} to ${destPath}: ${err.message}`);
233
+ }
234
+ });
235
+ await Promise.all(operations);
236
+ }
237
+ /**
238
+ * Install template with IDE-specific folder selection.
239
+ * Supports selective installation - only installs items that don't already exist.
240
+ *
241
+ * Template structure:
242
+ * - Non-dot folders (e.g., _bmad/, GSR/) are installed if not already present
243
+ * - Dot folders (e.g., .claude/, .windsurf/) are installed only if matching IDE flag and not already present
244
+ *
245
+ * @param config - Installation configuration
246
+ * @param skipExisting - If true, skip items that already exist (default: true for regeneration support)
247
+ * @returns Installation result with list of installed and skipped folders
248
+ * @throws Error if template doesn't exist or requested IDE folder not found
249
+ */
250
+ export async function installTemplate(config, skipExisting = true) {
251
+ const { templateName, targetDir, ides, templatePath } = config;
252
+ // Verify template exists
253
+ try {
254
+ await fs.access(templatePath);
255
+ }
256
+ catch {
257
+ throw new Error(`Template '${templateName}' not found at ${templatePath}. ` +
258
+ `This indicates a corrupted installation. Please reinstall aiwcli.`);
259
+ }
260
+ // Scan template directory to classify folders (excluding test/cache patterns)
261
+ const entries = await fs.readdir(templatePath, { withFileTypes: true });
262
+ const directories = entries.filter((entry) => entry.isDirectory() && !shouldExclude(entry.name));
263
+ const nonDotFolders = [];
264
+ const dotFolders = new Map(); // ide name -> folder name
265
+ for (const dir of directories) {
266
+ if (dir.name.startsWith('.')) {
267
+ // Extract IDE name from dot folder (e.g., '.claude' -> 'claude')
268
+ const ideName = dir.name.slice(1);
269
+ dotFolders.set(ideName, dir.name);
270
+ }
271
+ else {
272
+ nonDotFolders.push(dir.name);
273
+ }
274
+ }
275
+ // Validate requested IDE folders exist in template
276
+ const availableIdes = [...dotFolders.keys()];
277
+ const missingIdes = ides.filter((ide) => !dotFolders.has(ide));
278
+ if (missingIdes.length > 0) {
279
+ throw new Error(`IDE '${missingIdes[0]}' not available for template '${templateName}'. ` +
280
+ `Available: ${availableIdes.join(', ')}`);
281
+ }
282
+ const installedFolders = [];
283
+ const skippedFolders = [];
284
+ const mergedFolders = [];
285
+ let mergedFileCount = 0;
286
+ // Create .aiwcli container folder for method-specific files
287
+ const resolver = new IdePathResolver(targetDir);
288
+ const containerDir = resolver.getAiwcliContainer();
289
+ await fs.mkdir(containerDir, { recursive: true });
290
+ // Install non-dot folders into .aiwcli/ container (skip if already exist and skipExisting is true)
291
+ const nonDotInstalls = nonDotFolders.map(async (folder) => {
292
+ const srcPath = join(templatePath, folder);
293
+ // Destination is inside .aiwcli/ container
294
+ const destPath = join(containerDir, folder);
295
+ if (skipExisting && (await pathExists(destPath))) {
296
+ return { folder, skipped: true };
297
+ }
298
+ await copyDir(srcPath, destPath);
299
+ return { folder, skipped: false };
300
+ });
301
+ const nonDotResults = await Promise.all(nonDotInstalls);
302
+ for (const result of nonDotResults) {
303
+ if (result.skipped) {
304
+ skippedFolders.push(result.folder);
305
+ }
306
+ else {
307
+ installedFolders.push(result.folder);
308
+ }
309
+ }
310
+ // Install root-level _shared directory (shared across all templates)
311
+ // This is at templates/_shared, not inside the specific template directory
312
+ // Exclude IDE config folders (.claude, .windsurf) - they are used for settings merging only
313
+ const templatesRoot = dirname(templatePath);
314
+ const rootSharedSrc = join(templatesRoot, '_shared');
315
+ const rootSharedDest = join(containerDir, '_shared');
316
+ if (await pathExists(rootSharedSrc)) {
317
+ if (skipExisting && (await pathExists(rootSharedDest))) {
318
+ skippedFolders.push('_shared');
319
+ }
320
+ else {
321
+ await copyDir(rootSharedSrc, rootSharedDest, true); // excludeIdeFolders = true
322
+ installedFolders.push('_shared');
323
+ }
324
+ }
325
+ // Install matching IDE folders
326
+ // If folder exists, merge content recursively by looking for method name folders
327
+ const ideInstalls = ides.map(async (ide) => {
328
+ const folderName = dotFolders.get(ide);
329
+ if (folderName) {
330
+ const srcPath = join(templatePath, folderName);
331
+ const destPath = resolver.getIdeDir(ide);
332
+ if (await pathExists(destPath)) {
333
+ if (skipExisting) {
334
+ // Folder exists - merge template content by finding method-named folders
335
+ const mergeResult = await mergeTemplateContent(srcPath, destPath, templateName);
336
+ return {
337
+ folder: folderName,
338
+ skipped: false,
339
+ merged: true,
340
+ mergedFiles: mergeResult.copiedFiles.length,
341
+ };
342
+ }
343
+ // skipExisting is false, so overwrite
344
+ await copyDir(srcPath, destPath);
345
+ return { folder: folderName, skipped: false, merged: false, mergedFiles: 0 };
346
+ }
347
+ await copyDir(srcPath, destPath);
348
+ return { folder: folderName, skipped: false, merged: false, mergedFiles: 0 };
349
+ }
350
+ return null;
351
+ });
352
+ const ideResults = (await Promise.all(ideInstalls)).filter((result) => result !== null);
353
+ for (const result of ideResults) {
354
+ if (result.merged) {
355
+ mergedFolders.push(result.folder);
356
+ mergedFileCount += result.mergedFiles;
357
+ }
358
+ else if (result.skipped) {
359
+ skippedFolders.push(result.folder);
360
+ }
361
+ else {
362
+ installedFolders.push(result.folder);
363
+ }
364
+ }
365
+ // Settings merging is now handled by the caller via mergeMethodsSettings()
366
+ // This allows unified merging of _shared + method-specific settings
367
+ return {
368
+ installedFolders,
369
+ skippedFolders,
370
+ mergedFolders,
371
+ mergedFileCount,
372
+ sharedSettingsMerged: false, // Deprecated, kept for backwards compatibility
373
+ templatePath,
374
+ };
375
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Template Linter
3
+ *
4
+ * Validates workflow markdown files within templates for:
5
+ * - Correct output paths (_output/{method}/)
6
+ * - No cross-method contamination (gsd referencing bmad paths)
7
+ * - Proper template references (_{method}/templates/)
8
+ * - Correct workflow references (/method:workflow)
9
+ */
10
+ export interface LintViolation {
11
+ file: string;
12
+ line: number;
13
+ match: string;
14
+ message: string;
15
+ rule: string;
16
+ }
17
+ export interface LintRule {
18
+ description: string;
19
+ /** Message template - use {match} for the matched text */
20
+ message: string;
21
+ name: string;
22
+ /** Regex to find violations - matches indicate problems */
23
+ pattern: RegExp;
24
+ }
25
+ /**
26
+ * Generate lint rules for a specific method
27
+ * Rules detect when a method's workflows reference other methods incorrectly
28
+ */
29
+ export declare function getRulesForMethod(method: string): LintRule[];
30
+ /**
31
+ * Lint a single file's content
32
+ */
33
+ export declare function lintFileContent(content: string, filePath: string, rules: LintRule[]): LintViolation[];
34
+ /**
35
+ * Get all markdown files in a template method directory
36
+ */
37
+ export declare function getTemplateMarkdownFiles(templatesDir: string, method: string): string[];
38
+ /**
39
+ * Lint all markdown files for a specific template method
40
+ */
41
+ export declare function lintTemplateMethod(templatesDir: string, method: string): LintViolation[];
42
+ /**
43
+ * Lint all template methods in a templates directory
44
+ */
45
+ export declare function lintAllTemplates(templatesDir: string): Map<string, LintViolation[]>;
46
+ /**
47
+ * Format violations for display
48
+ */
49
+ export declare function formatViolations(violations: LintViolation[], basePath?: string): string;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Template Linter
3
+ *
4
+ * Validates workflow markdown files within templates for:
5
+ * - Correct output paths (_output/{method}/)
6
+ * - No cross-method contamination (gsd referencing bmad paths)
7
+ * - Proper template references (_{method}/templates/)
8
+ * - Correct workflow references (/method:workflow)
9
+ */
10
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
11
+ import path from 'node:path';
12
+ /**
13
+ * Escape special regex characters in a string
14
+ */
15
+ function escapeRegex(str) {
16
+ return str.replaceAll(/[$()*+.?[\\\]^{|}]/g, String.raw `\$&`);
17
+ }
18
+ /**
19
+ * Generate lint rules for a specific method
20
+ * Rules detect when a method's workflows reference other methods incorrectly
21
+ */
22
+ export function getRulesForMethod(method) {
23
+ // Known methods to check against
24
+ const allMethods = ['cc-native'];
25
+ const otherMethods = allMethods.filter((m) => m !== method);
26
+ // Create regex pattern that matches other methods but not the current one
27
+ const otherMethodsPattern = otherMethods.map((m) => escapeRegex(m)).join('|');
28
+ return [
29
+ {
30
+ description: `Output paths should use _output/${method}/, not other methods`,
31
+ message: `Wrong method in output path: {match} (should be _output/${method}/)`,
32
+ name: 'wrong-output-path',
33
+ pattern: new RegExp(`_output/(${otherMethodsPattern})/`, 'g'),
34
+ },
35
+ {
36
+ description: `Template references should use _${method}/templates/, not other methods`,
37
+ message: `Wrong method in template reference: {match} (should be _${method}/templates/)`,
38
+ name: 'wrong-template-ref',
39
+ pattern: new RegExp(`_(${otherMethodsPattern})/templates/`, 'g'),
40
+ },
41
+ {
42
+ description: `Workflow references should use /${method}:, not other methods`,
43
+ message: `Wrong method in workflow reference: {match} (should be /${method}:)`,
44
+ name: 'wrong-workflow-ref',
45
+ pattern: new RegExp(`/(${otherMethodsPattern}):`, 'g'),
46
+ },
47
+ {
48
+ description: `Method folder references should use _${method}/, not other methods`,
49
+ message: `Wrong method in folder reference: {match} (should be _${method}/workflows/)`,
50
+ name: 'wrong-method-folder',
51
+ pattern: new RegExp(`_(${otherMethodsPattern})/workflows/`, 'g'),
52
+ },
53
+ // Note: bare-output-file rule removed as it produces too many false positives
54
+ // in README documentation that explains workflow outputs.
55
+ // The cross-method contamination rules are the primary value of this linter.
56
+ ];
57
+ }
58
+ /**
59
+ * Check a single line for violations of a rule
60
+ */
61
+ function checkLineForViolations(line, lineNum, filePath, rule) {
62
+ const violations = [];
63
+ // Skip code block fences and comments
64
+ if (line.trim().startsWith('```') || line.trim().startsWith('<!--')) {
65
+ return violations;
66
+ }
67
+ // Reset regex for this line
68
+ rule.pattern.lastIndex = 0;
69
+ let match;
70
+ while ((match = rule.pattern.exec(line)) !== null) {
71
+ violations.push({
72
+ file: filePath,
73
+ line: lineNum + 1,
74
+ match: match[0],
75
+ message: rule.message.replace('{match}', match[0]),
76
+ rule: rule.name,
77
+ });
78
+ }
79
+ return violations;
80
+ }
81
+ /**
82
+ * Lint a single file's content
83
+ */
84
+ export function lintFileContent(content, filePath, rules) {
85
+ const violations = [];
86
+ const lines = content.split('\n');
87
+ for (const rule of rules) {
88
+ // Reset regex state for each rule
89
+ rule.pattern.lastIndex = 0;
90
+ for (const [lineNum, line] of lines.entries()) {
91
+ const lineViolations = checkLineForViolations(line, lineNum, filePath, rule);
92
+ violations.push(...lineViolations);
93
+ }
94
+ }
95
+ return violations;
96
+ }
97
+ /**
98
+ * Get all markdown files in a template method directory
99
+ */
100
+ export function getTemplateMarkdownFiles(templatesDir, method) {
101
+ const methodDir = path.join(templatesDir, method);
102
+ const files = [];
103
+ if (!existsSync(methodDir)) {
104
+ return files;
105
+ }
106
+ function walkDir(dir) {
107
+ const entries = readdirSync(dir, { withFileTypes: true });
108
+ for (const entry of entries) {
109
+ const fullPath = path.join(dir, entry.name);
110
+ if (entry.isDirectory()) {
111
+ // Skip node_modules and hidden dirs (except .claude, .windsurf, .cursor)
112
+ if (entry.name === 'node_modules' ||
113
+ (entry.name.startsWith('.') &&
114
+ !entry.name.startsWith('.claude') &&
115
+ !entry.name.startsWith('.windsurf') &&
116
+ !entry.name.startsWith('.cursor'))) {
117
+ continue;
118
+ }
119
+ walkDir(fullPath);
120
+ }
121
+ else if (entry.name.endsWith('.md') && !entry.name.endsWith('.template')) {
122
+ // Only lint .md files, not .md.template files (those have placeholders)
123
+ files.push(fullPath);
124
+ }
125
+ }
126
+ }
127
+ walkDir(methodDir);
128
+ return files;
129
+ }
130
+ /**
131
+ * Lint all markdown files for a specific template method
132
+ */
133
+ export function lintTemplateMethod(templatesDir, method) {
134
+ const rules = getRulesForMethod(method);
135
+ const files = getTemplateMarkdownFiles(templatesDir, method);
136
+ const allViolations = [];
137
+ for (const filePath of files) {
138
+ const content = readFileSync(filePath, 'utf8');
139
+ const violations = lintFileContent(content, filePath, rules);
140
+ allViolations.push(...violations);
141
+ }
142
+ return allViolations;
143
+ }
144
+ /**
145
+ * Lint all template methods in a templates directory
146
+ */
147
+ export function lintAllTemplates(templatesDir) {
148
+ const results = new Map();
149
+ const methods = ['cc-native'];
150
+ for (const method of methods) {
151
+ const methodDir = path.join(templatesDir, method);
152
+ if (existsSync(methodDir)) {
153
+ results.set(method, lintTemplateMethod(templatesDir, method));
154
+ }
155
+ }
156
+ return results;
157
+ }
158
+ /**
159
+ * Format violations for display
160
+ */
161
+ export function formatViolations(violations, basePath) {
162
+ if (violations.length === 0) {
163
+ return 'No violations found.';
164
+ }
165
+ return violations
166
+ .map((v) => {
167
+ const relativePath = basePath
168
+ ? path.relative(basePath, v.file)
169
+ : v.file;
170
+ return `${relativePath}:${v.line} [${v.rule}] ${v.message}`;
171
+ })
172
+ .join('\n');
173
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Content folder types that should be recursively merged rather than skipped.
3
+ * These are the canonical folder names used in templates for organizing content.
4
+ */
5
+ export declare const CONTENT_FOLDER_TYPES: readonly ["agents", "commands", "workflows", "tasks"];
6
+ /**
7
+ * Result of merging content folders
8
+ */
9
+ export interface MergeResult {
10
+ /** Files that were copied */
11
+ copiedFiles: string[];
12
+ /** Directories that were created */
13
+ createdDirs: string[];
14
+ /** Files that were skipped (already exist) */
15
+ skippedFiles: string[];
16
+ }
17
+ /**
18
+ * Recursively find folders matching the method name within a directory tree.
19
+ * This finds folders like `.claude/commands/bmad/` when methodName is 'bmad'.
20
+ *
21
+ * @param dir - Directory to search
22
+ * @param methodName - Method name to look for (e.g., 'bmad', 'gsd')
23
+ * @returns Array of paths to matching folders
24
+ */
25
+ export declare function findMethodFolders(dir: string, methodName: string): Promise<string[]>;
26
+ /**
27
+ * Merge template content into an existing IDE folder.
28
+ * Recursively finds folders matching the method name and merges their content.
29
+ *
30
+ * This enables adding new agents, commands, workflows, and tasks from a template
31
+ * even when the IDE folder (e.g., .claude) already exists.
32
+ *
33
+ * @param templateIdePath - Path to the template's IDE folder (e.g., template/.claude)
34
+ * @param targetIdePath - Path to the target's IDE folder (e.g., project/.claude)
35
+ * @param methodName - Method name to look for (e.g., 'bmad', 'gsd')
36
+ * @returns Merge results
37
+ */
38
+ export declare function mergeTemplateContent(templateIdePath: string, targetIdePath: string, methodName: string): Promise<MergeResult>;
39
+ /**
40
+ * Merge content type folders (agents, commands, workflows, tasks) from template to target.
41
+ * This is an alternative approach that looks for specific folder types rather than method names.
42
+ *
43
+ * @param templateIdePath - Path to the template's IDE folder
44
+ * @param targetIdePath - Path to the target's IDE folder
45
+ * @returns Merge results
46
+ */
47
+ export declare function mergeContentTypeFolders(templateIdePath: string, targetIdePath: string): Promise<MergeResult>;