@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,101 +0,0 @@
1
- import chalk from 'chalk';
2
- import { stateManager } from '../lib/state-manager';
3
- import { launchctlManager } from '../lib/launchctl-manager';
4
- import { statusChecker } from '../lib/status-checker';
5
- import { parseMetalMemoryFromLog } from '../utils/file-utils';
6
- import { autoRotateIfNeeded, formatFileSize } from '../utils/log-utils';
7
-
8
- export async function startCommand(identifier: string): Promise<void> {
9
- // Initialize state manager
10
- await stateManager.initialize();
11
-
12
- // 1. Find server by identifier
13
- const server = await stateManager.findServer(identifier);
14
- if (!server) {
15
- throw new Error(
16
- `Server not found: ${identifier}\n\n` +
17
- `Use: llamacpp ps\n` +
18
- `Or create a new server: llamacpp server create <model>`
19
- );
20
- }
21
-
22
- // 2. Check if already running
23
- if (server.status === 'running') {
24
- console.log(
25
- chalk.yellow(
26
- `⚠️ Server ${server.modelName} is already running on port ${server.port}`
27
- )
28
- );
29
- return;
30
- }
31
-
32
- console.log(chalk.blue(`▶️ Starting ${server.modelName} (port ${server.port})...`));
33
-
34
- // 3. Auto-rotate logs if they exceed 100MB
35
- try {
36
- const result = await autoRotateIfNeeded(server.stdoutPath, server.stderrPath, 100);
37
- if (result.rotated) {
38
- console.log(chalk.dim('Auto-rotated large log files:'));
39
- for (const file of result.files) {
40
- console.log(chalk.dim(` → ${file}`));
41
- }
42
- }
43
- } catch (error) {
44
- // Non-fatal, just warn
45
- console.log(chalk.yellow(`⚠️ Failed to rotate logs: ${(error as Error).message}`));
46
- }
47
-
48
- // 4. Ensure plist exists (recreate if missing)
49
- try {
50
- await launchctlManager.createPlist(server);
51
- } catch (error) {
52
- // May already exist, that's okay
53
- }
54
-
55
- // 5. Load service if needed
56
- try {
57
- await launchctlManager.loadService(server.plistPath);
58
- } catch (error) {
59
- // May already be loaded, that's okay
60
- }
61
-
62
- // 6. Start the service
63
- try {
64
- await launchctlManager.startService(server.label);
65
- } catch (error) {
66
- throw new Error(`Failed to start service: ${(error as Error).message}`);
67
- }
68
-
69
- // 7. Wait for startup
70
- console.log(chalk.dim('Waiting for server to start...'));
71
- const started = await launchctlManager.waitForServiceStart(server.label, 5000);
72
-
73
- if (!started) {
74
- throw new Error(
75
- `Server failed to start. Check logs with: llamacpp server logs ${server.id}`
76
- );
77
- }
78
-
79
- // 8. Update server status
80
- let updatedServer = await statusChecker.updateServerStatus(server);
81
-
82
- // 9. Parse Metal (GPU) memory allocation if not already captured
83
- if (!updatedServer.metalMemoryMB) {
84
- console.log(chalk.dim('Detecting Metal (GPU) memory allocation...'));
85
- await new Promise(resolve => setTimeout(resolve, 8000)); // 8 second delay
86
- const metalMemoryMB = await parseMetalMemoryFromLog(updatedServer.stderrPath);
87
- if (metalMemoryMB) {
88
- updatedServer = { ...updatedServer, metalMemoryMB };
89
- await stateManager.saveServerConfig(updatedServer);
90
- console.log(chalk.dim(`Metal memory: ${metalMemoryMB.toFixed(0)} MB`));
91
- }
92
- }
93
-
94
- // 10. Display success
95
- console.log();
96
- console.log(chalk.green('✅ Server started successfully!'));
97
- console.log();
98
- console.log(chalk.dim(`Connect: http://localhost:${server.port}`));
99
- console.log(chalk.dim(`View logs: llamacpp server logs ${server.id}`));
100
- console.log(chalk.dim(`Stop: llamacpp server stop ${server.id}`));
101
- }
@@ -1,39 +0,0 @@
1
- import chalk from 'chalk';
2
- import { stateManager } from '../lib/state-manager';
3
- import { launchctlManager } from '../lib/launchctl-manager';
4
- import { statusChecker } from '../lib/status-checker';
5
-
6
- export async function stopCommand(identifier: string): Promise<void> {
7
- // Find server
8
- const server = await stateManager.findServer(identifier);
9
- if (!server) {
10
- throw new Error(`Server not found: ${identifier}\n\nUse: llamacpp ps`);
11
- }
12
-
13
- // Check if already stopped
14
- if (server.status === 'stopped') {
15
- console.log(chalk.yellow(`⚠️ Server ${server.modelName} is already stopped`));
16
- return;
17
- }
18
-
19
- console.log(chalk.blue(`⏹️ Stopping ${server.modelName} (port ${server.port})...`));
20
-
21
- // Unload the service (removes from launchd management - won't auto-restart)
22
- try {
23
- await launchctlManager.unloadService(server.plistPath);
24
- } catch (error) {
25
- throw new Error(`Failed to unload service: ${(error as Error).message}`);
26
- }
27
-
28
- // Wait for clean shutdown
29
- const stopped = await launchctlManager.waitForServiceStop(server.label, 5000);
30
-
31
- if (!stopped) {
32
- console.log(chalk.yellow('⚠️ Server did not stop cleanly (timeout)'));
33
- }
34
-
35
- // Update server status
36
- await statusChecker.updateServerStatus(server);
37
-
38
- console.log(chalk.green('✅ Server stopped'));
39
- }
@@ -1,25 +0,0 @@
1
- import chalk from 'chalk';
2
- import blessed from 'blessed';
3
- import { stateManager } from '../lib/state-manager.js';
4
- import { statusChecker } from '../lib/status-checker.js';
5
- import { createRootNavigator } from '../tui/RootNavigator.js';
6
-
7
- export async function tuiCommand(): Promise<void> {
8
- const servers = await stateManager.getAllServers();
9
-
10
- if (servers.length === 0) {
11
- console.log(chalk.yellow('No servers configured.'));
12
- console.log(chalk.dim('\nCreate a server: llamacpp server create <model-filename>'));
13
- return;
14
- }
15
-
16
- const serversWithStatus = await statusChecker.updateAllServerStatuses();
17
-
18
- const screen = blessed.screen({
19
- smartCSR: true,
20
- title: 'llama.cpp Server Monitor',
21
- fullUnicode: true,
22
- });
23
-
24
- await createRootNavigator(screen, serversWithStatus);
25
- }
@@ -1,435 +0,0 @@
1
- import * as path from 'path';
2
- import * as fs from 'fs/promises';
3
- import * as crypto from 'crypto';
4
- import { AdminConfig } from '../types/admin-config';
5
- import { execCommand, execAsync } from '../utils/process-utils';
6
- import {
7
- ensureDir,
8
- writeJsonAtomic,
9
- readJson,
10
- fileExists,
11
- getConfigDir,
12
- getLogsDir,
13
- getLaunchAgentsDir,
14
- writeFileAtomic,
15
- } from '../utils/file-utils';
16
-
17
- export interface AdminServiceStatus {
18
- isRunning: boolean;
19
- pid: number | null;
20
- exitCode: number | null;
21
- lastExitReason?: string;
22
- }
23
-
24
- export class AdminManager {
25
- private configDir: string;
26
- private logsDir: string;
27
- private configPath: string;
28
- private launchAgentsDir: string;
29
-
30
- constructor() {
31
- this.configDir = getConfigDir();
32
- this.logsDir = getLogsDir();
33
- this.configPath = path.join(this.configDir, 'admin.json');
34
- this.launchAgentsDir = getLaunchAgentsDir();
35
- }
36
-
37
- /**
38
- * Initialize admin directories
39
- */
40
- async initialize(): Promise<void> {
41
- await ensureDir(this.configDir);
42
- await ensureDir(this.logsDir);
43
- await ensureDir(this.launchAgentsDir);
44
- }
45
-
46
- /**
47
- * Generate a secure random API key
48
- */
49
- generateApiKey(): string {
50
- return crypto.randomBytes(32).toString('hex');
51
- }
52
-
53
- /**
54
- * Get default admin configuration
55
- */
56
- getDefaultConfig(): AdminConfig {
57
- return {
58
- id: 'admin',
59
- port: 9200,
60
- host: '127.0.0.1',
61
- apiKey: this.generateApiKey(),
62
- label: 'com.llama.admin',
63
- plistPath: path.join(this.launchAgentsDir, 'com.llama.admin.plist'),
64
- stdoutPath: path.join(this.logsDir, 'admin.stdout'),
65
- stderrPath: path.join(this.logsDir, 'admin.stderr'),
66
- requestTimeout: 30000,
67
- verbose: false,
68
- status: 'stopped',
69
- createdAt: new Date().toISOString(),
70
- };
71
- }
72
-
73
- /**
74
- * Load admin configuration
75
- */
76
- async loadConfig(): Promise<AdminConfig | null> {
77
- if (!(await fileExists(this.configPath))) {
78
- return null;
79
- }
80
- return await readJson<AdminConfig>(this.configPath);
81
- }
82
-
83
- /**
84
- * Save admin configuration
85
- */
86
- async saveConfig(config: AdminConfig): Promise<void> {
87
- await writeJsonAtomic(this.configPath, config);
88
- }
89
-
90
- /**
91
- * Update admin configuration with partial changes
92
- */
93
- async updateConfig(updates: Partial<AdminConfig>): Promise<void> {
94
- const existingConfig = await this.loadConfig();
95
- if (!existingConfig) {
96
- throw new Error('Admin configuration not found');
97
- }
98
- const updatedConfig = { ...existingConfig, ...updates };
99
- await this.saveConfig(updatedConfig);
100
- }
101
-
102
- /**
103
- * Delete admin configuration
104
- */
105
- async deleteConfig(): Promise<void> {
106
- if (await fileExists(this.configPath)) {
107
- await fs.unlink(this.configPath);
108
- }
109
- }
110
-
111
- /**
112
- * Regenerate API key
113
- */
114
- async regenerateApiKey(): Promise<string> {
115
- const config = await this.loadConfig();
116
- if (!config) {
117
- throw new Error('Admin configuration not found');
118
- }
119
- const newApiKey = this.generateApiKey();
120
- await this.updateConfig({ apiKey: newApiKey });
121
- return newApiKey;
122
- }
123
-
124
- /**
125
- * Generate plist XML content for the admin service
126
- */
127
- generatePlist(config: AdminConfig): string {
128
- // Find the compiled admin-server.js file
129
- // In dev mode (tsx), __dirname is src/lib/
130
- // In production, __dirname is dist/lib/
131
- // Always use the compiled dist version for launchctl
132
- let adminServerPath: string;
133
- if (__dirname.includes('/src/')) {
134
- // Dev mode - point to dist/lib/admin-server.js
135
- const projectRoot = path.resolve(__dirname, '../..');
136
- adminServerPath = path.join(projectRoot, 'dist/lib/admin-server.js');
137
- } else {
138
- // Production mode - already in dist/lib/
139
- adminServerPath = path.join(__dirname, 'admin-server.js');
140
- }
141
-
142
- // Use the current Node.js executable path (resolves symlinks)
143
- const nodePath = process.execPath;
144
-
145
- const args = [
146
- nodePath,
147
- adminServerPath,
148
- '--config', this.configPath,
149
- ];
150
-
151
- const argsXml = args.map(arg => ` <string>${arg}</string>`).join('\n');
152
-
153
- return `<?xml version="1.0" encoding="UTF-8"?>
154
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
155
- "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
156
- <plist version="1.0">
157
- <dict>
158
- <key>Label</key>
159
- <string>${config.label}</string>
160
-
161
- <key>ProgramArguments</key>
162
- <array>
163
- ${argsXml}
164
- </array>
165
-
166
- <key>RunAtLoad</key>
167
- <false/>
168
-
169
- <key>KeepAlive</key>
170
- <dict>
171
- <key>Crashed</key>
172
- <true/>
173
- <key>SuccessfulExit</key>
174
- <false/>
175
- </dict>
176
-
177
- <key>StandardOutPath</key>
178
- <string>${config.stdoutPath}</string>
179
-
180
- <key>StandardErrorPath</key>
181
- <string>${config.stderrPath}</string>
182
-
183
- <key>WorkingDirectory</key>
184
- <string>/tmp</string>
185
-
186
- <key>ThrottleInterval</key>
187
- <integer>10</integer>
188
- </dict>
189
- </plist>
190
- `;
191
- }
192
-
193
- /**
194
- * Create and write plist file
195
- */
196
- async createPlist(config: AdminConfig): Promise<void> {
197
- const plistContent = this.generatePlist(config);
198
- await writeFileAtomic(config.plistPath, plistContent);
199
- }
200
-
201
- /**
202
- * Delete plist file
203
- */
204
- async deletePlist(config: AdminConfig): Promise<void> {
205
- if (await fileExists(config.plistPath)) {
206
- await fs.unlink(config.plistPath);
207
- }
208
- }
209
-
210
- /**
211
- * Load service (register with launchctl)
212
- */
213
- async loadService(plistPath: string): Promise<void> {
214
- await execCommand(`launchctl load "${plistPath}"`);
215
- }
216
-
217
- /**
218
- * Unload service (unregister from launchctl)
219
- */
220
- async unloadService(plistPath: string): Promise<void> {
221
- try {
222
- await execCommand(`launchctl unload "${plistPath}"`);
223
- } catch (error) {
224
- // Ignore errors if service is not loaded
225
- }
226
- }
227
-
228
- /**
229
- * Start service
230
- */
231
- async startService(label: string): Promise<void> {
232
- await execCommand(`launchctl start ${label}`);
233
- }
234
-
235
- /**
236
- * Stop service
237
- */
238
- async stopService(label: string): Promise<void> {
239
- await execCommand(`launchctl stop ${label}`);
240
- }
241
-
242
- /**
243
- * Get service status from launchctl
244
- */
245
- async getServiceStatus(label: string): Promise<AdminServiceStatus> {
246
- try {
247
- const { stdout } = await execAsync(`launchctl list | grep ${label}`);
248
- const lines = stdout.trim().split('\n');
249
-
250
- for (const line of lines) {
251
- const parts = line.split(/\s+/);
252
- if (parts.length >= 3) {
253
- const pidStr = parts[0].trim();
254
- const exitCodeStr = parts[1].trim();
255
- const serviceLabel = parts[2].trim();
256
-
257
- if (serviceLabel === label) {
258
- const pid = pidStr !== '-' ? parseInt(pidStr, 10) : null;
259
- const exitCode = exitCodeStr !== '-' ? parseInt(exitCodeStr, 10) : null;
260
- const isRunning = pid !== null;
261
-
262
- return {
263
- isRunning,
264
- pid,
265
- exitCode,
266
- lastExitReason: this.interpretExitCode(exitCode),
267
- };
268
- }
269
- }
270
- }
271
-
272
- return {
273
- isRunning: false,
274
- pid: null,
275
- exitCode: null,
276
- };
277
- } catch (error) {
278
- return {
279
- isRunning: false,
280
- pid: null,
281
- exitCode: null,
282
- };
283
- }
284
- }
285
-
286
- /**
287
- * Interpret exit code to human-readable reason
288
- */
289
- private interpretExitCode(code: number | null): string | undefined {
290
- if (code === null || code === 0) return undefined;
291
- if (code === -9) return 'Force killed (SIGKILL)';
292
- if (code === -15) return 'Terminated (SIGTERM)';
293
- return `Exit code: ${code}`;
294
- }
295
-
296
- /**
297
- * Wait for service to start (with timeout)
298
- */
299
- async waitForServiceStart(label: string, timeoutMs = 5000): Promise<boolean> {
300
- const startTime = Date.now();
301
- while (Date.now() - startTime < timeoutMs) {
302
- const status = await this.getServiceStatus(label);
303
- if (status.isRunning) {
304
- return true;
305
- }
306
- await new Promise((resolve) => setTimeout(resolve, 500));
307
- }
308
- return false;
309
- }
310
-
311
- /**
312
- * Wait for service to stop (with timeout)
313
- */
314
- async waitForServiceStop(label: string, timeoutMs = 5000): Promise<boolean> {
315
- const startTime = Date.now();
316
- while (Date.now() - startTime < timeoutMs) {
317
- const status = await this.getServiceStatus(label);
318
- if (!status.isRunning) {
319
- return true;
320
- }
321
- await new Promise((resolve) => setTimeout(resolve, 500));
322
- }
323
- return false;
324
- }
325
-
326
- /**
327
- * Start admin service
328
- */
329
- async start(): Promise<void> {
330
- await this.initialize();
331
-
332
- let config = await this.loadConfig();
333
- if (!config) {
334
- // Create default config
335
- config = this.getDefaultConfig();
336
- await this.saveConfig(config);
337
- }
338
-
339
- // Check if already running
340
- if (config.status === 'running') {
341
- throw new Error('Admin service is already running');
342
- }
343
-
344
- // Check for throttled state (exit code 78)
345
- const currentStatus = await this.getServiceStatus(config.label);
346
- if (currentStatus.exitCode === 78) {
347
- // Service is throttled - clean up and start fresh
348
- await this.unloadService(config.plistPath);
349
- await this.deletePlist(config);
350
- // Give launchd a moment to clean up
351
- await new Promise((resolve) => setTimeout(resolve, 1000));
352
- }
353
-
354
- // Create plist
355
- await this.createPlist(config);
356
-
357
- // Load and start service
358
- try {
359
- await this.loadService(config.plistPath);
360
- } catch (error) {
361
- // May already be loaded
362
- }
363
-
364
- await this.startService(config.label);
365
-
366
- // Wait for startup
367
- const started = await this.waitForServiceStart(config.label, 5000);
368
- if (!started) {
369
- throw new Error('Admin service failed to start');
370
- }
371
-
372
- // Update config
373
- const status = await this.getServiceStatus(config.label);
374
- await this.updateConfig({
375
- status: 'running',
376
- pid: status.pid || undefined,
377
- lastStarted: new Date().toISOString(),
378
- });
379
- }
380
-
381
- /**
382
- * Stop admin service
383
- */
384
- async stop(): Promise<void> {
385
- const config = await this.loadConfig();
386
- if (!config) {
387
- throw new Error('Admin configuration not found');
388
- }
389
-
390
- if (config.status !== 'running') {
391
- throw new Error('Admin service is not running');
392
- }
393
-
394
- // Unload service
395
- await this.unloadService(config.plistPath);
396
-
397
- // Wait for shutdown
398
- await this.waitForServiceStop(config.label, 5000);
399
-
400
- // Update config
401
- await this.updateConfig({
402
- status: 'stopped',
403
- pid: undefined,
404
- lastStopped: new Date().toISOString(),
405
- });
406
- }
407
-
408
- /**
409
- * Restart admin service
410
- */
411
- async restart(): Promise<void> {
412
- try {
413
- await this.stop();
414
- } catch (error) {
415
- // May not be running
416
- }
417
- await this.start();
418
- }
419
-
420
- /**
421
- * Get admin status
422
- */
423
- async getStatus(): Promise<{ config: AdminConfig; status: AdminServiceStatus } | null> {
424
- const config = await this.loadConfig();
425
- if (!config) {
426
- return null;
427
- }
428
-
429
- const status = await this.getServiceStatus(config.label);
430
- return { config, status };
431
- }
432
- }
433
-
434
- // Export singleton instance
435
- export const adminManager = new AdminManager();