@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/commands/start.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/stop.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/tui.ts
DELETED
|
@@ -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
|
-
}
|
package/src/lib/admin-manager.ts
DELETED
|
@@ -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();
|