@wundr.io/cli 1.0.1 → 1.0.2-dev.20260530180455.e1307186

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 (69) hide show
  1. package/bin/wundr.js +13 -5
  2. package/package.json +30 -9
  3. package/src/ai/ai-service.ts +6 -4
  4. package/src/ai/claude-client.ts +6 -2
  5. package/src/ai/conversation-manager.ts +12 -5
  6. package/src/cli.ts +42 -13
  7. package/src/commands/ai.ts +340 -64
  8. package/src/commands/alignment.ts +1212 -0
  9. package/src/commands/analyze-optimized.ts +371 -33
  10. package/src/commands/analyze.ts +8 -6
  11. package/src/commands/batch.ts +166 -26
  12. package/src/commands/chat.ts +20 -10
  13. package/src/commands/claude-init.ts +31 -27
  14. package/src/commands/claude-setup.ts +761 -81
  15. package/src/commands/computer-setup.ts +524 -12
  16. package/src/commands/create-command.ts +3 -3
  17. package/src/commands/create.ts +9 -6
  18. package/src/commands/dashboard.ts +11 -6
  19. package/src/commands/govern.ts +11 -6
  20. package/src/commands/governance.ts +1005 -0
  21. package/src/commands/guardian.ts +887 -0
  22. package/src/commands/init.ts +104 -11
  23. package/src/commands/orchestrator.ts +789 -0
  24. package/src/commands/performance-optimizer.ts +15 -10
  25. package/src/commands/plugins.ts +8 -5
  26. package/src/commands/project-update.ts +1156 -0
  27. package/src/commands/rag.ts +1011 -0
  28. package/src/commands/session.ts +631 -0
  29. package/src/commands/setup.ts +42 -344
  30. package/src/commands/test-init.ts +3 -2
  31. package/src/commands/test.ts +3 -2
  32. package/src/commands/watch.ts +21 -11
  33. package/src/commands/worktree.ts +1057 -0
  34. package/src/context/context-manager.ts +5 -2
  35. package/src/context/session-manager.ts +18 -7
  36. package/src/framework/command-interface.ts +520 -0
  37. package/src/framework/command-registry.ts +942 -0
  38. package/src/framework/completion-exporter.ts +383 -0
  39. package/src/framework/debug-logger.ts +519 -0
  40. package/src/framework/error-handler.ts +867 -0
  41. package/src/framework/help-generator.ts +540 -0
  42. package/src/framework/index.ts +169 -0
  43. package/src/framework/interactive-repl.ts +703 -0
  44. package/src/framework/output-formatter.ts +834 -0
  45. package/src/framework/progress-manager.ts +539 -0
  46. package/src/index.ts +3 -2
  47. package/src/interactive/interactive-mode.ts +14 -7
  48. package/src/lib/conflict-resolution.ts +818 -0
  49. package/src/lib/merge-strategy.ts +550 -0
  50. package/src/lib/safety-mechanisms.ts +451 -0
  51. package/src/lib/state-detection.ts +1030 -0
  52. package/src/nlp/command-mapper.ts +8 -3
  53. package/src/nlp/command-parser.ts +5 -2
  54. package/src/nlp/intent-parser.ts +23 -9
  55. package/src/plugins/plugin-manager.ts +50 -24
  56. package/src/tests/computer-setup-integration.test.ts +46 -15
  57. package/src/types/index.ts +1 -1
  58. package/src/types/modules.d.ts +425 -1
  59. package/src/utils/backup-rollback-manager.ts +19 -14
  60. package/src/utils/claude-config-installer.ts +119 -28
  61. package/src/utils/config-manager.ts +9 -6
  62. package/src/utils/error-handler.ts +3 -1
  63. package/src/utils/logger.ts +35 -12
  64. package/templates/batch/ci-cd.yaml +7 -7
  65. package/test-suites/api/health.spec.ts +20 -23
  66. package/test-suites/helpers/test-config.ts +14 -13
  67. package/test-suites/ui/accessibility.spec.ts +27 -22
  68. package/test-suites/ui/smoke.spec.ts +26 -21
  69. package/src/commands/computer-setup-commands.ts +0 -869
@@ -0,0 +1,789 @@
1
+ /**
2
+ * Orchestrator Daemon CLI Commands
3
+ * Manages the Orchestrator Daemon for agent orchestration
4
+ */
5
+
6
+ import * as fs from 'fs/promises';
7
+ import { existsSync } from 'fs';
8
+ import * as os from 'os';
9
+ import * as path from 'path';
10
+
11
+ import chalk from 'chalk';
12
+ import { Command } from 'commander';
13
+ import ora from 'ora';
14
+ import YAML from 'yaml';
15
+
16
+ // Constants
17
+ const ORCHESTRATOR_CONFIG_DIR = path.join(
18
+ os.homedir(),
19
+ '.wundr',
20
+ 'orchestrator-daemon'
21
+ );
22
+ const ORCHESTRATOR_CONFIG_FILE = path.join(
23
+ ORCHESTRATOR_CONFIG_DIR,
24
+ 'config.yaml'
25
+ );
26
+ const ORCHESTRATOR_PID_FILE = path.join(ORCHESTRATOR_CONFIG_DIR, 'daemon.pid');
27
+ const ORCHESTRATOR_LOG_FILE = path.join(ORCHESTRATOR_CONFIG_DIR, 'daemon.log');
28
+
29
+ // Types
30
+ interface OrchestratorConfig {
31
+ daemon: {
32
+ port: number;
33
+ host: string;
34
+ name: string;
35
+ maxSessions: number;
36
+ heartbeatInterval: number;
37
+ shutdownTimeout: number;
38
+ };
39
+ identity: {
40
+ name: string;
41
+ email: string;
42
+ slackHandle: string;
43
+ };
44
+ subsystems: {
45
+ triage: {
46
+ memoryBankPath: string;
47
+ enableRAG: boolean;
48
+ };
49
+ intervention: {
50
+ enabled: boolean;
51
+ autoRollbackOnCritical: boolean;
52
+ };
53
+ telemetry: {
54
+ enabled: boolean;
55
+ flushInterval: number;
56
+ };
57
+ };
58
+ safety: {
59
+ autoApprovePatterns: string[];
60
+ alwaysRejectPatterns: string[];
61
+ escalationPatterns: string[];
62
+ };
63
+ budget: {
64
+ dailyLimit: number;
65
+ monthlyLimit: number;
66
+ warningThreshold: number;
67
+ criticalThreshold: number;
68
+ };
69
+ }
70
+
71
+ interface DaemonStatus {
72
+ running: boolean;
73
+ pid?: number;
74
+ uptime?: string;
75
+ port?: number;
76
+ host?: string;
77
+ sessionCount?: number;
78
+ queueDepth?: number;
79
+ health?: 'healthy' | 'degraded' | 'unhealthy';
80
+ subsystems?: Record<string, string>;
81
+ }
82
+
83
+ // Utility functions
84
+ function getDefaultConfig(): OrchestratorConfig {
85
+ return {
86
+ daemon: {
87
+ port: 8787,
88
+ host: '127.0.0.1',
89
+ name: 'orchestrator-daemon',
90
+ maxSessions: 100,
91
+ heartbeatInterval: 30000,
92
+ shutdownTimeout: 10000,
93
+ },
94
+ identity: {
95
+ name: 'Orchestrator',
96
+ email: 'orchestrator@wundr.local',
97
+ slackHandle: '@orchestrator',
98
+ },
99
+ subsystems: {
100
+ triage: {
101
+ memoryBankPath: path.join(ORCHESTRATOR_CONFIG_DIR, 'memory-bank'),
102
+ enableRAG: false,
103
+ },
104
+ intervention: {
105
+ enabled: true,
106
+ autoRollbackOnCritical: false,
107
+ },
108
+ telemetry: {
109
+ enabled: true,
110
+ flushInterval: 10000,
111
+ },
112
+ },
113
+ safety: {
114
+ autoApprovePatterns: ['read|cat|ls|grep|find', 'npm test|yarn test|jest'],
115
+ alwaysRejectPatterns: ['rm\\s+-rf\\s+/', 'git\\s+push.*--force'],
116
+ escalationPatterns: ['deploy.*prod', 'password|secret|token|api.?key'],
117
+ },
118
+ budget: {
119
+ dailyLimit: 1000000,
120
+ monthlyLimit: 20000000,
121
+ warningThreshold: 0.8,
122
+ criticalThreshold: 0.95,
123
+ },
124
+ };
125
+ }
126
+
127
+ async function loadConfig(): Promise<OrchestratorConfig> {
128
+ try {
129
+ if (existsSync(ORCHESTRATOR_CONFIG_FILE)) {
130
+ const content = await fs.readFile(ORCHESTRATOR_CONFIG_FILE, 'utf-8');
131
+ const parsed = YAML.parse(content) as Partial<OrchestratorConfig>;
132
+ return { ...getDefaultConfig(), ...parsed };
133
+ }
134
+ } catch (error) {
135
+ // Fall through to default
136
+ }
137
+ return getDefaultConfig();
138
+ }
139
+
140
+ async function saveConfig(config: OrchestratorConfig): Promise<void> {
141
+ await fs.mkdir(ORCHESTRATOR_CONFIG_DIR, { recursive: true });
142
+ await fs.writeFile(ORCHESTRATOR_CONFIG_FILE, YAML.stringify(config), 'utf-8');
143
+ }
144
+
145
+ async function ensureConfigDir(): Promise<void> {
146
+ if (!existsSync(ORCHESTRATOR_CONFIG_DIR)) {
147
+ await fs.mkdir(ORCHESTRATOR_CONFIG_DIR, { recursive: true });
148
+ }
149
+ }
150
+
151
+ async function getDaemonStatus(): Promise<DaemonStatus> {
152
+ const status: DaemonStatus = { running: false };
153
+
154
+ try {
155
+ if (existsSync(ORCHESTRATOR_PID_FILE)) {
156
+ const pidContent = await fs.readFile(ORCHESTRATOR_PID_FILE, 'utf-8');
157
+ const pid = parseInt(pidContent.trim(), 10);
158
+
159
+ // Check if process is running
160
+ try {
161
+ process.kill(pid, 0);
162
+ status.running = true;
163
+ status.pid = pid;
164
+
165
+ // Try to read status from socket or API
166
+ const config = await loadConfig();
167
+ status.port = config.daemon.port;
168
+ status.host = config.daemon.host;
169
+
170
+ // Read additional status info if available
171
+ const statusFile = path.join(ORCHESTRATOR_CONFIG_DIR, 'status.json');
172
+ if (existsSync(statusFile)) {
173
+ const statusContent = await fs.readFile(statusFile, 'utf-8');
174
+ const statusData = JSON.parse(statusContent);
175
+ status.uptime = formatUptime(statusData.uptime);
176
+ status.sessionCount = statusData.sessionCount ?? 0;
177
+ status.queueDepth = statusData.queueDepth ?? 0;
178
+ status.health = statusData.health ?? 'unknown';
179
+ status.subsystems = statusData.subsystems ?? {};
180
+ }
181
+ } catch {
182
+ // Process not running, clean up stale PID file
183
+ await fs.unlink(ORCHESTRATOR_PID_FILE).catch(() => {});
184
+ }
185
+ }
186
+ } catch (error) {
187
+ // Ignore errors
188
+ }
189
+
190
+ return status;
191
+ }
192
+
193
+ function formatUptime(ms: number): string {
194
+ const seconds = Math.floor(ms / 1000);
195
+ const minutes = Math.floor(seconds / 60);
196
+ const hours = Math.floor(minutes / 60);
197
+ const days = Math.floor(hours / 24);
198
+
199
+ if (days > 0) {
200
+ return `${days}d ${hours % 24}h ${minutes % 60}m`;
201
+ }
202
+ if (hours > 0) {
203
+ return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
204
+ }
205
+ if (minutes > 0) {
206
+ return `${minutes}m ${seconds % 60}s`;
207
+ }
208
+ return `${seconds}s`;
209
+ }
210
+
211
+ // Create Orchestrator command
212
+ export function createOrchestratorCommand(): Command {
213
+ const command = new Command('orchestrator')
214
+ .description('Manage the Orchestrator Daemon for agent orchestration')
215
+ .addHelpText(
216
+ 'after',
217
+ chalk.gray(`
218
+ Examples:
219
+ ${chalk.green('wundr orchestrator start')} Start the Orchestrator Daemon
220
+ ${chalk.green('wundr orchestrator start --port 9000')} Start on custom port
221
+ ${chalk.green('wundr orchestrator status')} Check daemon status
222
+ ${chalk.green('wundr orchestrator stop')} Stop the daemon gracefully
223
+ ${chalk.green('wundr orchestrator config show')} View current configuration
224
+ ${chalk.green('wundr orchestrator config set daemon.port=9000')} Update configuration
225
+ `)
226
+ );
227
+
228
+ // Start command
229
+ command
230
+ .command('start')
231
+ .description('Start the Orchestrator Daemon')
232
+ .option('-p, --port <number>', 'Port to listen on')
233
+ .option('-c, --config <path>', 'Path to configuration file')
234
+ .option('-v, --verbose', 'Enable verbose logging')
235
+ .option('--detach', 'Run daemon in background (detached mode)')
236
+ .action(async options => {
237
+ await startDaemon(options);
238
+ });
239
+
240
+ // Status command (default)
241
+ command
242
+ .command('status', { isDefault: true })
243
+ .description('Check Orchestrator Daemon status')
244
+ .option('--json', 'Output as JSON')
245
+ .action(async options => {
246
+ await showStatus(options);
247
+ });
248
+
249
+ // Stop command
250
+ command
251
+ .command('stop')
252
+ .description('Stop the Orchestrator Daemon gracefully')
253
+ .option('-f, --force', 'Force immediate termination')
254
+ .option('-t, --timeout <ms>', 'Shutdown timeout in milliseconds', '10000')
255
+ .action(async options => {
256
+ await stopDaemon(options);
257
+ });
258
+
259
+ // Config command group
260
+ const configCmd = command
261
+ .command('config')
262
+ .description('View or edit Orchestrator configuration');
263
+
264
+ configCmd
265
+ .command('show')
266
+ .description('Display current configuration')
267
+ .option('--json', 'Output as JSON instead of YAML')
268
+ .action(async options => {
269
+ await showConfig(options);
270
+ });
271
+
272
+ configCmd
273
+ .command('set <key=value>')
274
+ .description('Set a configuration value (e.g., daemon.port=9000)')
275
+ .action(async keyValue => {
276
+ await setConfig(keyValue);
277
+ });
278
+
279
+ configCmd
280
+ .command('reset')
281
+ .description('Reset configuration to defaults')
282
+ .option('--force', 'Skip confirmation')
283
+ .action(async options => {
284
+ await resetConfig(options);
285
+ });
286
+
287
+ // Logs command
288
+ command
289
+ .command('logs')
290
+ .description('View Orchestrator Daemon logs')
291
+ .option('-f, --follow', 'Follow log output')
292
+ .option('-n, --lines <number>', 'Number of lines to show', '50')
293
+ .action(async options => {
294
+ await viewLogs(options);
295
+ });
296
+
297
+ return command;
298
+ }
299
+
300
+ // Command implementations
301
+ async function startDaemon(options: {
302
+ port?: string;
303
+ config?: string;
304
+ verbose?: boolean;
305
+ detach?: boolean;
306
+ }): Promise<void> {
307
+ const spinner = ora('Starting Orchestrator Daemon...').start();
308
+
309
+ try {
310
+ await ensureConfigDir();
311
+
312
+ // Check if already running
313
+ const currentStatus = await getDaemonStatus();
314
+ if (currentStatus.running) {
315
+ spinner.fail(
316
+ `Orchestrator Daemon is already running (PID: ${currentStatus.pid}, port: ${currentStatus.port})`
317
+ );
318
+ return;
319
+ }
320
+
321
+ // Load configuration
322
+ let config: OrchestratorConfig;
323
+ if (options.config && existsSync(options.config)) {
324
+ const content = await fs.readFile(options.config, 'utf-8');
325
+ config = { ...getDefaultConfig(), ...YAML.parse(content) };
326
+ spinner.text = `Loading config from ${options.config}...`;
327
+ } else {
328
+ config = await loadConfig();
329
+ }
330
+
331
+ // Apply CLI overrides
332
+ if (options.port) {
333
+ config.daemon.port = parseInt(options.port, 10);
334
+ }
335
+
336
+ // Save current config for daemon use
337
+ await saveConfig(config);
338
+
339
+ spinner.text = 'Initializing Orchestrator Daemon subsystems...';
340
+
341
+ // Dynamically import OrchestratorDaemon at runtime to avoid TypeScript rootDir issues
342
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
343
+ let daemon: any;
344
+ try {
345
+ // Use require for CommonJS compatibility or dynamic import for ESM
346
+ const daemonModulePath = require
347
+ .resolve('@wundr/orchestrator-daemon')
348
+ .replace(/\.js$/, '');
349
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
350
+ const daemonModule = require(daemonModulePath);
351
+ const OrchestratorDaemon =
352
+ daemonModule.OrchestratorDaemon ||
353
+ daemonModule.default?.OrchestratorDaemon;
354
+
355
+ if (!OrchestratorDaemon) {
356
+ throw new Error('OrchestratorDaemon class not found in module');
357
+ }
358
+
359
+ daemon = new OrchestratorDaemon({
360
+ name: config.daemon.name,
361
+ port: config.daemon.port,
362
+ host: config.daemon.host,
363
+ maxSessions: config.daemon.maxSessions,
364
+ heartbeatInterval: config.daemon.heartbeatInterval,
365
+ shutdownTimeout: config.daemon.shutdownTimeout,
366
+ verbose: options.verbose ?? false,
367
+ });
368
+ } catch (importError) {
369
+ // Fallback: try to spawn the daemon as a subprocess
370
+ spinner.fail('Failed to load Orchestrator Daemon module');
371
+ console.error(
372
+ chalk.red('\nThe Orchestrator Daemon module could not be loaded.')
373
+ );
374
+ console.error(chalk.gray('Options to resolve:'));
375
+ console.error(
376
+ chalk.white(' 1. Install: npm install @wundr/orchestrator-daemon')
377
+ );
378
+ console.error(
379
+ chalk.white(
380
+ ' 2. Or build from source: cd scripts/orchestrator-daemon && npm run build'
381
+ )
382
+ );
383
+ console.error(
384
+ chalk.gray(
385
+ `\nError: ${importError instanceof Error ? importError.message : String(importError)}`
386
+ )
387
+ );
388
+ return;
389
+ }
390
+
391
+ // Write PID file
392
+ await fs.writeFile(ORCHESTRATOR_PID_FILE, String(process.pid));
393
+
394
+ // Set up status updates
395
+ const updateStatus = () => {
396
+ const status = daemon.getStatus();
397
+ const statusData = {
398
+ uptime: status.uptime,
399
+ sessionCount: status.metrics?.activeSessions ?? 0,
400
+ queueDepth: 0,
401
+ health: status.status,
402
+ subsystems: Object.fromEntries(
403
+ Object.entries(status.subsystems ?? {}).map(
404
+ ([k, v]: [string, unknown]) => [
405
+ k,
406
+ (v as { status?: string })?.status ?? 'unknown',
407
+ ]
408
+ )
409
+ ),
410
+ };
411
+ fs.writeFile(
412
+ path.join(ORCHESTRATOR_CONFIG_DIR, 'status.json'),
413
+ JSON.stringify(statusData, null, 2)
414
+ ).catch(() => {});
415
+ };
416
+
417
+ daemon.on('healthCheck', updateStatus);
418
+
419
+ // Set up cleanup handlers
420
+ const cleanup = async () => {
421
+ await fs.unlink(ORCHESTRATOR_PID_FILE).catch(() => {});
422
+ await fs
423
+ .unlink(path.join(ORCHESTRATOR_CONFIG_DIR, 'status.json'))
424
+ .catch(() => {});
425
+ };
426
+
427
+ daemon.on('stopped', cleanup);
428
+
429
+ // Start daemon
430
+ await daemon.start();
431
+
432
+ spinner.succeed(
433
+ `Orchestrator Daemon started successfully on ${config.daemon.host}:${config.daemon.port}`
434
+ );
435
+
436
+ console.log(chalk.gray('\nDaemon Information:'));
437
+ console.log(chalk.white(` PID: ${process.pid}`));
438
+ console.log(chalk.white(` Port: ${config.daemon.port}`));
439
+ console.log(chalk.white(` Host: ${config.daemon.host}`));
440
+ console.log(chalk.white(` Config: ${ORCHESTRATOR_CONFIG_FILE}`));
441
+ console.log(chalk.white(` Logs: ${ORCHESTRATOR_LOG_FILE}`));
442
+
443
+ if (options.verbose) {
444
+ console.log(chalk.gray('\nVerbose mode enabled - showing detailed logs'));
445
+ }
446
+
447
+ console.log(chalk.green('\nPress Ctrl+C to stop the daemon.\n'));
448
+
449
+ // Keep process running
450
+ if (!options.detach) {
451
+ await new Promise<void>(resolve => {
452
+ daemon.on('stopped', resolve);
453
+ });
454
+ }
455
+ } catch (error) {
456
+ spinner.fail('Failed to start Orchestrator Daemon');
457
+ console.error(
458
+ chalk.red(error instanceof Error ? error.message : String(error))
459
+ );
460
+ }
461
+ }
462
+
463
+ async function showStatus(options: { json?: boolean }): Promise<void> {
464
+ const spinner = ora('Checking Orchestrator Daemon status...').start();
465
+
466
+ try {
467
+ const status = await getDaemonStatus();
468
+ const config = await loadConfig();
469
+
470
+ spinner.stop();
471
+
472
+ if (options.json) {
473
+ console.log(
474
+ JSON.stringify(
475
+ {
476
+ ...status,
477
+ config: {
478
+ port: config.daemon.port,
479
+ host: config.daemon.host,
480
+ maxSessions: config.daemon.maxSessions,
481
+ },
482
+ },
483
+ null,
484
+ 2
485
+ )
486
+ );
487
+ return;
488
+ }
489
+
490
+ console.log(chalk.cyan('\nOrchestrator Daemon Status\n'));
491
+ console.log(chalk.gray('='.repeat(50)));
492
+
493
+ if (status.running) {
494
+ console.log(chalk.green('Status: RUNNING'));
495
+ console.log(chalk.white(`PID: ${status.pid}`));
496
+ console.log(chalk.white(`Host: ${status.host}:${status.port}`));
497
+
498
+ if (status.uptime) {
499
+ console.log(chalk.white(`Uptime: ${status.uptime}`));
500
+ }
501
+
502
+ if (status.sessionCount !== undefined) {
503
+ console.log(chalk.white(`Sessions: ${status.sessionCount}`));
504
+ }
505
+
506
+ if (status.queueDepth !== undefined) {
507
+ console.log(chalk.white(`Queue Depth: ${status.queueDepth}`));
508
+ }
509
+
510
+ if (status.health) {
511
+ const healthColor =
512
+ status.health === 'healthy'
513
+ ? chalk.green
514
+ : status.health === 'degraded'
515
+ ? chalk.yellow
516
+ : chalk.red;
517
+ console.log(
518
+ healthColor(`Health: ${status.health.toUpperCase()}`)
519
+ );
520
+ }
521
+
522
+ if (status.subsystems && Object.keys(status.subsystems).length > 0) {
523
+ console.log(chalk.gray('\nSubsystems:'));
524
+ for (const [name, state] of Object.entries(status.subsystems)) {
525
+ const stateColor =
526
+ state === 'running'
527
+ ? chalk.green
528
+ : state === 'error'
529
+ ? chalk.red
530
+ : chalk.yellow;
531
+ console.log(` ${chalk.white(name.padEnd(15))} ${stateColor(state)}`);
532
+ }
533
+ }
534
+ } else {
535
+ console.log(chalk.yellow('Status: STOPPED'));
536
+ console.log(chalk.gray('\nDaemon is not running.'));
537
+ console.log(chalk.gray('Start it with: wundr orchestrator start'));
538
+ }
539
+
540
+ console.log(chalk.gray('\n' + '='.repeat(50)));
541
+ console.log(chalk.gray(`Config: ${ORCHESTRATOR_CONFIG_FILE}`));
542
+ console.log('');
543
+ } catch (error) {
544
+ spinner.fail('Failed to get daemon status');
545
+ console.error(
546
+ chalk.red(error instanceof Error ? error.message : String(error))
547
+ );
548
+ }
549
+ }
550
+
551
+ async function stopDaemon(options: {
552
+ force?: boolean;
553
+ timeout?: string;
554
+ }): Promise<void> {
555
+ const spinner = ora('Stopping Orchestrator Daemon...').start();
556
+
557
+ try {
558
+ const status = await getDaemonStatus();
559
+
560
+ if (!status.running || !status.pid) {
561
+ spinner.info('Orchestrator Daemon is not running');
562
+ return;
563
+ }
564
+
565
+ const pid = status.pid;
566
+ const timeout = parseInt(options.timeout ?? '10000', 10);
567
+
568
+ if (options.force) {
569
+ spinner.text = 'Force stopping daemon...';
570
+ process.kill(pid, 'SIGKILL');
571
+ await fs.unlink(ORCHESTRATOR_PID_FILE).catch(() => {});
572
+ await fs
573
+ .unlink(path.join(ORCHESTRATOR_CONFIG_DIR, 'status.json'))
574
+ .catch(() => {});
575
+ spinner.succeed('Orchestrator Daemon force stopped');
576
+ return;
577
+ }
578
+
579
+ // Send SIGTERM for graceful shutdown
580
+ spinner.text = `Sending shutdown signal (timeout: ${timeout}ms)...`;
581
+ process.kill(pid, 'SIGTERM');
582
+
583
+ // Wait for process to exit
584
+ const startTime = Date.now();
585
+ while (Date.now() - startTime < timeout) {
586
+ try {
587
+ process.kill(pid, 0);
588
+ await new Promise(resolve => setTimeout(resolve, 500));
589
+ } catch {
590
+ // Process exited
591
+ await fs.unlink(ORCHESTRATOR_PID_FILE).catch(() => {});
592
+ await fs
593
+ .unlink(path.join(ORCHESTRATOR_CONFIG_DIR, 'status.json'))
594
+ .catch(() => {});
595
+ spinner.succeed('Orchestrator Daemon stopped gracefully');
596
+ return;
597
+ }
598
+ }
599
+
600
+ // Timeout reached, force kill
601
+ spinner.text = 'Graceful shutdown timed out, forcing stop...';
602
+ process.kill(pid, 'SIGKILL');
603
+ await fs.unlink(ORCHESTRATOR_PID_FILE).catch(() => {});
604
+ await fs
605
+ .unlink(path.join(ORCHESTRATOR_CONFIG_DIR, 'status.json'))
606
+ .catch(() => {});
607
+ spinner.warn('Orchestrator Daemon stopped (forced after timeout)');
608
+ } catch (error) {
609
+ spinner.fail('Failed to stop Orchestrator Daemon');
610
+ console.error(
611
+ chalk.red(error instanceof Error ? error.message : String(error))
612
+ );
613
+ }
614
+ }
615
+
616
+ async function showConfig(options: { json?: boolean }): Promise<void> {
617
+ try {
618
+ const config = await loadConfig();
619
+
620
+ console.log(chalk.cyan('\nOrchestrator Daemon Configuration\n'));
621
+ console.log(chalk.gray(`File: ${ORCHESTRATOR_CONFIG_FILE}`));
622
+ console.log(chalk.gray('='.repeat(50) + '\n'));
623
+
624
+ if (options.json) {
625
+ console.log(JSON.stringify(config, null, 2));
626
+ } else {
627
+ console.log(YAML.stringify(config));
628
+ }
629
+ } catch (error) {
630
+ console.error(
631
+ chalk.red(error instanceof Error ? error.message : String(error))
632
+ );
633
+ }
634
+ }
635
+
636
+ async function setConfig(keyValue: string): Promise<void> {
637
+ try {
638
+ const [keyPath, ...valueParts] = keyValue.split('=');
639
+ const valueStr = valueParts.join('=');
640
+
641
+ if (!keyPath || valueStr === undefined) {
642
+ console.error(
643
+ chalk.red(
644
+ 'Invalid format. Use: wundr orchestrator config set <key>=<value>'
645
+ )
646
+ );
647
+ console.error(
648
+ chalk.gray('Example: wundr orchestrator config set daemon.port=9000')
649
+ );
650
+ return;
651
+ }
652
+
653
+ const config = await loadConfig();
654
+ const keys = keyPath.split('.');
655
+
656
+ // Navigate to parent object
657
+ let obj: Record<string, unknown> = config as unknown as Record<
658
+ string,
659
+ unknown
660
+ >;
661
+ for (let i = 0; i < keys.length - 1; i++) {
662
+ const key = keys[i];
663
+ if (key && typeof obj[key] === 'object' && obj[key] !== null) {
664
+ obj = obj[key] as Record<string, unknown>;
665
+ } else {
666
+ console.error(chalk.red(`Invalid key path: ${keyPath}`));
667
+ return;
668
+ }
669
+ }
670
+
671
+ const lastKey = keys[keys.length - 1];
672
+ if (!lastKey) {
673
+ console.error(chalk.red('Invalid key path'));
674
+ return;
675
+ }
676
+
677
+ // Parse and set value
678
+ let value: unknown;
679
+ try {
680
+ value = JSON.parse(valueStr);
681
+ } catch {
682
+ value = valueStr;
683
+ }
684
+
685
+ const oldValue = obj[lastKey];
686
+ obj[lastKey] = value;
687
+
688
+ await saveConfig(config);
689
+
690
+ console.log(chalk.green('Configuration updated:'));
691
+ console.log(
692
+ chalk.white(
693
+ ` ${keyPath}: ${JSON.stringify(oldValue)} -> ${JSON.stringify(value)}`
694
+ )
695
+ );
696
+ console.log(chalk.gray('\nRestart the daemon for changes to take effect.'));
697
+ } catch (error) {
698
+ console.error(
699
+ chalk.red(error instanceof Error ? error.message : String(error))
700
+ );
701
+ }
702
+ }
703
+
704
+ async function resetConfig(options: { force?: boolean }): Promise<void> {
705
+ try {
706
+ if (!options.force) {
707
+ // Dynamic import for inquirer
708
+ const inquirer = await import('inquirer');
709
+ const answers = await inquirer.default.prompt([
710
+ {
711
+ type: 'confirm',
712
+ name: 'confirm',
713
+ message: 'Reset Orchestrator Daemon configuration to defaults?',
714
+ default: false,
715
+ },
716
+ ]);
717
+
718
+ if (!answers.confirm) {
719
+ console.log(chalk.yellow('Cancelled.'));
720
+ return;
721
+ }
722
+ }
723
+
724
+ const config = getDefaultConfig();
725
+ await saveConfig(config);
726
+
727
+ console.log(chalk.green('Configuration reset to defaults.'));
728
+ console.log(chalk.gray(`Saved to: ${ORCHESTRATOR_CONFIG_FILE}`));
729
+ } catch (error) {
730
+ console.error(
731
+ chalk.red(error instanceof Error ? error.message : String(error))
732
+ );
733
+ }
734
+ }
735
+
736
+ async function viewLogs(options: {
737
+ follow?: boolean;
738
+ lines?: string;
739
+ }): Promise<void> {
740
+ const lines = parseInt(options.lines ?? '50', 10);
741
+
742
+ if (!existsSync(ORCHESTRATOR_LOG_FILE)) {
743
+ console.log(chalk.yellow('No log file found.'));
744
+ console.log(chalk.gray(`Expected location: ${ORCHESTRATOR_LOG_FILE}`));
745
+ console.log(
746
+ chalk.gray('Start the daemon with --verbose to enable logging.')
747
+ );
748
+ return;
749
+ }
750
+
751
+ try {
752
+ if (options.follow) {
753
+ console.log(
754
+ chalk.cyan(`Following logs from ${ORCHESTRATOR_LOG_FILE}...`)
755
+ );
756
+ console.log(chalk.gray('Press Ctrl+C to stop.\n'));
757
+
758
+ const { spawn } = await import('child_process');
759
+ const tail = spawn(
760
+ 'tail',
761
+ ['-f', '-n', String(lines), ORCHESTRATOR_LOG_FILE],
762
+ {
763
+ stdio: 'inherit',
764
+ }
765
+ );
766
+
767
+ await new Promise<void>(resolve => {
768
+ process.on('SIGINT', () => {
769
+ tail.kill();
770
+ resolve();
771
+ });
772
+ tail.on('close', () => resolve());
773
+ });
774
+ } else {
775
+ const content = await fs.readFile(ORCHESTRATOR_LOG_FILE, 'utf-8');
776
+ const logLines = content.split('\n');
777
+ const lastLines = logLines.slice(-lines).join('\n');
778
+
779
+ console.log(
780
+ chalk.cyan(`Last ${lines} lines from ${ORCHESTRATOR_LOG_FILE}:\n`)
781
+ );
782
+ console.log(lastLines);
783
+ }
784
+ } catch (error) {
785
+ console.error(
786
+ chalk.red(error instanceof Error ? error.message : String(error))
787
+ );
788
+ }
789
+ }