@appkit/llamacpp-cli 1.12.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +294 -168
- package/dist/cli.js +35 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/launch/claude.d.ts +6 -0
- package/dist/commands/launch/claude.d.ts.map +1 -0
- package/dist/commands/launch/claude.js +277 -0
- package/dist/commands/launch/claude.js.map +1 -0
- package/dist/lib/integration-checker.d.ts +26 -0
- package/dist/lib/integration-checker.d.ts.map +1 -0
- package/dist/lib/integration-checker.js +77 -0
- package/dist/lib/integration-checker.js.map +1 -0
- package/dist/lib/router-manager.d.ts +4 -0
- package/dist/lib/router-manager.d.ts.map +1 -1
- package/dist/lib/router-manager.js +10 -0
- package/dist/lib/router-manager.js.map +1 -1
- package/dist/lib/router-server.d.ts +13 -0
- package/dist/lib/router-server.d.ts.map +1 -1
- package/dist/lib/router-server.js +267 -7
- package/dist/lib/router-server.js.map +1 -1
- package/dist/types/integration-config.d.ts +28 -0
- package/dist/types/integration-config.d.ts.map +1 -0
- package/dist/types/integration-config.js +3 -0
- package/dist/types/integration-config.js.map +1 -0
- package/package.json +10 -2
- package/web/dist/assets/index-Bin89Lwr.css +1 -0
- package/web/dist/assets/index-CVmonw3T.js +17 -0
- package/web/{index.html → dist/index.html} +2 -1
- package/.versionrc.json +0 -16
- package/CHANGELOG.md +0 -213
- package/docs/images/.gitkeep +0 -1
- package/docs/images/web-ui-servers.png +0 -0
- package/src/cli.ts +0 -523
- package/src/commands/admin/config.ts +0 -121
- package/src/commands/admin/logs.ts +0 -91
- package/src/commands/admin/restart.ts +0 -26
- package/src/commands/admin/start.ts +0 -27
- package/src/commands/admin/status.ts +0 -84
- package/src/commands/admin/stop.ts +0 -16
- package/src/commands/config-global.ts +0 -38
- package/src/commands/config.ts +0 -323
- package/src/commands/create.ts +0 -183
- package/src/commands/delete.ts +0 -74
- package/src/commands/list.ts +0 -37
- package/src/commands/logs-all.ts +0 -251
- package/src/commands/logs.ts +0 -345
- package/src/commands/monitor.ts +0 -110
- package/src/commands/ps.ts +0 -84
- package/src/commands/pull.ts +0 -44
- package/src/commands/rm.ts +0 -107
- package/src/commands/router/config.ts +0 -116
- package/src/commands/router/logs.ts +0 -256
- package/src/commands/router/restart.ts +0 -36
- package/src/commands/router/start.ts +0 -60
- package/src/commands/router/status.ts +0 -119
- package/src/commands/router/stop.ts +0 -33
- package/src/commands/run.ts +0 -233
- package/src/commands/search.ts +0 -107
- package/src/commands/server-show.ts +0 -161
- package/src/commands/show.ts +0 -207
- package/src/commands/start.ts +0 -101
- package/src/commands/stop.ts +0 -39
- package/src/commands/tui.ts +0 -25
- package/src/lib/admin-manager.ts +0 -435
- package/src/lib/admin-server.ts +0 -1243
- package/src/lib/config-generator.ts +0 -130
- package/src/lib/download-job-manager.ts +0 -213
- package/src/lib/history-manager.ts +0 -172
- package/src/lib/launchctl-manager.ts +0 -225
- package/src/lib/metrics-aggregator.ts +0 -257
- package/src/lib/model-downloader.ts +0 -328
- package/src/lib/model-scanner.ts +0 -157
- package/src/lib/model-search.ts +0 -114
- package/src/lib/models-dir-setup.ts +0 -46
- package/src/lib/port-manager.ts +0 -80
- package/src/lib/router-logger.ts +0 -201
- package/src/lib/router-manager.ts +0 -414
- package/src/lib/router-server.ts +0 -538
- package/src/lib/state-manager.ts +0 -206
- package/src/lib/status-checker.ts +0 -113
- package/src/lib/system-collector.ts +0 -315
- package/src/tui/ConfigApp.ts +0 -1085
- package/src/tui/HistoricalMonitorApp.ts +0 -587
- package/src/tui/ModelsApp.ts +0 -368
- package/src/tui/MonitorApp.ts +0 -386
- package/src/tui/MultiServerMonitorApp.ts +0 -1833
- package/src/tui/RootNavigator.ts +0 -74
- package/src/tui/SearchApp.ts +0 -511
- package/src/tui/SplashScreen.ts +0 -149
- package/src/types/admin-config.ts +0 -25
- package/src/types/global-config.ts +0 -26
- package/src/types/history-types.ts +0 -39
- package/src/types/model-info.ts +0 -8
- package/src/types/monitor-types.ts +0 -162
- package/src/types/router-config.ts +0 -25
- package/src/types/server-config.ts +0 -46
- package/src/utils/downsample-utils.ts +0 -128
- package/src/utils/file-utils.ts +0 -146
- package/src/utils/format-utils.ts +0 -98
- package/src/utils/log-parser.ts +0 -284
- package/src/utils/log-utils.ts +0 -178
- package/src/utils/process-utils.ts +0 -316
- package/src/utils/prompt-utils.ts +0 -47
- package/test-load.sh +0 -100
- package/tsconfig.json +0 -20
- package/web/eslint.config.js +0 -23
- package/web/llamacpp-web-dist.tar.gz +0 -0
- package/web/package-lock.json +0 -4017
- package/web/package.json +0 -38
- package/web/postcss.config.js +0 -6
- package/web/src/App.css +0 -42
- package/web/src/App.tsx +0 -86
- package/web/src/assets/react.svg +0 -1
- package/web/src/components/ApiKeyPrompt.tsx +0 -71
- package/web/src/components/CreateServerModal.tsx +0 -372
- package/web/src/components/DownloadProgress.tsx +0 -123
- package/web/src/components/Nav.tsx +0 -89
- package/web/src/components/RouterConfigModal.tsx +0 -240
- package/web/src/components/SearchModal.tsx +0 -306
- package/web/src/components/ServerConfigModal.tsx +0 -291
- package/web/src/hooks/useApi.ts +0 -259
- package/web/src/index.css +0 -42
- package/web/src/lib/api.ts +0 -226
- package/web/src/main.tsx +0 -10
- package/web/src/pages/Dashboard.tsx +0 -103
- package/web/src/pages/Models.tsx +0 -258
- package/web/src/pages/Router.tsx +0 -270
- package/web/src/pages/RouterLogs.tsx +0 -201
- package/web/src/pages/ServerLogs.tsx +0 -553
- package/web/src/pages/Servers.tsx +0 -358
- package/web/src/types/api.ts +0 -140
- package/web/tailwind.config.js +0 -31
- package/web/tsconfig.app.json +0 -28
- package/web/tsconfig.json +0 -7
- package/web/tsconfig.node.json +0 -26
- package/web/vite.config.ts +0 -25
- /package/web/{public → dist}/vite.svg +0 -0
package/src/lib/state-manager.ts
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
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
|
-
* Update a server configuration with partial changes
|
|
86
|
-
*/
|
|
87
|
-
async updateServerConfig(id: string, updates: Partial<ServerConfig>): Promise<void> {
|
|
88
|
-
const existingConfig = await this.loadServerConfig(id);
|
|
89
|
-
if (!existingConfig) {
|
|
90
|
-
throw new Error(`Server configuration not found: ${id}`);
|
|
91
|
-
}
|
|
92
|
-
const updatedConfig = { ...existingConfig, ...updates };
|
|
93
|
-
await this.saveServerConfig(updatedConfig);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Delete a server configuration
|
|
98
|
-
*/
|
|
99
|
-
async deleteServerConfig(id: string): Promise<void> {
|
|
100
|
-
const configPath = path.join(this.serversDir, `${id}.json`);
|
|
101
|
-
if (await fileExists(configPath)) {
|
|
102
|
-
await fs.unlink(configPath);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Get all server configurations
|
|
108
|
-
*/
|
|
109
|
-
async getAllServers(): Promise<ServerConfig[]> {
|
|
110
|
-
await ensureDir(this.serversDir);
|
|
111
|
-
const files = await fs.readdir(this.serversDir);
|
|
112
|
-
const configFiles = files.filter((f) => f.endsWith('.json'));
|
|
113
|
-
|
|
114
|
-
const servers: ServerConfig[] = [];
|
|
115
|
-
for (const file of configFiles) {
|
|
116
|
-
const filePath = path.join(this.serversDir, file);
|
|
117
|
-
try {
|
|
118
|
-
const config = await readJson<ServerConfig>(filePath);
|
|
119
|
-
servers.push(config);
|
|
120
|
-
} catch (error) {
|
|
121
|
-
console.error(`Failed to load server config ${file}:`, error);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return servers;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Find a server by port
|
|
130
|
-
*/
|
|
131
|
-
async findServerByPort(port: number): Promise<ServerConfig | null> {
|
|
132
|
-
const servers = await this.getAllServers();
|
|
133
|
-
return servers.find((s) => s.port === port) || null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Find a server by model name (fuzzy match)
|
|
138
|
-
*/
|
|
139
|
-
async findServerByModelName(name: string): Promise<ServerConfig | null> {
|
|
140
|
-
const servers = await this.getAllServers();
|
|
141
|
-
const nameLower = name.toLowerCase();
|
|
142
|
-
|
|
143
|
-
// Try exact ID match first
|
|
144
|
-
const exactMatch = servers.find((s) => s.id === nameLower);
|
|
145
|
-
if (exactMatch) return exactMatch;
|
|
146
|
-
|
|
147
|
-
// Try partial match on model name or ID
|
|
148
|
-
const partialMatch = servers.find(
|
|
149
|
-
(s) =>
|
|
150
|
-
s.modelName.toLowerCase().includes(nameLower) ||
|
|
151
|
-
s.id.toLowerCase().includes(nameLower)
|
|
152
|
-
);
|
|
153
|
-
return partialMatch || null;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Find a server by identifier (ID, model name, or port)
|
|
158
|
-
*/
|
|
159
|
-
async findServer(identifier: string): Promise<ServerConfig | null> {
|
|
160
|
-
// Try as port number
|
|
161
|
-
const port = parseInt(identifier, 10);
|
|
162
|
-
if (!isNaN(port)) {
|
|
163
|
-
const server = await this.findServerByPort(port);
|
|
164
|
-
if (server) return server;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Try as ID or model name
|
|
168
|
-
return await this.findServerByModelName(identifier);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Check if a server exists for a given model
|
|
173
|
-
*/
|
|
174
|
-
async serverExistsForModel(modelPath: string): Promise<boolean> {
|
|
175
|
-
const servers = await this.getAllServers();
|
|
176
|
-
return servers.some((s) => s.modelPath === modelPath);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Get all used ports
|
|
181
|
-
*/
|
|
182
|
-
async getUsedPorts(): Promise<Set<number>> {
|
|
183
|
-
const servers = await this.getAllServers();
|
|
184
|
-
return new Set(servers.map((s) => s.port));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Get the configured models directory
|
|
189
|
-
*/
|
|
190
|
-
async getModelsDirectory(): Promise<string> {
|
|
191
|
-
const config = await this.loadGlobalConfig();
|
|
192
|
-
return config.modelsDirectory;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Set the models directory
|
|
197
|
-
*/
|
|
198
|
-
async setModelsDirectory(directory: string): Promise<void> {
|
|
199
|
-
const config = await this.loadGlobalConfig();
|
|
200
|
-
config.modelsDirectory = directory;
|
|
201
|
-
await this.saveGlobalConfig(config);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Export singleton instance
|
|
206
|
-
export const stateManager = new StateManager();
|
|
@@ -1,113 +0,0 @@
|
|
|
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();
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
import { execCommand, spawnAndReadOneLine } from '../utils/process-utils.js';
|
|
2
|
-
import { SystemMetrics } from '../types/monitor-types.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* System metrics collector using macmon (optional) and vm_stat (fallback)
|
|
6
|
-
* Provides GPU, CPU, ANE, and memory metrics on macOS
|
|
7
|
-
*/
|
|
8
|
-
export class SystemCollector {
|
|
9
|
-
private macmonPath: string;
|
|
10
|
-
private macmonAvailable: boolean | null = null;
|
|
11
|
-
private lastSystemMetrics: SystemMetrics | null = null;
|
|
12
|
-
private lastCollectionTime: number = 0;
|
|
13
|
-
private readonly CACHE_TTL_MS = 4000; // Cache for 4 seconds (longer than macmon spawn time)
|
|
14
|
-
private collectingLock: Promise<SystemMetrics> | null = null;
|
|
15
|
-
private pCoreCount: number = 0;
|
|
16
|
-
private eCoreCount: number = 0;
|
|
17
|
-
private totalCores: number = 0;
|
|
18
|
-
|
|
19
|
-
constructor(macmonPath: string = '/opt/homebrew/bin/macmon') {
|
|
20
|
-
this.macmonPath = macmonPath;
|
|
21
|
-
this.initializeCoreCount();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Get CPU core counts for weighted average calculation
|
|
26
|
-
*/
|
|
27
|
-
private async initializeCoreCount(): Promise<void> {
|
|
28
|
-
try {
|
|
29
|
-
const { execCommand } = await import('../utils/process-utils.js');
|
|
30
|
-
|
|
31
|
-
// Try to get P-core and E-core counts separately (Apple Silicon)
|
|
32
|
-
try {
|
|
33
|
-
const pCores = await execCommand('sysctl -n hw.perflevel0.physicalcpu 2>/dev/null');
|
|
34
|
-
const eCores = await execCommand('sysctl -n hw.perflevel1.physicalcpu 2>/dev/null');
|
|
35
|
-
this.pCoreCount = parseInt(pCores, 10) || 0;
|
|
36
|
-
this.eCoreCount = parseInt(eCores, 10) || 0;
|
|
37
|
-
} catch {
|
|
38
|
-
// Fall back to total core count if perflevel not available
|
|
39
|
-
const total = await execCommand('sysctl -n hw.ncpu 2>/dev/null');
|
|
40
|
-
this.totalCores = parseInt(total, 10) || 0;
|
|
41
|
-
// Assume equal split if we can't get individual counts
|
|
42
|
-
this.pCoreCount = Math.floor(this.totalCores / 2);
|
|
43
|
-
this.eCoreCount = this.totalCores - this.pCoreCount;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
this.totalCores = this.pCoreCount + this.eCoreCount;
|
|
47
|
-
} catch {
|
|
48
|
-
// Default to 8 cores if we can't detect
|
|
49
|
-
this.pCoreCount = 4;
|
|
50
|
-
this.eCoreCount = 4;
|
|
51
|
-
this.totalCores = 8;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Check if macmon is available
|
|
57
|
-
*/
|
|
58
|
-
private async checkMacmonAvailability(): Promise<boolean> {
|
|
59
|
-
if (this.macmonAvailable !== null) {
|
|
60
|
-
return this.macmonAvailable;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const result = await execCommand(`which ${this.macmonPath} 2>/dev/null`);
|
|
65
|
-
this.macmonAvailable = result.length > 0;
|
|
66
|
-
} catch {
|
|
67
|
-
this.macmonAvailable = false;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return this.macmonAvailable;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Parse macmon JSON output
|
|
75
|
-
* Expected format from 'macmon pipe':
|
|
76
|
-
* {
|
|
77
|
-
* "gpu_usage": [count, percentage],
|
|
78
|
-
* "pcpu_usage": [count, percentage],
|
|
79
|
-
* "ecpu_usage": [count, percentage],
|
|
80
|
-
* "ane_power": number,
|
|
81
|
-
* "temp": {"cpu_temp_avg": number, "gpu_temp_avg": number}
|
|
82
|
-
* }
|
|
83
|
-
*/
|
|
84
|
-
private parseMacmonJson(jsonLine: string): {
|
|
85
|
-
gpuUsage?: number;
|
|
86
|
-
cpuUsage?: number;
|
|
87
|
-
aneUsage?: number;
|
|
88
|
-
temperature?: number;
|
|
89
|
-
} {
|
|
90
|
-
try {
|
|
91
|
-
const data = JSON.parse(jsonLine);
|
|
92
|
-
|
|
93
|
-
// GPU usage (second element of array, convert decimal to percentage)
|
|
94
|
-
const gpuUsage = data.gpu_usage?.[1] !== undefined
|
|
95
|
-
? data.gpu_usage[1] * 100
|
|
96
|
-
: undefined;
|
|
97
|
-
|
|
98
|
-
// CPU usage (weighted average of P-cores and E-cores)
|
|
99
|
-
// Each core type reports 0.0-1.0 utilization
|
|
100
|
-
// Calculate weighted average: (P% * Pcount + E% * Ecount) / totalCores
|
|
101
|
-
const pcpuUsage = data.pcpu_usage?.[1] || 0; // 0.0-1.0
|
|
102
|
-
const ecpuUsage = data.ecpu_usage?.[1] || 0; // 0.0-1.0
|
|
103
|
-
|
|
104
|
-
let cpuUsage: number | undefined;
|
|
105
|
-
if (this.totalCores > 0) {
|
|
106
|
-
// Weighted average normalized to 0-100%
|
|
107
|
-
cpuUsage = ((pcpuUsage * this.pCoreCount) + (ecpuUsage * this.eCoreCount)) / this.totalCores * 100;
|
|
108
|
-
} else {
|
|
109
|
-
// Fallback: simple average if core counts not available
|
|
110
|
-
cpuUsage = ((pcpuUsage + ecpuUsage) / 2) * 100;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ANE usage (estimate from power draw - macmon doesn't provide usage %)
|
|
114
|
-
// If ANE power > 0.1W, consider it active (rough estimate)
|
|
115
|
-
const aneUsage = data.ane_power > 0.1
|
|
116
|
-
? Math.min((data.ane_power / 8.0) * 100, 100) // Assume ~8W max for ANE
|
|
117
|
-
: 0;
|
|
118
|
-
|
|
119
|
-
// Temperature (use GPU temp if available, otherwise CPU)
|
|
120
|
-
const temperature = data.temp?.gpu_temp_avg || data.temp?.cpu_temp_avg;
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
gpuUsage,
|
|
124
|
-
cpuUsage: cpuUsage > 0 ? cpuUsage : undefined,
|
|
125
|
-
aneUsage: aneUsage > 1 ? aneUsage : undefined,
|
|
126
|
-
temperature,
|
|
127
|
-
};
|
|
128
|
-
} catch {
|
|
129
|
-
return {};
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Collect macmon metrics (GPU, CPU, ANE)
|
|
135
|
-
* Uses 'macmon pipe' which outputs one JSON line per update
|
|
136
|
-
* Spawns macmon, reads one line, and kills it to prevent process leaks
|
|
137
|
-
*/
|
|
138
|
-
private async getMacmonMetrics(): Promise<{
|
|
139
|
-
gpuUsage?: number;
|
|
140
|
-
cpuUsage?: number;
|
|
141
|
-
aneUsage?: number;
|
|
142
|
-
temperature?: number;
|
|
143
|
-
} | null> {
|
|
144
|
-
const available = await this.checkMacmonAvailability();
|
|
145
|
-
if (!available) {
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
// Spawn macmon pipe, read one line, and kill it
|
|
151
|
-
// This prevents orphaned macmon processes
|
|
152
|
-
// Timeout set to 5s because macmon can take 3-4s to produce first line
|
|
153
|
-
const output = await spawnAndReadOneLine(this.macmonPath, ['pipe'], 5000);
|
|
154
|
-
|
|
155
|
-
if (!output) {
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return this.parseMacmonJson(output);
|
|
160
|
-
} catch {
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Parse vm_stat output for memory metrics
|
|
167
|
-
* Expected format:
|
|
168
|
-
* Pages free: 123456.
|
|
169
|
-
* Pages active: 234567.
|
|
170
|
-
* Pages inactive: 345678.
|
|
171
|
-
* Pages speculative: 45678.
|
|
172
|
-
* Pages throttled: 0.
|
|
173
|
-
* Pages wired down: 123456.
|
|
174
|
-
* Pages purgeable count: 0.
|
|
175
|
-
* "Translation faults": 12345678.
|
|
176
|
-
* Pages copy-on-write: 123456.
|
|
177
|
-
* ...
|
|
178
|
-
*/
|
|
179
|
-
private parseVmStatOutput(output: string): {
|
|
180
|
-
memoryUsed: number;
|
|
181
|
-
} {
|
|
182
|
-
const lines = output.split('\n');
|
|
183
|
-
const pageSize = 16384; // 16KB on Apple Silicon
|
|
184
|
-
let pagesActive = 0;
|
|
185
|
-
let pagesWired = 0;
|
|
186
|
-
let pagesCompressed = 0;
|
|
187
|
-
|
|
188
|
-
for (const line of lines) {
|
|
189
|
-
const match = line.match(/Pages (.*?):\s+(\d+)\./);
|
|
190
|
-
if (match) {
|
|
191
|
-
const name = match[1].toLowerCase();
|
|
192
|
-
const value = parseInt(match[2], 10);
|
|
193
|
-
|
|
194
|
-
if (name === 'active') pagesActive = value;
|
|
195
|
-
else if (name === 'wired down') pagesWired = value;
|
|
196
|
-
else if (name === 'compressed') pagesCompressed = value;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Calculate used memory (active + wired + compressed)
|
|
201
|
-
// This matches what Activity Monitor and macmon report as "used"
|
|
202
|
-
const usedPages = pagesActive + pagesWired + pagesCompressed;
|
|
203
|
-
const memoryUsed = usedPages * pageSize;
|
|
204
|
-
|
|
205
|
-
return { memoryUsed };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Get total system memory from sysctl
|
|
210
|
-
* Returns installed RAM size in bytes
|
|
211
|
-
*/
|
|
212
|
-
private async getTotalMemory(): Promise<number> {
|
|
213
|
-
try {
|
|
214
|
-
const output = await execCommand('sysctl -n hw.memsize 2>/dev/null');
|
|
215
|
-
return parseInt(output.trim(), 10) || 0;
|
|
216
|
-
} catch {
|
|
217
|
-
return 0;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Collect vm_stat memory metrics + total system memory from sysctl
|
|
223
|
-
*/
|
|
224
|
-
private async getMemoryMetrics(): Promise<{
|
|
225
|
-
memoryUsed: number;
|
|
226
|
-
memoryTotal: number;
|
|
227
|
-
}> {
|
|
228
|
-
try {
|
|
229
|
-
// Get used memory from vm_stat
|
|
230
|
-
const vmStatOutput = await execCommand('vm_stat 2>/dev/null');
|
|
231
|
-
const { memoryUsed } = this.parseVmStatOutput(vmStatOutput);
|
|
232
|
-
|
|
233
|
-
// Get total installed RAM from sysctl (this is accurate)
|
|
234
|
-
const memoryTotal = await this.getTotalMemory();
|
|
235
|
-
|
|
236
|
-
return { memoryUsed, memoryTotal };
|
|
237
|
-
} catch {
|
|
238
|
-
// Fallback to zeros if commands fail
|
|
239
|
-
return { memoryUsed: 0, memoryTotal: 0 };
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Collect all system metrics
|
|
245
|
-
* Attempts macmon first (GPU/CPU/ANE), always gets memory from vm_stat + sysctl
|
|
246
|
-
* Caches results for 4s to prevent spawning multiple macmon processes
|
|
247
|
-
*/
|
|
248
|
-
async collectSystemMetrics(): Promise<SystemMetrics> {
|
|
249
|
-
const now = Date.now();
|
|
250
|
-
|
|
251
|
-
// Return cached data if still fresh
|
|
252
|
-
if (this.lastSystemMetrics && (now - this.lastCollectionTime) < this.CACHE_TTL_MS) {
|
|
253
|
-
return this.lastSystemMetrics;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// If already collecting, wait for that to finish
|
|
257
|
-
if (this.collectingLock) {
|
|
258
|
-
return this.collectingLock;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Start fresh collection
|
|
262
|
-
this.collectingLock = this.doCollectSystemMetrics();
|
|
263
|
-
|
|
264
|
-
try {
|
|
265
|
-
const metrics = await this.collectingLock;
|
|
266
|
-
this.lastSystemMetrics = metrics;
|
|
267
|
-
this.lastCollectionTime = now;
|
|
268
|
-
return metrics;
|
|
269
|
-
} finally {
|
|
270
|
-
this.collectingLock = null;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Internal method to actually collect system metrics
|
|
276
|
-
* Called by collectSystemMetrics with caching/locking
|
|
277
|
-
*/
|
|
278
|
-
private async doCollectSystemMetrics(): Promise<SystemMetrics> {
|
|
279
|
-
const warnings: string[] = [];
|
|
280
|
-
const now = Date.now();
|
|
281
|
-
|
|
282
|
-
// Try macmon first for GPU/CPU/ANE
|
|
283
|
-
const macmonMetrics = await this.getMacmonMetrics();
|
|
284
|
-
|
|
285
|
-
// Always get memory from vm_stat + sysctl (accurate total from sysctl)
|
|
286
|
-
const memoryMetrics = await this.getMemoryMetrics();
|
|
287
|
-
|
|
288
|
-
// Determine source and add warnings
|
|
289
|
-
let source: 'macmon' | 'vm_stat' | 'none';
|
|
290
|
-
if (macmonMetrics) {
|
|
291
|
-
source = 'macmon';
|
|
292
|
-
} else if (memoryMetrics.memoryTotal > 0) {
|
|
293
|
-
source = 'vm_stat';
|
|
294
|
-
warnings.push('macmon not available - showing memory metrics only');
|
|
295
|
-
} else {
|
|
296
|
-
source = 'none';
|
|
297
|
-
warnings.push('Unable to collect system metrics');
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return {
|
|
301
|
-
gpuUsage: macmonMetrics?.gpuUsage,
|
|
302
|
-
cpuUsage: macmonMetrics?.cpuUsage,
|
|
303
|
-
aneUsage: macmonMetrics?.aneUsage,
|
|
304
|
-
temperature: macmonMetrics?.temperature,
|
|
305
|
-
memoryUsed: memoryMetrics.memoryUsed,
|
|
306
|
-
memoryTotal: memoryMetrics.memoryTotal,
|
|
307
|
-
timestamp: now,
|
|
308
|
-
source,
|
|
309
|
-
warnings: warnings.length > 0 ? warnings : undefined,
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Export singleton instance
|
|
315
|
-
export const systemCollector = new SystemCollector();
|