@hanzo/dev 1.2.0 → 2.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.
@@ -0,0 +1,379 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { glob } from 'glob';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { spawn, ChildProcess } from 'child_process';
7
+ import { EventEmitter } from 'events';
8
+
9
+ export interface SwarmOptions {
10
+ provider: 'claude' | 'openai' | 'gemini' | 'grok' | 'local';
11
+ count: number;
12
+ prompt: string;
13
+ cwd?: string;
14
+ pattern?: string;
15
+ autoLogin?: boolean;
16
+ }
17
+
18
+ export interface SwarmAgent {
19
+ id: string;
20
+ process?: ChildProcess;
21
+ file?: string;
22
+ status: 'idle' | 'busy' | 'done' | 'error';
23
+ result?: string;
24
+ error?: string;
25
+ }
26
+
27
+ export class SwarmRunner extends EventEmitter {
28
+ private agents: Map<string, SwarmAgent> = new Map();
29
+ private fileQueue: string[] = [];
30
+ private options: SwarmOptions;
31
+ private activeCount: number = 0;
32
+
33
+ constructor(options: SwarmOptions) {
34
+ super();
35
+ this.options = {
36
+ cwd: process.cwd(),
37
+ pattern: '**/*',
38
+ autoLogin: true,
39
+ ...options
40
+ };
41
+ }
42
+
43
+ async run(): Promise<void> {
44
+ const spinner = ora(`Initializing swarm with ${this.options.count} agents...`).start();
45
+
46
+ try {
47
+ // Find files to process
48
+ this.fileQueue = await this.findFiles();
49
+ spinner.succeed(`Found ${this.fileQueue.length} files to process`);
50
+
51
+ if (this.fileQueue.length === 0) {
52
+ console.log(chalk.yellow('No files found matching pattern'));
53
+ return;
54
+ }
55
+
56
+ // Initialize agent pool
57
+ const agentCount = Math.min(this.options.count, this.fileQueue.length);
58
+ spinner.start(`Spawning ${agentCount} agents...`);
59
+
60
+ for (let i = 0; i < agentCount; i++) {
61
+ const agent: SwarmAgent = {
62
+ id: `agent-${i}`,
63
+ status: 'idle'
64
+ };
65
+ this.agents.set(agent.id, agent);
66
+ }
67
+
68
+ spinner.succeed(`Spawned ${agentCount} agents`);
69
+
70
+ // Process files in parallel
71
+ spinner.start('Processing files...');
72
+ const startTime = Date.now();
73
+
74
+ // Start processing
75
+ await this.processFiles();
76
+
77
+ const duration = (Date.now() - startTime) / 1000;
78
+ spinner.succeed(`Completed in ${duration.toFixed(1)}s`);
79
+
80
+ // Show results
81
+ this.showResults();
82
+
83
+ } catch (error) {
84
+ spinner.fail(`Swarm error: ${error}`);
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ private async findFiles(): Promise<string[]> {
90
+ return new Promise((resolve, reject) => {
91
+ const options = {
92
+ cwd: this.options.cwd,
93
+ nodir: true,
94
+ ignore: [
95
+ '**/node_modules/**',
96
+ '**/.git/**',
97
+ '**/dist/**',
98
+ '**/build/**',
99
+ '**/*.min.js',
100
+ '**/*.map'
101
+ ]
102
+ };
103
+
104
+ glob(this.options.pattern || '**/*', options, (err, files) => {
105
+ if (err) {
106
+ reject(err);
107
+ } else {
108
+ // Filter to only editable files
109
+ const editableFiles = files.filter(file => {
110
+ const ext = path.extname(file);
111
+ return ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.cpp', '.c', '.h', '.go', '.rs', '.rb', '.php', '.swift', '.kt', '.scala', '.r', '.m', '.mm', '.md', '.txt', '.json', '.xml', '.yaml', '.yml', '.toml', '.ini', '.conf', '.sh', '.bash', '.zsh', '.fish', '.ps1', '.bat', '.cmd'].includes(ext);
112
+ });
113
+ resolve(editableFiles);
114
+ }
115
+ });
116
+ });
117
+ }
118
+
119
+ private async processFiles(): Promise<void> {
120
+ const promises: Promise<void>[] = [];
121
+
122
+ // Start initial batch of work
123
+ for (const [id, agent] of this.agents) {
124
+ if (this.fileQueue.length > 0) {
125
+ promises.push(this.processNextFile(agent));
126
+ }
127
+ }
128
+
129
+ // Wait for all agents to complete
130
+ await Promise.all(promises);
131
+ }
132
+
133
+ private async processNextFile(agent: SwarmAgent): Promise<void> {
134
+ while (this.fileQueue.length > 0) {
135
+ const file = this.fileQueue.shift();
136
+ if (!file) break;
137
+
138
+ agent.file = file;
139
+ agent.status = 'busy';
140
+ this.activeCount++;
141
+
142
+ try {
143
+ await this.processFile(agent, file);
144
+ agent.status = 'done';
145
+ } catch (error) {
146
+ agent.status = 'error';
147
+ agent.error = error instanceof Error ? error.message : String(error);
148
+ } finally {
149
+ this.activeCount--;
150
+ }
151
+ }
152
+ }
153
+
154
+ private async processFile(agent: SwarmAgent, file: string): Promise<void> {
155
+ const fullPath = path.join(this.options.cwd!, file);
156
+
157
+ // Build command based on provider
158
+ const command = this.buildCommand(file);
159
+
160
+ return new Promise((resolve, reject) => {
161
+ const child = spawn(command.cmd, command.args, {
162
+ cwd: this.options.cwd,
163
+ env: {
164
+ ...process.env,
165
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
166
+ OPENAI_API_KEY: process.env.OPENAI_API_KEY,
167
+ GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
168
+ GROK_API_KEY: process.env.GROK_API_KEY,
169
+ // Auto-accept edits for non-interactive mode
170
+ CLAUDE_CODE_PERMISSION_MODE: 'acceptEdits'
171
+ },
172
+ shell: true
173
+ });
174
+
175
+ agent.process = child;
176
+
177
+ let output = '';
178
+ let error = '';
179
+
180
+ child.stdout?.on('data', (data) => {
181
+ output += data.toString();
182
+ });
183
+
184
+ child.stderr?.on('data', (data) => {
185
+ error += data.toString();
186
+ });
187
+
188
+ child.on('close', (code) => {
189
+ if (code === 0) {
190
+ agent.result = output;
191
+ resolve();
192
+ } else {
193
+ reject(new Error(`Process exited with code ${code}: ${error}`));
194
+ }
195
+ });
196
+
197
+ child.on('error', (err) => {
198
+ reject(err);
199
+ });
200
+ });
201
+ }
202
+
203
+ private buildCommand(file: string): { cmd: string, args: string[] } {
204
+ const fullPath = path.join(this.options.cwd!, file);
205
+ const filePrompt = `${this.options.prompt}\n\nFile: ${file}`;
206
+
207
+ switch (this.options.provider) {
208
+ case 'claude':
209
+ return {
210
+ cmd: 'claude',
211
+ args: [
212
+ '-p',
213
+ filePrompt,
214
+ '--max-turns', '5',
215
+ '--allowedTools', 'Read,Write,Edit',
216
+ '--permission-mode', 'acceptEdits'
217
+ ]
218
+ };
219
+
220
+ case 'openai':
221
+ return {
222
+ cmd: 'openai',
223
+ args: [
224
+ 'chat',
225
+ '--prompt', filePrompt,
226
+ '--file', fullPath,
227
+ '--edit'
228
+ ]
229
+ };
230
+
231
+ case 'gemini':
232
+ return {
233
+ cmd: 'gemini',
234
+ args: [
235
+ 'edit',
236
+ fullPath,
237
+ '--prompt', filePrompt
238
+ ]
239
+ };
240
+
241
+ case 'grok':
242
+ return {
243
+ cmd: 'grok',
244
+ args: [
245
+ '--edit',
246
+ fullPath,
247
+ '--prompt', filePrompt
248
+ ]
249
+ };
250
+
251
+ case 'local':
252
+ return {
253
+ cmd: 'dev',
254
+ args: [
255
+ 'agent',
256
+ filePrompt
257
+ ]
258
+ };
259
+
260
+ default:
261
+ throw new Error(`Unknown provider: ${this.options.provider}`);
262
+ }
263
+ }
264
+
265
+ private showResults(): void {
266
+ console.log(chalk.bold.cyan('\nšŸ“Š Swarm Results\n'));
267
+
268
+ let successful = 0;
269
+ let failed = 0;
270
+
271
+ for (const [id, agent] of this.agents) {
272
+ if (agent.status === 'done') {
273
+ successful++;
274
+ console.log(chalk.green(`āœ“ ${agent.file || id}`));
275
+ } else if (agent.status === 'error') {
276
+ failed++;
277
+ console.log(chalk.red(`āœ— ${agent.file || id}: ${agent.error}`));
278
+ }
279
+ }
280
+
281
+ console.log(chalk.gray('\n─────────────────'));
282
+ console.log(chalk.white('Total files:'), this.fileQueue.length + successful + failed);
283
+ console.log(chalk.green('Successful:'), successful);
284
+ if (failed > 0) {
285
+ console.log(chalk.red('Failed:'), failed);
286
+ }
287
+ }
288
+
289
+ async ensureProviderAuth(): Promise<boolean> {
290
+ switch (this.options.provider) {
291
+ case 'claude':
292
+ return this.ensureClaudeAuth();
293
+ case 'openai':
294
+ return !!process.env.OPENAI_API_KEY;
295
+ case 'gemini':
296
+ return !!process.env.GOOGLE_API_KEY || !!process.env.GEMINI_API_KEY;
297
+ case 'grok':
298
+ return !!process.env.GROK_API_KEY;
299
+ case 'local':
300
+ return true;
301
+ default:
302
+ return false;
303
+ }
304
+ }
305
+
306
+ private async ensureClaudeAuth(): Promise<boolean> {
307
+ // Check if already authenticated
308
+ try {
309
+ const testResult = await new Promise<boolean>((resolve) => {
310
+ const child = spawn('claude', ['-p', 'test', '--max-turns', '1'], {
311
+ env: process.env,
312
+ shell: true
313
+ });
314
+
315
+ let hasError = false;
316
+ let resolved = false;
317
+
318
+ const cleanup = () => {
319
+ if (!resolved) {
320
+ resolved = true;
321
+ clearTimeout(timeout);
322
+ child.kill();
323
+ }
324
+ };
325
+
326
+ child.stderr?.on('data', (data) => {
327
+ const output = data.toString();
328
+ if (output.includes('not authenticated') || output.includes('API key')) {
329
+ hasError = true;
330
+ }
331
+ });
332
+
333
+ child.on('close', () => {
334
+ cleanup();
335
+ resolve(!hasError);
336
+ });
337
+
338
+ // Timeout after 5 seconds
339
+ const timeout = setTimeout(() => {
340
+ cleanup();
341
+ resolve(!hasError);
342
+ }, 5000);
343
+ });
344
+
345
+ if (testResult) {
346
+ return true;
347
+ }
348
+
349
+ // Try to login automatically if we have API key
350
+ if (process.env.ANTHROPIC_API_KEY && this.options.autoLogin) {
351
+ console.log(chalk.yellow('Attempting automatic Claude login...'));
352
+
353
+ const loginResult = await new Promise<boolean>((resolve) => {
354
+ const child = spawn('claude', ['login'], {
355
+ env: {
356
+ ...process.env,
357
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY
358
+ },
359
+ shell: true,
360
+ stdio: 'inherit'
361
+ });
362
+
363
+ child.on('close', (code) => {
364
+ resolve(code === 0);
365
+ });
366
+ });
367
+
368
+ if (loginResult) {
369
+ console.log(chalk.green('āœ“ Claude login successful'));
370
+ return true;
371
+ }
372
+ }
373
+
374
+ return false;
375
+ } catch (error) {
376
+ return false;
377
+ }
378
+ }
379
+ }