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,284 @@
|
|
|
1
|
+
// cli/lib/commands/swarm.js
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { getProvider } from '../providers/index.js';
|
|
5
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { glob } from 'glob';
|
|
9
|
+
import { projectGraph } from '../mcp/graph.js';
|
|
10
|
+
import { updateState, loadState, saveState } from './state.js';
|
|
11
|
+
|
|
12
|
+
const AGENT_PIPELINE = [
|
|
13
|
+
{ name: 'planner', description: 'Break down task into steps', tier: '1-planning' },
|
|
14
|
+
{ name: 'cto', description: 'Define architecture', tier: '1-planning' },
|
|
15
|
+
{ name: 'auth', description: 'Security & authentication review', tier: '3-security' },
|
|
16
|
+
{ name: 'database', description: 'Design schema', tier: '2-implementation' },
|
|
17
|
+
{ name: 'backend', description: 'Implement API', tier: '2-implementation' },
|
|
18
|
+
{ name: 'frontend', description: 'Build UI', tier: '2-implementation' },
|
|
19
|
+
{ name: 'testing', description: 'Write tests', tier: '4-quality' },
|
|
20
|
+
{ name: 'reviewer', description: 'Code review', tier: '4-quality' }
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
async function runAgent(agent, task, context, previousOutput, provider) {
|
|
24
|
+
const agentPrompt = await loadAgentPrompt(agent.name);
|
|
25
|
+
const prompt = `
|
|
26
|
+
${agentPrompt}
|
|
27
|
+
|
|
28
|
+
## Context
|
|
29
|
+
${context}
|
|
30
|
+
|
|
31
|
+
## Previous Agent Output
|
|
32
|
+
${previousOutput}
|
|
33
|
+
|
|
34
|
+
## Task
|
|
35
|
+
${task}
|
|
36
|
+
|
|
37
|
+
Provide your output for the next agent in the pipeline.
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
// Standardize provider call
|
|
41
|
+
const response = provider.complete
|
|
42
|
+
? await provider.complete(prompt)
|
|
43
|
+
: await provider.generate('', prompt);
|
|
44
|
+
|
|
45
|
+
return typeof response === 'string'
|
|
46
|
+
? response
|
|
47
|
+
: (response.content || response.text || JSON.stringify(response));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function ensureLogDirectory() {
|
|
51
|
+
const logDir = join(process.cwd(), '.ultra-dex', 'swarm-logs');
|
|
52
|
+
await mkdir(logDir, { recursive: true });
|
|
53
|
+
return logDir;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function writeSwarmLog(logDir, task, results, stats) {
|
|
57
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
58
|
+
const logPath = join(logDir, `swarm-${timestamp}.json`);
|
|
59
|
+
const logData = {
|
|
60
|
+
task,
|
|
61
|
+
timestamp: new Date().toISOString(),
|
|
62
|
+
stats,
|
|
63
|
+
results
|
|
64
|
+
};
|
|
65
|
+
await writeFile(logPath, JSON.stringify(logData, null, 2));
|
|
66
|
+
return logPath;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function swarmCommand(task, options) {
|
|
70
|
+
console.log(chalk.cyan.bold('\n🐝 Ultra-Dex Swarm Mode v3.0\n'));
|
|
71
|
+
console.log(chalk.white(`Task: "${task}"\n`));
|
|
72
|
+
|
|
73
|
+
const startTime = Date.now();
|
|
74
|
+
|
|
75
|
+
if (options.dryRun) {
|
|
76
|
+
console.log(chalk.yellow('Dry run - showing pipeline:\n'));
|
|
77
|
+
AGENT_PIPELINE.forEach((agent, i) => {
|
|
78
|
+
console.log(` ${i + 1}. @${agent.name} - ${agent.description} [${agent.tier}]`);
|
|
79
|
+
});
|
|
80
|
+
if (options.parallel) {
|
|
81
|
+
console.log(chalk.blue('\nℹ️ Parallel execution enabled for 2-implementation tier'));
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Load context & Graph (God Mode)
|
|
87
|
+
const contextPath = join(process.cwd(), 'CONTEXT.md');
|
|
88
|
+
const planPath = join(process.cwd(), 'IMPLEMENTATION-PLAN.md');
|
|
89
|
+
|
|
90
|
+
let context = '';
|
|
91
|
+
if (existsSync(contextPath)) {
|
|
92
|
+
context += await readFile(contextPath, 'utf-8');
|
|
93
|
+
}
|
|
94
|
+
if (existsSync(planPath)) {
|
|
95
|
+
context += '\n\n' + await readFile(planPath, 'utf-8');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Inject Code Graph into Context
|
|
99
|
+
const spinnerGraph = ora('🧠 Scanning Codebase Graph...').start();
|
|
100
|
+
try {
|
|
101
|
+
const graphSummary = await projectGraph.scan();
|
|
102
|
+
context += `\n\n## Codebase Graph Summary\n- Total Files: ${graphSummary.nodeCount}\n- Total Dependencies: ${graphSummary.edgeCount}\n- Files Analyzed: ${graphSummary.files.join(', ')}\n`;
|
|
103
|
+
spinnerGraph.succeed('Codebase Graph integrated into context.');
|
|
104
|
+
} catch (e) {
|
|
105
|
+
spinnerGraph.warn('Codebase Graph scan failed, proceeding with limited context.');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Get AI provider
|
|
109
|
+
const provider = getProvider();
|
|
110
|
+
if (!provider) {
|
|
111
|
+
console.log(chalk.red('No AI provider configured. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_AI_KEY'));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Ensure log directory exists
|
|
116
|
+
const logDir = await ensureLogDirectory();
|
|
117
|
+
|
|
118
|
+
// Update State to indicate Swarm is running
|
|
119
|
+
const state = await loadState() || { project: { mode: 'GOD_MODE' }, agents: { active: [] } };
|
|
120
|
+
state.agents = state.agents || { active: [] };
|
|
121
|
+
state.updatedAt = new Date().toISOString();
|
|
122
|
+
await saveState(state);
|
|
123
|
+
|
|
124
|
+
// Run pipeline
|
|
125
|
+
let previousOutput = '';
|
|
126
|
+
const agentResults = [];
|
|
127
|
+
const agentTimings = {};
|
|
128
|
+
|
|
129
|
+
// Define execution tiers (sorted by tier number)
|
|
130
|
+
const executionTiers = options.parallel
|
|
131
|
+
? [
|
|
132
|
+
{ name: '1-Planning', agents: AGENT_PIPELINE.filter(a => a.tier === '1-planning'), parallel: false },
|
|
133
|
+
{ name: '2-Implementation', agents: AGENT_PIPELINE.filter(a => a.tier === '2-implementation'), parallel: true },
|
|
134
|
+
{ name: '3-Security', agents: AGENT_PIPELINE.filter(a => a.tier === '3-security'), parallel: false },
|
|
135
|
+
{ name: '4-Quality', agents: AGENT_PIPELINE.filter(a => a.tier === '4-quality'), parallel: false }
|
|
136
|
+
]
|
|
137
|
+
: [{ name: 'All', agents: AGENT_PIPELINE, parallel: false }];
|
|
138
|
+
|
|
139
|
+
for (const tier of executionTiers) {
|
|
140
|
+
if (tier.agents.length === 0) continue;
|
|
141
|
+
|
|
142
|
+
if (options.parallel) {
|
|
143
|
+
console.log(chalk.blue.bold(`\n📦 Tier: ${tier.name}`));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (tier.parallel) {
|
|
147
|
+
// Parallel Execution for implementation tier
|
|
148
|
+
const tierStart = Date.now();
|
|
149
|
+
const promises = tier.agents.map(async (agent) => {
|
|
150
|
+
const agentStart = Date.now();
|
|
151
|
+
const spinner = ora(`Running @${agent.name}...`).start();
|
|
152
|
+
|
|
153
|
+
// Update state with active agent
|
|
154
|
+
const currentState = await loadState();
|
|
155
|
+
if (currentState) {
|
|
156
|
+
currentState.agents.active.push(agent.name);
|
|
157
|
+
await saveState(currentState);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const result = await runAgent(agent, task, context, previousOutput, provider);
|
|
162
|
+
const duration = Date.now() - agentStart;
|
|
163
|
+
agentTimings[agent.name] = duration;
|
|
164
|
+
spinner.succeed(chalk.green(` @${agent.name} complete`) + chalk.gray(` (${duration}ms)`));
|
|
165
|
+
|
|
166
|
+
// Remove active agent from state
|
|
167
|
+
const stateDone = await loadState();
|
|
168
|
+
if (stateDone) {
|
|
169
|
+
stateDone.agents.active = stateDone.agents.active.filter(a => a !== agent.name);
|
|
170
|
+
await saveState(stateDone);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { agent: agent.name, result, success: true };
|
|
174
|
+
} catch (error) {
|
|
175
|
+
const duration = Date.now() - agentStart;
|
|
176
|
+
agentTimings[agent.name] = duration;
|
|
177
|
+
spinner.fail(chalk.red(` @${agent.name} failed: ${error.message}`) + chalk.gray(` (${duration}ms)`));
|
|
178
|
+
|
|
179
|
+
const stateFail = await loadState();
|
|
180
|
+
if (stateFail) {
|
|
181
|
+
stateFail.agents.active = stateFail.agents.active.filter(a => a !== agent.name);
|
|
182
|
+
await saveState(stateFail);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { agent: agent.name, error: error.message, success: false };
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const results = await Promise.all(promises);
|
|
190
|
+
agentResults.push(...results);
|
|
191
|
+
previousOutput += '\n\n' + results.filter(r => r.success).map(r => r.result).join('\n\n');
|
|
192
|
+
console.log(chalk.gray(` Tier completed in ${Date.now() - tierStart}ms`));
|
|
193
|
+
|
|
194
|
+
} else {
|
|
195
|
+
// Serial Execution
|
|
196
|
+
for (const agent of tier.agents) {
|
|
197
|
+
const agentStart = Date.now();
|
|
198
|
+
const spinner = ora(`Running @${agent.name}...`).start();
|
|
199
|
+
|
|
200
|
+
// Update state
|
|
201
|
+
const currentState = await loadState();
|
|
202
|
+
if (currentState) {
|
|
203
|
+
currentState.agents.active.push(agent.name);
|
|
204
|
+
await saveState(currentState);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const result = await runAgent(agent, task, context, previousOutput, provider);
|
|
209
|
+
const duration = Date.now() - agentStart;
|
|
210
|
+
agentTimings[agent.name] = duration;
|
|
211
|
+
previousOutput = result;
|
|
212
|
+
spinner.succeed(chalk.green(` @${agent.name} complete`) + chalk.gray(` (${duration}ms)`));
|
|
213
|
+
console.log(chalk.gray(` → ${result.slice(0, 100).replace(/\n/g, ' ')}...`));
|
|
214
|
+
agentResults.push({ agent: agent.name, result, success: true });
|
|
215
|
+
|
|
216
|
+
// Remove active agent
|
|
217
|
+
const stateDone = await loadState();
|
|
218
|
+
if (stateDone) {
|
|
219
|
+
stateDone.agents.active = stateDone.agents.active.filter(a => a !== agent.name);
|
|
220
|
+
await saveState(stateDone);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
} catch (error) {
|
|
224
|
+
const duration = Date.now() - agentStart;
|
|
225
|
+
agentTimings[agent.name] = duration;
|
|
226
|
+
spinner.fail(chalk.red(` @${agent.name} failed: ${error.message}`) + chalk.gray(` (${duration}ms)`));
|
|
227
|
+
agentResults.push({ agent: agent.name, error: error.message, success: false });
|
|
228
|
+
|
|
229
|
+
const stateFail = await loadState();
|
|
230
|
+
if (stateFail) {
|
|
231
|
+
stateFail.agents.active = stateFail.agents.active.filter(a => a !== agent.name);
|
|
232
|
+
await saveState(stateFail);
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const totalDuration = Date.now() - startTime;
|
|
241
|
+
const successCount = agentResults.filter(r => r.success).length;
|
|
242
|
+
const failCount = agentResults.filter(r => !r.success).length;
|
|
243
|
+
|
|
244
|
+
// Final state update
|
|
245
|
+
await updateState();
|
|
246
|
+
|
|
247
|
+
// Write log
|
|
248
|
+
const stats = {
|
|
249
|
+
totalDuration,
|
|
250
|
+
agentTimings,
|
|
251
|
+
successCount,
|
|
252
|
+
failCount,
|
|
253
|
+
parallel: options.parallel || false
|
|
254
|
+
};
|
|
255
|
+
const logPath = await writeSwarmLog(logDir, task, agentResults, stats);
|
|
256
|
+
|
|
257
|
+
// Summary
|
|
258
|
+
console.log(chalk.cyan.bold('\n📊 Execution Stats:'));
|
|
259
|
+
console.log(chalk.white(` Total time: ${totalDuration}ms`));
|
|
260
|
+
console.log(chalk.green(` Succeeded: ${successCount}`) + chalk.red(` Failed: ${failCount}`));
|
|
261
|
+
Object.entries(agentTimings).forEach(([agent, time]) => {
|
|
262
|
+
console.log(chalk.gray(` • @${agent}: ${time}ms`));
|
|
263
|
+
});
|
|
264
|
+
console.log(chalk.gray(`\n Log saved: ${logPath}`));
|
|
265
|
+
|
|
266
|
+
console.log(chalk.green.bold('\n✅ Swarm complete!\n'));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function loadAgentPrompt(name) {
|
|
270
|
+
// Use glob to find agent file recursively
|
|
271
|
+
const files = await glob(`agents/**/${name}.md`, { ignore: 'node_modules/**' });
|
|
272
|
+
|
|
273
|
+
if (files.length > 0) {
|
|
274
|
+
return await readFile(files[0], 'utf-8');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Fallback to direct check
|
|
278
|
+
const directPath = join(process.cwd(), 'agents', `${name}.md`);
|
|
279
|
+
if (existsSync(directPath)) {
|
|
280
|
+
return await readFile(directPath, 'utf-8');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return `You are the @${name} agent.`;
|
|
284
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ultra-dex sync command
|
|
3
|
+
* Synchronizes project state and graph across devices
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { loadState, saveState } from './state.js';
|
|
10
|
+
import { buildGraph } from '../utils/graph.js';
|
|
11
|
+
import { snapshotContext } from '../utils/sync.js';
|
|
12
|
+
import { validateSafePath } from '../utils/validation.js';
|
|
13
|
+
|
|
14
|
+
export function registerSyncCommand(program) {
|
|
15
|
+
program
|
|
16
|
+
.command('sync')
|
|
17
|
+
.description('Synchronize project state and graph (God Mode Sync)')
|
|
18
|
+
.option('-d, --dir <directory>', 'Project directory to sync', '.')
|
|
19
|
+
.option('--push', 'Push local state to sync target')
|
|
20
|
+
.option('--pull', 'Pull state from sync target')
|
|
21
|
+
.option('--target <path>', 'Sync target (local folder or s3-like)', '.ultra/sync')
|
|
22
|
+
.action(async (options) => {
|
|
23
|
+
console.log(chalk.cyan('\n🔄 Ultra-Dex State Sync\n'));
|
|
24
|
+
|
|
25
|
+
const dirValidation = validateSafePath(options.dir, 'Project directory');
|
|
26
|
+
if (dirValidation !== true) {
|
|
27
|
+
console.log(chalk.red(dirValidation));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const projectDir = path.resolve(options.dir);
|
|
32
|
+
|
|
33
|
+
// 1. Snapshot Context (Updates CONTEXT.md)
|
|
34
|
+
const syncResult = await snapshotContext(projectDir);
|
|
35
|
+
console.log(chalk.green(` ✅ Context Snapshot Complete (${syncResult.summary.fileCount} Files scanned)`));
|
|
36
|
+
if (syncResult.updated) {
|
|
37
|
+
console.log(chalk.gray(' CONTEXT.md updated with latest project structure.'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const syncTarget = path.resolve(projectDir, options.target);
|
|
41
|
+
await fs.mkdir(syncTarget, { recursive: true });
|
|
42
|
+
|
|
43
|
+
if (options.push) {
|
|
44
|
+
await handlePush(projectDir, syncTarget);
|
|
45
|
+
} else if (options.pull) {
|
|
46
|
+
await handlePull(projectDir, syncTarget);
|
|
47
|
+
} else {
|
|
48
|
+
// Default: Bidirectional Sync (Simplified for Phase 2.1)
|
|
49
|
+
console.log(chalk.yellow('\nDefaulting to PUSH local state to target.'));
|
|
50
|
+
await handlePush(projectDir, syncTarget);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function handlePush(projectDir, target) {
|
|
56
|
+
const spinner = (await import('ora')).default('Pushing state to sync target...').start();
|
|
57
|
+
try {
|
|
58
|
+
// Note: loadState/saveState might need to be aware of projectDir if they use relative paths
|
|
59
|
+
// For now they use process.cwd() which might be wrong if --dir is used.
|
|
60
|
+
// However, let's keep it simple as most commands assume CWD = project root unless --dir is used.
|
|
61
|
+
|
|
62
|
+
const state = await loadState();
|
|
63
|
+
if (!state) throw new Error('No local state found');
|
|
64
|
+
|
|
65
|
+
const graph = await buildGraph();
|
|
66
|
+
|
|
67
|
+
const bundle = {
|
|
68
|
+
state,
|
|
69
|
+
graph,
|
|
70
|
+
timestamp: new Date().toISOString(),
|
|
71
|
+
machine: process.env.USER || 'unknown'
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
await fs.writeFile(path.join(target, 'sync-bundle.json'), JSON.stringify(bundle, null, 2));
|
|
75
|
+
spinner.succeed(chalk.green(`State pushed to ${target}`));
|
|
76
|
+
} catch (e) {
|
|
77
|
+
spinner.fail(chalk.red(`Push failed: ${e.message}`));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function handlePull(projectDir, target) {
|
|
82
|
+
const spinner = (await import('ora')).default('Pulling state from sync target...').start();
|
|
83
|
+
try {
|
|
84
|
+
const bundleContent = await fs.readFile(path.join(target, 'sync-bundle.json'), 'utf8');
|
|
85
|
+
const bundle = JSON.parse(bundleContent);
|
|
86
|
+
|
|
87
|
+
await saveState(bundle.state);
|
|
88
|
+
spinner.succeed(chalk.green('Local state updated from sync bundle.'));
|
|
89
|
+
console.log(chalk.gray(` Bundle Timestamp: ${bundle.timestamp}`));
|
|
90
|
+
console.log(chalk.gray(` Source Machine: ${bundle.machine}`));
|
|
91
|
+
} catch (e) {
|
|
92
|
+
spinner.fail(chalk.red(`Pull failed: ${e.message}`));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
|
|
7
|
+
const TEAM_DIR = '.ultra-dex';
|
|
8
|
+
const TEAM_FILE = 'team.json';
|
|
9
|
+
const TEAM_PATH = path.resolve(process.cwd(), TEAM_DIR, TEAM_FILE);
|
|
10
|
+
const VALID_ROLES = ['admin', 'member', 'viewer'];
|
|
11
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
12
|
+
|
|
13
|
+
function normalizeEmail(email) {
|
|
14
|
+
return email.trim().toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isValidEmail(email) {
|
|
18
|
+
return EMAIL_REGEX.test(email);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function loadTeamConfig() {
|
|
22
|
+
try {
|
|
23
|
+
const content = await fs.readFile(TEAM_PATH, 'utf8');
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error.code === 'ENOENT') {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function saveTeamConfig(config) {
|
|
34
|
+
const dir = path.resolve(process.cwd(), TEAM_DIR);
|
|
35
|
+
await fs.mkdir(dir, { recursive: true });
|
|
36
|
+
await fs.writeFile(TEAM_PATH, JSON.stringify(config, null, 2));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function requireTeamConfig(team) {
|
|
40
|
+
if (!team) {
|
|
41
|
+
console.log(chalk.red('\n❌ Team not initialized. Run "ultra-dex team init" first.\n'));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatTable(rows) {
|
|
47
|
+
const headers = ['Email', 'Role', 'Added'];
|
|
48
|
+
const widths = headers.map((header, index) => {
|
|
49
|
+
const maxRow = rows.reduce((max, row) => Math.max(max, row[index].length), header.length);
|
|
50
|
+
return Math.max(header.length, maxRow);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const headerLine = headers
|
|
54
|
+
.map((header, index) => header.padEnd(widths[index]))
|
|
55
|
+
.join(' | ');
|
|
56
|
+
const divider = widths.map((width) => '-'.repeat(width)).join('-|-');
|
|
57
|
+
|
|
58
|
+
const body = rows
|
|
59
|
+
.map((row) => row.map((cell, index) => cell.padEnd(widths[index])).join(' | '))
|
|
60
|
+
.join('\n');
|
|
61
|
+
|
|
62
|
+
return `${headerLine}\n${divider}\n${body}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildInitCommand() {
|
|
66
|
+
const command = new Command('init');
|
|
67
|
+
command
|
|
68
|
+
.description('Initialize team configuration')
|
|
69
|
+
.action(async () => {
|
|
70
|
+
const existing = await loadTeamConfig();
|
|
71
|
+
if (existing) {
|
|
72
|
+
const { overwrite } = await inquirer.prompt([
|
|
73
|
+
{
|
|
74
|
+
type: 'confirm',
|
|
75
|
+
name: 'overwrite',
|
|
76
|
+
message: 'Team config already exists. Overwrite it?',
|
|
77
|
+
default: false,
|
|
78
|
+
},
|
|
79
|
+
]);
|
|
80
|
+
if (!overwrite) {
|
|
81
|
+
console.log(chalk.yellow('\nCanceled.\n'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const answers = await inquirer.prompt([
|
|
87
|
+
{
|
|
88
|
+
type: 'input',
|
|
89
|
+
name: 'name',
|
|
90
|
+
message: 'Team name:',
|
|
91
|
+
validate: (input) => (input.trim().length > 0 ? true : 'Team name is required'),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
type: 'input',
|
|
95
|
+
name: 'description',
|
|
96
|
+
message: 'Team description:',
|
|
97
|
+
default: '',
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
const config = {
|
|
102
|
+
name: answers.name.trim(),
|
|
103
|
+
description: answers.description.trim(),
|
|
104
|
+
members: [],
|
|
105
|
+
createdAt: new Date().toISOString(),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
await saveTeamConfig(config);
|
|
109
|
+
console.log(chalk.green('\n✅ Team config created at .ultra-dex/team.json\n'));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return command;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildAddCommand() {
|
|
116
|
+
const command = new Command('add');
|
|
117
|
+
command
|
|
118
|
+
.description('Add a team member')
|
|
119
|
+
.argument('<email>', 'Member email')
|
|
120
|
+
.option('-r, --role <role>', 'Role (admin|member|viewer)', 'member')
|
|
121
|
+
.action(async (email, options) => {
|
|
122
|
+
const normalizedEmail = normalizeEmail(email);
|
|
123
|
+
if (!isValidEmail(normalizedEmail)) {
|
|
124
|
+
console.log(chalk.red('\n❌ Invalid email format.\n'));
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const role = options.role?.toLowerCase() || 'member';
|
|
129
|
+
if (!VALID_ROLES.includes(role)) {
|
|
130
|
+
console.log(chalk.red(`\n❌ Invalid role. Use one of: ${VALID_ROLES.join(', ')}.\n`));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const team = await loadTeamConfig();
|
|
135
|
+
requireTeamConfig(team);
|
|
136
|
+
|
|
137
|
+
const exists = team.members.some((member) => normalizeEmail(member.email) === normalizedEmail);
|
|
138
|
+
if (exists) {
|
|
139
|
+
console.log(chalk.yellow(`\n⚠️ ${normalizedEmail} is already on the team.\n`));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
team.members.push({
|
|
144
|
+
email: normalizedEmail,
|
|
145
|
+
role,
|
|
146
|
+
addedAt: new Date().toISOString(),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await saveTeamConfig(team);
|
|
150
|
+
console.log(chalk.green(`\n✅ Added ${normalizedEmail} as ${role}.\n`));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return command;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildListCommand() {
|
|
157
|
+
const command = new Command('list');
|
|
158
|
+
command
|
|
159
|
+
.description('List team members')
|
|
160
|
+
.action(async () => {
|
|
161
|
+
const team = await loadTeamConfig();
|
|
162
|
+
requireTeamConfig(team);
|
|
163
|
+
|
|
164
|
+
if (!team.members.length) {
|
|
165
|
+
console.log(chalk.yellow('\nNo team members yet.\n'));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const rows = team.members.map((member) => [
|
|
170
|
+
member.email,
|
|
171
|
+
member.role,
|
|
172
|
+
member.addedAt,
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
console.log('\n' + chalk.bold('Team Members'));
|
|
176
|
+
console.log(formatTable(rows));
|
|
177
|
+
console.log('');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return command;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildRemoveCommand() {
|
|
184
|
+
const command = new Command('remove');
|
|
185
|
+
command
|
|
186
|
+
.description('Remove a team member')
|
|
187
|
+
.argument('<email>', 'Member email')
|
|
188
|
+
.action(async (email) => {
|
|
189
|
+
const normalizedEmail = normalizeEmail(email);
|
|
190
|
+
if (!isValidEmail(normalizedEmail)) {
|
|
191
|
+
console.log(chalk.red('\n❌ Invalid email format.\n'));
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const team = await loadTeamConfig();
|
|
196
|
+
requireTeamConfig(team);
|
|
197
|
+
|
|
198
|
+
const index = team.members.findIndex((member) => normalizeEmail(member.email) === normalizedEmail);
|
|
199
|
+
if (index === -1) {
|
|
200
|
+
console.log(chalk.red(`\n❌ ${normalizedEmail} not found in team.\n`));
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const { confirm } = await inquirer.prompt([
|
|
205
|
+
{
|
|
206
|
+
type: 'confirm',
|
|
207
|
+
name: 'confirm',
|
|
208
|
+
message: `Remove ${normalizedEmail} from the team?`,
|
|
209
|
+
default: false,
|
|
210
|
+
},
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
if (!confirm) {
|
|
214
|
+
console.log(chalk.yellow('\nCanceled.\n'));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
team.members.splice(index, 1);
|
|
219
|
+
await saveTeamConfig(team);
|
|
220
|
+
console.log(chalk.green(`\n✅ Removed ${normalizedEmail}.\n`));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return command;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function buildConfigCommand() {
|
|
227
|
+
const command = new Command('config');
|
|
228
|
+
command
|
|
229
|
+
.description('Show or update team settings')
|
|
230
|
+
.argument('[key]', 'Setting key (name, description)')
|
|
231
|
+
.argument('[value]', 'Setting value')
|
|
232
|
+
.action(async (key, value) => {
|
|
233
|
+
const team = await loadTeamConfig();
|
|
234
|
+
requireTeamConfig(team);
|
|
235
|
+
|
|
236
|
+
const validKeys = ['name', 'description'];
|
|
237
|
+
if (!key) {
|
|
238
|
+
console.log(chalk.bold('\nTeam Settings\n'));
|
|
239
|
+
console.log(chalk.gray(`Name: ${team.name || '-'}`));
|
|
240
|
+
console.log(chalk.gray(`Description: ${team.description || '-'}`));
|
|
241
|
+
console.log(chalk.gray(`Created: ${team.createdAt || '-'}`));
|
|
242
|
+
console.log(chalk.gray(`Members: ${team.members.length}`));
|
|
243
|
+
console.log('');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!validKeys.includes(key)) {
|
|
248
|
+
console.log(chalk.red(`\n❌ Invalid key. Use: ${validKeys.join(', ')}.\n`));
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (value === undefined) {
|
|
253
|
+
console.log(chalk.gray(`\n${key}: ${team[key] || '-'}\n`));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
team[key] = value.trim();
|
|
258
|
+
await saveTeamConfig(team);
|
|
259
|
+
console.log(chalk.green(`\n✅ Updated ${key}.\n`));
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return command;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function registerTeamCommand(program) {
|
|
266
|
+
const team = program
|
|
267
|
+
.command('team')
|
|
268
|
+
.description('Team collaboration');
|
|
269
|
+
|
|
270
|
+
team.addCommand(buildInitCommand());
|
|
271
|
+
team.addCommand(buildAddCommand());
|
|
272
|
+
team.addCommand(buildListCommand());
|
|
273
|
+
team.addCommand(buildRemoveCommand());
|
|
274
|
+
team.addCommand(buildConfigCommand());
|
|
275
|
+
}
|