@appkit/llamacpp-cli 1.11.0 → 1.12.1

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 (126) hide show
  1. package/README.md +572 -170
  2. package/dist/cli.js +99 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/admin/config.d.ts +10 -0
  5. package/dist/commands/admin/config.d.ts.map +1 -0
  6. package/dist/commands/admin/config.js +100 -0
  7. package/dist/commands/admin/config.js.map +1 -0
  8. package/dist/commands/admin/logs.d.ts +10 -0
  9. package/dist/commands/admin/logs.d.ts.map +1 -0
  10. package/dist/commands/admin/logs.js +114 -0
  11. package/dist/commands/admin/logs.js.map +1 -0
  12. package/dist/commands/admin/restart.d.ts +2 -0
  13. package/dist/commands/admin/restart.d.ts.map +1 -0
  14. package/dist/commands/admin/restart.js +29 -0
  15. package/dist/commands/admin/restart.js.map +1 -0
  16. package/dist/commands/admin/start.d.ts +2 -0
  17. package/dist/commands/admin/start.d.ts.map +1 -0
  18. package/dist/commands/admin/start.js +30 -0
  19. package/dist/commands/admin/start.js.map +1 -0
  20. package/dist/commands/admin/status.d.ts +2 -0
  21. package/dist/commands/admin/status.d.ts.map +1 -0
  22. package/dist/commands/admin/status.js +82 -0
  23. package/dist/commands/admin/status.js.map +1 -0
  24. package/dist/commands/admin/stop.d.ts +2 -0
  25. package/dist/commands/admin/stop.d.ts.map +1 -0
  26. package/dist/commands/admin/stop.js +21 -0
  27. package/dist/commands/admin/stop.js.map +1 -0
  28. package/dist/commands/logs.d.ts +1 -0
  29. package/dist/commands/logs.d.ts.map +1 -1
  30. package/dist/commands/logs.js +22 -0
  31. package/dist/commands/logs.js.map +1 -1
  32. package/dist/lib/admin-manager.d.ts +111 -0
  33. package/dist/lib/admin-manager.d.ts.map +1 -0
  34. package/dist/lib/admin-manager.js +413 -0
  35. package/dist/lib/admin-manager.js.map +1 -0
  36. package/dist/lib/admin-server.d.ts +148 -0
  37. package/dist/lib/admin-server.d.ts.map +1 -0
  38. package/dist/lib/admin-server.js +1161 -0
  39. package/dist/lib/admin-server.js.map +1 -0
  40. package/dist/lib/download-job-manager.d.ts +64 -0
  41. package/dist/lib/download-job-manager.d.ts.map +1 -0
  42. package/dist/lib/download-job-manager.js +164 -0
  43. package/dist/lib/download-job-manager.js.map +1 -0
  44. package/dist/tui/MultiServerMonitorApp.js +1 -1
  45. package/dist/types/admin-config.d.ts +19 -0
  46. package/dist/types/admin-config.d.ts.map +1 -0
  47. package/dist/types/admin-config.js +3 -0
  48. package/dist/types/admin-config.js.map +1 -0
  49. package/dist/utils/log-parser.d.ts +9 -0
  50. package/dist/utils/log-parser.d.ts.map +1 -1
  51. package/dist/utils/log-parser.js +11 -0
  52. package/dist/utils/log-parser.js.map +1 -1
  53. package/package.json +10 -2
  54. package/web/README.md +429 -0
  55. package/web/dist/assets/index-Bin89Lwr.css +1 -0
  56. package/web/dist/assets/index-CVmonw3T.js +17 -0
  57. package/web/dist/index.html +14 -0
  58. package/web/dist/vite.svg +1 -0
  59. package/.versionrc.json +0 -16
  60. package/CHANGELOG.md +0 -203
  61. package/MONITORING-ACCURACY-FIX.md +0 -199
  62. package/PER-PROCESS-METRICS.md +0 -190
  63. package/docs/images/.gitkeep +0 -1
  64. package/src/cli.ts +0 -423
  65. package/src/commands/config-global.ts +0 -38
  66. package/src/commands/config.ts +0 -323
  67. package/src/commands/create.ts +0 -183
  68. package/src/commands/delete.ts +0 -74
  69. package/src/commands/list.ts +0 -37
  70. package/src/commands/logs-all.ts +0 -251
  71. package/src/commands/logs.ts +0 -321
  72. package/src/commands/monitor.ts +0 -110
  73. package/src/commands/ps.ts +0 -84
  74. package/src/commands/pull.ts +0 -44
  75. package/src/commands/rm.ts +0 -107
  76. package/src/commands/router/config.ts +0 -116
  77. package/src/commands/router/logs.ts +0 -256
  78. package/src/commands/router/restart.ts +0 -36
  79. package/src/commands/router/start.ts +0 -60
  80. package/src/commands/router/status.ts +0 -119
  81. package/src/commands/router/stop.ts +0 -33
  82. package/src/commands/run.ts +0 -233
  83. package/src/commands/search.ts +0 -107
  84. package/src/commands/server-show.ts +0 -161
  85. package/src/commands/show.ts +0 -207
  86. package/src/commands/start.ts +0 -101
  87. package/src/commands/stop.ts +0 -39
  88. package/src/commands/tui.ts +0 -25
  89. package/src/lib/config-generator.ts +0 -130
  90. package/src/lib/history-manager.ts +0 -172
  91. package/src/lib/launchctl-manager.ts +0 -225
  92. package/src/lib/metrics-aggregator.ts +0 -257
  93. package/src/lib/model-downloader.ts +0 -328
  94. package/src/lib/model-scanner.ts +0 -157
  95. package/src/lib/model-search.ts +0 -114
  96. package/src/lib/models-dir-setup.ts +0 -46
  97. package/src/lib/port-manager.ts +0 -80
  98. package/src/lib/router-logger.ts +0 -201
  99. package/src/lib/router-manager.ts +0 -414
  100. package/src/lib/router-server.ts +0 -538
  101. package/src/lib/state-manager.ts +0 -206
  102. package/src/lib/status-checker.ts +0 -113
  103. package/src/lib/system-collector.ts +0 -315
  104. package/src/tui/ConfigApp.ts +0 -1085
  105. package/src/tui/HistoricalMonitorApp.ts +0 -587
  106. package/src/tui/ModelsApp.ts +0 -368
  107. package/src/tui/MonitorApp.ts +0 -386
  108. package/src/tui/MultiServerMonitorApp.ts +0 -1833
  109. package/src/tui/RootNavigator.ts +0 -74
  110. package/src/tui/SearchApp.ts +0 -511
  111. package/src/tui/SplashScreen.ts +0 -149
  112. package/src/types/global-config.ts +0 -26
  113. package/src/types/history-types.ts +0 -39
  114. package/src/types/model-info.ts +0 -8
  115. package/src/types/monitor-types.ts +0 -162
  116. package/src/types/router-config.ts +0 -25
  117. package/src/types/server-config.ts +0 -46
  118. package/src/utils/downsample-utils.ts +0 -128
  119. package/src/utils/file-utils.ts +0 -146
  120. package/src/utils/format-utils.ts +0 -98
  121. package/src/utils/log-parser.ts +0 -271
  122. package/src/utils/log-utils.ts +0 -178
  123. package/src/utils/process-utils.ts +0 -316
  124. package/src/utils/prompt-utils.ts +0 -47
  125. package/test-load.sh +0 -100
  126. package/tsconfig.json +0 -20
@@ -1,80 +0,0 @@
1
- import { isPortInUse } from '../utils/process-utils';
2
- import { stateManager } from './state-manager';
3
-
4
- export class PortManager {
5
- private readonly portRangeStart = 9000;
6
- private readonly portRangeEnd = 9999;
7
-
8
- /**
9
- * Find an available port in the range
10
- */
11
- async findAvailablePort(startPort?: number): Promise<number> {
12
- const start = startPort || this.portRangeStart;
13
-
14
- // Get ports used by existing servers
15
- const usedPorts = await stateManager.getUsedPorts();
16
-
17
- // Find first available port
18
- for (let port = start; port <= this.portRangeEnd; port++) {
19
- if (!usedPorts.has(port)) {
20
- // Check if port is actually available (not used by other processes)
21
- const inUse = await isPortInUse(port);
22
- if (!inUse) {
23
- return port;
24
- }
25
- }
26
- }
27
-
28
- throw new Error(`No available ports in range ${start}-${this.portRangeEnd}`);
29
- }
30
-
31
- /**
32
- * Check if a port is available
33
- */
34
- async isPortAvailable(port: number): Promise<boolean> {
35
- // Check if port is in valid range
36
- if (port < 1024 || port > 65535) {
37
- return false;
38
- }
39
-
40
- // Check if port is used by any server
41
- const usedPorts = await stateManager.getUsedPorts();
42
- if (usedPorts.has(port)) {
43
- return false;
44
- }
45
-
46
- // Check if port is actually in use
47
- return !(await isPortInUse(port));
48
- }
49
-
50
- /**
51
- * Validate a port number
52
- */
53
- validatePort(port: number): void {
54
- if (port < 1024) {
55
- throw new Error('Port must be >= 1024 (ports below 1024 require root)');
56
- }
57
- if (port > 65535) {
58
- throw new Error('Port must be <= 65535');
59
- }
60
- }
61
-
62
- /**
63
- * Find a server using a given port
64
- */
65
- async findServerByPort(port: number) {
66
- return await stateManager.findServerByPort(port);
67
- }
68
-
69
- /**
70
- * Check for port conflicts
71
- */
72
- async checkPortConflict(port: number, exceptId?: string): Promise<boolean> {
73
- const servers = await stateManager.getAllServers();
74
- const conflict = servers.find((s) => s.port === port && s.id !== exceptId);
75
- return conflict !== undefined;
76
- }
77
- }
78
-
79
- // Export singleton instance
80
- export const portManager = new PortManager();
@@ -1,201 +0,0 @@
1
- import * as fs from 'fs/promises';
2
- import * as path from 'path';
3
- import { getLogsDir } from '../utils/file-utils';
4
-
5
- export interface RouterLogEntry {
6
- timestamp: string;
7
- model: string;
8
- endpoint: string;
9
- method: string;
10
- status: 'success' | 'error';
11
- statusCode: number;
12
- durationMs: number;
13
- error?: string;
14
- backend?: string; // e.g., "localhost:9001"
15
- prompt?: string; // First part of the prompt/message
16
- }
17
-
18
- export class RouterLogger {
19
- private logFilePath: string;
20
- private verbose: boolean;
21
-
22
- constructor(verbose: boolean = false) {
23
- this.verbose = verbose;
24
- this.logFilePath = path.join(getLogsDir(), 'router.log');
25
- }
26
-
27
- /**
28
- * Log a request with timing and outcome
29
- */
30
- async logRequest(entry: RouterLogEntry): Promise<void> {
31
- // Human-readable format for console
32
- const humanLog = this.formatHumanReadable(entry);
33
-
34
- // Output request activity to stdout (separate from system messages on stderr)
35
- console.log(humanLog);
36
-
37
- // Verbose mode: append detailed JSON to log file
38
- if (this.verbose) {
39
- const jsonLog = JSON.stringify(entry) + '\n';
40
- try {
41
- await fs.appendFile(this.logFilePath, jsonLog, 'utf-8');
42
- } catch (error) {
43
- console.error('[Router Logger] Failed to write to log file:', error);
44
- }
45
- }
46
- }
47
-
48
- /**
49
- * Format log entry for human reading (console output)
50
- */
51
- private formatHumanReadable(entry: RouterLogEntry): string {
52
- const { timestamp, model, endpoint, method, status, statusCode, durationMs, error, backend, prompt } = entry;
53
-
54
- // Color coding based on status (using ANSI codes)
55
- const statusColor = status === 'success' ? '\x1b[32m' : '\x1b[31m'; // Green or Red
56
- const resetColor = '\x1b[0m';
57
-
58
- // Base log format (no [Router] prefix, no icons)
59
- let log = `${statusColor}${statusCode}${resetColor} ${method} ${endpoint} → ${model}`;
60
-
61
- // Add backend if available
62
- if (backend) {
63
- log += ` (${backend})`;
64
- }
65
-
66
- // Add duration
67
- log += ` ${durationMs}ms`;
68
-
69
- // Add prompt preview if available
70
- if (prompt) {
71
- log += ` | "${prompt}"`;
72
- }
73
-
74
- // Add error if present
75
- if (error) {
76
- log += ` | Error: ${error}`;
77
- }
78
-
79
- return log;
80
- }
81
-
82
- /**
83
- * Format log entry for LLM parsing (verbose JSON format)
84
- */
85
- static formatForLLM(entry: RouterLogEntry): string {
86
- return JSON.stringify(entry, null, 2);
87
- }
88
-
89
- /**
90
- * Read log file and return all entries (for verbose mode)
91
- */
92
- async readLogs(limit?: number): Promise<RouterLogEntry[]> {
93
- try {
94
- const content = await fs.readFile(this.logFilePath, 'utf-8');
95
- const lines = content.trim().split('\n').filter(line => line);
96
-
97
- // Parse JSON entries
98
- const entries = lines
99
- .map(line => {
100
- try {
101
- return JSON.parse(line) as RouterLogEntry;
102
- } catch {
103
- return null;
104
- }
105
- })
106
- .filter((entry): entry is RouterLogEntry => entry !== null);
107
-
108
- // Apply limit if specified
109
- if (limit && limit > 0) {
110
- return entries.slice(-limit);
111
- }
112
-
113
- return entries;
114
- } catch (error) {
115
- // Log file doesn't exist or can't be read
116
- return [];
117
- }
118
- }
119
-
120
- /**
121
- * Clear the log file
122
- */
123
- async clearLogs(): Promise<void> {
124
- try {
125
- await fs.writeFile(this.logFilePath, '', 'utf-8');
126
- console.error('[Router Logger] Log file cleared');
127
- } catch (error) {
128
- console.error('[Router Logger] Failed to clear log file:', error);
129
- }
130
- }
131
-
132
- /**
133
- * Get log file size
134
- */
135
- async getLogFileSize(): Promise<number> {
136
- try {
137
- const stats = await fs.stat(this.logFilePath);
138
- return stats.size;
139
- } catch {
140
- return 0;
141
- }
142
- }
143
-
144
- /**
145
- * Rotate log file if it exceeds threshold
146
- */
147
- async rotateIfNeeded(thresholdMB: number = 100): Promise<boolean> {
148
- const size = await this.getLogFileSize();
149
- const thresholdBytes = thresholdMB * 1024 * 1024;
150
-
151
- if (size > thresholdBytes) {
152
- try {
153
- // Generate timestamp
154
- const timestamp = new Date()
155
- .toISOString()
156
- .replace(/T/, '-')
157
- .replace(/:/g, '-')
158
- .replace(/\..+/, '');
159
-
160
- const logsDir = getLogsDir();
161
- const archivedPath = path.join(logsDir, `router.${timestamp}.log`);
162
-
163
- // Rename current log to archived version
164
- await fs.rename(this.logFilePath, archivedPath);
165
-
166
- console.error(`[Router Logger] Rotated log file to ${archivedPath}`);
167
- return true;
168
- } catch (error) {
169
- console.error('[Router Logger] Failed to rotate log file:', error);
170
- return false;
171
- }
172
- }
173
-
174
- return false;
175
- }
176
- }
177
-
178
- /**
179
- * Utility class for tracking request timing
180
- */
181
- export class RequestTimer {
182
- private startTime: number;
183
-
184
- constructor() {
185
- this.startTime = Date.now();
186
- }
187
-
188
- /**
189
- * Get elapsed time in milliseconds
190
- */
191
- elapsed(): number {
192
- return Date.now() - this.startTime;
193
- }
194
-
195
- /**
196
- * Get current ISO timestamp
197
- */
198
- static now(): string {
199
- return new Date().toISOString();
200
- }
201
- }
@@ -1,414 +0,0 @@
1
- import * as path from 'path';
2
- import * as fs from 'fs/promises';
3
- import { RouterConfig } from '../types/router-config';
4
- import { execCommand, execAsync } from '../utils/process-utils';
5
- import {
6
- ensureDir,
7
- writeJsonAtomic,
8
- readJson,
9
- fileExists,
10
- getConfigDir,
11
- getLogsDir,
12
- getLaunchAgentsDir,
13
- writeFileAtomic,
14
- } from '../utils/file-utils';
15
-
16
- export interface RouterServiceStatus {
17
- isRunning: boolean;
18
- pid: number | null;
19
- exitCode: number | null;
20
- lastExitReason?: string;
21
- }
22
-
23
- export class RouterManager {
24
- private configDir: string;
25
- private logsDir: string;
26
- private configPath: string;
27
- private launchAgentsDir: string;
28
-
29
- constructor() {
30
- this.configDir = getConfigDir();
31
- this.logsDir = getLogsDir();
32
- this.configPath = path.join(this.configDir, 'router.json');
33
- this.launchAgentsDir = getLaunchAgentsDir();
34
- }
35
-
36
- /**
37
- * Initialize router directories
38
- */
39
- async initialize(): Promise<void> {
40
- await ensureDir(this.configDir);
41
- await ensureDir(this.logsDir);
42
- await ensureDir(this.launchAgentsDir);
43
- }
44
-
45
- /**
46
- * Get default router configuration
47
- */
48
- getDefaultConfig(): RouterConfig {
49
- return {
50
- id: 'router',
51
- port: 9100,
52
- host: '127.0.0.1',
53
- label: 'com.llama.router',
54
- plistPath: path.join(this.launchAgentsDir, 'com.llama.router.plist'),
55
- stdoutPath: path.join(this.logsDir, 'router.stdout'),
56
- stderrPath: path.join(this.logsDir, 'router.stderr'),
57
- healthCheckInterval: 5000,
58
- requestTimeout: 120000,
59
- verbose: false,
60
- status: 'stopped',
61
- createdAt: new Date().toISOString(),
62
- };
63
- }
64
-
65
- /**
66
- * Load router configuration
67
- */
68
- async loadConfig(): Promise<RouterConfig | null> {
69
- if (!(await fileExists(this.configPath))) {
70
- return null;
71
- }
72
- return await readJson<RouterConfig>(this.configPath);
73
- }
74
-
75
- /**
76
- * Save router configuration
77
- */
78
- async saveConfig(config: RouterConfig): Promise<void> {
79
- await writeJsonAtomic(this.configPath, config);
80
- }
81
-
82
- /**
83
- * Update router configuration with partial changes
84
- */
85
- async updateConfig(updates: Partial<RouterConfig>): Promise<void> {
86
- const existingConfig = await this.loadConfig();
87
- if (!existingConfig) {
88
- throw new Error('Router configuration not found');
89
- }
90
- const updatedConfig = { ...existingConfig, ...updates };
91
- await this.saveConfig(updatedConfig);
92
- }
93
-
94
- /**
95
- * Delete router configuration
96
- */
97
- async deleteConfig(): Promise<void> {
98
- if (await fileExists(this.configPath)) {
99
- await fs.unlink(this.configPath);
100
- }
101
- }
102
-
103
- /**
104
- * Generate plist XML content for the router
105
- */
106
- generatePlist(config: RouterConfig): string {
107
- // Find the compiled router-server.js file
108
- // In dev mode (tsx), __dirname is src/lib/
109
- // In production, __dirname is dist/lib/
110
- // Always use the compiled dist version for launchctl
111
- let routerServerPath: string;
112
- if (__dirname.includes('/src/')) {
113
- // Dev mode - point to dist/lib/router-server.js
114
- const projectRoot = path.resolve(__dirname, '../..');
115
- routerServerPath = path.join(projectRoot, 'dist/lib/router-server.js');
116
- } else {
117
- // Production mode - already in dist/lib/
118
- routerServerPath = path.join(__dirname, 'router-server.js');
119
- }
120
-
121
- // Use the current Node.js executable path (resolves symlinks)
122
- const nodePath = process.execPath;
123
-
124
- const args = [
125
- nodePath,
126
- routerServerPath,
127
- '--config', this.configPath,
128
- ];
129
-
130
- const argsXml = args.map(arg => ` <string>${arg}</string>`).join('\n');
131
-
132
- return `<?xml version="1.0" encoding="UTF-8"?>
133
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
134
- "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
135
- <plist version="1.0">
136
- <dict>
137
- <key>Label</key>
138
- <string>${config.label}</string>
139
-
140
- <key>ProgramArguments</key>
141
- <array>
142
- ${argsXml}
143
- </array>
144
-
145
- <key>RunAtLoad</key>
146
- <false/>
147
-
148
- <key>KeepAlive</key>
149
- <dict>
150
- <key>Crashed</key>
151
- <true/>
152
- <key>SuccessfulExit</key>
153
- <false/>
154
- </dict>
155
-
156
- <key>StandardOutPath</key>
157
- <string>${config.stdoutPath}</string>
158
-
159
- <key>StandardErrorPath</key>
160
- <string>${config.stderrPath}</string>
161
-
162
- <key>WorkingDirectory</key>
163
- <string>/tmp</string>
164
-
165
- <key>ThrottleInterval</key>
166
- <integer>10</integer>
167
- </dict>
168
- </plist>
169
- `;
170
- }
171
-
172
- /**
173
- * Create and write plist file
174
- */
175
- async createPlist(config: RouterConfig): Promise<void> {
176
- const plistContent = this.generatePlist(config);
177
- await writeFileAtomic(config.plistPath, plistContent);
178
- }
179
-
180
- /**
181
- * Delete plist file
182
- */
183
- async deletePlist(config: RouterConfig): Promise<void> {
184
- if (await fileExists(config.plistPath)) {
185
- await fs.unlink(config.plistPath);
186
- }
187
- }
188
-
189
- /**
190
- * Load service (register with launchctl)
191
- */
192
- async loadService(plistPath: string): Promise<void> {
193
- await execCommand(`launchctl load "${plistPath}"`);
194
- }
195
-
196
- /**
197
- * Unload service (unregister from launchctl)
198
- */
199
- async unloadService(plistPath: string): Promise<void> {
200
- try {
201
- await execCommand(`launchctl unload "${plistPath}"`);
202
- } catch (error) {
203
- // Ignore errors if service is not loaded
204
- }
205
- }
206
-
207
- /**
208
- * Start service
209
- */
210
- async startService(label: string): Promise<void> {
211
- await execCommand(`launchctl start ${label}`);
212
- }
213
-
214
- /**
215
- * Stop service
216
- */
217
- async stopService(label: string): Promise<void> {
218
- await execCommand(`launchctl stop ${label}`);
219
- }
220
-
221
- /**
222
- * Get service status from launchctl
223
- */
224
- async getServiceStatus(label: string): Promise<RouterServiceStatus> {
225
- try {
226
- const { stdout } = await execAsync(`launchctl list | grep ${label}`);
227
- const lines = stdout.trim().split('\n');
228
-
229
- for (const line of lines) {
230
- const parts = line.split(/\s+/);
231
- if (parts.length >= 3) {
232
- const pidStr = parts[0].trim();
233
- const exitCodeStr = parts[1].trim();
234
- const serviceLabel = parts[2].trim();
235
-
236
- if (serviceLabel === label) {
237
- const pid = pidStr !== '-' ? parseInt(pidStr, 10) : null;
238
- const exitCode = exitCodeStr !== '-' ? parseInt(exitCodeStr, 10) : null;
239
- const isRunning = pid !== null;
240
-
241
- return {
242
- isRunning,
243
- pid,
244
- exitCode,
245
- lastExitReason: this.interpretExitCode(exitCode),
246
- };
247
- }
248
- }
249
- }
250
-
251
- return {
252
- isRunning: false,
253
- pid: null,
254
- exitCode: null,
255
- };
256
- } catch (error) {
257
- return {
258
- isRunning: false,
259
- pid: null,
260
- exitCode: null,
261
- };
262
- }
263
- }
264
-
265
- /**
266
- * Interpret exit code to human-readable reason
267
- */
268
- private interpretExitCode(code: number | null): string | undefined {
269
- if (code === null || code === 0) return undefined;
270
- if (code === -9) return 'Force killed (SIGKILL)';
271
- if (code === -15) return 'Terminated (SIGTERM)';
272
- return `Exit code: ${code}`;
273
- }
274
-
275
- /**
276
- * Wait for service to start (with timeout)
277
- */
278
- async waitForServiceStart(label: string, timeoutMs = 5000): Promise<boolean> {
279
- const startTime = Date.now();
280
- while (Date.now() - startTime < timeoutMs) {
281
- const status = await this.getServiceStatus(label);
282
- if (status.isRunning) {
283
- return true;
284
- }
285
- await new Promise((resolve) => setTimeout(resolve, 500));
286
- }
287
- return false;
288
- }
289
-
290
- /**
291
- * Wait for service to stop (with timeout)
292
- */
293
- async waitForServiceStop(label: string, timeoutMs = 5000): Promise<boolean> {
294
- const startTime = Date.now();
295
- while (Date.now() - startTime < timeoutMs) {
296
- const status = await this.getServiceStatus(label);
297
- if (!status.isRunning) {
298
- return true;
299
- }
300
- await new Promise((resolve) => setTimeout(resolve, 500));
301
- }
302
- return false;
303
- }
304
-
305
- /**
306
- * Start router service
307
- */
308
- async start(): Promise<void> {
309
- await this.initialize();
310
-
311
- let config = await this.loadConfig();
312
- if (!config) {
313
- // Create default config
314
- config = this.getDefaultConfig();
315
- await this.saveConfig(config);
316
- }
317
-
318
- // Check if already running
319
- if (config.status === 'running') {
320
- throw new Error('Router is already running');
321
- }
322
-
323
- // Check for throttled state (exit code 78)
324
- const currentStatus = await this.getServiceStatus(config.label);
325
- if (currentStatus.exitCode === 78) {
326
- // Service is throttled - clean up and start fresh
327
- await this.unloadService(config.plistPath);
328
- await this.deletePlist(config);
329
- // Give launchd a moment to clean up
330
- await new Promise((resolve) => setTimeout(resolve, 1000));
331
- }
332
-
333
- // Create plist
334
- await this.createPlist(config);
335
-
336
- // Load and start service
337
- try {
338
- await this.loadService(config.plistPath);
339
- } catch (error) {
340
- // May already be loaded
341
- }
342
-
343
- await this.startService(config.label);
344
-
345
- // Wait for startup
346
- const started = await this.waitForServiceStart(config.label, 5000);
347
- if (!started) {
348
- throw new Error('Router failed to start');
349
- }
350
-
351
- // Update config
352
- const status = await this.getServiceStatus(config.label);
353
- await this.updateConfig({
354
- status: 'running',
355
- pid: status.pid || undefined,
356
- lastStarted: new Date().toISOString(),
357
- });
358
- }
359
-
360
- /**
361
- * Stop router service
362
- */
363
- async stop(): Promise<void> {
364
- const config = await this.loadConfig();
365
- if (!config) {
366
- throw new Error('Router configuration not found');
367
- }
368
-
369
- if (config.status !== 'running') {
370
- throw new Error('Router is not running');
371
- }
372
-
373
- // Unload service
374
- await this.unloadService(config.plistPath);
375
-
376
- // Wait for shutdown
377
- await this.waitForServiceStop(config.label, 5000);
378
-
379
- // Update config
380
- await this.updateConfig({
381
- status: 'stopped',
382
- pid: undefined,
383
- lastStopped: new Date().toISOString(),
384
- });
385
- }
386
-
387
- /**
388
- * Restart router service
389
- */
390
- async restart(): Promise<void> {
391
- try {
392
- await this.stop();
393
- } catch (error) {
394
- // May not be running
395
- }
396
- await this.start();
397
- }
398
-
399
- /**
400
- * Get router status
401
- */
402
- async getStatus(): Promise<{ config: RouterConfig; status: RouterServiceStatus } | null> {
403
- const config = await this.loadConfig();
404
- if (!config) {
405
- return null;
406
- }
407
-
408
- const status = await this.getServiceStatus(config.label);
409
- return { config, status };
410
- }
411
- }
412
-
413
- // Export singleton instance
414
- export const routerManager = new RouterManager();