@appkit/llamacpp-cli 1.12.0 → 1.13.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 (136) hide show
  1. package/README.md +294 -168
  2. package/dist/cli.js +35 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/launch/claude.d.ts +6 -0
  5. package/dist/commands/launch/claude.d.ts.map +1 -0
  6. package/dist/commands/launch/claude.js +277 -0
  7. package/dist/commands/launch/claude.js.map +1 -0
  8. package/dist/lib/integration-checker.d.ts +26 -0
  9. package/dist/lib/integration-checker.d.ts.map +1 -0
  10. package/dist/lib/integration-checker.js +77 -0
  11. package/dist/lib/integration-checker.js.map +1 -0
  12. package/dist/lib/router-manager.d.ts +4 -0
  13. package/dist/lib/router-manager.d.ts.map +1 -1
  14. package/dist/lib/router-manager.js +10 -0
  15. package/dist/lib/router-manager.js.map +1 -1
  16. package/dist/lib/router-server.d.ts +13 -0
  17. package/dist/lib/router-server.d.ts.map +1 -1
  18. package/dist/lib/router-server.js +267 -7
  19. package/dist/lib/router-server.js.map +1 -1
  20. package/dist/types/integration-config.d.ts +28 -0
  21. package/dist/types/integration-config.d.ts.map +1 -0
  22. package/dist/types/integration-config.js +3 -0
  23. package/dist/types/integration-config.js.map +1 -0
  24. package/package.json +10 -2
  25. package/web/dist/assets/index-Bin89Lwr.css +1 -0
  26. package/web/dist/assets/index-CVmonw3T.js +17 -0
  27. package/web/{index.html → dist/index.html} +2 -1
  28. package/.versionrc.json +0 -16
  29. package/CHANGELOG.md +0 -213
  30. package/docs/images/.gitkeep +0 -1
  31. package/docs/images/web-ui-servers.png +0 -0
  32. package/src/cli.ts +0 -523
  33. package/src/commands/admin/config.ts +0 -121
  34. package/src/commands/admin/logs.ts +0 -91
  35. package/src/commands/admin/restart.ts +0 -26
  36. package/src/commands/admin/start.ts +0 -27
  37. package/src/commands/admin/status.ts +0 -84
  38. package/src/commands/admin/stop.ts +0 -16
  39. package/src/commands/config-global.ts +0 -38
  40. package/src/commands/config.ts +0 -323
  41. package/src/commands/create.ts +0 -183
  42. package/src/commands/delete.ts +0 -74
  43. package/src/commands/list.ts +0 -37
  44. package/src/commands/logs-all.ts +0 -251
  45. package/src/commands/logs.ts +0 -345
  46. package/src/commands/monitor.ts +0 -110
  47. package/src/commands/ps.ts +0 -84
  48. package/src/commands/pull.ts +0 -44
  49. package/src/commands/rm.ts +0 -107
  50. package/src/commands/router/config.ts +0 -116
  51. package/src/commands/router/logs.ts +0 -256
  52. package/src/commands/router/restart.ts +0 -36
  53. package/src/commands/router/start.ts +0 -60
  54. package/src/commands/router/status.ts +0 -119
  55. package/src/commands/router/stop.ts +0 -33
  56. package/src/commands/run.ts +0 -233
  57. package/src/commands/search.ts +0 -107
  58. package/src/commands/server-show.ts +0 -161
  59. package/src/commands/show.ts +0 -207
  60. package/src/commands/start.ts +0 -101
  61. package/src/commands/stop.ts +0 -39
  62. package/src/commands/tui.ts +0 -25
  63. package/src/lib/admin-manager.ts +0 -435
  64. package/src/lib/admin-server.ts +0 -1243
  65. package/src/lib/config-generator.ts +0 -130
  66. package/src/lib/download-job-manager.ts +0 -213
  67. package/src/lib/history-manager.ts +0 -172
  68. package/src/lib/launchctl-manager.ts +0 -225
  69. package/src/lib/metrics-aggregator.ts +0 -257
  70. package/src/lib/model-downloader.ts +0 -328
  71. package/src/lib/model-scanner.ts +0 -157
  72. package/src/lib/model-search.ts +0 -114
  73. package/src/lib/models-dir-setup.ts +0 -46
  74. package/src/lib/port-manager.ts +0 -80
  75. package/src/lib/router-logger.ts +0 -201
  76. package/src/lib/router-manager.ts +0 -414
  77. package/src/lib/router-server.ts +0 -538
  78. package/src/lib/state-manager.ts +0 -206
  79. package/src/lib/status-checker.ts +0 -113
  80. package/src/lib/system-collector.ts +0 -315
  81. package/src/tui/ConfigApp.ts +0 -1085
  82. package/src/tui/HistoricalMonitorApp.ts +0 -587
  83. package/src/tui/ModelsApp.ts +0 -368
  84. package/src/tui/MonitorApp.ts +0 -386
  85. package/src/tui/MultiServerMonitorApp.ts +0 -1833
  86. package/src/tui/RootNavigator.ts +0 -74
  87. package/src/tui/SearchApp.ts +0 -511
  88. package/src/tui/SplashScreen.ts +0 -149
  89. package/src/types/admin-config.ts +0 -25
  90. package/src/types/global-config.ts +0 -26
  91. package/src/types/history-types.ts +0 -39
  92. package/src/types/model-info.ts +0 -8
  93. package/src/types/monitor-types.ts +0 -162
  94. package/src/types/router-config.ts +0 -25
  95. package/src/types/server-config.ts +0 -46
  96. package/src/utils/downsample-utils.ts +0 -128
  97. package/src/utils/file-utils.ts +0 -146
  98. package/src/utils/format-utils.ts +0 -98
  99. package/src/utils/log-parser.ts +0 -284
  100. package/src/utils/log-utils.ts +0 -178
  101. package/src/utils/process-utils.ts +0 -316
  102. package/src/utils/prompt-utils.ts +0 -47
  103. package/test-load.sh +0 -100
  104. package/tsconfig.json +0 -20
  105. package/web/eslint.config.js +0 -23
  106. package/web/llamacpp-web-dist.tar.gz +0 -0
  107. package/web/package-lock.json +0 -4017
  108. package/web/package.json +0 -38
  109. package/web/postcss.config.js +0 -6
  110. package/web/src/App.css +0 -42
  111. package/web/src/App.tsx +0 -86
  112. package/web/src/assets/react.svg +0 -1
  113. package/web/src/components/ApiKeyPrompt.tsx +0 -71
  114. package/web/src/components/CreateServerModal.tsx +0 -372
  115. package/web/src/components/DownloadProgress.tsx +0 -123
  116. package/web/src/components/Nav.tsx +0 -89
  117. package/web/src/components/RouterConfigModal.tsx +0 -240
  118. package/web/src/components/SearchModal.tsx +0 -306
  119. package/web/src/components/ServerConfigModal.tsx +0 -291
  120. package/web/src/hooks/useApi.ts +0 -259
  121. package/web/src/index.css +0 -42
  122. package/web/src/lib/api.ts +0 -226
  123. package/web/src/main.tsx +0 -10
  124. package/web/src/pages/Dashboard.tsx +0 -103
  125. package/web/src/pages/Models.tsx +0 -258
  126. package/web/src/pages/Router.tsx +0 -270
  127. package/web/src/pages/RouterLogs.tsx +0 -201
  128. package/web/src/pages/ServerLogs.tsx +0 -553
  129. package/web/src/pages/Servers.tsx +0 -358
  130. package/web/src/types/api.ts +0 -140
  131. package/web/tailwind.config.js +0 -31
  132. package/web/tsconfig.app.json +0 -28
  133. package/web/tsconfig.json +0 -7
  134. package/web/tsconfig.node.json +0 -26
  135. package/web/vite.config.ts +0 -25
  136. /package/web/{public → dist}/vite.svg +0 -0
@@ -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();