@wundr.io/cli 1.0.10 → 1.0.12

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 (269) hide show
  1. package/bin/wundr.js +8 -4
  2. package/package.json +23 -23
  3. package/src/ai/ai-service.ts +16 -17
  4. package/src/ai/claude-client.ts +16 -16
  5. package/src/ai/conversation-manager.ts +29 -29
  6. package/src/cli.ts +4 -4
  7. package/src/commands/ai.ts +246 -78
  8. package/src/commands/alignment.ts +74 -74
  9. package/src/commands/analyze-optimized.ts +111 -78
  10. package/src/commands/analyze.ts +14 -14
  11. package/src/commands/batch.ts +179 -42
  12. package/src/commands/chat.ts +37 -30
  13. package/src/commands/claude-init.ts +41 -45
  14. package/src/commands/claude-setup.ts +204 -119
  15. package/src/commands/computer-setup.ts +85 -43
  16. package/src/commands/create-command.ts +4 -4
  17. package/src/commands/create.ts +27 -27
  18. package/src/commands/dashboard.ts +24 -24
  19. package/src/commands/govern.ts +25 -25
  20. package/src/commands/governance.ts +34 -34
  21. package/src/commands/guardian.ts +56 -56
  22. package/src/commands/init.ts +25 -22
  23. package/src/commands/orchestrator.ts +68 -41
  24. package/src/commands/performance-optimizer.ts +34 -35
  25. package/src/commands/plugins.ts +27 -27
  26. package/src/commands/project-update.ts +175 -72
  27. package/src/commands/rag.ts +185 -78
  28. package/src/commands/session.ts +35 -35
  29. package/src/commands/setup.ts +40 -344
  30. package/src/commands/test-init.ts +3 -3
  31. package/src/commands/test.ts +4 -4
  32. package/src/commands/watch.ts +28 -29
  33. package/src/commands/worktree.ts +49 -49
  34. package/src/context/context-manager.ts +10 -10
  35. package/src/context/session-manager.ts +41 -41
  36. package/src/framework/command-interface.ts +520 -0
  37. package/src/framework/command-registry.ts +942 -0
  38. package/src/framework/completion-exporter.ts +383 -0
  39. package/src/framework/debug-logger.ts +519 -0
  40. package/src/framework/error-handler.ts +867 -0
  41. package/src/framework/help-generator.ts +540 -0
  42. package/src/framework/index.ts +169 -0
  43. package/src/framework/interactive-repl.ts +703 -0
  44. package/src/framework/output-formatter.ts +834 -0
  45. package/src/framework/progress-manager.ts +539 -0
  46. package/src/index.ts +4 -4
  47. package/src/interactive/interactive-mode.ts +16 -16
  48. package/src/lib/conflict-resolution.ts +799 -9
  49. package/src/lib/merge-strategy.ts +529 -7
  50. package/src/lib/safety-mechanisms.ts +422 -18
  51. package/src/lib/state-detection.ts +1015 -13
  52. package/src/nlp/command-mapper.ts +29 -29
  53. package/src/nlp/command-parser.ts +17 -17
  54. package/src/nlp/intent-classifier.ts +7 -7
  55. package/src/nlp/intent-parser.ts +54 -52
  56. package/src/plugins/plugin-manager.ts +61 -39
  57. package/src/tests/computer-setup-integration.test.ts +46 -15
  58. package/src/types/modules.d.ts +424 -1
  59. package/src/utils/backup-rollback-manager.ts +11 -8
  60. package/src/utils/config-manager.ts +3 -3
  61. package/src/utils/error-handler.ts +2 -2
  62. package/src/utils/logger.ts +22 -22
  63. package/templates/batch/ci-cd.yaml +7 -7
  64. package/test-suites/api/health.spec.ts +20 -23
  65. package/test-suites/helpers/test-config.ts +14 -13
  66. package/test-suites/ui/accessibility.spec.ts +27 -22
  67. package/test-suites/ui/smoke.spec.ts +26 -21
  68. package/LICENSE +0 -21
  69. package/dist/ai/ai-service.d.ts +0 -152
  70. package/dist/ai/ai-service.d.ts.map +0 -1
  71. package/dist/ai/ai-service.js +0 -430
  72. package/dist/ai/ai-service.js.map +0 -1
  73. package/dist/ai/claude-client.d.ts +0 -130
  74. package/dist/ai/claude-client.d.ts.map +0 -1
  75. package/dist/ai/claude-client.js +0 -340
  76. package/dist/ai/claude-client.js.map +0 -1
  77. package/dist/ai/conversation-manager.d.ts +0 -164
  78. package/dist/ai/conversation-manager.d.ts.map +0 -1
  79. package/dist/ai/conversation-manager.js +0 -614
  80. package/dist/ai/conversation-manager.js.map +0 -1
  81. package/dist/ai/index.d.ts +0 -5
  82. package/dist/ai/index.d.ts.map +0 -1
  83. package/dist/ai/index.js +0 -8
  84. package/dist/ai/index.js.map +0 -1
  85. package/dist/cli.d.ts +0 -36
  86. package/dist/cli.d.ts.map +0 -1
  87. package/dist/cli.js +0 -192
  88. package/dist/cli.js.map +0 -1
  89. package/dist/commands/ai.d.ts +0 -89
  90. package/dist/commands/ai.d.ts.map +0 -1
  91. package/dist/commands/ai.js +0 -799
  92. package/dist/commands/ai.js.map +0 -1
  93. package/dist/commands/alignment.d.ts +0 -78
  94. package/dist/commands/alignment.d.ts.map +0 -1
  95. package/dist/commands/alignment.js +0 -817
  96. package/dist/commands/alignment.js.map +0 -1
  97. package/dist/commands/analyze-optimized.d.ts +0 -14
  98. package/dist/commands/analyze-optimized.d.ts.map +0 -1
  99. package/dist/commands/analyze-optimized.js +0 -600
  100. package/dist/commands/analyze-optimized.js.map +0 -1
  101. package/dist/commands/analyze.d.ts +0 -65
  102. package/dist/commands/analyze.d.ts.map +0 -1
  103. package/dist/commands/analyze.js +0 -435
  104. package/dist/commands/analyze.js.map +0 -1
  105. package/dist/commands/batch.d.ts +0 -71
  106. package/dist/commands/batch.d.ts.map +0 -1
  107. package/dist/commands/batch.js +0 -738
  108. package/dist/commands/batch.js.map +0 -1
  109. package/dist/commands/chat.d.ts +0 -71
  110. package/dist/commands/chat.d.ts.map +0 -1
  111. package/dist/commands/chat.js +0 -674
  112. package/dist/commands/chat.js.map +0 -1
  113. package/dist/commands/claude-init.d.ts +0 -28
  114. package/dist/commands/claude-init.d.ts.map +0 -1
  115. package/dist/commands/claude-init.js +0 -591
  116. package/dist/commands/claude-init.js.map +0 -1
  117. package/dist/commands/claude-setup.d.ts +0 -119
  118. package/dist/commands/claude-setup.d.ts.map +0 -1
  119. package/dist/commands/claude-setup.js +0 -1073
  120. package/dist/commands/claude-setup.js.map +0 -1
  121. package/dist/commands/computer-setup-commands.d.ts +0 -53
  122. package/dist/commands/computer-setup-commands.d.ts.map +0 -1
  123. package/dist/commands/computer-setup-commands.js +0 -705
  124. package/dist/commands/computer-setup-commands.js.map +0 -1
  125. package/dist/commands/computer-setup.d.ts +0 -7
  126. package/dist/commands/computer-setup.d.ts.map +0 -1
  127. package/dist/commands/computer-setup.js +0 -849
  128. package/dist/commands/computer-setup.js.map +0 -1
  129. package/dist/commands/create-command.d.ts +0 -7
  130. package/dist/commands/create-command.d.ts.map +0 -1
  131. package/dist/commands/create-command.js +0 -158
  132. package/dist/commands/create-command.js.map +0 -1
  133. package/dist/commands/create.d.ts +0 -74
  134. package/dist/commands/create.d.ts.map +0 -1
  135. package/dist/commands/create.js +0 -556
  136. package/dist/commands/create.js.map +0 -1
  137. package/dist/commands/dashboard.d.ts +0 -91
  138. package/dist/commands/dashboard.d.ts.map +0 -1
  139. package/dist/commands/dashboard.js +0 -538
  140. package/dist/commands/dashboard.js.map +0 -1
  141. package/dist/commands/govern.d.ts +0 -70
  142. package/dist/commands/govern.d.ts.map +0 -1
  143. package/dist/commands/govern.js +0 -481
  144. package/dist/commands/govern.js.map +0 -1
  145. package/dist/commands/governance.d.ts +0 -17
  146. package/dist/commands/governance.d.ts.map +0 -1
  147. package/dist/commands/governance.js +0 -703
  148. package/dist/commands/governance.js.map +0 -1
  149. package/dist/commands/guardian.d.ts +0 -20
  150. package/dist/commands/guardian.d.ts.map +0 -1
  151. package/dist/commands/guardian.js +0 -597
  152. package/dist/commands/guardian.js.map +0 -1
  153. package/dist/commands/init.d.ts +0 -59
  154. package/dist/commands/init.d.ts.map +0 -1
  155. package/dist/commands/init.js +0 -650
  156. package/dist/commands/init.js.map +0 -1
  157. package/dist/commands/orchestrator.d.ts +0 -7
  158. package/dist/commands/orchestrator.d.ts.map +0 -1
  159. package/dist/commands/orchestrator.js +0 -571
  160. package/dist/commands/orchestrator.js.map +0 -1
  161. package/dist/commands/performance-optimizer.d.ts +0 -30
  162. package/dist/commands/performance-optimizer.d.ts.map +0 -1
  163. package/dist/commands/performance-optimizer.js +0 -650
  164. package/dist/commands/performance-optimizer.js.map +0 -1
  165. package/dist/commands/plugins.d.ts +0 -87
  166. package/dist/commands/plugins.d.ts.map +0 -1
  167. package/dist/commands/plugins.js +0 -685
  168. package/dist/commands/plugins.js.map +0 -1
  169. package/dist/commands/rag.d.ts +0 -7
  170. package/dist/commands/rag.d.ts.map +0 -1
  171. package/dist/commands/rag.js +0 -748
  172. package/dist/commands/rag.js.map +0 -1
  173. package/dist/commands/session.d.ts +0 -41
  174. package/dist/commands/session.d.ts.map +0 -1
  175. package/dist/commands/session.js +0 -441
  176. package/dist/commands/session.js.map +0 -1
  177. package/dist/commands/setup.d.ts +0 -29
  178. package/dist/commands/setup.d.ts.map +0 -1
  179. package/dist/commands/setup.js +0 -397
  180. package/dist/commands/setup.js.map +0 -1
  181. package/dist/commands/test-init.d.ts +0 -9
  182. package/dist/commands/test-init.d.ts.map +0 -1
  183. package/dist/commands/test-init.js +0 -222
  184. package/dist/commands/test-init.js.map +0 -1
  185. package/dist/commands/test.d.ts +0 -25
  186. package/dist/commands/test.d.ts.map +0 -1
  187. package/dist/commands/test.js +0 -217
  188. package/dist/commands/test.js.map +0 -1
  189. package/dist/commands/vp.d.ts +0 -7
  190. package/dist/commands/vp.d.ts.map +0 -1
  191. package/dist/commands/vp.js +0 -571
  192. package/dist/commands/vp.js.map +0 -1
  193. package/dist/commands/watch.d.ts +0 -76
  194. package/dist/commands/watch.d.ts.map +0 -1
  195. package/dist/commands/watch.js +0 -613
  196. package/dist/commands/watch.js.map +0 -1
  197. package/dist/commands/worktree.d.ts +0 -63
  198. package/dist/commands/worktree.d.ts.map +0 -1
  199. package/dist/commands/worktree.js +0 -774
  200. package/dist/commands/worktree.js.map +0 -1
  201. package/dist/context/context-manager.d.ts +0 -155
  202. package/dist/context/context-manager.d.ts.map +0 -1
  203. package/dist/context/context-manager.js +0 -383
  204. package/dist/context/context-manager.js.map +0 -1
  205. package/dist/context/index.d.ts +0 -3
  206. package/dist/context/index.d.ts.map +0 -1
  207. package/dist/context/index.js +0 -6
  208. package/dist/context/index.js.map +0 -1
  209. package/dist/context/session-manager.d.ts +0 -207
  210. package/dist/context/session-manager.d.ts.map +0 -1
  211. package/dist/context/session-manager.js +0 -686
  212. package/dist/context/session-manager.js.map +0 -1
  213. package/dist/index.d.ts +0 -8
  214. package/dist/index.d.ts.map +0 -1
  215. package/dist/index.js +0 -51
  216. package/dist/index.js.map +0 -1
  217. package/dist/interactive/interactive-mode.d.ts +0 -76
  218. package/dist/interactive/interactive-mode.d.ts.map +0 -1
  219. package/dist/interactive/interactive-mode.js +0 -732
  220. package/dist/interactive/interactive-mode.js.map +0 -1
  221. package/dist/nlp/command-mapper.d.ts +0 -174
  222. package/dist/nlp/command-mapper.d.ts.map +0 -1
  223. package/dist/nlp/command-mapper.js +0 -624
  224. package/dist/nlp/command-mapper.js.map +0 -1
  225. package/dist/nlp/command-parser.d.ts +0 -106
  226. package/dist/nlp/command-parser.d.ts.map +0 -1
  227. package/dist/nlp/command-parser.js +0 -417
  228. package/dist/nlp/command-parser.js.map +0 -1
  229. package/dist/nlp/index.d.ts +0 -5
  230. package/dist/nlp/index.d.ts.map +0 -1
  231. package/dist/nlp/index.js +0 -8
  232. package/dist/nlp/index.js.map +0 -1
  233. package/dist/nlp/intent-classifier.d.ts +0 -59
  234. package/dist/nlp/intent-classifier.d.ts.map +0 -1
  235. package/dist/nlp/intent-classifier.js +0 -384
  236. package/dist/nlp/intent-classifier.js.map +0 -1
  237. package/dist/nlp/intent-parser.d.ts +0 -152
  238. package/dist/nlp/intent-parser.d.ts.map +0 -1
  239. package/dist/nlp/intent-parser.js +0 -744
  240. package/dist/nlp/intent-parser.js.map +0 -1
  241. package/dist/plugins/plugin-manager.d.ts +0 -120
  242. package/dist/plugins/plugin-manager.d.ts.map +0 -1
  243. package/dist/plugins/plugin-manager.js +0 -595
  244. package/dist/plugins/plugin-manager.js.map +0 -1
  245. package/dist/types/index.d.ts +0 -224
  246. package/dist/types/index.d.ts.map +0 -1
  247. package/dist/types/index.js +0 -3
  248. package/dist/types/index.js.map +0 -1
  249. package/dist/utils/backup-rollback-manager.d.ts +0 -72
  250. package/dist/utils/backup-rollback-manager.d.ts.map +0 -1
  251. package/dist/utils/backup-rollback-manager.js +0 -289
  252. package/dist/utils/backup-rollback-manager.js.map +0 -1
  253. package/dist/utils/claude-config-installer.d.ts +0 -98
  254. package/dist/utils/claude-config-installer.d.ts.map +0 -1
  255. package/dist/utils/claude-config-installer.js +0 -678
  256. package/dist/utils/claude-config-installer.js.map +0 -1
  257. package/dist/utils/config-manager.d.ts +0 -73
  258. package/dist/utils/config-manager.d.ts.map +0 -1
  259. package/dist/utils/config-manager.js +0 -339
  260. package/dist/utils/config-manager.js.map +0 -1
  261. package/dist/utils/error-handler.d.ts +0 -46
  262. package/dist/utils/error-handler.d.ts.map +0 -1
  263. package/dist/utils/error-handler.js +0 -169
  264. package/dist/utils/error-handler.js.map +0 -1
  265. package/dist/utils/logger.d.ts +0 -25
  266. package/dist/utils/logger.d.ts.map +0 -1
  267. package/dist/utils/logger.js +0 -105
  268. package/dist/utils/logger.js.map +0 -1
  269. package/src/commands/computer-setup-commands.ts +0 -872
@@ -1,28 +1,1030 @@
1
1
  /**
2
- * State Detection - Stub implementation
3
- * TODO: Implement full state detection system
2
+ * State Detection - Full implementation
3
+ *
4
+ * Scans the file system to detect project type, git state, and configuration
5
+ * state. Uses only Node.js built-ins (fs, path, crypto, child_process).
4
6
  */
5
7
 
8
+ import * as crypto from 'crypto';
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import { execSync } from 'child_process';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Public interfaces
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export interface CustomizationInfo {
18
+ file: string;
19
+ type: string;
20
+ description: string;
21
+ }
22
+
23
+ export interface GitStatus {
24
+ isRepository: boolean;
25
+ branch?: string;
26
+ isDirty: boolean;
27
+ hasUncommittedChanges: boolean;
28
+ hasUntrackedFiles: boolean;
29
+ stagedFiles: string[];
30
+ modifiedFiles: string[];
31
+ untrackedFiles: string[];
32
+ }
33
+
34
+ export interface AgentInfo {
35
+ name: string;
36
+ type: string;
37
+ configPath: string;
38
+ isValid: boolean;
39
+ }
40
+
41
+ export interface AgentState {
42
+ hasAgents: boolean;
43
+ agentCount: number;
44
+ agents: AgentInfo[];
45
+ }
46
+
47
+ export interface HookInfo {
48
+ name: string;
49
+ configPath: string;
50
+ isEnabled: boolean;
51
+ type: string;
52
+ }
53
+
54
+ export interface HookState {
55
+ hasHooks: boolean;
56
+ hookCount: number;
57
+ hooks: HookInfo[];
58
+ }
59
+
60
+ export interface CustomizationState {
61
+ hasCustomizations: boolean;
62
+ customizedFiles: string[];
63
+ addedFiles: string[];
64
+ removedFiles: string[];
65
+ checksumMismatches: string[];
66
+ }
67
+
68
+ export interface ConflictEntry {
69
+ type: 'version' | 'config' | 'file';
70
+ severity: 'error' | 'warning' | 'info';
71
+ description: string;
72
+ }
73
+
74
+ export interface ConflictState {
75
+ hasConflicts: boolean;
76
+ conflicts: ConflictEntry[];
77
+ }
78
+
79
+ export interface ClaudeConfigInfo {
80
+ exists: boolean;
81
+ path?: string;
82
+ isValid: boolean;
83
+ }
84
+
85
+ export interface MCPConfigInfo {
86
+ exists: boolean;
87
+ path?: string;
88
+ isValid: boolean;
89
+ servers: string[];
90
+ }
91
+
92
+ export interface WundrConfigInfo {
93
+ exists: boolean;
94
+ path?: string;
95
+ isValid: boolean;
96
+ version?: string;
97
+ }
98
+
99
+ /**
100
+ * Full project state as detected from the file system.
101
+ *
102
+ * The original minimal interface fields (type, customizations, dependencies,
103
+ * healthScore, isWundrOutdated, recommendations, wundrVersion) are preserved
104
+ * alongside the richer fields required by the command layer and test suite.
105
+ */
6
106
  export interface ProjectState {
107
+ // ---- original interface fields (preserved) --------------------------------
108
+ /** Detected project type: 'node', 'python', 'go', 'java', 'unknown', etc. */
7
109
  type: string;
8
- customizations: CustomizationInfo[];
110
+ /** Customization details (legacy list form) */
111
+ customizations: CustomizationState;
112
+ /** Key/value dependency map from the primary manifest */
9
113
  dependencies: Record<string, string>;
10
- healthScore?: number;
114
+ healthScore: number;
11
115
  isWundrOutdated?: boolean;
12
- recommendations?: string[];
116
+ recommendations: string[];
13
117
  wundrVersion?: string;
118
+
119
+ // ---- extended fields used by project-update.ts and the test suite --------
120
+ projectPath: string;
121
+ detectedAt: Date;
122
+
123
+ hasWundr: boolean;
124
+ hasClaudeConfig: boolean;
125
+ hasMCPConfig: boolean;
126
+ hasWundrConfig: boolean;
127
+ hasPackageJson: boolean;
128
+
129
+ packageName?: string;
130
+ packageVersion?: string;
131
+ isMonorepo: boolean;
132
+ workspaces: string[];
133
+
134
+ claudeConfigPath?: string;
135
+ mcpConfigPath?: string;
136
+ wundrConfigPath?: string;
137
+
138
+ latestWundrVersion?: string;
139
+ isPartialInstallation: boolean;
140
+ missingComponents: string[];
141
+
142
+ git: GitStatus;
143
+ agents: AgentState;
144
+ hooks: HookState;
145
+ conflicts: ConflictState;
14
146
  }
15
147
 
16
- export interface CustomizationInfo {
17
- file: string;
18
- type: string;
19
- description: string;
148
+ // ---------------------------------------------------------------------------
149
+ // Checksum helpers
150
+ // ---------------------------------------------------------------------------
151
+
152
+ /**
153
+ * Compute the SHA-256 hex digest of a file. Returns null if the file does not
154
+ * exist or cannot be read.
155
+ */
156
+ export async function computeFileChecksum(
157
+ filePath: string
158
+ ): Promise<string | null> {
159
+ try {
160
+ const content = fs.readFileSync(filePath);
161
+ return crypto.createHash('sha256').update(content).digest('hex');
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Compute checksums for a list of relative file paths within a root directory.
169
+ * Files that cannot be read are silently skipped.
170
+ */
171
+ export async function computeChecksums(
172
+ root: string,
173
+ files: string[]
174
+ ): Promise<Map<string, string>> {
175
+ const result = new Map<string, string>();
176
+ for (const file of files) {
177
+ const checksum = await computeFileChecksum(path.join(root, file));
178
+ if (checksum !== null) {
179
+ result.set(file, checksum);
180
+ }
181
+ }
182
+ return result;
183
+ }
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Individual sub-detectors
187
+ // ---------------------------------------------------------------------------
188
+
189
+ /**
190
+ * Detect git repository status by reading .git/HEAD and running `git status`.
191
+ * Falls back to a safe default when the directory is not a git repo or git is
192
+ * not available.
193
+ */
194
+ export async function detectGitStatus(projectPath: string): Promise<GitStatus> {
195
+ const gitDir = path.join(projectPath, '.git');
196
+
197
+ if (!fs.existsSync(gitDir)) {
198
+ return {
199
+ isRepository: false,
200
+ isDirty: false,
201
+ hasUncommittedChanges: false,
202
+ hasUntrackedFiles: false,
203
+ stagedFiles: [],
204
+ modifiedFiles: [],
205
+ untrackedFiles: [],
206
+ };
207
+ }
208
+
209
+ // Read current branch from HEAD
210
+ let branch: string | undefined;
211
+ try {
212
+ const headPath = path.join(gitDir, 'HEAD');
213
+ if (fs.existsSync(headPath)) {
214
+ const headContent = fs.readFileSync(headPath, 'utf8').trim();
215
+ const refMatch = headContent.match(/^ref: refs\/heads\/(.+)$/);
216
+ if (refMatch) {
217
+ branch = refMatch[1];
218
+ } else {
219
+ // Detached HEAD – use the short commit hash
220
+ branch = headContent.slice(0, 7);
221
+ }
222
+ }
223
+ } catch {
224
+ // ignore – branch will be undefined
225
+ }
226
+
227
+ // Run git status for file-level information
228
+ const stagedFiles: string[] = [];
229
+ const modifiedFiles: string[] = [];
230
+ const untrackedFiles: string[] = [];
231
+
232
+ try {
233
+ const output = execSync('git status --porcelain', {
234
+ cwd: projectPath,
235
+ encoding: 'utf8',
236
+ stdio: ['pipe', 'pipe', 'pipe'],
237
+ });
238
+
239
+ for (const line of output.split('\n')) {
240
+ if (!line) continue;
241
+ const xy = line.slice(0, 2);
242
+ const file = line.slice(3).trim();
243
+ const staged = xy[0] !== ' ' && xy[0] !== '?';
244
+ const unstaged = xy[1] !== ' ' && xy[1] !== '?';
245
+ const untracked = xy === '??';
246
+
247
+ if (staged) stagedFiles.push(file);
248
+ if (unstaged) modifiedFiles.push(file);
249
+ if (untracked) untrackedFiles.push(file);
250
+ }
251
+ } catch {
252
+ // git may not be in PATH or repo may be bare – leave arrays empty
253
+ }
254
+
255
+ const hasUncommittedChanges =
256
+ stagedFiles.length > 0 || modifiedFiles.length > 0;
257
+ const hasUntrackedFiles = untrackedFiles.length > 0;
258
+ const isDirty = hasUncommittedChanges || hasUntrackedFiles;
259
+
260
+ return {
261
+ isRepository: true,
262
+ branch,
263
+ isDirty,
264
+ hasUncommittedChanges,
265
+ hasUntrackedFiles,
266
+ stagedFiles,
267
+ modifiedFiles,
268
+ untrackedFiles,
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Detect Claude configuration (CLAUDE.md).
274
+ * Searches the project root and .claude/ subdirectory.
275
+ */
276
+ export async function detectClaudeConfig(
277
+ projectPath: string
278
+ ): Promise<ClaudeConfigInfo> {
279
+ const candidates = [
280
+ path.join(projectPath, 'CLAUDE.md'),
281
+ path.join(projectPath, '.claude', 'CLAUDE.md'),
282
+ ];
283
+
284
+ for (const candidate of candidates) {
285
+ if (fs.existsSync(candidate)) {
286
+ let isValid = false;
287
+ try {
288
+ const content = fs.readFileSync(candidate, 'utf8');
289
+ isValid = content.trim().length > 0;
290
+ } catch {
291
+ // unreadable file is considered invalid
292
+ }
293
+ return { exists: true, path: candidate, isValid };
294
+ }
295
+ }
296
+
297
+ return { exists: false, isValid: false };
298
+ }
299
+
300
+ /**
301
+ * Detect MCP server configuration.
302
+ * Searches for .mcp/config.json or .mcp.json at the project root.
303
+ */
304
+ export async function detectMCPConfig(
305
+ projectPath: string
306
+ ): Promise<MCPConfigInfo> {
307
+ const candidates = [
308
+ path.join(projectPath, '.mcp', 'config.json'),
309
+ path.join(projectPath, '.mcp.json'),
310
+ path.join(projectPath, 'mcp.json'),
311
+ ];
312
+
313
+ for (const candidate of candidates) {
314
+ if (fs.existsSync(candidate)) {
315
+ let isValid = false;
316
+ const servers: string[] = [];
317
+ try {
318
+ const raw = fs.readFileSync(candidate, 'utf8');
319
+ const parsed = JSON.parse(raw);
320
+ isValid = true;
321
+ if (parsed.servers && typeof parsed.servers === 'object') {
322
+ servers.push(...Object.keys(parsed.servers));
323
+ }
324
+ } catch {
325
+ // JSON parse error – file exists but is invalid
326
+ }
327
+ return { exists: true, path: candidate, isValid, servers };
328
+ }
329
+ }
330
+
331
+ return { exists: false, isValid: false, servers: [] };
332
+ }
333
+
334
+ /**
335
+ * Detect Wundr project configuration.
336
+ * Searches for wundr.config.json, .wundr.json, or .wundr/config.json.
337
+ */
338
+ export async function detectWundrConfig(
339
+ projectPath: string
340
+ ): Promise<WundrConfigInfo> {
341
+ const candidates = [
342
+ path.join(projectPath, 'wundr.config.json'),
343
+ path.join(projectPath, '.wundr.json'),
344
+ path.join(projectPath, '.wundr', 'config.json'),
345
+ ];
346
+
347
+ for (const candidate of candidates) {
348
+ if (fs.existsSync(candidate)) {
349
+ let isValid = false;
350
+ let version: string | undefined;
351
+ try {
352
+ const raw = fs.readFileSync(candidate, 'utf8');
353
+ const parsed = JSON.parse(raw);
354
+ isValid = true;
355
+ version =
356
+ typeof parsed.version === 'string' ? parsed.version : undefined;
357
+ } catch {
358
+ // invalid JSON
359
+ }
360
+ return { exists: true, path: candidate, isValid, version };
361
+ }
362
+ }
363
+
364
+ return { exists: false, isValid: false };
365
+ }
366
+
367
+ /**
368
+ * Detect agent configuration files in known locations.
369
+ */
370
+ export async function detectAgents(projectPath: string): Promise<AgentState> {
371
+ const agentDirs = [
372
+ path.join(projectPath, '.claude', 'agents'),
373
+ path.join(projectPath, '.wundr', 'agents'),
374
+ ];
375
+
376
+ const agents: AgentInfo[] = [];
377
+
378
+ for (const dir of agentDirs) {
379
+ if (!fs.existsSync(dir)) continue;
380
+
381
+ let entries: fs.Dirent[] = [];
382
+ try {
383
+ entries = fs.readdirSync(dir, { withFileTypes: true });
384
+ } catch {
385
+ continue;
386
+ }
387
+
388
+ for (const entry of entries) {
389
+ if (!entry.isFile()) continue;
390
+ if (
391
+ !entry.name.endsWith('.json') &&
392
+ !entry.name.endsWith('.yaml') &&
393
+ !entry.name.endsWith('.yml')
394
+ ) {
395
+ continue;
396
+ }
397
+
398
+ const configPath = path.join(dir, entry.name);
399
+ let name = entry.name.replace(/\.(json|ya?ml)$/, '');
400
+ let type = 'unknown';
401
+ let isValid = false;
402
+
403
+ try {
404
+ const raw = fs.readFileSync(configPath, 'utf8');
405
+ const parsed = JSON.parse(raw);
406
+ isValid = true;
407
+ if (typeof parsed.name === 'string') name = parsed.name;
408
+ if (typeof parsed.type === 'string') type = parsed.type;
409
+ } catch {
410
+ // YAML or broken JSON – still record the agent as invalid
411
+ }
412
+
413
+ agents.push({ name, type, configPath, isValid });
414
+ }
415
+ }
416
+
417
+ return {
418
+ hasAgents: agents.length > 0,
419
+ agentCount: agents.length,
420
+ agents,
421
+ };
422
+ }
423
+
424
+ /**
425
+ * Detect lifecycle hook configuration files in known hook directories.
426
+ */
427
+ export async function detectHooks(projectPath: string): Promise<HookState> {
428
+ const hookDirs = [
429
+ path.join(projectPath, '.husky'),
430
+ path.join(projectPath, '.claude', 'hooks'),
431
+ path.join(projectPath, '.wundr', 'hooks'),
432
+ path.join(projectPath, '.git', 'hooks'),
433
+ ];
434
+
435
+ const hooks: HookInfo[] = [];
436
+
437
+ for (const dir of hookDirs) {
438
+ if (!fs.existsSync(dir)) continue;
439
+
440
+ let entries: fs.Dirent[] = [];
441
+ try {
442
+ entries = fs.readdirSync(dir, { withFileTypes: true });
443
+ } catch {
444
+ continue;
445
+ }
446
+
447
+ for (const entry of entries) {
448
+ if (!entry.isFile()) continue;
449
+ // Skip hidden files / directories
450
+ if (entry.name.startsWith('.')) continue;
451
+
452
+ const configPath = path.join(dir, entry.name);
453
+ const isSample = entry.name.endsWith('.sample');
454
+ const hookName = isSample
455
+ ? entry.name.replace(/\.sample$/, '')
456
+ : entry.name;
457
+
458
+ // Infer a human-readable type from the hook name
459
+ let type = 'shell';
460
+ if (entry.name.endsWith('.json')) type = 'json';
461
+ else if (entry.name.endsWith('.ts') || entry.name.endsWith('.js')) {
462
+ type = 'script';
463
+ }
464
+
465
+ hooks.push({
466
+ name: hookName,
467
+ configPath,
468
+ isEnabled: !isSample,
469
+ type,
470
+ });
471
+ }
472
+ }
473
+
474
+ return {
475
+ hasHooks: hooks.length > 0,
476
+ hookCount: hooks.length,
477
+ hooks,
478
+ };
20
479
  }
21
480
 
22
- export async function detectProjectState(): Promise<ProjectState> {
23
- throw new Error('State detection not yet implemented');
481
+ /**
482
+ * Detect file-level customizations relative to an optional checksum baseline.
483
+ *
484
+ * When no baseline is provided the function reports files that exist in
485
+ * wundr-managed directories (e.g. .claude/) as "added" files.
486
+ */
487
+ export async function detectCustomizations(
488
+ projectPath: string,
489
+ baseline?: Map<string, string>
490
+ ): Promise<CustomizationState> {
491
+ const addedFiles: string[] = [];
492
+ const checksumMismatches: string[] = [];
493
+ const removedFiles: string[] = [];
494
+
495
+ if (baseline && baseline.size > 0) {
496
+ // Compare each baseline entry against the current file system
497
+ for (const [relFile, expectedChecksum] of baseline.entries()) {
498
+ const absPath = path.join(projectPath, relFile);
499
+ if (!fs.existsSync(absPath)) {
500
+ removedFiles.push(relFile);
501
+ continue;
502
+ }
503
+ const actual = await computeFileChecksum(absPath);
504
+ if (actual !== null && actual !== expectedChecksum) {
505
+ checksumMismatches.push(relFile);
506
+ }
507
+ }
508
+ }
509
+
510
+ // Scan managed directories for files that are not in the baseline
511
+ const managedDirs = [
512
+ path.join(projectPath, '.claude'),
513
+ path.join(projectPath, '.wundr'),
514
+ ];
515
+
516
+ for (const dir of managedDirs) {
517
+ if (!fs.existsSync(dir)) continue;
518
+
519
+ let entries: fs.Dirent[] = [];
520
+ try {
521
+ entries = fs.readdirSync(dir, { withFileTypes: true });
522
+ } catch {
523
+ continue;
524
+ }
525
+
526
+ for (const entry of entries) {
527
+ if (!entry.isFile()) continue;
528
+ const relPath = path.relative(projectPath, path.join(dir, entry.name));
529
+ // Only flag as added if it's not already in the baseline
530
+ if (!baseline || !baseline.has(relPath)) {
531
+ addedFiles.push(relPath);
532
+ }
533
+ }
534
+ }
535
+
536
+ const customizedFiles = [...new Set([...checksumMismatches, ...addedFiles])];
537
+
538
+ const hasCustomizations =
539
+ addedFiles.length > 0 ||
540
+ checksumMismatches.length > 0 ||
541
+ removedFiles.length > 0;
542
+
543
+ return {
544
+ hasCustomizations,
545
+ customizedFiles,
546
+ addedFiles,
547
+ removedFiles,
548
+ checksumMismatches,
549
+ };
24
550
  }
25
551
 
26
- export function getStateSummary(_state: ProjectState): string {
27
- return 'State detection not yet implemented';
552
+ /**
553
+ * Detect conflicts within the project state.
554
+ *
555
+ * Accepts a partial state object so it can be called both before and after
556
+ * full detection completes.
557
+ */
558
+ export async function detectConflicts(
559
+ projectPath: string,
560
+ state: Partial<ProjectState>
561
+ ): Promise<ConflictState> {
562
+ const conflicts: ConflictEntry[] = [];
563
+
564
+ // Version conflict: installed version is significantly behind latest
565
+ if (state.wundrVersion && state.latestWundrVersion) {
566
+ const current = state.wundrVersion;
567
+ const latest = state.latestWundrVersion;
568
+ if (current !== latest) {
569
+ const [cMaj] = current.split('.').map(Number);
570
+ const [lMaj] = latest.split('.').map(Number);
571
+ const severity =
572
+ lMaj !== undefined && cMaj !== undefined && lMaj > cMaj
573
+ ? 'error'
574
+ : 'warning';
575
+ conflicts.push({
576
+ type: 'version',
577
+ severity,
578
+ description: `Installed version ${current} is behind latest ${latest}`,
579
+ });
580
+ }
581
+ }
582
+
583
+ // Config conflict: multiple wundr config files
584
+ const wundrConfigFiles = [
585
+ path.join(projectPath, 'wundr.config.json'),
586
+ path.join(projectPath, '.wundr.json'),
587
+ path.join(projectPath, '.wundr', 'config.json'),
588
+ ].filter(f => fs.existsSync(f));
589
+
590
+ if (wundrConfigFiles.length > 1) {
591
+ conflicts.push({
592
+ type: 'config',
593
+ severity: 'warning',
594
+ description: `Multiple Wundr config files found: ${wundrConfigFiles.map(f => path.basename(f)).join(', ')}`,
595
+ });
596
+ }
597
+
598
+ // Config conflict: multiple MCP config files
599
+ const mcpConfigFiles = [
600
+ path.join(projectPath, '.mcp', 'config.json'),
601
+ path.join(projectPath, '.mcp.json'),
602
+ path.join(projectPath, 'mcp.json'),
603
+ ].filter(f => fs.existsSync(f));
604
+
605
+ if (mcpConfigFiles.length > 1) {
606
+ conflicts.push({
607
+ type: 'config',
608
+ severity: 'warning',
609
+ description: `Multiple MCP config files found: ${mcpConfigFiles.map(f => path.basename(f)).join(', ')}`,
610
+ });
611
+ }
612
+
613
+ // File conflict: uncommitted git changes block clean update
614
+ const git = state.git;
615
+ if (git?.isRepository && git?.isDirty) {
616
+ conflicts.push({
617
+ type: 'file',
618
+ severity: 'warning',
619
+ description:
620
+ 'Working tree has uncommitted changes. Consider committing or stashing before updating.',
621
+ });
622
+ }
623
+
624
+ return {
625
+ hasConflicts: conflicts.length > 0,
626
+ conflicts,
627
+ };
628
+ }
629
+
630
+ // ---------------------------------------------------------------------------
631
+ // Detect project type
632
+ // ---------------------------------------------------------------------------
633
+
634
+ /**
635
+ * Detect the primary project type from well-known manifest files.
636
+ */
637
+ function detectProjectType(projectPath: string): string {
638
+ if (fs.existsSync(path.join(projectPath, 'package.json'))) return 'node';
639
+ if (fs.existsSync(path.join(projectPath, 'requirements.txt')))
640
+ return 'python';
641
+ if (fs.existsSync(path.join(projectPath, 'Pipfile'))) return 'python';
642
+ if (fs.existsSync(path.join(projectPath, 'pyproject.toml'))) return 'python';
643
+ if (fs.existsSync(path.join(projectPath, 'go.mod'))) return 'go';
644
+ if (fs.existsSync(path.join(projectPath, 'pom.xml'))) return 'java';
645
+ if (fs.existsSync(path.join(projectPath, 'build.gradle'))) return 'java';
646
+ if (fs.existsSync(path.join(projectPath, 'Cargo.toml'))) return 'rust';
647
+ if (fs.existsSync(path.join(projectPath, 'composer.json'))) return 'php';
648
+ if (fs.existsSync(path.join(projectPath, 'Gemfile'))) return 'ruby';
649
+ return 'unknown';
650
+ }
651
+
652
+ // ---------------------------------------------------------------------------
653
+ // Health score and recommendations
654
+ // ---------------------------------------------------------------------------
655
+
656
+ function computeHealthScore(
657
+ state: Omit<ProjectState, 'healthScore' | 'recommendations'>
658
+ ): number {
659
+ let score = 0;
660
+
661
+ if (state.hasPackageJson) score += 15;
662
+ if (state.hasClaudeConfig) score += 20;
663
+ if (state.hasMCPConfig) score += 15;
664
+ if (state.hasWundrConfig) score += 15;
665
+ if (state.agents.hasAgents) score += 10;
666
+ if (state.hooks.hasHooks) score += 10;
667
+ if (state.git.isRepository) score += 10;
668
+ if (!state.isWundrOutdated) score += 5;
669
+
670
+ return Math.min(100, score);
671
+ }
672
+
673
+ function buildRecommendations(
674
+ state: Omit<ProjectState, 'recommendations'>
675
+ ): string[] {
676
+ const recs: string[] = [];
677
+
678
+ if (!state.hasClaudeConfig) {
679
+ recs.push('Add a CLAUDE.md configuration file to your project root.');
680
+ }
681
+ if (!state.hasMCPConfig) {
682
+ recs.push('Configure MCP servers by adding .mcp/config.json.');
683
+ }
684
+ if (!state.hasWundrConfig) {
685
+ recs.push(
686
+ 'Add wundr.config.json to declare the Wundr version for this project.'
687
+ );
688
+ }
689
+ if (!state.agents.hasAgents) {
690
+ recs.push(
691
+ 'Define agent configurations in .claude/agents/ for better automation.'
692
+ );
693
+ }
694
+ if (!state.hooks.hasHooks) {
695
+ recs.push(
696
+ 'Set up lifecycle hooks (e.g. Husky pre-commit) to enforce quality gates.'
697
+ );
698
+ }
699
+ if (!state.git.isRepository) {
700
+ recs.push('Initialise a git repository to enable version tracking.');
701
+ }
702
+ if (state.isWundrOutdated) {
703
+ recs.push(
704
+ `Update Wundr from ${state.wundrVersion} to ${state.latestWundrVersion} to get the latest features.`
705
+ );
706
+ }
707
+ if (state.conflicts.hasConflicts) {
708
+ recs.push(
709
+ 'Resolve detected configuration conflicts before running an update.'
710
+ );
711
+ }
712
+
713
+ return recs;
714
+ }
715
+
716
+ // ---------------------------------------------------------------------------
717
+ // Main detection entry point
718
+ // ---------------------------------------------------------------------------
719
+
720
+ export interface DetectProjectStateOptions {
721
+ /** The latest known Wundr CLI version (used for outdated detection). */
722
+ latestVersion?: string;
723
+ }
724
+
725
+ /**
726
+ * Analyse the project at `projectPath` and return a comprehensive
727
+ * `ProjectState` snapshot.
728
+ *
729
+ * When `projectPath` is omitted `process.cwd()` is used so that callers
730
+ * such as `wundr update check` can call `detectProjectState()` with no
731
+ * arguments.
732
+ */
733
+ export async function detectProjectState(
734
+ projectPath: string = process.cwd(),
735
+ options: DetectProjectStateOptions = {}
736
+ ): Promise<ProjectState> {
737
+ // Resolve to an absolute path and bail out gracefully for non-existent dirs
738
+ const resolvedPath = path.resolve(projectPath);
739
+ const pathExists = fs.existsSync(resolvedPath);
740
+
741
+ if (!pathExists) {
742
+ return buildEmptyState(resolvedPath, options);
743
+ }
744
+
745
+ // ---- project type ---------------------------------------------------------
746
+ const type = detectProjectType(resolvedPath);
747
+
748
+ // ---- package.json ---------------------------------------------------------
749
+ let hasPackageJson = false;
750
+ let packageName: string | undefined;
751
+ let packageVersion: string | undefined;
752
+ let isMonorepo = false;
753
+ let workspaces: string[] = [];
754
+ let dependencies: Record<string, string> = {};
755
+
756
+ const pkgPath = path.join(resolvedPath, 'package.json');
757
+ if (fs.existsSync(pkgPath)) {
758
+ hasPackageJson = true;
759
+ try {
760
+ const raw = fs.readFileSync(pkgPath, 'utf8');
761
+ const pkg = JSON.parse(raw);
762
+ packageName = typeof pkg.name === 'string' ? pkg.name : undefined;
763
+ packageVersion =
764
+ typeof pkg.version === 'string' ? pkg.version : undefined;
765
+
766
+ if (pkg.workspaces) {
767
+ isMonorepo = true;
768
+ workspaces = Array.isArray(pkg.workspaces)
769
+ ? pkg.workspaces
770
+ : Array.isArray(pkg.workspaces?.packages)
771
+ ? pkg.workspaces.packages
772
+ : [];
773
+ }
774
+
775
+ if (pkg.dependencies && typeof pkg.dependencies === 'object') {
776
+ dependencies = { ...pkg.dependencies };
777
+ }
778
+ } catch {
779
+ // corrupted package.json – hasPackageJson is still true
780
+ }
781
+ }
782
+
783
+ // ---- wundr CLI version in node_modules ------------------------------------
784
+ let wundrVersion: string | undefined;
785
+ const wundrPkgPaths = [
786
+ path.join(resolvedPath, 'node_modules', '@wundr.io', 'cli', 'package.json'),
787
+ path.join(resolvedPath, 'node_modules', '@wundr', 'cli', 'package.json'),
788
+ ];
789
+
790
+ for (const wPkg of wundrPkgPaths) {
791
+ if (fs.existsSync(wPkg)) {
792
+ try {
793
+ const raw = fs.readFileSync(wPkg, 'utf8');
794
+ const parsed = JSON.parse(raw);
795
+ if (typeof parsed.version === 'string') {
796
+ wundrVersion = parsed.version;
797
+ break;
798
+ }
799
+ } catch {
800
+ // ignore
801
+ }
802
+ }
803
+ }
804
+
805
+ // ---- sub-detectors --------------------------------------------------------
806
+ const [git, claudeConfig, mcpConfig, wundrConfig, agents, hooks] =
807
+ await Promise.all([
808
+ detectGitStatus(resolvedPath),
809
+ detectClaudeConfig(resolvedPath),
810
+ detectMCPConfig(resolvedPath),
811
+ detectWundrConfig(resolvedPath),
812
+ detectAgents(resolvedPath),
813
+ detectHooks(resolvedPath),
814
+ ]);
815
+
816
+ const hasClaudeConfig = claudeConfig.exists;
817
+ const hasMCPConfig = mcpConfig.exists;
818
+ const hasWundrConfig = wundrConfig.exists;
819
+
820
+ // Prefer version from wundr.config.json over node_modules
821
+ if (!wundrVersion && wundrConfig.version) {
822
+ wundrVersion = wundrConfig.version;
823
+ }
824
+
825
+ const latestWundrVersion = options.latestVersion;
826
+
827
+ // ---- outdated check -------------------------------------------------------
828
+ let isWundrOutdated = false;
829
+ if (
830
+ wundrVersion &&
831
+ latestWundrVersion &&
832
+ wundrVersion !== latestWundrVersion
833
+ ) {
834
+ isWundrOutdated = true;
835
+ }
836
+
837
+ // ---- customizations -------------------------------------------------------
838
+ const customizations = await detectCustomizations(resolvedPath);
839
+
840
+ // ---- hasWundr -------------------------------------------------------------
841
+ const hasWundr =
842
+ fs.existsSync(path.join(resolvedPath, 'CLAUDE.md')) ||
843
+ fs.existsSync(path.join(resolvedPath, 'wundr.config.json')) ||
844
+ fs.existsSync(path.join(resolvedPath, '.wundr')) ||
845
+ wundrVersion !== undefined;
846
+
847
+ // ---- missing components ---------------------------------------------------
848
+ const missingComponents: string[] = [];
849
+ if (!hasClaudeConfig) missingComponents.push('CLAUDE.md');
850
+ if (!hasMCPConfig) missingComponents.push('mcp-config');
851
+ if (!hasWundrConfig) missingComponents.push('wundr-config');
852
+
853
+ const isPartialInstallation = hasWundr && missingComponents.length > 0;
854
+
855
+ // ---- partial state for conflict detection ---------------------------------
856
+ const partialState = {
857
+ wundrVersion,
858
+ latestWundrVersion,
859
+ git,
860
+ projectPath: resolvedPath,
861
+ type,
862
+ customizations,
863
+ dependencies,
864
+ isWundrOutdated,
865
+ hasPackageJson,
866
+ packageName,
867
+ packageVersion,
868
+ isMonorepo,
869
+ workspaces,
870
+ hasWundr,
871
+ hasClaudeConfig,
872
+ hasMCPConfig,
873
+ hasWundrConfig,
874
+ claudeConfigPath: claudeConfig.path,
875
+ mcpConfigPath: mcpConfig.path,
876
+ wundrConfigPath: wundrConfig.path,
877
+ latestWundrVersion,
878
+ isPartialInstallation,
879
+ missingComponents,
880
+ agents,
881
+ hooks,
882
+ detectedAt: new Date(),
883
+ };
884
+
885
+ const conflicts = await detectConflicts(resolvedPath, partialState);
886
+
887
+ // ---- health score and recommendations -------------------------------------
888
+ const stateWithoutScore = { ...partialState, conflicts };
889
+ const healthScore = computeHealthScore(stateWithoutScore);
890
+ const recommendations = buildRecommendations({
891
+ ...stateWithoutScore,
892
+ healthScore,
893
+ });
894
+
895
+ return {
896
+ ...stateWithoutScore,
897
+ healthScore,
898
+ recommendations,
899
+ };
900
+ }
901
+
902
+ // ---------------------------------------------------------------------------
903
+ // Helper: hasWundrInstalled
904
+ // ---------------------------------------------------------------------------
905
+
906
+ /**
907
+ * Quick check for whether Wundr is installed in the given directory.
908
+ */
909
+ export async function hasWundrInstalled(projectPath: string): Promise<boolean> {
910
+ return (
911
+ fs.existsSync(path.join(projectPath, 'CLAUDE.md')) ||
912
+ fs.existsSync(path.join(projectPath, 'wundr.config.json')) ||
913
+ fs.existsSync(path.join(projectPath, '.wundr'))
914
+ );
915
+ }
916
+
917
+ // ---------------------------------------------------------------------------
918
+ // Summary helper
919
+ // ---------------------------------------------------------------------------
920
+
921
+ /**
922
+ * Build a human-readable summary string from a `ProjectState`.
923
+ */
924
+ export function getStateSummary(state: ProjectState): string {
925
+ const lines: string[] = [];
926
+
927
+ lines.push('=== Project State Summary ===');
928
+
929
+ if (state.packageName) {
930
+ lines.push(
931
+ `Project: ${state.packageName}${state.packageVersion ? ` v${state.packageVersion}` : ''}`
932
+ );
933
+ } else {
934
+ lines.push(`Project path: ${state.projectPath}`);
935
+ }
936
+
937
+ lines.push(`Type: ${state.type}`);
938
+ lines.push(`Health Score: ${state.healthScore}/100`);
939
+
940
+ if (state.wundrVersion) {
941
+ lines.push(`Wundr version: ${state.wundrVersion}`);
942
+ }
943
+
944
+ if (state.isWundrOutdated) {
945
+ lines.push(`Update available: ${state.latestWundrVersion}`);
946
+ }
947
+
948
+ lines.push(
949
+ `Git: ${state.git.isRepository ? `${state.git.branch ?? 'detached HEAD'}${state.git.isDirty ? ' (dirty)' : ''}` : 'not a repository'}`
950
+ );
951
+
952
+ lines.push(`Claude config: ${state.hasClaudeConfig ? 'present' : 'missing'}`);
953
+ lines.push(`MCP config: ${state.hasMCPConfig ? 'present' : 'missing'}`);
954
+ lines.push(`Wundr config: ${state.hasWundrConfig ? 'present' : 'missing'}`);
955
+ lines.push(`Agents: ${state.agents.agentCount}`);
956
+ lines.push(`Hooks: ${state.hooks.hookCount}`);
957
+
958
+ if (state.conflicts.hasConflicts) {
959
+ lines.push(`Conflicts: ${state.conflicts.conflicts.length}`);
960
+ for (const c of state.conflicts.conflicts) {
961
+ lines.push(` [${c.severity}] ${c.description}`);
962
+ }
963
+ }
964
+
965
+ if (state.recommendations.length > 0) {
966
+ lines.push('\nRecommendations:');
967
+ for (const rec of state.recommendations) {
968
+ lines.push(` - ${rec}`);
969
+ }
970
+ }
971
+
972
+ return lines.join('\n');
973
+ }
974
+
975
+ // ---------------------------------------------------------------------------
976
+ // Private helper: build a safe empty state for non-existent paths
977
+ // ---------------------------------------------------------------------------
978
+
979
+ function buildEmptyState(
980
+ projectPath: string,
981
+ options: DetectProjectStateOptions
982
+ ): ProjectState {
983
+ const git: GitStatus = {
984
+ isRepository: false,
985
+ isDirty: false,
986
+ hasUncommittedChanges: false,
987
+ hasUntrackedFiles: false,
988
+ stagedFiles: [],
989
+ modifiedFiles: [],
990
+ untrackedFiles: [],
991
+ };
992
+
993
+ const agents: AgentState = { hasAgents: false, agentCount: 0, agents: [] };
994
+ const hooks: HookState = { hasHooks: false, hookCount: 0, hooks: [] };
995
+ const customizations: CustomizationState = {
996
+ hasCustomizations: false,
997
+ customizedFiles: [],
998
+ addedFiles: [],
999
+ removedFiles: [],
1000
+ checksumMismatches: [],
1001
+ };
1002
+ const conflicts: ConflictState = { hasConflicts: false, conflicts: [] };
1003
+
1004
+ const base = {
1005
+ projectPath,
1006
+ detectedAt: new Date(),
1007
+ type: 'unknown',
1008
+ hasWundr: false,
1009
+ hasClaudeConfig: false,
1010
+ hasMCPConfig: false,
1011
+ hasWundrConfig: false,
1012
+ hasPackageJson: false,
1013
+ isMonorepo: false,
1014
+ workspaces: [],
1015
+ dependencies: {},
1016
+ isPartialInstallation: false,
1017
+ missingComponents: [] as string[],
1018
+ git,
1019
+ agents,
1020
+ hooks,
1021
+ customizations,
1022
+ conflicts,
1023
+ isWundrOutdated: false,
1024
+ latestWundrVersion: options.latestVersion,
1025
+ healthScore: 0,
1026
+ };
1027
+
1028
+ const recommendations = buildRecommendations(base);
1029
+ return { ...base, recommendations };
28
1030
  }