@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,177 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import { ServerConfig } from '../types/server-config';
|
|
4
|
+
import { GlobalConfig, DEFAULT_GLOBAL_CONFIG } from '../types/global-config';
|
|
5
|
+
import {
|
|
6
|
+
ensureDir,
|
|
7
|
+
writeJsonAtomic,
|
|
8
|
+
readJson,
|
|
9
|
+
fileExists,
|
|
10
|
+
getConfigDir,
|
|
11
|
+
getServersDir,
|
|
12
|
+
getLogsDir,
|
|
13
|
+
getGlobalConfigPath,
|
|
14
|
+
getModelsDir,
|
|
15
|
+
getLaunchAgentsDir,
|
|
16
|
+
} from '../utils/file-utils';
|
|
17
|
+
|
|
18
|
+
export class StateManager {
|
|
19
|
+
private configDir: string;
|
|
20
|
+
private serversDir: string;
|
|
21
|
+
private logsDir: string;
|
|
22
|
+
private globalConfigPath: string;
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
this.configDir = getConfigDir();
|
|
26
|
+
this.serversDir = getServersDir();
|
|
27
|
+
this.logsDir = getLogsDir();
|
|
28
|
+
this.globalConfigPath = getGlobalConfigPath();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Initialize config directories
|
|
33
|
+
*/
|
|
34
|
+
async initialize(): Promise<void> {
|
|
35
|
+
await ensureDir(this.configDir);
|
|
36
|
+
await ensureDir(this.serversDir);
|
|
37
|
+
await ensureDir(this.logsDir);
|
|
38
|
+
await ensureDir(getLaunchAgentsDir());
|
|
39
|
+
|
|
40
|
+
// Create default global config if it doesn't exist
|
|
41
|
+
if (!(await fileExists(this.globalConfigPath))) {
|
|
42
|
+
const defaultConfig: GlobalConfig = {
|
|
43
|
+
...DEFAULT_GLOBAL_CONFIG,
|
|
44
|
+
modelsDirectory: getModelsDir(),
|
|
45
|
+
};
|
|
46
|
+
await this.saveGlobalConfig(defaultConfig);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Load global configuration
|
|
52
|
+
*/
|
|
53
|
+
async loadGlobalConfig(): Promise<GlobalConfig> {
|
|
54
|
+
await this.initialize();
|
|
55
|
+
return await readJson<GlobalConfig>(this.globalConfigPath);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Save global configuration
|
|
60
|
+
*/
|
|
61
|
+
async saveGlobalConfig(config: GlobalConfig): Promise<void> {
|
|
62
|
+
await writeJsonAtomic(this.globalConfigPath, config);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Load a server configuration by ID
|
|
67
|
+
*/
|
|
68
|
+
async loadServerConfig(id: string): Promise<ServerConfig | null> {
|
|
69
|
+
const configPath = path.join(this.serversDir, `${id}.json`);
|
|
70
|
+
if (!(await fileExists(configPath))) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return await readJson<ServerConfig>(configPath);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Save a server configuration
|
|
78
|
+
*/
|
|
79
|
+
async saveServerConfig(config: ServerConfig): Promise<void> {
|
|
80
|
+
const configPath = path.join(this.serversDir, `${config.id}.json`);
|
|
81
|
+
await writeJsonAtomic(configPath, config);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Delete a server configuration
|
|
86
|
+
*/
|
|
87
|
+
async deleteServerConfig(id: string): Promise<void> {
|
|
88
|
+
const configPath = path.join(this.serversDir, `${id}.json`);
|
|
89
|
+
if (await fileExists(configPath)) {
|
|
90
|
+
await fs.unlink(configPath);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get all server configurations
|
|
96
|
+
*/
|
|
97
|
+
async getAllServers(): Promise<ServerConfig[]> {
|
|
98
|
+
await ensureDir(this.serversDir);
|
|
99
|
+
const files = await fs.readdir(this.serversDir);
|
|
100
|
+
const configFiles = files.filter((f) => f.endsWith('.json'));
|
|
101
|
+
|
|
102
|
+
const servers: ServerConfig[] = [];
|
|
103
|
+
for (const file of configFiles) {
|
|
104
|
+
const filePath = path.join(this.serversDir, file);
|
|
105
|
+
try {
|
|
106
|
+
const config = await readJson<ServerConfig>(filePath);
|
|
107
|
+
servers.push(config);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(`Failed to load server config ${file}:`, error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return servers;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Find a server by port
|
|
118
|
+
*/
|
|
119
|
+
async findServerByPort(port: number): Promise<ServerConfig | null> {
|
|
120
|
+
const servers = await this.getAllServers();
|
|
121
|
+
return servers.find((s) => s.port === port) || null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Find a server by model name (fuzzy match)
|
|
126
|
+
*/
|
|
127
|
+
async findServerByModelName(name: string): Promise<ServerConfig | null> {
|
|
128
|
+
const servers = await this.getAllServers();
|
|
129
|
+
const nameLower = name.toLowerCase();
|
|
130
|
+
|
|
131
|
+
// Try exact ID match first
|
|
132
|
+
const exactMatch = servers.find((s) => s.id === nameLower);
|
|
133
|
+
if (exactMatch) return exactMatch;
|
|
134
|
+
|
|
135
|
+
// Try partial match on model name or ID
|
|
136
|
+
const partialMatch = servers.find(
|
|
137
|
+
(s) =>
|
|
138
|
+
s.modelName.toLowerCase().includes(nameLower) ||
|
|
139
|
+
s.id.toLowerCase().includes(nameLower)
|
|
140
|
+
);
|
|
141
|
+
return partialMatch || null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Find a server by identifier (ID, model name, or port)
|
|
146
|
+
*/
|
|
147
|
+
async findServer(identifier: string): Promise<ServerConfig | null> {
|
|
148
|
+
// Try as port number
|
|
149
|
+
const port = parseInt(identifier, 10);
|
|
150
|
+
if (!isNaN(port)) {
|
|
151
|
+
const server = await this.findServerByPort(port);
|
|
152
|
+
if (server) return server;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Try as ID or model name
|
|
156
|
+
return await this.findServerByModelName(identifier);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if a server exists for a given model
|
|
161
|
+
*/
|
|
162
|
+
async serverExistsForModel(modelPath: string): Promise<boolean> {
|
|
163
|
+
const servers = await this.getAllServers();
|
|
164
|
+
return servers.some((s) => s.modelPath === modelPath);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get all used ports
|
|
169
|
+
*/
|
|
170
|
+
async getUsedPorts(): Promise<Set<number>> {
|
|
171
|
+
const servers = await this.getAllServers();
|
|
172
|
+
return new Set(servers.map((s) => s.port));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Export singleton instance
|
|
177
|
+
export const stateManager = new StateManager();
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { ServerConfig, ServerStatus } from '../types/server-config';
|
|
2
|
+
import { launchctlManager, ServiceStatus } from './launchctl-manager';
|
|
3
|
+
import { isPortInUse, isProcessRunning } from '../utils/process-utils';
|
|
4
|
+
import { stateManager } from './state-manager';
|
|
5
|
+
|
|
6
|
+
export class StatusChecker {
|
|
7
|
+
/**
|
|
8
|
+
* Check the real-time status of a server
|
|
9
|
+
*/
|
|
10
|
+
async checkServer(config: ServerConfig): Promise<ServiceStatus & { portListening: boolean }> {
|
|
11
|
+
// Get launchctl status
|
|
12
|
+
const launchStatus = await launchctlManager.getServiceStatus(config.label);
|
|
13
|
+
|
|
14
|
+
// Cross-check port
|
|
15
|
+
const portListening = await isPortInUse(config.port);
|
|
16
|
+
|
|
17
|
+
// Verify PID if reported
|
|
18
|
+
if (launchStatus.pid) {
|
|
19
|
+
const pidRunning = await isProcessRunning(launchStatus.pid);
|
|
20
|
+
if (!pidRunning) {
|
|
21
|
+
// PID reported but process not running
|
|
22
|
+
return {
|
|
23
|
+
...launchStatus,
|
|
24
|
+
isRunning: false,
|
|
25
|
+
portListening,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
...launchStatus,
|
|
32
|
+
portListening,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Determine server status based on checks
|
|
38
|
+
*/
|
|
39
|
+
determineStatus(serviceStatus: ServiceStatus, portListening: boolean): ServerStatus {
|
|
40
|
+
if (serviceStatus.isRunning && portListening) {
|
|
41
|
+
return 'running';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!serviceStatus.isRunning && serviceStatus.exitCode && serviceStatus.exitCode !== 0) {
|
|
45
|
+
return 'crashed';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return 'stopped';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Update a server's status in its config
|
|
53
|
+
*/
|
|
54
|
+
async updateServerStatus(config: ServerConfig): Promise<ServerConfig> {
|
|
55
|
+
const status = await this.checkServer(config);
|
|
56
|
+
const newStatus = this.determineStatus(status, status.portListening);
|
|
57
|
+
|
|
58
|
+
const updatedConfig: ServerConfig = {
|
|
59
|
+
...config,
|
|
60
|
+
status: newStatus,
|
|
61
|
+
pid: status.pid || undefined,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Update timestamps
|
|
65
|
+
if (newStatus === 'running' && config.status !== 'running') {
|
|
66
|
+
updatedConfig.lastStarted = new Date().toISOString();
|
|
67
|
+
} else if (newStatus === 'stopped' && config.status === 'running') {
|
|
68
|
+
updatedConfig.lastStopped = new Date().toISOString();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Save updated config
|
|
72
|
+
await stateManager.saveServerConfig(updatedConfig);
|
|
73
|
+
|
|
74
|
+
return updatedConfig;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Update status for all servers
|
|
79
|
+
*/
|
|
80
|
+
async updateAllServerStatuses(): Promise<ServerConfig[]> {
|
|
81
|
+
const servers = await stateManager.getAllServers();
|
|
82
|
+
const updated: ServerConfig[] = [];
|
|
83
|
+
|
|
84
|
+
for (const server of servers) {
|
|
85
|
+
const updatedServer = await this.updateServerStatus(server);
|
|
86
|
+
updated.push(updatedServer);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return updated;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Find crashed servers
|
|
94
|
+
*/
|
|
95
|
+
async findCrashedServers(): Promise<ServerConfig[]> {
|
|
96
|
+
const servers = await stateManager.getAllServers();
|
|
97
|
+
const crashed: ServerConfig[] = [];
|
|
98
|
+
|
|
99
|
+
for (const server of servers) {
|
|
100
|
+
if (server.status === 'running') {
|
|
101
|
+
const status = await this.checkServer(server);
|
|
102
|
+
if (!status.isRunning && status.exitCode !== 0 && status.exitCode !== null) {
|
|
103
|
+
crashed.push(server);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return crashed;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Export singleton instance
|
|
113
|
+
export const statusChecker = new StatusChecker();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface GlobalConfig {
|
|
2
|
+
version: string;
|
|
3
|
+
defaultPort: number;
|
|
4
|
+
modelsDirectory: string; // ~/models expanded to full path
|
|
5
|
+
llamaServerBinary: string; // /opt/homebrew/bin/llama-server
|
|
6
|
+
defaults: {
|
|
7
|
+
threads: number;
|
|
8
|
+
ctxSize: number;
|
|
9
|
+
gpuLayers: number;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default global configuration
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
|
|
17
|
+
version: '1.0.0',
|
|
18
|
+
defaultPort: 9000,
|
|
19
|
+
modelsDirectory: '', // Set at runtime
|
|
20
|
+
llamaServerBinary: '/opt/homebrew/bin/llama-server',
|
|
21
|
+
defaults: {
|
|
22
|
+
threads: 8,
|
|
23
|
+
ctxSize: 8192,
|
|
24
|
+
gpuLayers: 60,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ModelInfo {
|
|
2
|
+
filename: string; // Original filename
|
|
3
|
+
path: string; // Full absolute path
|
|
4
|
+
size: number; // File size in bytes
|
|
5
|
+
sizeFormatted: string; // Human-readable size (e.g., "1.9 GB")
|
|
6
|
+
modified: Date; // Last modified date
|
|
7
|
+
exists: boolean; // File exists and is readable
|
|
8
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type ServerStatus = 'running' | 'stopped' | 'crashed';
|
|
2
|
+
|
|
3
|
+
export interface ServerConfig {
|
|
4
|
+
id: string; // Sanitized model name (unique identifier)
|
|
5
|
+
modelPath: string; // Full path to GGUF file
|
|
6
|
+
modelName: string; // Display name (original filename)
|
|
7
|
+
port: number; // Server port
|
|
8
|
+
|
|
9
|
+
// llama-server configuration
|
|
10
|
+
threads: number;
|
|
11
|
+
ctxSize: number;
|
|
12
|
+
gpuLayers: number;
|
|
13
|
+
embeddings: boolean; // Always true
|
|
14
|
+
jinja: boolean; // Always true
|
|
15
|
+
|
|
16
|
+
// State tracking
|
|
17
|
+
status: ServerStatus;
|
|
18
|
+
pid?: number;
|
|
19
|
+
createdAt: string; // ISO timestamp
|
|
20
|
+
lastStarted?: string; // ISO timestamp
|
|
21
|
+
lastStopped?: string; // ISO timestamp
|
|
22
|
+
|
|
23
|
+
// launchctl metadata
|
|
24
|
+
plistPath: string; // Full path to plist file
|
|
25
|
+
label: string; // launchctl service label (com.llama.<id>)
|
|
26
|
+
|
|
27
|
+
// Logging
|
|
28
|
+
stdoutPath: string; // Path to stdout log
|
|
29
|
+
stderrPath: string; // Path to stderr log
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Sanitize a model filename to create a valid server ID
|
|
34
|
+
* Example: "llama-3.2-3b-instruct-q4_k_m.gguf" → "llama-3-2-3b-instruct-q4-k-m"
|
|
35
|
+
*/
|
|
36
|
+
export function sanitizeModelName(modelName: string): string {
|
|
37
|
+
return modelName
|
|
38
|
+
.replace(/\.gguf$/i, '') // Remove .gguf extension
|
|
39
|
+
.replace(/[^a-zA-Z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
|
|
40
|
+
.toLowerCase() // Lowercase
|
|
41
|
+
.replace(/^-+|-+$/g, ''); // Trim hyphens from ends
|
|
42
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Ensure a directory exists, creating it if necessary
|
|
7
|
+
*/
|
|
8
|
+
export async function ensureDir(dirPath: string): Promise<void> {
|
|
9
|
+
try {
|
|
10
|
+
await fs.mkdir(dirPath, { recursive: true, mode: 0o755 });
|
|
11
|
+
} catch (error) {
|
|
12
|
+
// Ignore error if directory already exists
|
|
13
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Write a file atomically (write to temp, then rename)
|
|
21
|
+
*/
|
|
22
|
+
export async function writeFileAtomic(filePath: string, content: string): Promise<void> {
|
|
23
|
+
const tempPath = `${filePath}.tmp`;
|
|
24
|
+
await fs.writeFile(tempPath, content, 'utf-8');
|
|
25
|
+
await fs.rename(tempPath, filePath);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Write JSON to a file atomically
|
|
30
|
+
*/
|
|
31
|
+
export async function writeJsonAtomic(filePath: string, data: any): Promise<void> {
|
|
32
|
+
const content = JSON.stringify(data, null, 2);
|
|
33
|
+
await writeFileAtomic(filePath, content);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Read and parse JSON file
|
|
38
|
+
*/
|
|
39
|
+
export async function readJson<T>(filePath: string): Promise<T> {
|
|
40
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
41
|
+
return JSON.parse(content);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if a file exists
|
|
46
|
+
*/
|
|
47
|
+
export async function fileExists(filePath: string): Promise<boolean> {
|
|
48
|
+
try {
|
|
49
|
+
await fs.access(filePath);
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get the llamacpp config directory (~/.llamacpp)
|
|
58
|
+
*/
|
|
59
|
+
export function getConfigDir(): string {
|
|
60
|
+
return path.join(os.homedir(), '.llamacpp');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get the servers directory (~/.llamacpp/servers)
|
|
65
|
+
*/
|
|
66
|
+
export function getServersDir(): string {
|
|
67
|
+
return path.join(getConfigDir(), 'servers');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get the logs directory (~/.llamacpp/logs)
|
|
72
|
+
*/
|
|
73
|
+
export function getLogsDir(): string {
|
|
74
|
+
return path.join(getConfigDir(), 'logs');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get the global config file path
|
|
79
|
+
*/
|
|
80
|
+
export function getGlobalConfigPath(): string {
|
|
81
|
+
return path.join(getConfigDir(), 'config.json');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get the models directory (~/models)
|
|
86
|
+
*/
|
|
87
|
+
export function getModelsDir(): string {
|
|
88
|
+
return path.join(os.homedir(), 'models');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the LaunchAgents directory
|
|
93
|
+
*/
|
|
94
|
+
export function getLaunchAgentsDir(): string {
|
|
95
|
+
return path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Expand tilde (~) in path to home directory
|
|
100
|
+
*/
|
|
101
|
+
export function expandHome(filePath: string): string {
|
|
102
|
+
if (filePath.startsWith('~/')) {
|
|
103
|
+
return path.join(os.homedir(), filePath.slice(2));
|
|
104
|
+
}
|
|
105
|
+
return filePath;
|
|
106
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format bytes to human-readable size
|
|
3
|
+
* Example: 1900000000 → "1.9 GB"
|
|
4
|
+
*/
|
|
5
|
+
export function formatBytes(bytes: number): string {
|
|
6
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
7
|
+
let size = bytes;
|
|
8
|
+
let unitIndex = 0;
|
|
9
|
+
|
|
10
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
11
|
+
size /= 1024;
|
|
12
|
+
unitIndex++;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format a date to a human-readable string
|
|
20
|
+
* Example: "Nov 20, 2025 10:30 AM"
|
|
21
|
+
*/
|
|
22
|
+
export function formatDate(date: Date | string): string {
|
|
23
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
24
|
+
return d.toLocaleString('en-US', {
|
|
25
|
+
month: 'short',
|
|
26
|
+
day: 'numeric',
|
|
27
|
+
year: 'numeric',
|
|
28
|
+
hour: 'numeric',
|
|
29
|
+
minute: '2-digit',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format a date to a short string (no time)
|
|
35
|
+
* Example: "Nov 20"
|
|
36
|
+
*/
|
|
37
|
+
export function formatDateShort(date: Date | string): string {
|
|
38
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
39
|
+
return d.toLocaleString('en-US', {
|
|
40
|
+
month: 'short',
|
|
41
|
+
day: 'numeric',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Calculate uptime from start time
|
|
47
|
+
* Example: "2d 4h" or "45m" or "30s"
|
|
48
|
+
*/
|
|
49
|
+
export function formatUptime(startTime: string): string {
|
|
50
|
+
const start = new Date(startTime).getTime();
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const seconds = Math.floor((now - start) / 1000);
|
|
53
|
+
|
|
54
|
+
if (seconds < 60) return `${seconds}s`;
|
|
55
|
+
|
|
56
|
+
const minutes = Math.floor(seconds / 60);
|
|
57
|
+
if (minutes < 60) return `${minutes}m`;
|
|
58
|
+
|
|
59
|
+
const hours = Math.floor(minutes / 60);
|
|
60
|
+
if (hours < 24) return `${hours}h`;
|
|
61
|
+
|
|
62
|
+
const days = Math.floor(hours / 24);
|
|
63
|
+
const remainingHours = hours % 24;
|
|
64
|
+
return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Truncate a string to a maximum length
|
|
69
|
+
*/
|
|
70
|
+
export function truncate(str: string, maxLength: number): string {
|
|
71
|
+
if (str.length <= maxLength) return str;
|
|
72
|
+
return str.slice(0, maxLength - 3) + '...';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Pad a string to a specific length
|
|
77
|
+
*/
|
|
78
|
+
export function pad(str: string, length: number, char = ' '): string {
|
|
79
|
+
return str.padEnd(length, char);
|
|
80
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
|
|
4
|
+
export const execAsync = promisify(exec);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Execute a command and return stdout
|
|
8
|
+
* Throws on non-zero exit code
|
|
9
|
+
*/
|
|
10
|
+
export async function execCommand(command: string): Promise<string> {
|
|
11
|
+
const { stdout } = await execAsync(command);
|
|
12
|
+
return stdout.trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Execute a command and return both stdout and stderr
|
|
17
|
+
*/
|
|
18
|
+
export async function execCommandFull(command: string): Promise<{ stdout: string; stderr: string }> {
|
|
19
|
+
const { stdout, stderr } = await execAsync(command);
|
|
20
|
+
return {
|
|
21
|
+
stdout: stdout.trim(),
|
|
22
|
+
stderr: stderr.trim(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if a command exists in PATH
|
|
28
|
+
*/
|
|
29
|
+
export async function commandExists(command: string): Promise<boolean> {
|
|
30
|
+
try {
|
|
31
|
+
await execAsync(`which ${command}`);
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if a process is running by PID
|
|
40
|
+
*/
|
|
41
|
+
export async function isProcessRunning(pid: number): Promise<boolean> {
|
|
42
|
+
try {
|
|
43
|
+
await execAsync(`ps -p ${pid}`);
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if a port is in use
|
|
52
|
+
*/
|
|
53
|
+
export async function isPortInUse(port: number): Promise<boolean> {
|
|
54
|
+
try {
|
|
55
|
+
await execAsync(`lsof -iTCP:${port} -sTCP:LISTEN -t`);
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"moduleResolution": "node"
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|