ultra-dex 2.2.0 ā 3.1.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 +84 -122
- package/assets/agents/0-orchestration/orchestrator.md +2 -2
- package/assets/agents/00-AGENT_INDEX.md +1 -1
- package/assets/docs/LAUNCH-POSTS.md +1 -1
- package/assets/docs/QUICK-REFERENCE.md +12 -7
- package/assets/docs/ROADMAP.md +5 -5
- package/assets/docs/VISION-V2.md +1 -1
- package/assets/docs/WORKFLOW-DIAGRAMS.md +1 -1
- package/assets/hooks/pre-commit +98 -0
- package/assets/saas-plan/04-Imp-Template.md +1 -1
- package/assets/templates/README.md +1 -1
- package/bin/ultra-dex.js +93 -2096
- package/lib/commands/advanced.js +471 -0
- package/lib/commands/agent-builder.js +226 -0
- package/lib/commands/agents.js +101 -47
- package/lib/commands/auto-implement.js +68 -0
- package/lib/commands/build.js +73 -187
- 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 +397 -0
- package/lib/commands/export.js +408 -0
- package/lib/commands/fix.js +96 -0
- package/lib/commands/generate.js +96 -72
- package/lib/commands/hooks.js +251 -76
- package/lib/commands/init.js +56 -6
- 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/serve.js +188 -40
- package/lib/commands/state.js +354 -0
- package/lib/commands/swarm.js +284 -0
- package/lib/commands/sync.js +94 -0
- 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/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/context.js +2 -2
- package/lib/templates/custom-agent.md +10 -0
- package/lib/utils/fallback.js +4 -2
- package/lib/utils/files.js +7 -34
- package/lib/utils/graph.js +108 -0
- package/lib/utils/sync.js +216 -0
- package/package.json +22 -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,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ultra-dex doctor & config commands
|
|
3
|
+
* Diagnose setup issues and manage configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import inquirer from 'inquirer';
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
import { checkConfiguredProviders } from '../providers/index.js';
|
|
13
|
+
|
|
14
|
+
// Default configuration
|
|
15
|
+
const DEFAULT_CONFIG = {
|
|
16
|
+
version: '2.4.0',
|
|
17
|
+
provider: 'claude',
|
|
18
|
+
model: null, // Use provider default
|
|
19
|
+
minScore: 70,
|
|
20
|
+
autoWatch: false,
|
|
21
|
+
mcpPort: 3001,
|
|
22
|
+
hooks: {
|
|
23
|
+
preCommit: true,
|
|
24
|
+
prePush: false,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
async function loadConfig() {
|
|
29
|
+
// Check project-level config first
|
|
30
|
+
try {
|
|
31
|
+
const content = await fs.readFile(path.resolve(process.cwd(), '.ultra-dex.json'), 'utf8');
|
|
32
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(content), source: 'project' };
|
|
33
|
+
} catch { /* no project config */ }
|
|
34
|
+
|
|
35
|
+
// Check home directory config
|
|
36
|
+
try {
|
|
37
|
+
const homePath = path.join(process.env.HOME || process.env.USERPROFILE, '.ultra-dex.json');
|
|
38
|
+
const content = await fs.readFile(homePath, 'utf8');
|
|
39
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(content), source: 'global' };
|
|
40
|
+
} catch { /* no global config */ }
|
|
41
|
+
|
|
42
|
+
return { ...DEFAULT_CONFIG, source: 'default' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function saveConfig(config, global = false) {
|
|
46
|
+
const configPath = global
|
|
47
|
+
? path.join(process.env.HOME || process.env.USERPROFILE, '.ultra-dex.json')
|
|
48
|
+
: path.resolve(process.cwd(), '.ultra-dex.json');
|
|
49
|
+
|
|
50
|
+
const { source, ...configData } = config;
|
|
51
|
+
await fs.writeFile(configPath, JSON.stringify(configData, null, 2));
|
|
52
|
+
return configPath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function registerDoctorCommand(program) {
|
|
56
|
+
program
|
|
57
|
+
.command('doctor')
|
|
58
|
+
.description('Diagnose Ultra-Dex setup and configuration')
|
|
59
|
+
.option('--fix', 'Attempt to fix issues automatically')
|
|
60
|
+
.action(async (options) => {
|
|
61
|
+
console.log(chalk.cyan('\n𩺠Ultra-Dex Doctor\n'));
|
|
62
|
+
console.log(chalk.gray('Checking your setup...\n'));
|
|
63
|
+
|
|
64
|
+
const checks = [];
|
|
65
|
+
let hasErrors = false;
|
|
66
|
+
|
|
67
|
+
// Check 1: Node.js version
|
|
68
|
+
const nodeSpinner = ora('Checking Node.js version...').start();
|
|
69
|
+
try {
|
|
70
|
+
const nodeVersion = process.version;
|
|
71
|
+
const major = parseInt(nodeVersion.slice(1).split('.')[0]);
|
|
72
|
+
if (major >= 18) {
|
|
73
|
+
nodeSpinner.succeed(`Node.js ${nodeVersion} ā`);
|
|
74
|
+
checks.push({ name: 'Node.js', status: 'ok', detail: nodeVersion });
|
|
75
|
+
} else {
|
|
76
|
+
nodeSpinner.warn(`Node.js ${nodeVersion} (recommend >= 18)`);
|
|
77
|
+
checks.push({ name: 'Node.js', status: 'warn', detail: `${nodeVersion} - upgrade recommended` });
|
|
78
|
+
}
|
|
79
|
+
} catch (e) {
|
|
80
|
+
nodeSpinner.fail('Node.js check failed');
|
|
81
|
+
checks.push({ name: 'Node.js', status: 'error', detail: e.message });
|
|
82
|
+
hasErrors = true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check 2: Git
|
|
86
|
+
const gitSpinner = ora('Checking Git...').start();
|
|
87
|
+
try {
|
|
88
|
+
const gitVersion = execSync('git --version', { encoding: 'utf8' }).trim();
|
|
89
|
+
gitSpinner.succeed(`${gitVersion} ā`);
|
|
90
|
+
checks.push({ name: 'Git', status: 'ok', detail: gitVersion });
|
|
91
|
+
} catch {
|
|
92
|
+
gitSpinner.fail('Git not found');
|
|
93
|
+
checks.push({ name: 'Git', status: 'error', detail: 'Not installed' });
|
|
94
|
+
hasErrors = true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check 3: AI Providers
|
|
98
|
+
const providerSpinner = ora('Checking AI providers...').start();
|
|
99
|
+
const providers = checkConfiguredProviders();
|
|
100
|
+
const configuredProviders = providers.filter(p => p.configured);
|
|
101
|
+
|
|
102
|
+
if (configuredProviders.length > 0) {
|
|
103
|
+
providerSpinner.succeed(`AI providers: ${configuredProviders.map(p => p.name).join(', ')} ā`);
|
|
104
|
+
checks.push({ name: 'AI Providers', status: 'ok', detail: configuredProviders.map(p => p.name).join(', ') });
|
|
105
|
+
} else {
|
|
106
|
+
providerSpinner.warn('No AI providers configured');
|
|
107
|
+
checks.push({ name: 'AI Providers', status: 'warn', detail: 'Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check 4: Project structure
|
|
111
|
+
const structureSpinner = ora('Checking project structure...').start();
|
|
112
|
+
const requiredFiles = ['CONTEXT.md', 'IMPLEMENTATION-PLAN.md'];
|
|
113
|
+
const optionalFiles = ['CHECKLIST.md', 'QUICK-START.md', '.ultra/state.json'];
|
|
114
|
+
const foundRequired = [];
|
|
115
|
+
const foundOptional = [];
|
|
116
|
+
|
|
117
|
+
for (const file of requiredFiles) {
|
|
118
|
+
try {
|
|
119
|
+
await fs.access(path.resolve(process.cwd(), file));
|
|
120
|
+
foundRequired.push(file);
|
|
121
|
+
} catch { /* not found */ }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const file of optionalFiles) {
|
|
125
|
+
try {
|
|
126
|
+
await fs.access(path.resolve(process.cwd(), file));
|
|
127
|
+
foundOptional.push(file);
|
|
128
|
+
} catch { /* not found */ }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (foundRequired.length === requiredFiles.length) {
|
|
132
|
+
structureSpinner.succeed(`Project structure: ${foundRequired.length}/${requiredFiles.length} required files ā`);
|
|
133
|
+
checks.push({ name: 'Project Structure', status: 'ok', detail: `${foundRequired.join(', ')}` });
|
|
134
|
+
} else if (foundRequired.length > 0) {
|
|
135
|
+
structureSpinner.warn(`Project structure: ${foundRequired.length}/${requiredFiles.length} required files`);
|
|
136
|
+
checks.push({ name: 'Project Structure', status: 'warn', detail: `Missing: ${requiredFiles.filter(f => !foundRequired.includes(f)).join(', ')}` });
|
|
137
|
+
} else {
|
|
138
|
+
structureSpinner.info('No Ultra-Dex project found');
|
|
139
|
+
checks.push({ name: 'Project Structure', status: 'info', detail: 'Run `ultra-dex init` to create a project' });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check 5: Git hooks
|
|
143
|
+
const hooksSpinner = ora('Checking git hooks...').start();
|
|
144
|
+
try {
|
|
145
|
+
const hookPath = path.resolve(process.cwd(), '.git/hooks/pre-commit');
|
|
146
|
+
const hookContent = await fs.readFile(hookPath, 'utf8');
|
|
147
|
+
if (hookContent.includes('ultra-dex')) {
|
|
148
|
+
hooksSpinner.succeed('Pre-commit hook installed ā');
|
|
149
|
+
checks.push({ name: 'Git Hooks', status: 'ok', detail: 'Pre-commit active' });
|
|
150
|
+
} else {
|
|
151
|
+
hooksSpinner.info('Pre-commit hook exists but not Ultra-Dex');
|
|
152
|
+
checks.push({ name: 'Git Hooks', status: 'info', detail: 'Custom hook present' });
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
hooksSpinner.info('No pre-commit hook');
|
|
156
|
+
checks.push({ name: 'Git Hooks', status: 'info', detail: 'Run `ultra-dex pre-commit --install`' });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check 6: Configuration
|
|
160
|
+
const configSpinner = ora('Checking configuration...').start();
|
|
161
|
+
const config = await loadConfig();
|
|
162
|
+
configSpinner.succeed(`Config loaded from: ${config.source}`);
|
|
163
|
+
checks.push({ name: 'Configuration', status: 'ok', detail: `Source: ${config.source}` });
|
|
164
|
+
|
|
165
|
+
// Check 7: MCP Server port
|
|
166
|
+
const portSpinner = ora('Checking MCP server port...').start();
|
|
167
|
+
try {
|
|
168
|
+
const net = await import('net');
|
|
169
|
+
const server = net.createServer();
|
|
170
|
+
await new Promise((resolve, reject) => {
|
|
171
|
+
server.once('error', reject);
|
|
172
|
+
server.once('listening', () => {
|
|
173
|
+
server.close();
|
|
174
|
+
resolve();
|
|
175
|
+
});
|
|
176
|
+
server.listen(config.mcpPort);
|
|
177
|
+
});
|
|
178
|
+
portSpinner.succeed(`Port ${config.mcpPort} available ā`);
|
|
179
|
+
checks.push({ name: 'MCP Port', status: 'ok', detail: `Port ${config.mcpPort} free` });
|
|
180
|
+
} catch {
|
|
181
|
+
portSpinner.warn(`Port ${config.mcpPort} in use`);
|
|
182
|
+
checks.push({ name: 'MCP Port', status: 'warn', detail: `Port ${config.mcpPort} busy - change with config` });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Summary
|
|
186
|
+
console.log(chalk.bold('\nš Summary\n'));
|
|
187
|
+
console.log(chalk.gray('ā'.repeat(50)));
|
|
188
|
+
|
|
189
|
+
const okCount = checks.filter(c => c.status === 'ok').length;
|
|
190
|
+
const warnCount = checks.filter(c => c.status === 'warn').length;
|
|
191
|
+
const errorCount = checks.filter(c => c.status === 'error').length;
|
|
192
|
+
|
|
193
|
+
checks.forEach(check => {
|
|
194
|
+
const icon = check.status === 'ok' ? chalk.green('ā') :
|
|
195
|
+
check.status === 'warn' ? chalk.yellow('ā ') :
|
|
196
|
+
check.status === 'error' ? chalk.red('ā') :
|
|
197
|
+
chalk.blue('ā¹');
|
|
198
|
+
console.log(` ${icon} ${check.name.padEnd(18)} ${chalk.gray(check.detail)}`);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
console.log(chalk.gray('ā'.repeat(50)));
|
|
202
|
+
console.log(` ${chalk.green(okCount + ' passed')} ${chalk.yellow(warnCount + ' warnings')} ${chalk.red(errorCount + ' errors')}`);
|
|
203
|
+
|
|
204
|
+
if (hasErrors) {
|
|
205
|
+
console.log(chalk.red('\nā Some checks failed. Fix issues above.\n'));
|
|
206
|
+
process.exit(1);
|
|
207
|
+
} else if (warnCount > 0) {
|
|
208
|
+
console.log(chalk.yellow('\nā ļø Some warnings. Setup works but could be improved.\n'));
|
|
209
|
+
} else {
|
|
210
|
+
console.log(chalk.green('\nā
All checks passed! Ultra-Dex is ready.\n'));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Suggestions
|
|
214
|
+
if (configuredProviders.length === 0) {
|
|
215
|
+
console.log(chalk.cyan('š” To enable AI features, set an API key:'));
|
|
216
|
+
console.log(chalk.gray(' export ANTHROPIC_API_KEY=sk-ant-...'));
|
|
217
|
+
console.log(chalk.gray(' export OPENAI_API_KEY=sk-...'));
|
|
218
|
+
console.log(chalk.gray(' export GEMINI_API_KEY=...\n'));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (foundRequired.length === 0) {
|
|
222
|
+
console.log(chalk.cyan('š” To start a new project:'));
|
|
223
|
+
console.log(chalk.gray(' ultra-dex init\n'));
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function registerConfigCommand(program) {
|
|
229
|
+
program
|
|
230
|
+
.command('config')
|
|
231
|
+
.description('Manage Ultra-Dex configuration')
|
|
232
|
+
.option('--get <key>', 'Get a config value')
|
|
233
|
+
.option('--set <key=value>', 'Set a config value')
|
|
234
|
+
.option('--list', 'List all config values')
|
|
235
|
+
.option('--global', 'Use global config (~/.ultra-dex.json)')
|
|
236
|
+
.option('--init', 'Create a new config file')
|
|
237
|
+
.option('--mcp', 'Generate MCP config for Claude Desktop')
|
|
238
|
+
.action(async (options) => {
|
|
239
|
+
const config = await loadConfig();
|
|
240
|
+
|
|
241
|
+
if (options.mcp) {
|
|
242
|
+
// Generate MCP config for Claude Desktop
|
|
243
|
+
console.log(chalk.cyan('\nš MCP Configuration for Claude Desktop\n'));
|
|
244
|
+
|
|
245
|
+
const mcpConfig = {
|
|
246
|
+
"ultra-dex": {
|
|
247
|
+
"command": "npx",
|
|
248
|
+
"args": ["ultra-dex", "serve", "--port", String(config.mcpPort)],
|
|
249
|
+
"env": {}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
console.log(chalk.white('Add this to your Claude Desktop config:\n'));
|
|
254
|
+
console.log(chalk.gray('ā'.repeat(50)));
|
|
255
|
+
console.log(JSON.stringify({ mcpServers: mcpConfig }, null, 2));
|
|
256
|
+
console.log(chalk.gray('ā'.repeat(50)));
|
|
257
|
+
|
|
258
|
+
console.log(chalk.cyan('\nš Config file locations:'));
|
|
259
|
+
console.log(chalk.gray(' macOS: ~/Library/Application Support/Claude/claude_desktop_config.json'));
|
|
260
|
+
console.log(chalk.gray(' Windows: %APPDATA%\\Claude\\claude_desktop_config.json'));
|
|
261
|
+
console.log(chalk.gray(' Linux: ~/.config/Claude/claude_desktop_config.json\n'));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (options.init) {
|
|
266
|
+
const configPath = await saveConfig(DEFAULT_CONFIG, options.global);
|
|
267
|
+
console.log(chalk.green(`ā
Config created: ${configPath}`));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (options.list) {
|
|
272
|
+
console.log(chalk.cyan('\nāļø Ultra-Dex Configuration\n'));
|
|
273
|
+
console.log(chalk.gray(`Source: ${config.source}`));
|
|
274
|
+
console.log(chalk.gray('ā'.repeat(40)));
|
|
275
|
+
|
|
276
|
+
const { source, ...displayConfig } = config;
|
|
277
|
+
Object.entries(displayConfig).forEach(([key, value]) => {
|
|
278
|
+
const valueStr = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
279
|
+
console.log(` ${chalk.cyan(key.padEnd(15))} ${valueStr}`);
|
|
280
|
+
});
|
|
281
|
+
console.log('');
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (options.get) {
|
|
286
|
+
const value = config[options.get];
|
|
287
|
+
if (value !== undefined) {
|
|
288
|
+
console.log(typeof value === 'object' ? JSON.stringify(value, null, 2) : value);
|
|
289
|
+
} else {
|
|
290
|
+
console.log(chalk.yellow(`Key not found: ${options.get}`));
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (options.set) {
|
|
296
|
+
const [key, ...valueParts] = options.set.split('=');
|
|
297
|
+
const value = valueParts.join('=');
|
|
298
|
+
|
|
299
|
+
// Parse value
|
|
300
|
+
let parsedValue;
|
|
301
|
+
try {
|
|
302
|
+
parsedValue = JSON.parse(value);
|
|
303
|
+
} catch {
|
|
304
|
+
parsedValue = value;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
config[key] = parsedValue;
|
|
308
|
+
const configPath = await saveConfig(config, options.global);
|
|
309
|
+
console.log(chalk.green(`ā
Set ${key}=${value} in ${configPath}`));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Interactive mode
|
|
314
|
+
console.log(chalk.cyan('\nāļø Ultra-Dex Configuration\n'));
|
|
315
|
+
|
|
316
|
+
const { action } = await inquirer.prompt([{
|
|
317
|
+
type: 'list',
|
|
318
|
+
name: 'action',
|
|
319
|
+
message: 'What would you like to do?',
|
|
320
|
+
choices: [
|
|
321
|
+
{ name: 'View current config', value: 'view' },
|
|
322
|
+
{ name: 'Set default AI provider', value: 'provider' },
|
|
323
|
+
{ name: 'Set minimum alignment score', value: 'minScore' },
|
|
324
|
+
{ name: 'Set MCP server port', value: 'mcpPort' },
|
|
325
|
+
{ name: 'Generate MCP config for Claude', value: 'mcp' },
|
|
326
|
+
{ name: 'Create new config file', value: 'init' },
|
|
327
|
+
]
|
|
328
|
+
}]);
|
|
329
|
+
|
|
330
|
+
switch (action) {
|
|
331
|
+
case 'view':
|
|
332
|
+
console.log(chalk.gray('\n' + JSON.stringify(config, null, 2) + '\n'));
|
|
333
|
+
break;
|
|
334
|
+
|
|
335
|
+
case 'provider':
|
|
336
|
+
const { provider } = await inquirer.prompt([{
|
|
337
|
+
type: 'list',
|
|
338
|
+
name: 'provider',
|
|
339
|
+
message: 'Select default AI provider:',
|
|
340
|
+
choices: ['claude', 'openai', 'gemini'],
|
|
341
|
+
default: config.provider
|
|
342
|
+
}]);
|
|
343
|
+
config.provider = provider;
|
|
344
|
+
await saveConfig(config, options.global);
|
|
345
|
+
console.log(chalk.green(`\nā
Default provider set to: ${provider}\n`));
|
|
346
|
+
break;
|
|
347
|
+
|
|
348
|
+
case 'minScore':
|
|
349
|
+
const { minScore } = await inquirer.prompt([{
|
|
350
|
+
type: 'number',
|
|
351
|
+
name: 'minScore',
|
|
352
|
+
message: 'Minimum alignment score (0-100):',
|
|
353
|
+
default: config.minScore,
|
|
354
|
+
validate: n => n >= 0 && n <= 100 || 'Must be 0-100'
|
|
355
|
+
}]);
|
|
356
|
+
config.minScore = minScore;
|
|
357
|
+
await saveConfig(config, options.global);
|
|
358
|
+
console.log(chalk.green(`\nā
Minimum score set to: ${minScore}\n`));
|
|
359
|
+
break;
|
|
360
|
+
|
|
361
|
+
case 'mcpPort':
|
|
362
|
+
const { mcpPort } = await inquirer.prompt([{
|
|
363
|
+
type: 'number',
|
|
364
|
+
name: 'mcpPort',
|
|
365
|
+
message: 'MCP server port:',
|
|
366
|
+
default: config.mcpPort,
|
|
367
|
+
validate: n => n > 0 && n < 65536 || 'Invalid port'
|
|
368
|
+
}]);
|
|
369
|
+
config.mcpPort = mcpPort;
|
|
370
|
+
await saveConfig(config, options.global);
|
|
371
|
+
console.log(chalk.green(`\nā
MCP port set to: ${mcpPort}\n`));
|
|
372
|
+
break;
|
|
373
|
+
|
|
374
|
+
case 'mcp':
|
|
375
|
+
// Call the MCP generation
|
|
376
|
+
options.mcp = true;
|
|
377
|
+
await program.commands.find(c => c.name() === 'config').action(options);
|
|
378
|
+
break;
|
|
379
|
+
|
|
380
|
+
case 'init':
|
|
381
|
+
const { scope } = await inquirer.prompt([{
|
|
382
|
+
type: 'list',
|
|
383
|
+
name: 'scope',
|
|
384
|
+
message: 'Create config in:',
|
|
385
|
+
choices: [
|
|
386
|
+
{ name: 'This project (.ultra-dex.json)', value: 'project' },
|
|
387
|
+
{ name: 'Global (~/.ultra-dex.json)', value: 'global' },
|
|
388
|
+
]
|
|
389
|
+
}]);
|
|
390
|
+
const configPath = await saveConfig(DEFAULT_CONFIG, scope === 'global');
|
|
391
|
+
console.log(chalk.green(`\nā
Config created: ${configPath}\n`));
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export default { registerDoctorCommand, registerConfigCommand };
|