@wundr.io/cli 1.0.11 → 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.
- package/bin/wundr.js +8 -4
- package/package.json +23 -23
- package/src/ai/ai-service.ts +16 -17
- package/src/ai/claude-client.ts +16 -16
- package/src/ai/conversation-manager.ts +29 -29
- package/src/cli.ts +4 -4
- package/src/commands/ai.ts +246 -78
- package/src/commands/alignment.ts +74 -74
- package/src/commands/analyze-optimized.ts +111 -78
- package/src/commands/analyze.ts +14 -14
- package/src/commands/batch.ts +179 -42
- package/src/commands/chat.ts +37 -30
- package/src/commands/claude-init.ts +41 -45
- package/src/commands/claude-setup.ts +204 -119
- package/src/commands/computer-setup.ts +85 -43
- package/src/commands/create-command.ts +4 -4
- package/src/commands/create.ts +27 -27
- package/src/commands/dashboard.ts +24 -24
- package/src/commands/govern.ts +25 -25
- package/src/commands/governance.ts +34 -34
- package/src/commands/guardian.ts +56 -56
- package/src/commands/init.ts +25 -22
- package/src/commands/orchestrator.ts +68 -41
- package/src/commands/performance-optimizer.ts +34 -35
- package/src/commands/plugins.ts +27 -27
- package/src/commands/project-update.ts +175 -72
- package/src/commands/rag.ts +185 -78
- package/src/commands/session.ts +35 -35
- package/src/commands/setup.ts +40 -344
- package/src/commands/test-init.ts +3 -3
- package/src/commands/test.ts +4 -4
- package/src/commands/watch.ts +28 -29
- package/src/commands/worktree.ts +49 -49
- package/src/context/context-manager.ts +10 -10
- package/src/context/session-manager.ts +41 -41
- package/src/framework/command-interface.ts +520 -0
- package/src/framework/command-registry.ts +942 -0
- package/src/framework/completion-exporter.ts +383 -0
- package/src/framework/debug-logger.ts +519 -0
- package/src/framework/error-handler.ts +867 -0
- package/src/framework/help-generator.ts +540 -0
- package/src/framework/index.ts +169 -0
- package/src/framework/interactive-repl.ts +703 -0
- package/src/framework/output-formatter.ts +834 -0
- package/src/framework/progress-manager.ts +539 -0
- package/src/index.ts +4 -4
- package/src/interactive/interactive-mode.ts +16 -16
- package/src/lib/conflict-resolution.ts +799 -9
- package/src/lib/merge-strategy.ts +529 -7
- package/src/lib/safety-mechanisms.ts +422 -18
- package/src/lib/state-detection.ts +1015 -13
- package/src/nlp/command-mapper.ts +29 -29
- package/src/nlp/command-parser.ts +17 -17
- package/src/nlp/intent-classifier.ts +7 -7
- package/src/nlp/intent-parser.ts +54 -52
- package/src/plugins/plugin-manager.ts +61 -39
- package/src/tests/computer-setup-integration.test.ts +46 -15
- package/src/types/modules.d.ts +424 -1
- package/src/utils/backup-rollback-manager.ts +11 -8
- package/src/utils/config-manager.ts +3 -3
- package/src/utils/error-handler.ts +2 -2
- package/src/utils/logger.ts +22 -22
- package/templates/batch/ci-cd.yaml +7 -7
- package/test-suites/api/health.spec.ts +20 -23
- package/test-suites/helpers/test-config.ts +14 -13
- package/test-suites/ui/accessibility.spec.ts +27 -22
- package/test-suites/ui/smoke.spec.ts +26 -21
- package/LICENSE +0 -21
- package/dist/ai/ai-service.d.ts +0 -152
- package/dist/ai/ai-service.d.ts.map +0 -1
- package/dist/ai/ai-service.js +0 -430
- package/dist/ai/ai-service.js.map +0 -1
- package/dist/ai/claude-client.d.ts +0 -130
- package/dist/ai/claude-client.d.ts.map +0 -1
- package/dist/ai/claude-client.js +0 -340
- package/dist/ai/claude-client.js.map +0 -1
- package/dist/ai/conversation-manager.d.ts +0 -164
- package/dist/ai/conversation-manager.d.ts.map +0 -1
- package/dist/ai/conversation-manager.js +0 -614
- package/dist/ai/conversation-manager.js.map +0 -1
- package/dist/ai/index.d.ts +0 -5
- package/dist/ai/index.d.ts.map +0 -1
- package/dist/ai/index.js +0 -8
- package/dist/ai/index.js.map +0 -1
- package/dist/cli.d.ts +0 -36
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -192
- package/dist/cli.js.map +0 -1
- package/dist/commands/ai.d.ts +0 -89
- package/dist/commands/ai.d.ts.map +0 -1
- package/dist/commands/ai.js +0 -799
- package/dist/commands/ai.js.map +0 -1
- package/dist/commands/alignment.d.ts +0 -78
- package/dist/commands/alignment.d.ts.map +0 -1
- package/dist/commands/alignment.js +0 -817
- package/dist/commands/alignment.js.map +0 -1
- package/dist/commands/analyze-optimized.d.ts +0 -14
- package/dist/commands/analyze-optimized.d.ts.map +0 -1
- package/dist/commands/analyze-optimized.js +0 -600
- package/dist/commands/analyze-optimized.js.map +0 -1
- package/dist/commands/analyze.d.ts +0 -65
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js +0 -435
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/batch.d.ts +0 -71
- package/dist/commands/batch.d.ts.map +0 -1
- package/dist/commands/batch.js +0 -738
- package/dist/commands/batch.js.map +0 -1
- package/dist/commands/chat.d.ts +0 -71
- package/dist/commands/chat.d.ts.map +0 -1
- package/dist/commands/chat.js +0 -674
- package/dist/commands/chat.js.map +0 -1
- package/dist/commands/claude-init.d.ts +0 -28
- package/dist/commands/claude-init.d.ts.map +0 -1
- package/dist/commands/claude-init.js +0 -591
- package/dist/commands/claude-init.js.map +0 -1
- package/dist/commands/claude-setup.d.ts +0 -119
- package/dist/commands/claude-setup.d.ts.map +0 -1
- package/dist/commands/claude-setup.js +0 -1073
- package/dist/commands/claude-setup.js.map +0 -1
- package/dist/commands/computer-setup-commands.d.ts +0 -53
- package/dist/commands/computer-setup-commands.d.ts.map +0 -1
- package/dist/commands/computer-setup-commands.js +0 -705
- package/dist/commands/computer-setup-commands.js.map +0 -1
- package/dist/commands/computer-setup.d.ts +0 -7
- package/dist/commands/computer-setup.d.ts.map +0 -1
- package/dist/commands/computer-setup.js +0 -849
- package/dist/commands/computer-setup.js.map +0 -1
- package/dist/commands/create-command.d.ts +0 -7
- package/dist/commands/create-command.d.ts.map +0 -1
- package/dist/commands/create-command.js +0 -158
- package/dist/commands/create-command.js.map +0 -1
- package/dist/commands/create.d.ts +0 -74
- package/dist/commands/create.d.ts.map +0 -1
- package/dist/commands/create.js +0 -556
- package/dist/commands/create.js.map +0 -1
- package/dist/commands/dashboard.d.ts +0 -91
- package/dist/commands/dashboard.d.ts.map +0 -1
- package/dist/commands/dashboard.js +0 -538
- package/dist/commands/dashboard.js.map +0 -1
- package/dist/commands/govern.d.ts +0 -70
- package/dist/commands/govern.d.ts.map +0 -1
- package/dist/commands/govern.js +0 -481
- package/dist/commands/govern.js.map +0 -1
- package/dist/commands/governance.d.ts +0 -17
- package/dist/commands/governance.d.ts.map +0 -1
- package/dist/commands/governance.js +0 -703
- package/dist/commands/governance.js.map +0 -1
- package/dist/commands/guardian.d.ts +0 -20
- package/dist/commands/guardian.d.ts.map +0 -1
- package/dist/commands/guardian.js +0 -597
- package/dist/commands/guardian.js.map +0 -1
- package/dist/commands/init.d.ts +0 -59
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -650
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/orchestrator.d.ts +0 -7
- package/dist/commands/orchestrator.d.ts.map +0 -1
- package/dist/commands/orchestrator.js +0 -571
- package/dist/commands/orchestrator.js.map +0 -1
- package/dist/commands/performance-optimizer.d.ts +0 -30
- package/dist/commands/performance-optimizer.d.ts.map +0 -1
- package/dist/commands/performance-optimizer.js +0 -650
- package/dist/commands/performance-optimizer.js.map +0 -1
- package/dist/commands/plugins.d.ts +0 -87
- package/dist/commands/plugins.d.ts.map +0 -1
- package/dist/commands/plugins.js +0 -685
- package/dist/commands/plugins.js.map +0 -1
- package/dist/commands/rag.d.ts +0 -7
- package/dist/commands/rag.d.ts.map +0 -1
- package/dist/commands/rag.js +0 -748
- package/dist/commands/rag.js.map +0 -1
- package/dist/commands/session.d.ts +0 -41
- package/dist/commands/session.d.ts.map +0 -1
- package/dist/commands/session.js +0 -441
- package/dist/commands/session.js.map +0 -1
- package/dist/commands/setup.d.ts +0 -29
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/setup.js +0 -397
- package/dist/commands/setup.js.map +0 -1
- package/dist/commands/test-init.d.ts +0 -9
- package/dist/commands/test-init.d.ts.map +0 -1
- package/dist/commands/test-init.js +0 -222
- package/dist/commands/test-init.js.map +0 -1
- package/dist/commands/test.d.ts +0 -25
- package/dist/commands/test.d.ts.map +0 -1
- package/dist/commands/test.js +0 -217
- package/dist/commands/test.js.map +0 -1
- package/dist/commands/vp.d.ts +0 -7
- package/dist/commands/vp.d.ts.map +0 -1
- package/dist/commands/vp.js +0 -571
- package/dist/commands/vp.js.map +0 -1
- package/dist/commands/watch.d.ts +0 -76
- package/dist/commands/watch.d.ts.map +0 -1
- package/dist/commands/watch.js +0 -613
- package/dist/commands/watch.js.map +0 -1
- package/dist/commands/worktree.d.ts +0 -63
- package/dist/commands/worktree.d.ts.map +0 -1
- package/dist/commands/worktree.js +0 -774
- package/dist/commands/worktree.js.map +0 -1
- package/dist/context/context-manager.d.ts +0 -155
- package/dist/context/context-manager.d.ts.map +0 -1
- package/dist/context/context-manager.js +0 -383
- package/dist/context/context-manager.js.map +0 -1
- package/dist/context/index.d.ts +0 -3
- package/dist/context/index.d.ts.map +0 -1
- package/dist/context/index.js +0 -6
- package/dist/context/index.js.map +0 -1
- package/dist/context/session-manager.d.ts +0 -207
- package/dist/context/session-manager.d.ts.map +0 -1
- package/dist/context/session-manager.js +0 -686
- package/dist/context/session-manager.js.map +0 -1
- package/dist/index.d.ts +0 -8
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -51
- package/dist/index.js.map +0 -1
- package/dist/interactive/interactive-mode.d.ts +0 -76
- package/dist/interactive/interactive-mode.d.ts.map +0 -1
- package/dist/interactive/interactive-mode.js +0 -732
- package/dist/interactive/interactive-mode.js.map +0 -1
- package/dist/nlp/command-mapper.d.ts +0 -174
- package/dist/nlp/command-mapper.d.ts.map +0 -1
- package/dist/nlp/command-mapper.js +0 -624
- package/dist/nlp/command-mapper.js.map +0 -1
- package/dist/nlp/command-parser.d.ts +0 -106
- package/dist/nlp/command-parser.d.ts.map +0 -1
- package/dist/nlp/command-parser.js +0 -417
- package/dist/nlp/command-parser.js.map +0 -1
- package/dist/nlp/index.d.ts +0 -5
- package/dist/nlp/index.d.ts.map +0 -1
- package/dist/nlp/index.js +0 -8
- package/dist/nlp/index.js.map +0 -1
- package/dist/nlp/intent-classifier.d.ts +0 -59
- package/dist/nlp/intent-classifier.d.ts.map +0 -1
- package/dist/nlp/intent-classifier.js +0 -384
- package/dist/nlp/intent-classifier.js.map +0 -1
- package/dist/nlp/intent-parser.d.ts +0 -152
- package/dist/nlp/intent-parser.d.ts.map +0 -1
- package/dist/nlp/intent-parser.js +0 -744
- package/dist/nlp/intent-parser.js.map +0 -1
- package/dist/plugins/plugin-manager.d.ts +0 -120
- package/dist/plugins/plugin-manager.d.ts.map +0 -1
- package/dist/plugins/plugin-manager.js +0 -595
- package/dist/plugins/plugin-manager.js.map +0 -1
- package/dist/types/index.d.ts +0 -224
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -3
- package/dist/types/index.js.map +0 -1
- package/dist/utils/backup-rollback-manager.d.ts +0 -72
- package/dist/utils/backup-rollback-manager.d.ts.map +0 -1
- package/dist/utils/backup-rollback-manager.js +0 -289
- package/dist/utils/backup-rollback-manager.js.map +0 -1
- package/dist/utils/claude-config-installer.d.ts +0 -98
- package/dist/utils/claude-config-installer.d.ts.map +0 -1
- package/dist/utils/claude-config-installer.js +0 -678
- package/dist/utils/claude-config-installer.js.map +0 -1
- package/dist/utils/config-manager.d.ts +0 -73
- package/dist/utils/config-manager.d.ts.map +0 -1
- package/dist/utils/config-manager.js +0 -339
- package/dist/utils/config-manager.js.map +0 -1
- package/dist/utils/error-handler.d.ts +0 -46
- package/dist/utils/error-handler.d.ts.map +0 -1
- package/dist/utils/error-handler.js +0 -169
- package/dist/utils/error-handler.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -25
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -105
- package/dist/utils/logger.js.map +0 -1
- package/src/commands/computer-setup-commands.ts +0 -872
|
@@ -1,28 +1,1030 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* State Detection -
|
|
3
|
-
*
|
|
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
|
-
|
|
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
|
|
114
|
+
healthScore: number;
|
|
11
115
|
isWundrOutdated?: boolean;
|
|
12
|
-
recommendations
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
}
|