@appkit/llamacpp-cli 1.0.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/.versionrc.json +16 -0
- package/CHANGELOG.md +10 -0
- package/README.md +474 -0
- package/bin/llamacpp +26 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +196 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/delete.d.ts +2 -0
- package/dist/commands/delete.d.ts.map +1 -0
- package/dist/commands/delete.js +104 -0
- package/dist/commands/delete.js.map +1 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +37 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/logs.d.ts +8 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +57 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/ps.d.ts +2 -0
- package/dist/commands/ps.d.ts.map +1 -0
- package/dist/commands/ps.js +72 -0
- package/dist/commands/ps.js.map +1 -0
- package/dist/commands/pull.d.ts +6 -0
- package/dist/commands/pull.d.ts.map +1 -0
- package/dist/commands/pull.js +36 -0
- package/dist/commands/pull.js.map +1 -0
- package/dist/commands/rm.d.ts +2 -0
- package/dist/commands/rm.d.ts.map +1 -0
- package/dist/commands/rm.js +134 -0
- package/dist/commands/rm.js.map +1 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +198 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/search.d.ts +7 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +93 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/show.d.ts +6 -0
- package/dist/commands/show.d.ts.map +1 -0
- package/dist/commands/show.js +196 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/commands/start.d.ts +9 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +150 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/stop.d.ts +2 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +39 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/lib/config-generator.d.ts +30 -0
- package/dist/lib/config-generator.d.ts.map +1 -0
- package/dist/lib/config-generator.js +125 -0
- package/dist/lib/config-generator.js.map +1 -0
- package/dist/lib/launchctl-manager.d.ts +55 -0
- package/dist/lib/launchctl-manager.d.ts.map +1 -0
- package/dist/lib/launchctl-manager.js +227 -0
- package/dist/lib/launchctl-manager.js.map +1 -0
- package/dist/lib/model-downloader.d.ts +44 -0
- package/dist/lib/model-downloader.d.ts.map +1 -0
- package/dist/lib/model-downloader.js +248 -0
- package/dist/lib/model-downloader.js.map +1 -0
- package/dist/lib/model-scanner.d.ts +31 -0
- package/dist/lib/model-scanner.d.ts.map +1 -0
- package/dist/lib/model-scanner.js +145 -0
- package/dist/lib/model-scanner.js.map +1 -0
- package/dist/lib/model-search.d.ts +29 -0
- package/dist/lib/model-search.d.ts.map +1 -0
- package/dist/lib/model-search.js +131 -0
- package/dist/lib/model-search.js.map +1 -0
- package/dist/lib/port-manager.d.ts +26 -0
- package/dist/lib/port-manager.d.ts.map +1 -0
- package/dist/lib/port-manager.js +75 -0
- package/dist/lib/port-manager.js.map +1 -0
- package/dist/lib/state-manager.d.ts +59 -0
- package/dist/lib/state-manager.d.ts.map +1 -0
- package/dist/lib/state-manager.js +178 -0
- package/dist/lib/state-manager.js.map +1 -0
- package/dist/lib/status-checker.d.ts +28 -0
- package/dist/lib/status-checker.d.ts.map +1 -0
- package/dist/lib/status-checker.js +99 -0
- package/dist/lib/status-checker.js.map +1 -0
- package/dist/types/global-config.d.ts +16 -0
- package/dist/types/global-config.d.ts.map +1 -0
- package/dist/types/global-config.js +18 -0
- package/dist/types/global-config.js.map +1 -0
- package/dist/types/model-info.d.ts +9 -0
- package/dist/types/model-info.d.ts.map +1 -0
- package/dist/types/model-info.js +3 -0
- package/dist/types/model-info.js.map +1 -0
- package/dist/types/server-config.d.ts +27 -0
- package/dist/types/server-config.d.ts.map +1 -0
- package/dist/types/server-config.js +15 -0
- package/dist/types/server-config.js.map +1 -0
- package/dist/utils/file-utils.d.ts +49 -0
- package/dist/utils/file-utils.d.ts.map +1 -0
- package/dist/utils/file-utils.js +144 -0
- package/dist/utils/file-utils.js.map +1 -0
- package/dist/utils/format-utils.d.ts +29 -0
- package/dist/utils/format-utils.d.ts.map +1 -0
- package/dist/utils/format-utils.js +82 -0
- package/dist/utils/format-utils.js.map +1 -0
- package/dist/utils/process-utils.d.ts +27 -0
- package/dist/utils/process-utils.d.ts.map +1 -0
- package/dist/utils/process-utils.js +66 -0
- package/dist/utils/process-utils.js.map +1 -0
- package/package.json +56 -0
- package/src/cli.ts +195 -0
- package/src/commands/delete.ts +74 -0
- package/src/commands/list.ts +37 -0
- package/src/commands/logs.ts +61 -0
- package/src/commands/ps.ts +79 -0
- package/src/commands/pull.ts +40 -0
- package/src/commands/rm.ts +114 -0
- package/src/commands/run.ts +209 -0
- package/src/commands/search.ts +107 -0
- package/src/commands/show.ts +207 -0
- package/src/commands/start.ts +140 -0
- package/src/commands/stop.ts +39 -0
- package/src/lib/config-generator.ts +119 -0
- package/src/lib/launchctl-manager.ts +209 -0
- package/src/lib/model-downloader.ts +259 -0
- package/src/lib/model-scanner.ts +125 -0
- package/src/lib/model-search.ts +114 -0
- package/src/lib/port-manager.ts +80 -0
- package/src/lib/state-manager.ts +177 -0
- package/src/lib/status-checker.ts +113 -0
- package/src/types/global-config.ts +26 -0
- package/src/types/model-info.ts +8 -0
- package/src/types/server-config.ts +42 -0
- package/src/utils/file-utils.ts +106 -0
- package/src/utils/format-utils.ts +80 -0
- package/src/utils/process-utils.ts +60 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { modelScanner } from '../lib/model-scanner';
|
|
4
|
+
import { stateManager } from '../lib/state-manager';
|
|
5
|
+
import { configGenerator, ServerOptions } from '../lib/config-generator';
|
|
6
|
+
import { portManager } from '../lib/port-manager';
|
|
7
|
+
import { launchctlManager } from '../lib/launchctl-manager';
|
|
8
|
+
import { statusChecker } from '../lib/status-checker';
|
|
9
|
+
import { commandExists } from '../utils/process-utils';
|
|
10
|
+
import { formatBytes } from '../utils/format-utils';
|
|
11
|
+
import { ensureDir } from '../utils/file-utils';
|
|
12
|
+
|
|
13
|
+
interface StartOptions {
|
|
14
|
+
port?: number;
|
|
15
|
+
threads?: number;
|
|
16
|
+
ctxSize?: number;
|
|
17
|
+
gpuLayers?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function startCommand(model: string, options: StartOptions): Promise<void> {
|
|
21
|
+
// Initialize state manager
|
|
22
|
+
await stateManager.initialize();
|
|
23
|
+
|
|
24
|
+
// 1. Check if llama-server exists
|
|
25
|
+
if (!(await commandExists('llama-server'))) {
|
|
26
|
+
throw new Error('llama-server not found. Install with: brew install llama.cpp');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 2. Resolve model path
|
|
30
|
+
const modelPath = await modelScanner.resolveModelPath(model);
|
|
31
|
+
if (!modelPath) {
|
|
32
|
+
throw new Error(`Model not found: ${model}\n\nRun: llamacpp list`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const modelName = path.basename(modelPath);
|
|
36
|
+
|
|
37
|
+
// 3. Check if server already exists for this model
|
|
38
|
+
const existingServer = await stateManager.serverExistsForModel(modelPath);
|
|
39
|
+
if (existingServer) {
|
|
40
|
+
throw new Error(`Server already exists for ${modelName}\n\nUse: llamacpp ps`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 4. Get model size
|
|
44
|
+
const modelSize = await modelScanner.getModelSize(modelName);
|
|
45
|
+
if (!modelSize) {
|
|
46
|
+
throw new Error(`Failed to read model file: ${modelPath}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 5. Determine port
|
|
50
|
+
let port: number;
|
|
51
|
+
if (options.port) {
|
|
52
|
+
portManager.validatePort(options.port);
|
|
53
|
+
const available = await portManager.isPortAvailable(options.port);
|
|
54
|
+
if (!available) {
|
|
55
|
+
throw new Error(`Port ${options.port} is already in use`);
|
|
56
|
+
}
|
|
57
|
+
port = options.port;
|
|
58
|
+
} else {
|
|
59
|
+
port = await portManager.findAvailablePort();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 6. Generate server configuration
|
|
63
|
+
console.log(chalk.blue(`🚀 Starting server for ${modelName}\n`));
|
|
64
|
+
|
|
65
|
+
const serverOptions: ServerOptions = {
|
|
66
|
+
port: options.port,
|
|
67
|
+
threads: options.threads,
|
|
68
|
+
ctxSize: options.ctxSize,
|
|
69
|
+
gpuLayers: options.gpuLayers,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const config = await configGenerator.generateConfig(
|
|
73
|
+
modelPath,
|
|
74
|
+
modelName,
|
|
75
|
+
modelSize,
|
|
76
|
+
port,
|
|
77
|
+
serverOptions
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Display configuration
|
|
81
|
+
console.log(chalk.dim(`Model: ${modelPath}`));
|
|
82
|
+
console.log(chalk.dim(`Size: ${formatBytes(modelSize)}`));
|
|
83
|
+
console.log(chalk.dim(`Port: ${config.port}${options.port ? '' : ' (auto-assigned)'}`));
|
|
84
|
+
console.log(chalk.dim(`Threads: ${config.threads}`));
|
|
85
|
+
console.log(chalk.dim(`Context Size: ${config.ctxSize}`));
|
|
86
|
+
console.log(chalk.dim(`GPU Layers: ${config.gpuLayers}`));
|
|
87
|
+
console.log();
|
|
88
|
+
|
|
89
|
+
// 7. Ensure log directory exists
|
|
90
|
+
await ensureDir(path.dirname(config.stdoutPath));
|
|
91
|
+
|
|
92
|
+
// 8. Create plist file
|
|
93
|
+
console.log(chalk.dim('Creating launchctl service...'));
|
|
94
|
+
await launchctlManager.createPlist(config);
|
|
95
|
+
|
|
96
|
+
// 9. Load service
|
|
97
|
+
try {
|
|
98
|
+
await launchctlManager.loadService(config.plistPath);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
// Clean up plist if load fails
|
|
101
|
+
await launchctlManager.deletePlist(config.plistPath);
|
|
102
|
+
throw new Error(`Failed to load service: ${(error as Error).message}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 10. Start service
|
|
106
|
+
try {
|
|
107
|
+
await launchctlManager.startService(config.label);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
// Clean up if start fails
|
|
110
|
+
await launchctlManager.unloadService(config.plistPath);
|
|
111
|
+
await launchctlManager.deletePlist(config.plistPath);
|
|
112
|
+
throw new Error(`Failed to start service: ${(error as Error).message}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 11. Wait for startup
|
|
116
|
+
console.log(chalk.dim('Waiting for server to start...'));
|
|
117
|
+
const started = await launchctlManager.waitForServiceStart(config.label, 5000);
|
|
118
|
+
|
|
119
|
+
if (!started) {
|
|
120
|
+
// Clean up if startup fails
|
|
121
|
+
await launchctlManager.stopService(config.label);
|
|
122
|
+
await launchctlManager.unloadService(config.plistPath);
|
|
123
|
+
await launchctlManager.deletePlist(config.plistPath);
|
|
124
|
+
throw new Error('Server failed to start. Check logs with: llamacpp logs --errors');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 12. Update config with running status
|
|
128
|
+
const updatedConfig = await statusChecker.updateServerStatus(config);
|
|
129
|
+
|
|
130
|
+
// 13. Save server config
|
|
131
|
+
await stateManager.saveServerConfig(updatedConfig);
|
|
132
|
+
|
|
133
|
+
// 14. Display success message
|
|
134
|
+
console.log();
|
|
135
|
+
console.log(chalk.green('✅ Server started successfully!'));
|
|
136
|
+
console.log();
|
|
137
|
+
console.log(chalk.dim(`Connect: http://localhost:${config.port}`));
|
|
138
|
+
console.log(chalk.dim(`View logs: llamacpp logs ${config.id}`));
|
|
139
|
+
console.log(chalk.dim(`Stop: llamacpp stop ${config.id}`));
|
|
140
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
// Stop the service
|
|
22
|
+
try {
|
|
23
|
+
await launchctlManager.stopService(server.label);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(`Failed to stop 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
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { ServerConfig, sanitizeModelName } from '../types/server-config';
|
|
4
|
+
import { getLogsDir, getLaunchAgentsDir } from '../utils/file-utils';
|
|
5
|
+
import { stateManager } from './state-manager';
|
|
6
|
+
|
|
7
|
+
export interface ServerOptions {
|
|
8
|
+
port?: number;
|
|
9
|
+
threads?: number;
|
|
10
|
+
ctxSize?: number;
|
|
11
|
+
gpuLayers?: number;
|
|
12
|
+
embeddings?: boolean;
|
|
13
|
+
jinja?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SmartDefaults {
|
|
17
|
+
threads: number;
|
|
18
|
+
ctxSize: number;
|
|
19
|
+
gpuLayers: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ConfigGenerator {
|
|
23
|
+
/**
|
|
24
|
+
* Calculate smart defaults based on model size
|
|
25
|
+
*/
|
|
26
|
+
calculateSmartDefaults(modelSizeBytes: number): SmartDefaults {
|
|
27
|
+
const sizeGB = modelSizeBytes / (1024 ** 3);
|
|
28
|
+
|
|
29
|
+
// Context size based on model size
|
|
30
|
+
let ctxSize: number;
|
|
31
|
+
if (sizeGB < 1) {
|
|
32
|
+
ctxSize = 2048; // < 1GB: small context
|
|
33
|
+
} else if (sizeGB < 3) {
|
|
34
|
+
ctxSize = 4096; // 1-3GB: medium
|
|
35
|
+
} else if (sizeGB < 6) {
|
|
36
|
+
ctxSize = 8192; // 3-6GB: large
|
|
37
|
+
} else {
|
|
38
|
+
ctxSize = 16384; // 6GB+: very large
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// GPU layers - always max for Metal (macOS)
|
|
42
|
+
const gpuLayers = 60; // llama.cpp auto-detects optimal value
|
|
43
|
+
|
|
44
|
+
// Threads - use half of available cores (better performance)
|
|
45
|
+
const cpuCount = os.cpus().length;
|
|
46
|
+
const threads = Math.max(4, Math.floor(cpuCount / 2));
|
|
47
|
+
|
|
48
|
+
return { threads, ctxSize, gpuLayers };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate server configuration
|
|
53
|
+
*/
|
|
54
|
+
async generateConfig(
|
|
55
|
+
modelPath: string,
|
|
56
|
+
modelName: string,
|
|
57
|
+
modelSize: number,
|
|
58
|
+
port: number,
|
|
59
|
+
options?: ServerOptions
|
|
60
|
+
): Promise<ServerConfig> {
|
|
61
|
+
// Calculate smart defaults
|
|
62
|
+
const smartDefaults = this.calculateSmartDefaults(modelSize);
|
|
63
|
+
|
|
64
|
+
// Apply user overrides
|
|
65
|
+
const threads = options?.threads ?? smartDefaults.threads;
|
|
66
|
+
const ctxSize = options?.ctxSize ?? smartDefaults.ctxSize;
|
|
67
|
+
const gpuLayers = options?.gpuLayers ?? smartDefaults.gpuLayers;
|
|
68
|
+
const embeddings = options?.embeddings ?? true;
|
|
69
|
+
const jinja = options?.jinja ?? true;
|
|
70
|
+
|
|
71
|
+
// Generate server ID
|
|
72
|
+
const id = sanitizeModelName(modelName);
|
|
73
|
+
|
|
74
|
+
// Generate paths
|
|
75
|
+
const label = `com.llama.${id}`;
|
|
76
|
+
const plistPath = path.join(getLaunchAgentsDir(), `${label}.plist`);
|
|
77
|
+
const logsDir = getLogsDir();
|
|
78
|
+
const stdoutPath = path.join(logsDir, `${id}.stdout`);
|
|
79
|
+
const stderrPath = path.join(logsDir, `${id}.stderr`);
|
|
80
|
+
|
|
81
|
+
const config: ServerConfig = {
|
|
82
|
+
id,
|
|
83
|
+
modelPath,
|
|
84
|
+
modelName,
|
|
85
|
+
port,
|
|
86
|
+
threads,
|
|
87
|
+
ctxSize,
|
|
88
|
+
gpuLayers,
|
|
89
|
+
embeddings,
|
|
90
|
+
jinja,
|
|
91
|
+
status: 'stopped',
|
|
92
|
+
createdAt: new Date().toISOString(),
|
|
93
|
+
plistPath,
|
|
94
|
+
label,
|
|
95
|
+
stdoutPath,
|
|
96
|
+
stderrPath,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return config;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Merge global defaults with user options
|
|
104
|
+
*/
|
|
105
|
+
async mergeWithGlobalDefaults(options?: ServerOptions): Promise<Partial<ServerOptions>> {
|
|
106
|
+
const globalConfig = await stateManager.loadGlobalConfig();
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
threads: options?.threads ?? globalConfig.defaults.threads,
|
|
110
|
+
ctxSize: options?.ctxSize ?? globalConfig.defaults.ctxSize,
|
|
111
|
+
gpuLayers: options?.gpuLayers ?? globalConfig.defaults.gpuLayers,
|
|
112
|
+
embeddings: options?.embeddings ?? true,
|
|
113
|
+
jinja: options?.jinja ?? true,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Export singleton instance
|
|
119
|
+
export const configGenerator = new ConfigGenerator();
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import { ServerConfig } from '../types/server-config';
|
|
4
|
+
import { execCommand, execAsync } from '../utils/process-utils';
|
|
5
|
+
import { writeFileAtomic, fileExists } from '../utils/file-utils';
|
|
6
|
+
|
|
7
|
+
export interface ServiceStatus {
|
|
8
|
+
isRunning: boolean;
|
|
9
|
+
pid: number | null;
|
|
10
|
+
exitCode: number | null;
|
|
11
|
+
lastExitReason?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class LaunchctlManager {
|
|
15
|
+
/**
|
|
16
|
+
* Generate plist XML content for a server
|
|
17
|
+
*/
|
|
18
|
+
generatePlist(config: ServerConfig): string {
|
|
19
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
20
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
21
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
22
|
+
<plist version="1.0">
|
|
23
|
+
<dict>
|
|
24
|
+
<key>Label</key>
|
|
25
|
+
<string>${config.label}</string>
|
|
26
|
+
|
|
27
|
+
<key>ProgramArguments</key>
|
|
28
|
+
<array>
|
|
29
|
+
<string>/opt/homebrew/bin/llama-server</string>
|
|
30
|
+
<string>--model</string>
|
|
31
|
+
<string>${config.modelPath}</string>
|
|
32
|
+
<string>--port</string>
|
|
33
|
+
<string>${config.port}</string>
|
|
34
|
+
<string>--threads</string>
|
|
35
|
+
<string>${config.threads}</string>
|
|
36
|
+
<string>--ctx-size</string>
|
|
37
|
+
<string>${config.ctxSize}</string>
|
|
38
|
+
<string>--gpu-layers</string>
|
|
39
|
+
<string>${config.gpuLayers}</string>
|
|
40
|
+
<string>--embeddings</string>
|
|
41
|
+
<string>--jinja</string>
|
|
42
|
+
</array>
|
|
43
|
+
|
|
44
|
+
<key>RunAtLoad</key>
|
|
45
|
+
<false/>
|
|
46
|
+
|
|
47
|
+
<key>KeepAlive</key>
|
|
48
|
+
<dict>
|
|
49
|
+
<key>Crashed</key>
|
|
50
|
+
<true/>
|
|
51
|
+
<key>SuccessfulExit</key>
|
|
52
|
+
<false/>
|
|
53
|
+
</dict>
|
|
54
|
+
|
|
55
|
+
<key>StandardOutPath</key>
|
|
56
|
+
<string>${config.stdoutPath}</string>
|
|
57
|
+
|
|
58
|
+
<key>StandardErrorPath</key>
|
|
59
|
+
<string>${config.stderrPath}</string>
|
|
60
|
+
|
|
61
|
+
<key>WorkingDirectory</key>
|
|
62
|
+
<string>/tmp</string>
|
|
63
|
+
|
|
64
|
+
<key>ThrottleInterval</key>
|
|
65
|
+
<integer>10</integer>
|
|
66
|
+
</dict>
|
|
67
|
+
</plist>
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create and write plist file
|
|
73
|
+
*/
|
|
74
|
+
async createPlist(config: ServerConfig): Promise<void> {
|
|
75
|
+
const plistContent = this.generatePlist(config);
|
|
76
|
+
await writeFileAtomic(config.plistPath, plistContent);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Delete plist file
|
|
81
|
+
*/
|
|
82
|
+
async deletePlist(plistPath: string): Promise<void> {
|
|
83
|
+
if (await fileExists(plistPath)) {
|
|
84
|
+
await fs.unlink(plistPath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Load service (register with launchctl)
|
|
90
|
+
*/
|
|
91
|
+
async loadService(plistPath: string): Promise<void> {
|
|
92
|
+
await execCommand(`launchctl load "${plistPath}"`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Unload service (unregister from launchctl)
|
|
97
|
+
*/
|
|
98
|
+
async unloadService(plistPath: string): Promise<void> {
|
|
99
|
+
try {
|
|
100
|
+
await execCommand(`launchctl unload "${plistPath}"`);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// Ignore errors if service is not loaded
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Start service
|
|
108
|
+
*/
|
|
109
|
+
async startService(label: string): Promise<void> {
|
|
110
|
+
await execCommand(`launchctl start ${label}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Stop service
|
|
115
|
+
*/
|
|
116
|
+
async stopService(label: string): Promise<void> {
|
|
117
|
+
await execCommand(`launchctl stop ${label}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get service status from launchctl
|
|
122
|
+
*/
|
|
123
|
+
async getServiceStatus(label: string): Promise<ServiceStatus> {
|
|
124
|
+
try {
|
|
125
|
+
const { stdout } = await execAsync(`launchctl list | grep ${label}`);
|
|
126
|
+
const lines = stdout.trim().split('\n');
|
|
127
|
+
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
const parts = line.split(/\s+/);
|
|
130
|
+
if (parts.length >= 3) {
|
|
131
|
+
const pidStr = parts[0].trim();
|
|
132
|
+
const exitCodeStr = parts[1].trim();
|
|
133
|
+
const serviceLabel = parts[2].trim();
|
|
134
|
+
|
|
135
|
+
// Match the exact label
|
|
136
|
+
if (serviceLabel === label) {
|
|
137
|
+
const pid = pidStr !== '-' ? parseInt(pidStr, 10) : null;
|
|
138
|
+
const exitCode = exitCodeStr !== '-' ? parseInt(exitCodeStr, 10) : null;
|
|
139
|
+
const isRunning = pid !== null;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
isRunning,
|
|
143
|
+
pid,
|
|
144
|
+
exitCode,
|
|
145
|
+
lastExitReason: this.interpretExitCode(exitCode),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Service not found
|
|
152
|
+
return {
|
|
153
|
+
isRunning: false,
|
|
154
|
+
pid: null,
|
|
155
|
+
exitCode: null,
|
|
156
|
+
};
|
|
157
|
+
} catch (error) {
|
|
158
|
+
// Service not found or not loaded
|
|
159
|
+
return {
|
|
160
|
+
isRunning: false,
|
|
161
|
+
pid: null,
|
|
162
|
+
exitCode: null,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Interpret exit code to human-readable reason
|
|
169
|
+
*/
|
|
170
|
+
private interpretExitCode(code: number | null): string | undefined {
|
|
171
|
+
if (code === null || code === 0) return undefined;
|
|
172
|
+
if (code === -9) return 'Force killed (SIGKILL)';
|
|
173
|
+
if (code === -15) return 'Terminated (SIGTERM)';
|
|
174
|
+
return `Exit code: ${code}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Wait for service to start (with timeout)
|
|
179
|
+
*/
|
|
180
|
+
async waitForServiceStart(label: string, timeoutMs = 5000): Promise<boolean> {
|
|
181
|
+
const startTime = Date.now();
|
|
182
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
183
|
+
const status = await this.getServiceStatus(label);
|
|
184
|
+
if (status.isRunning) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Wait for service to stop (with timeout)
|
|
194
|
+
*/
|
|
195
|
+
async waitForServiceStop(label: string, timeoutMs = 5000): Promise<boolean> {
|
|
196
|
+
const startTime = Date.now();
|
|
197
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
198
|
+
const status = await this.getServiceStatus(label);
|
|
199
|
+
if (!status.isRunning) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Export singleton instance
|
|
209
|
+
export const launchctlManager = new LaunchctlManager();
|