ultra-dex 2.2.1 ā 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -151
- package/assets/agents/00-AGENT_INDEX.md +1 -1
- package/assets/code-patterns/clerk-middleware.ts +138 -0
- package/assets/code-patterns/prisma-schema.prisma +224 -0
- package/assets/code-patterns/rls-policies.sql +246 -0
- package/assets/code-patterns/server-actions.ts +191 -0
- package/assets/code-patterns/trpc-router.ts +258 -0
- package/assets/cursor-rules/13-ai-integration.mdc +155 -0
- package/assets/cursor-rules/14-server-components.mdc +81 -0
- package/assets/cursor-rules/15-server-actions.mdc +102 -0
- package/assets/cursor-rules/16-edge-middleware.mdc +105 -0
- package/assets/cursor-rules/17-streaming-ssr.mdc +138 -0
- package/assets/docs/LAUNCH-POSTS.md +1 -1
- package/assets/docs/QUICK-REFERENCE.md +9 -4
- package/assets/docs/VISION-V2.md +1 -1
- package/assets/hooks/pre-commit +98 -0
- package/assets/saas-plan/04-Imp-Template.md +1 -1
- package/bin/ultra-dex.js +132 -4
- package/lib/commands/advanced.js +471 -0
- package/lib/commands/agent-builder.js +226 -0
- package/lib/commands/agents.js +102 -42
- package/lib/commands/auto-implement.js +68 -0
- package/lib/commands/banner.js +43 -21
- package/lib/commands/build.js +78 -183
- package/lib/commands/ci-monitor.js +84 -0
- package/lib/commands/config.js +207 -0
- package/lib/commands/dashboard.js +770 -0
- package/lib/commands/diff.js +233 -0
- package/lib/commands/doctor.js +416 -0
- package/lib/commands/export.js +408 -0
- package/lib/commands/fix.js +96 -0
- package/lib/commands/generate.js +105 -78
- package/lib/commands/hooks.js +251 -76
- package/lib/commands/init.js +102 -54
- package/lib/commands/memory.js +80 -0
- package/lib/commands/plan.js +82 -0
- package/lib/commands/review.js +34 -5
- package/lib/commands/run.js +233 -0
- package/lib/commands/scaffold.js +151 -0
- package/lib/commands/serve.js +179 -146
- package/lib/commands/state.js +327 -0
- package/lib/commands/swarm.js +306 -0
- package/lib/commands/sync.js +82 -23
- package/lib/commands/team.js +275 -0
- package/lib/commands/upgrade.js +190 -0
- package/lib/commands/validate.js +34 -0
- package/lib/commands/verify.js +81 -0
- package/lib/commands/watch.js +79 -0
- package/lib/config/theme.js +47 -0
- package/lib/mcp/graph.js +92 -0
- package/lib/mcp/memory.js +95 -0
- package/lib/mcp/resources.js +152 -0
- package/lib/mcp/server.js +34 -0
- package/lib/mcp/tools.js +481 -0
- package/lib/mcp/websocket.js +117 -0
- package/lib/providers/index.js +49 -4
- package/lib/providers/ollama.js +136 -0
- package/lib/providers/router.js +63 -0
- package/lib/quality/scanner.js +128 -0
- package/lib/swarm/coordinator.js +97 -0
- package/lib/swarm/index.js +598 -0
- package/lib/swarm/protocol.js +677 -0
- package/lib/swarm/tiers.js +485 -0
- package/lib/templates/code/clerk-middleware.ts +138 -0
- package/lib/templates/code/prisma-schema.prisma +224 -0
- package/lib/templates/code/rls-policies.sql +246 -0
- package/lib/templates/code/server-actions.ts +191 -0
- package/lib/templates/code/trpc-router.ts +258 -0
- package/lib/templates/custom-agent.md +10 -0
- package/lib/themes/doomsday.js +229 -0
- package/lib/ui/index.js +5 -0
- package/lib/ui/interface.js +241 -0
- package/lib/ui/spinners.js +116 -0
- package/lib/ui/theme.js +183 -0
- package/lib/utils/agents.js +32 -0
- package/lib/utils/files.js +14 -0
- package/lib/utils/graph.js +108 -0
- package/lib/utils/help.js +64 -0
- package/lib/utils/messages.js +35 -0
- package/lib/utils/progress.js +24 -0
- package/lib/utils/prompts.js +47 -0
- package/lib/utils/spinners.js +46 -0
- package/lib/utils/status.js +31 -0
- package/lib/utils/tables.js +41 -0
- package/lib/utils/theme-state.js +9 -0
- package/lib/utils/version-display.js +32 -0
- package/package.json +31 -13
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// cli/lib/commands/diff.js
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { loadConfig } from './config.js';
|
|
7
|
+
|
|
8
|
+
const STATUS = {
|
|
9
|
+
DONE: 'done',
|
|
10
|
+
PARTIAL: 'partial',
|
|
11
|
+
MISSING: 'missing'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function diffCommand(options) {
|
|
15
|
+
if (!options.json) {
|
|
16
|
+
console.log(chalk.cyan.bold('\nš Ultra-Dex Diff - Plan vs Code v3.0\n'));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const planPath = join(process.cwd(), 'IMPLEMENTATION-PLAN.md');
|
|
20
|
+
if (!existsSync(planPath)) {
|
|
21
|
+
if (options.json) {
|
|
22
|
+
console.log(JSON.stringify({ error: 'No IMPLEMENTATION-PLAN.md found', sections: [], alignment: 0 }));
|
|
23
|
+
} else {
|
|
24
|
+
console.log(chalk.red('No IMPLEMENTATION-PLAN.md found'));
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const plan = readFileSync(planPath, 'utf-8');
|
|
30
|
+
|
|
31
|
+
// Extract planned sections
|
|
32
|
+
const plannedSections = extractSections(plan);
|
|
33
|
+
|
|
34
|
+
if (plannedSections.length === 0) {
|
|
35
|
+
if (options.json) {
|
|
36
|
+
console.log(JSON.stringify({ error: 'No sections found', sections: [], alignment: 0 }));
|
|
37
|
+
} else {
|
|
38
|
+
console.log(chalk.yellow('No sections found in IMPLEMENTATION-PLAN.md (looking for ## or ### headings)'));
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check implementation status
|
|
44
|
+
const config = loadConfig();
|
|
45
|
+
const results = checkImplementationStatus(plannedSections, config);
|
|
46
|
+
|
|
47
|
+
// Calculate alignment
|
|
48
|
+
const doneCount = results.filter(r => r.status === STATUS.DONE).length;
|
|
49
|
+
const partialCount = results.filter(r => r.status === STATUS.PARTIAL).length;
|
|
50
|
+
const missingCount = results.filter(r => r.status === STATUS.MISSING).length;
|
|
51
|
+
const alignment = Math.round(((doneCount + partialCount * 0.5) / results.length) * 100);
|
|
52
|
+
|
|
53
|
+
if (options.json) {
|
|
54
|
+
console.log(JSON.stringify({
|
|
55
|
+
alignment,
|
|
56
|
+
totalSections: results.length,
|
|
57
|
+
done: doneCount,
|
|
58
|
+
partial: partialCount,
|
|
59
|
+
missing: missingCount,
|
|
60
|
+
sections: results.map(r => ({
|
|
61
|
+
title: r.title,
|
|
62
|
+
status: r.status,
|
|
63
|
+
matches: r.matches
|
|
64
|
+
}))
|
|
65
|
+
}, null, 2));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Color output
|
|
70
|
+
console.log(chalk.white.bold('Planned vs Implemented:\n'));
|
|
71
|
+
|
|
72
|
+
results.forEach(({ title, status, matches }) => {
|
|
73
|
+
let icon, color;
|
|
74
|
+
switch (status) {
|
|
75
|
+
case STATUS.DONE:
|
|
76
|
+
icon = 'ā
';
|
|
77
|
+
color = 'green';
|
|
78
|
+
break;
|
|
79
|
+
case STATUS.PARTIAL:
|
|
80
|
+
icon = 'ā ļø';
|
|
81
|
+
color = 'yellow';
|
|
82
|
+
break;
|
|
83
|
+
case STATUS.MISSING:
|
|
84
|
+
icon = 'ā';
|
|
85
|
+
color = 'red';
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
console.log(` ${icon} ${chalk[color](title)}`);
|
|
89
|
+
if (matches && matches.length > 0 && status !== STATUS.MISSING) {
|
|
90
|
+
matches.slice(0, 2).forEach(m => {
|
|
91
|
+
console.log(chalk.gray(` ā ${m}`));
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Summary
|
|
97
|
+
console.log(chalk.white.bold('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
98
|
+
const alignColor = alignment >= 80 ? 'green' : alignment >= 50 ? 'yellow' : 'red';
|
|
99
|
+
console.log(chalk[alignColor].bold(`Alignment: ${alignment}%`));
|
|
100
|
+
console.log(chalk.gray(` ${chalk.green(`Done: ${doneCount}`)} | ${chalk.yellow(`Partial: ${partialCount}`)} | ${chalk.red(`Missing: ${missingCount}`)}`));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function extractSections(plan) {
|
|
104
|
+
const sections = [];
|
|
105
|
+
const lines = plan.split('\n');
|
|
106
|
+
let currentSection = null;
|
|
107
|
+
let sectionContent = [];
|
|
108
|
+
|
|
109
|
+
lines.forEach(line => {
|
|
110
|
+
const headingMatch = line.match(/^(#{2,3})\s+(.+)$/);
|
|
111
|
+
if (headingMatch) {
|
|
112
|
+
if (currentSection) {
|
|
113
|
+
sections.push({
|
|
114
|
+
level: currentSection.level,
|
|
115
|
+
title: currentSection.title,
|
|
116
|
+
content: sectionContent.join('\n'),
|
|
117
|
+
keywords: extractKeywords(currentSection.title, sectionContent.join('\n'))
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
currentSection = {
|
|
121
|
+
level: headingMatch[1].length,
|
|
122
|
+
title: headingMatch[2].trim()
|
|
123
|
+
};
|
|
124
|
+
sectionContent = [];
|
|
125
|
+
} else if (currentSection) {
|
|
126
|
+
sectionContent.push(line);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (currentSection) {
|
|
131
|
+
sections.push({
|
|
132
|
+
level: currentSection.level,
|
|
133
|
+
title: currentSection.title,
|
|
134
|
+
content: sectionContent.join('\n'),
|
|
135
|
+
keywords: extractKeywords(currentSection.title, sectionContent.join('\n'))
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return sections;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function extractKeywords(title, content) {
|
|
143
|
+
const combined = `${title} ${content}`.toLowerCase();
|
|
144
|
+
const words = combined.match(/\b[a-z][a-z0-9]+\b/g) || [];
|
|
145
|
+
|
|
146
|
+
// Filter common words and keep meaningful ones
|
|
147
|
+
const stopwords = new Set(['the', 'and', 'for', 'with', 'this', 'that', 'from', 'will', 'have', 'has', 'are', 'was', 'were', 'been', 'being', 'would', 'could', 'should', 'can', 'may', 'might', 'must', 'shall', 'need', 'use', 'used', 'using', 'make', 'made', 'get', 'set', 'add', 'new', 'each', 'all', 'any', 'some', 'one', 'two']);
|
|
148
|
+
|
|
149
|
+
return [...new Set(words.filter(w => w.length > 3 && !stopwords.has(w)))];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function checkImplementationStatus(sections, config = {}) {
|
|
153
|
+
// Use configured directories or fallback to defaults
|
|
154
|
+
const searchDirs = config.includeDirs || [config.srcDir || (
|
|
155
|
+
existsSync(join(process.cwd(), 'src')) ? 'src' :
|
|
156
|
+
existsSync(join(process.cwd(), 'app')) ? 'app' :
|
|
157
|
+
existsSync(join(process.cwd(), 'lib')) ? 'lib' : null
|
|
158
|
+
)].filter(Boolean);
|
|
159
|
+
|
|
160
|
+
return sections.map(section => {
|
|
161
|
+
const { keywords } = section;
|
|
162
|
+
const matches = [];
|
|
163
|
+
let matchCount = 0;
|
|
164
|
+
|
|
165
|
+
if (searchDirs.length > 0 && keywords.length > 0) {
|
|
166
|
+
// Search codebase for keywords
|
|
167
|
+
for (const keyword of keywords.slice(0, 5)) {
|
|
168
|
+
for (const dir of searchDirs) {
|
|
169
|
+
try {
|
|
170
|
+
const result = execSync(`grep -ril "${keyword}" ${dir} 2>/dev/null || true`, {
|
|
171
|
+
encoding: 'utf-8',
|
|
172
|
+
maxBuffer: 1024 * 1024
|
|
173
|
+
}).trim();
|
|
174
|
+
|
|
175
|
+
if (result) {
|
|
176
|
+
const files = result.split('\n').filter(Boolean);
|
|
177
|
+
if (files.length > 0) {
|
|
178
|
+
matchCount++;
|
|
179
|
+
matches.push(...files.slice(0, 2).map(f => f.replace(process.cwd() + '/', '')));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch (e) {
|
|
183
|
+
// grep failed, continue
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Also check for matching file names
|
|
190
|
+
for (const dir of searchDirs) {
|
|
191
|
+
if (!existsSync(join(process.cwd(), dir))) continue;
|
|
192
|
+
try {
|
|
193
|
+
const files = readdirSync(join(process.cwd(), dir), { recursive: true });
|
|
194
|
+
const titleWords = section.title.toLowerCase().split(/\s+/).filter(w => w.length > 3);
|
|
195
|
+
titleWords.forEach(word => {
|
|
196
|
+
const matchingFiles = files.filter(f => f.toLowerCase().includes(word));
|
|
197
|
+
if (matchingFiles.length > 0) {
|
|
198
|
+
matchCount++;
|
|
199
|
+
matches.push(...matchingFiles.slice(0, 2).map(f => `${dir}/${f}`));
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
} catch (e) {
|
|
203
|
+
// Error reading dir
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Determine status based on matches
|
|
208
|
+
const uniqueMatches = [...new Set(matches)];
|
|
209
|
+
let status;
|
|
210
|
+
if (matchCount >= 2 || uniqueMatches.length >= 2) {
|
|
211
|
+
status = STATUS.DONE;
|
|
212
|
+
} else if (matchCount > 0 || uniqueMatches.length > 0) {
|
|
213
|
+
status = STATUS.PARTIAL;
|
|
214
|
+
} else {
|
|
215
|
+
status = STATUS.MISSING;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
title: section.title,
|
|
220
|
+
status,
|
|
221
|
+
matches: uniqueMatches.slice(0, 3)
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function searchInCode(keyword, dir) {
|
|
227
|
+
try {
|
|
228
|
+
const files = readdirSync(join(process.cwd(), dir), { recursive: true });
|
|
229
|
+
return files.some(f => f.toLowerCase().includes(keyword));
|
|
230
|
+
} catch (e) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ultra-dex doctor & config commands
|
|
3
|
+
* Diagnose setup issues and manage configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import { checkConfiguredProviders } from '../providers/index.js';
|
|
12
|
+
import { icons, header, statusLine } from '../utils/status.js';
|
|
13
|
+
import { createSpinner } from '../utils/spinners.js';
|
|
14
|
+
|
|
15
|
+
// Default configuration
|
|
16
|
+
const DEFAULT_CONFIG = {
|
|
17
|
+
version: '2.4.0',
|
|
18
|
+
provider: 'claude',
|
|
19
|
+
model: null, // Use provider default
|
|
20
|
+
minScore: 70,
|
|
21
|
+
autoWatch: false,
|
|
22
|
+
mcpPort: 3001,
|
|
23
|
+
hooks: {
|
|
24
|
+
preCommit: true,
|
|
25
|
+
prePush: false,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
async function loadConfig() {
|
|
30
|
+
// Check project-level config first
|
|
31
|
+
try {
|
|
32
|
+
const content = await fs.readFile(path.resolve(process.cwd(), '.ultra-dex.json'), 'utf8');
|
|
33
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(content), source: 'project' };
|
|
34
|
+
} catch { /* no project config */ }
|
|
35
|
+
|
|
36
|
+
// Check home directory config
|
|
37
|
+
try {
|
|
38
|
+
const homePath = path.join(process.env.HOME || process.env.USERPROFILE, '.ultra-dex.json');
|
|
39
|
+
const content = await fs.readFile(homePath, 'utf8');
|
|
40
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(content), source: 'global' };
|
|
41
|
+
} catch { /* no global config */ }
|
|
42
|
+
|
|
43
|
+
return { ...DEFAULT_CONFIG, source: 'default' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function saveConfig(config, global = false) {
|
|
47
|
+
const configPath = global
|
|
48
|
+
? path.join(process.env.HOME || process.env.USERPROFILE, '.ultra-dex.json')
|
|
49
|
+
: path.resolve(process.cwd(), '.ultra-dex.json');
|
|
50
|
+
|
|
51
|
+
const { source, ...configData } = config;
|
|
52
|
+
await fs.writeFile(configPath, JSON.stringify(configData, null, 2));
|
|
53
|
+
return configPath;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function registerDoctorCommand(program) {
|
|
57
|
+
program
|
|
58
|
+
.command('doctor')
|
|
59
|
+
.description('System Diagnostics - Check System Health')
|
|
60
|
+
.option('--fix', 'Attempt to fix issues automatically')
|
|
61
|
+
.action(async (options) => {
|
|
62
|
+
header('System Health Diagnostics');
|
|
63
|
+
console.log(chalk.gray(' Analyzing system components...\n'));
|
|
64
|
+
|
|
65
|
+
const checks = [];
|
|
66
|
+
let hasErrors = false;
|
|
67
|
+
|
|
68
|
+
// Check 1: Node.js version
|
|
69
|
+
const nodeSpinner = createSpinner('Scanning Node.js environment...');
|
|
70
|
+
nodeSpinner.start();
|
|
71
|
+
try {
|
|
72
|
+
const nodeVersion = process.version;
|
|
73
|
+
const major = parseInt(nodeVersion.slice(1).split('.')[0]);
|
|
74
|
+
if (major >= 18) {
|
|
75
|
+
nodeSpinner.succeed(`Node.js ${nodeVersion} ā`);
|
|
76
|
+
checks.push({ name: 'Node.js', status: 'ok', detail: nodeVersion });
|
|
77
|
+
} else {
|
|
78
|
+
nodeSpinner.warn(`Node.js ${nodeVersion} (recommend >= 18)`);
|
|
79
|
+
checks.push({ name: 'Node.js', status: 'warn', detail: `${nodeVersion} - upgrade recommended` });
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {
|
|
82
|
+
nodeSpinner.fail('Node.js check failed');
|
|
83
|
+
checks.push({ name: 'Node.js', status: 'error', detail: e.message });
|
|
84
|
+
hasErrors = true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check 2: Git
|
|
88
|
+
const gitSpinner = createSpinner('Checking Git repository...');
|
|
89
|
+
gitSpinner.start();
|
|
90
|
+
try {
|
|
91
|
+
const gitVersion = execSync('git --version', { encoding: 'utf8' }).trim();
|
|
92
|
+
gitSpinner.succeed(`${gitVersion} ā`);
|
|
93
|
+
checks.push({ name: 'Git', status: 'ok', detail: gitVersion });
|
|
94
|
+
} catch {
|
|
95
|
+
gitSpinner.fail('Git not found');
|
|
96
|
+
checks.push({ name: 'Git', status: 'error', detail: 'Not installed' });
|
|
97
|
+
hasErrors = true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check 3: AI Providers
|
|
101
|
+
const providerSpinner = createSpinner('Locating AI Providers...');
|
|
102
|
+
providerSpinner.start();
|
|
103
|
+
const providers = checkConfiguredProviders();
|
|
104
|
+
const configuredProviders = providers.filter(p => p.configured);
|
|
105
|
+
|
|
106
|
+
if (configuredProviders.length > 0) {
|
|
107
|
+
providerSpinner.succeed(`Providers found: ${configuredProviders.map(p => p.name).join(', ')} ā`);
|
|
108
|
+
checks.push({ name: 'AI Providers', status: 'ok', detail: configuredProviders.map(p => p.name).join(', ') });
|
|
109
|
+
} else {
|
|
110
|
+
providerSpinner.warn('No AI Providers found');
|
|
111
|
+
checks.push({ name: 'AI Providers', status: 'warn', detail: 'Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY' });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check 4: Project structure
|
|
115
|
+
const structureSpinner = createSpinner('Verifying Project Structure...');
|
|
116
|
+
structureSpinner.start();
|
|
117
|
+
const requiredFiles = ['CONTEXT.md', 'IMPLEMENTATION-PLAN.md'];
|
|
118
|
+
const optionalFiles = ['CHECKLIST.md', 'QUICK-START.md', '.ultra/state.json'];
|
|
119
|
+
const foundRequired = [];
|
|
120
|
+
const foundOptional = [];
|
|
121
|
+
|
|
122
|
+
for (const file of requiredFiles) {
|
|
123
|
+
try {
|
|
124
|
+
await fs.access(path.resolve(process.cwd(), file));
|
|
125
|
+
foundRequired.push(file);
|
|
126
|
+
} catch { /* not found */ }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const file of optionalFiles) {
|
|
130
|
+
try {
|
|
131
|
+
await fs.access(path.resolve(process.cwd(), file));
|
|
132
|
+
foundOptional.push(file);
|
|
133
|
+
} catch { /* not found */ }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (foundRequired.length === requiredFiles.length) {
|
|
137
|
+
structureSpinner.succeed(`Structure valid: ${foundRequired.length}/${requiredFiles.length} required artifacts ā`);
|
|
138
|
+
checks.push({ name: 'Project Structure', status: 'ok', detail: `${foundRequired.join(', ')}` });
|
|
139
|
+
} else if (foundRequired.length > 0) {
|
|
140
|
+
structureSpinner.warn(`Structure incomplete: ${foundRequired.length}/${requiredFiles.length} required artifacts`);
|
|
141
|
+
checks.push({ name: 'Project Structure', status: 'warn', detail: `Missing: ${requiredFiles.filter(f => !foundRequired.includes(f)).join(', ')}` });
|
|
142
|
+
} else {
|
|
143
|
+
structureSpinner.info('No Ultra-Dex project found');
|
|
144
|
+
checks.push({ name: 'Project Structure', status: 'info', detail: 'Run `ultra-dex init` to create a new project' });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check 5: Git hooks
|
|
148
|
+
const hooksSpinner = createSpinner('Checking Git hooks...');
|
|
149
|
+
hooksSpinner.start();
|
|
150
|
+
try {
|
|
151
|
+
const hookPath = path.resolve(process.cwd(), '.git/hooks/pre-commit');
|
|
152
|
+
const hookContent = await fs.readFile(hookPath, 'utf8');
|
|
153
|
+
if (hookContent.includes('ultra-dex')) {
|
|
154
|
+
hooksSpinner.succeed('Pre-commit active ā');
|
|
155
|
+
checks.push({ name: 'Git Hooks', status: 'ok', detail: 'Pre-commit active' });
|
|
156
|
+
} else {
|
|
157
|
+
hooksSpinner.info('Pre-commit active but not Ultra-Dex');
|
|
158
|
+
checks.push({ name: 'Git Hooks', status: 'info', detail: 'Custom hook present' });
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
hooksSpinner.info('No pre-commit hook');
|
|
162
|
+
checks.push({ name: 'Git Hooks', status: 'info', detail: 'Run `ultra-dex pre-commit --install`' });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check 6: Configuration
|
|
166
|
+
const configSpinner = createSpinner('Reading Configuration...');
|
|
167
|
+
configSpinner.start();
|
|
168
|
+
const config = await loadConfig();
|
|
169
|
+
configSpinner.succeed(`Configuration loaded from: ${config.source}`);
|
|
170
|
+
checks.push({ name: 'Configuration', status: 'ok', detail: `Source: ${config.source}` });
|
|
171
|
+
|
|
172
|
+
// Check 7: MCP Server port
|
|
173
|
+
const portSpinner = createSpinner('Checking MCP Port...');
|
|
174
|
+
portSpinner.start();
|
|
175
|
+
try {
|
|
176
|
+
const net = await import('net');
|
|
177
|
+
const server = net.createServer();
|
|
178
|
+
await new Promise((resolve, reject) => {
|
|
179
|
+
server.once('error', reject);
|
|
180
|
+
server.once('listening', () => {
|
|
181
|
+
server.close();
|
|
182
|
+
resolve();
|
|
183
|
+
});
|
|
184
|
+
server.listen(config.mcpPort);
|
|
185
|
+
});
|
|
186
|
+
portSpinner.succeed(`Port ${config.mcpPort} open ā`);
|
|
187
|
+
checks.push({ name: 'MCP Port', status: 'ok', detail: `Port ${config.mcpPort} free` });
|
|
188
|
+
} catch {
|
|
189
|
+
portSpinner.warn(`Portal ${config.mcpPort} blocked`);
|
|
190
|
+
checks.push({ name: 'MCP Port', status: 'warn', detail: `Port ${config.mcpPort} busy - change with config` });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Summary
|
|
194
|
+
header('Diagnostics Report');
|
|
195
|
+
|
|
196
|
+
const okCount = checks.filter(c => c.status === 'ok').length;
|
|
197
|
+
const warnCount = checks.filter(c => c.status === 'warn').length;
|
|
198
|
+
const errorCount = checks.filter(c => c.status === 'error').length;
|
|
199
|
+
|
|
200
|
+
checks.forEach(check => {
|
|
201
|
+
let icon;
|
|
202
|
+
if (check.status === 'ok') icon = icons.success;
|
|
203
|
+
else if (check.status === 'warn') icon = icons.warning;
|
|
204
|
+
else if (check.status === 'error') icon = icons.error;
|
|
205
|
+
else icon = icons.info;
|
|
206
|
+
|
|
207
|
+
statusLine(icon, `${check.name.padEnd(18)} ${chalk.gray(check.detail)}`);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
console.log(chalk.gray(' ' + 'ā'.repeat(50)));
|
|
211
|
+
console.log(` ${chalk.green(okCount + ' passed')} ${chalk.yellow(warnCount + ' warnings')} ${chalk.red(errorCount + ' errors')}`);
|
|
212
|
+
|
|
213
|
+
if (hasErrors) {
|
|
214
|
+
console.log(chalk.red('\nā System check failed. Fix issues above.\n'));
|
|
215
|
+
process.exit(1);
|
|
216
|
+
} else if (warnCount > 0) {
|
|
217
|
+
console.log(chalk.yellow('\nā ļø System operational but has warnings.\n'));
|
|
218
|
+
} else {
|
|
219
|
+
console.log(chalk.green('\nā
All systems operational.\n'));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Suggestions
|
|
223
|
+
if (configuredProviders.length === 0) {
|
|
224
|
+
console.log(chalk.cyan('š” To configure AI providers, set an API key:'));
|
|
225
|
+
console.log(chalk.gray(' export ANTHROPIC_API_KEY=sk-ant-...'));
|
|
226
|
+
console.log(chalk.gray(' export OPENAI_API_KEY=sk-...'));
|
|
227
|
+
console.log(chalk.gray(' export GEMINI_API_KEY=...\n'));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (foundRequired.length === 0) {
|
|
231
|
+
console.log(chalk.cyan('š” To initialize a new project:'));
|
|
232
|
+
console.log(chalk.gray(' ultra-dex init\n'));
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function registerConfigCommand(program) {
|
|
238
|
+
program
|
|
239
|
+
.command('config')
|
|
240
|
+
.description('Manage Ultra-Dex configuration')
|
|
241
|
+
.option('--get <key>', 'Get a config value')
|
|
242
|
+
.option('--set <key=value>', 'Set a config value')
|
|
243
|
+
.option('--list', 'List all config values')
|
|
244
|
+
.option('--global', 'Use global config (~/.ultra-dex.json)')
|
|
245
|
+
.option('--init', 'Create a new config file')
|
|
246
|
+
.option('--mcp', 'Generate MCP config for Claude Desktop')
|
|
247
|
+
.action(async (options) => {
|
|
248
|
+
const config = await loadConfig();
|
|
249
|
+
|
|
250
|
+
if (options.mcp) {
|
|
251
|
+
// Generate MCP config for Claude Desktop
|
|
252
|
+
console.log(chalk.cyan('\nš MCP Configuration for Claude Desktop\n'));
|
|
253
|
+
|
|
254
|
+
const mcpConfig = {
|
|
255
|
+
"ultra-dex": {
|
|
256
|
+
"command": "npx",
|
|
257
|
+
"args": ["ultra-dex", "serve", "--port", String(config.mcpPort)],
|
|
258
|
+
"env": {}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
console.log(chalk.white('Add this to your Claude Desktop config:\n'));
|
|
263
|
+
console.log(chalk.gray('ā'.repeat(50)));
|
|
264
|
+
console.log(JSON.stringify({ mcpServers: mcpConfig }, null, 2));
|
|
265
|
+
console.log(chalk.gray('ā'.repeat(50)));
|
|
266
|
+
|
|
267
|
+
console.log(chalk.cyan('\nš Config file locations:'));
|
|
268
|
+
console.log(chalk.gray(' macOS: ~/Library/Application Support/Claude/claude_desktop_config.json'));
|
|
269
|
+
console.log(chalk.gray(' Windows: %APPDATA%\Claude\claude_desktop_config.json'));
|
|
270
|
+
console.log(chalk.gray(' Linux: ~/.config/Claude/claude_desktop_config.json\n'));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (options.init) {
|
|
275
|
+
const configPath = await saveConfig(DEFAULT_CONFIG, options.global);
|
|
276
|
+
console.log(chalk.green(`ā
Config created: ${configPath}`));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (options.list) {
|
|
281
|
+
console.log(chalk.cyan('\nāļø Ultra-Dex Configuration\n'));
|
|
282
|
+
console.log(chalk.gray(`Source: ${config.source}`));
|
|
283
|
+
console.log(chalk.gray('ā'.repeat(40)));
|
|
284
|
+
|
|
285
|
+
const { source, ...displayConfig } = config;
|
|
286
|
+
Object.entries(displayConfig).forEach(([key, value]) => {
|
|
287
|
+
const valueStr = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
288
|
+
console.log(` ${chalk.cyan(key.padEnd(15))} ${valueStr}`);
|
|
289
|
+
});
|
|
290
|
+
console.log('');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (options.get) {
|
|
295
|
+
const value = config[options.get];
|
|
296
|
+
if (value !== undefined) {
|
|
297
|
+
console.log(typeof value === 'object' ? JSON.stringify(value, null, 2) : value);
|
|
298
|
+
} else {
|
|
299
|
+
console.log(chalk.yellow(`Key not found: ${options.get}`));
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (options.set) {
|
|
305
|
+
const [key, ...valueParts] = options.set.split('=');
|
|
306
|
+
const value = valueParts.join('=');
|
|
307
|
+
|
|
308
|
+
// Parse value
|
|
309
|
+
let parsedValue;
|
|
310
|
+
try {
|
|
311
|
+
parsedValue = JSON.parse(value);
|
|
312
|
+
} catch {
|
|
313
|
+
parsedValue = value;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
config[key] = parsedValue;
|
|
317
|
+
const configPath = await saveConfig(config, options.global);
|
|
318
|
+
console.log(chalk.green(`ā
Set ${key}=${value} in ${configPath}`));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Interactive mode
|
|
323
|
+
console.log(chalk.cyan('\nāļø Ultra-Dex Configuration\n'));
|
|
324
|
+
|
|
325
|
+
const { action } = await inquirer.prompt([
|
|
326
|
+
{
|
|
327
|
+
type: 'list',
|
|
328
|
+
name: 'action',
|
|
329
|
+
message: 'What would you like to do?',
|
|
330
|
+
choices: [
|
|
331
|
+
{ name: 'View current config', value: 'view' },
|
|
332
|
+
{ name: 'Set default AI provider', value: 'provider' },
|
|
333
|
+
{ name: 'Set minimum alignment score', value: 'minScore' },
|
|
334
|
+
{ name: 'Set MCP server port', value: 'mcpPort' },
|
|
335
|
+
{ name: 'Generate MCP config for Claude', value: 'mcp' },
|
|
336
|
+
{ name: 'Create new config file', value: 'init' },
|
|
337
|
+
]
|
|
338
|
+
}
|
|
339
|
+
]);
|
|
340
|
+
|
|
341
|
+
switch (action) {
|
|
342
|
+
case 'view':
|
|
343
|
+
console.log(chalk.gray('\n' + JSON.stringify(config, null, 2) + '\n'));
|
|
344
|
+
break;
|
|
345
|
+
|
|
346
|
+
case 'provider':
|
|
347
|
+
const { provider } = await inquirer.prompt([
|
|
348
|
+
{
|
|
349
|
+
type: 'list',
|
|
350
|
+
name: 'provider',
|
|
351
|
+
message: 'Select default AI provider:',
|
|
352
|
+
choices: ['claude', 'openai', 'gemini'],
|
|
353
|
+
default: config.provider
|
|
354
|
+
}
|
|
355
|
+
]);
|
|
356
|
+
config.provider = provider;
|
|
357
|
+
await saveConfig(config, options.global);
|
|
358
|
+
console.log(chalk.green(`\nā
Default provider set to: ${provider}\n`));
|
|
359
|
+
break;
|
|
360
|
+
|
|
361
|
+
case 'minScore':
|
|
362
|
+
const { minScore } = await inquirer.prompt([
|
|
363
|
+
{
|
|
364
|
+
type: 'number',
|
|
365
|
+
name: 'minScore',
|
|
366
|
+
message: 'Minimum alignment score (0-100):',
|
|
367
|
+
default: config.minScore,
|
|
368
|
+
validate: n => n >= 0 && n <= 100 || 'Must be 0-100'
|
|
369
|
+
}
|
|
370
|
+
]);
|
|
371
|
+
config.minScore = minScore;
|
|
372
|
+
await saveConfig(config, options.global);
|
|
373
|
+
console.log(chalk.green(`\nā
Minimum score set to: ${minScore}\n`));
|
|
374
|
+
break;
|
|
375
|
+
|
|
376
|
+
case 'mcpPort':
|
|
377
|
+
const { mcpPort } = await inquirer.prompt([
|
|
378
|
+
{
|
|
379
|
+
type: 'number',
|
|
380
|
+
name: 'mcpPort',
|
|
381
|
+
message: 'MCP server port:',
|
|
382
|
+
default: config.mcpPort,
|
|
383
|
+
validate: n => n > 0 && n < 65536 || 'Invalid port'
|
|
384
|
+
}
|
|
385
|
+
]);
|
|
386
|
+
config.mcpPort = mcpPort;
|
|
387
|
+
await saveConfig(config, options.global);
|
|
388
|
+
console.log(chalk.green(`\nā
MCP port set to: ${mcpPort}\n`));
|
|
389
|
+
break;
|
|
390
|
+
|
|
391
|
+
case 'mcp':
|
|
392
|
+
// Call the MCP generation
|
|
393
|
+
options.mcp = true;
|
|
394
|
+
await program.commands.find(c => c.name() === 'config').action(options);
|
|
395
|
+
break;
|
|
396
|
+
|
|
397
|
+
case 'init':
|
|
398
|
+
const { scope } = await inquirer.prompt([
|
|
399
|
+
{
|
|
400
|
+
type: 'list',
|
|
401
|
+
name: 'scope',
|
|
402
|
+
message: 'Create config in:',
|
|
403
|
+
choices: [
|
|
404
|
+
{ name: 'This project (.ultra-dex.json)', value: 'project' },
|
|
405
|
+
{ name: 'Global (~/.ultra-dex.json)', value: 'global' },
|
|
406
|
+
]
|
|
407
|
+
}
|
|
408
|
+
]);
|
|
409
|
+
const configPath = await saveConfig(DEFAULT_CONFIG, scope === 'global');
|
|
410
|
+
console.log(chalk.green(`\nā
Config created: ${configPath}\n`));
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export default { registerDoctorCommand, registerConfigCommand };
|