@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.
- package/README.md +294 -168
- package/dist/cli.js +35 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/launch/claude.d.ts +6 -0
- package/dist/commands/launch/claude.d.ts.map +1 -0
- package/dist/commands/launch/claude.js +277 -0
- package/dist/commands/launch/claude.js.map +1 -0
- package/dist/lib/integration-checker.d.ts +26 -0
- package/dist/lib/integration-checker.d.ts.map +1 -0
- package/dist/lib/integration-checker.js +77 -0
- package/dist/lib/integration-checker.js.map +1 -0
- package/dist/lib/router-manager.d.ts +4 -0
- package/dist/lib/router-manager.d.ts.map +1 -1
- package/dist/lib/router-manager.js +10 -0
- package/dist/lib/router-manager.js.map +1 -1
- package/dist/lib/router-server.d.ts +13 -0
- package/dist/lib/router-server.d.ts.map +1 -1
- package/dist/lib/router-server.js +267 -7
- package/dist/lib/router-server.js.map +1 -1
- package/dist/types/integration-config.d.ts +28 -0
- package/dist/types/integration-config.d.ts.map +1 -0
- package/dist/types/integration-config.js +3 -0
- package/dist/types/integration-config.js.map +1 -0
- package/package.json +10 -2
- package/web/dist/assets/index-Bin89Lwr.css +1 -0
- package/web/dist/assets/index-CVmonw3T.js +17 -0
- package/web/{index.html → dist/index.html} +2 -1
- package/.versionrc.json +0 -16
- package/CHANGELOG.md +0 -213
- package/docs/images/.gitkeep +0 -1
- package/docs/images/web-ui-servers.png +0 -0
- package/src/cli.ts +0 -523
- package/src/commands/admin/config.ts +0 -121
- package/src/commands/admin/logs.ts +0 -91
- package/src/commands/admin/restart.ts +0 -26
- package/src/commands/admin/start.ts +0 -27
- package/src/commands/admin/status.ts +0 -84
- package/src/commands/admin/stop.ts +0 -16
- package/src/commands/config-global.ts +0 -38
- package/src/commands/config.ts +0 -323
- package/src/commands/create.ts +0 -183
- package/src/commands/delete.ts +0 -74
- package/src/commands/list.ts +0 -37
- package/src/commands/logs-all.ts +0 -251
- package/src/commands/logs.ts +0 -345
- package/src/commands/monitor.ts +0 -110
- package/src/commands/ps.ts +0 -84
- package/src/commands/pull.ts +0 -44
- package/src/commands/rm.ts +0 -107
- package/src/commands/router/config.ts +0 -116
- package/src/commands/router/logs.ts +0 -256
- package/src/commands/router/restart.ts +0 -36
- package/src/commands/router/start.ts +0 -60
- package/src/commands/router/status.ts +0 -119
- package/src/commands/router/stop.ts +0 -33
- package/src/commands/run.ts +0 -233
- package/src/commands/search.ts +0 -107
- package/src/commands/server-show.ts +0 -161
- package/src/commands/show.ts +0 -207
- package/src/commands/start.ts +0 -101
- package/src/commands/stop.ts +0 -39
- package/src/commands/tui.ts +0 -25
- package/src/lib/admin-manager.ts +0 -435
- package/src/lib/admin-server.ts +0 -1243
- package/src/lib/config-generator.ts +0 -130
- package/src/lib/download-job-manager.ts +0 -213
- package/src/lib/history-manager.ts +0 -172
- package/src/lib/launchctl-manager.ts +0 -225
- package/src/lib/metrics-aggregator.ts +0 -257
- package/src/lib/model-downloader.ts +0 -328
- package/src/lib/model-scanner.ts +0 -157
- package/src/lib/model-search.ts +0 -114
- package/src/lib/models-dir-setup.ts +0 -46
- package/src/lib/port-manager.ts +0 -80
- package/src/lib/router-logger.ts +0 -201
- package/src/lib/router-manager.ts +0 -414
- package/src/lib/router-server.ts +0 -538
- package/src/lib/state-manager.ts +0 -206
- package/src/lib/status-checker.ts +0 -113
- package/src/lib/system-collector.ts +0 -315
- package/src/tui/ConfigApp.ts +0 -1085
- package/src/tui/HistoricalMonitorApp.ts +0 -587
- package/src/tui/ModelsApp.ts +0 -368
- package/src/tui/MonitorApp.ts +0 -386
- package/src/tui/MultiServerMonitorApp.ts +0 -1833
- package/src/tui/RootNavigator.ts +0 -74
- package/src/tui/SearchApp.ts +0 -511
- package/src/tui/SplashScreen.ts +0 -149
- package/src/types/admin-config.ts +0 -25
- package/src/types/global-config.ts +0 -26
- package/src/types/history-types.ts +0 -39
- package/src/types/model-info.ts +0 -8
- package/src/types/monitor-types.ts +0 -162
- package/src/types/router-config.ts +0 -25
- package/src/types/server-config.ts +0 -46
- package/src/utils/downsample-utils.ts +0 -128
- package/src/utils/file-utils.ts +0 -146
- package/src/utils/format-utils.ts +0 -98
- package/src/utils/log-parser.ts +0 -284
- package/src/utils/log-utils.ts +0 -178
- package/src/utils/process-utils.ts +0 -316
- package/src/utils/prompt-utils.ts +0 -47
- package/test-load.sh +0 -100
- package/tsconfig.json +0 -20
- package/web/eslint.config.js +0 -23
- package/web/llamacpp-web-dist.tar.gz +0 -0
- package/web/package-lock.json +0 -4017
- package/web/package.json +0 -38
- package/web/postcss.config.js +0 -6
- package/web/src/App.css +0 -42
- package/web/src/App.tsx +0 -86
- package/web/src/assets/react.svg +0 -1
- package/web/src/components/ApiKeyPrompt.tsx +0 -71
- package/web/src/components/CreateServerModal.tsx +0 -372
- package/web/src/components/DownloadProgress.tsx +0 -123
- package/web/src/components/Nav.tsx +0 -89
- package/web/src/components/RouterConfigModal.tsx +0 -240
- package/web/src/components/SearchModal.tsx +0 -306
- package/web/src/components/ServerConfigModal.tsx +0 -291
- package/web/src/hooks/useApi.ts +0 -259
- package/web/src/index.css +0 -42
- package/web/src/lib/api.ts +0 -226
- package/web/src/main.tsx +0 -10
- package/web/src/pages/Dashboard.tsx +0 -103
- package/web/src/pages/Models.tsx +0 -258
- package/web/src/pages/Router.tsx +0 -270
- package/web/src/pages/RouterLogs.tsx +0 -201
- package/web/src/pages/ServerLogs.tsx +0 -553
- package/web/src/pages/Servers.tsx +0 -358
- package/web/src/types/api.ts +0 -140
- package/web/tailwind.config.js +0 -31
- package/web/tsconfig.app.json +0 -28
- package/web/tsconfig.json +0 -7
- package/web/tsconfig.node.json +0 -26
- package/web/vite.config.ts +0 -25
- /package/web/{public → dist}/vite.svg +0 -0
package/src/lib/port-manager.ts
DELETED
|
@@ -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();
|
package/src/lib/router-logger.ts
DELETED
|
@@ -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();
|