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.
Files changed (61) hide show
  1. package/README.md +84 -122
  2. package/assets/agents/0-orchestration/orchestrator.md +2 -2
  3. package/assets/agents/00-AGENT_INDEX.md +1 -1
  4. package/assets/docs/LAUNCH-POSTS.md +1 -1
  5. package/assets/docs/QUICK-REFERENCE.md +12 -7
  6. package/assets/docs/ROADMAP.md +5 -5
  7. package/assets/docs/VISION-V2.md +1 -1
  8. package/assets/docs/WORKFLOW-DIAGRAMS.md +1 -1
  9. package/assets/hooks/pre-commit +98 -0
  10. package/assets/saas-plan/04-Imp-Template.md +1 -1
  11. package/assets/templates/README.md +1 -1
  12. package/bin/ultra-dex.js +93 -2096
  13. package/lib/commands/advanced.js +471 -0
  14. package/lib/commands/agent-builder.js +226 -0
  15. package/lib/commands/agents.js +101 -47
  16. package/lib/commands/auto-implement.js +68 -0
  17. package/lib/commands/build.js +73 -187
  18. package/lib/commands/ci-monitor.js +84 -0
  19. package/lib/commands/config.js +207 -0
  20. package/lib/commands/dashboard.js +770 -0
  21. package/lib/commands/diff.js +233 -0
  22. package/lib/commands/doctor.js +397 -0
  23. package/lib/commands/export.js +408 -0
  24. package/lib/commands/fix.js +96 -0
  25. package/lib/commands/generate.js +96 -72
  26. package/lib/commands/hooks.js +251 -76
  27. package/lib/commands/init.js +56 -6
  28. package/lib/commands/memory.js +80 -0
  29. package/lib/commands/plan.js +82 -0
  30. package/lib/commands/review.js +34 -5
  31. package/lib/commands/run.js +233 -0
  32. package/lib/commands/serve.js +188 -40
  33. package/lib/commands/state.js +354 -0
  34. package/lib/commands/swarm.js +284 -0
  35. package/lib/commands/sync.js +94 -0
  36. package/lib/commands/team.js +275 -0
  37. package/lib/commands/upgrade.js +190 -0
  38. package/lib/commands/validate.js +34 -0
  39. package/lib/commands/verify.js +81 -0
  40. package/lib/commands/watch.js +79 -0
  41. package/lib/mcp/graph.js +92 -0
  42. package/lib/mcp/memory.js +95 -0
  43. package/lib/mcp/resources.js +152 -0
  44. package/lib/mcp/server.js +34 -0
  45. package/lib/mcp/tools.js +481 -0
  46. package/lib/mcp/websocket.js +117 -0
  47. package/lib/providers/index.js +49 -4
  48. package/lib/providers/ollama.js +136 -0
  49. package/lib/providers/router.js +63 -0
  50. package/lib/quality/scanner.js +128 -0
  51. package/lib/swarm/coordinator.js +97 -0
  52. package/lib/swarm/index.js +598 -0
  53. package/lib/swarm/protocol.js +677 -0
  54. package/lib/swarm/tiers.js +485 -0
  55. package/lib/templates/context.js +2 -2
  56. package/lib/templates/custom-agent.md +10 -0
  57. package/lib/utils/fallback.js +4 -2
  58. package/lib/utils/files.js +7 -34
  59. package/lib/utils/graph.js +108 -0
  60. package/lib/utils/sync.js +216 -0
  61. 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
+ }