@appkit/llamacpp-cli 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/MONITORING-ACCURACY-FIX.md +199 -0
  3. package/PER-PROCESS-METRICS.md +190 -0
  4. package/README.md +124 -9
  5. package/dist/cli.js +32 -7
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/config.d.ts.map +1 -1
  8. package/dist/commands/config.js +15 -1
  9. package/dist/commands/config.js.map +1 -1
  10. package/dist/commands/create.d.ts.map +1 -1
  11. package/dist/commands/create.js +12 -4
  12. package/dist/commands/create.js.map +1 -1
  13. package/dist/commands/delete.js +12 -10
  14. package/dist/commands/delete.js.map +1 -1
  15. package/dist/commands/logs-all.d.ts +9 -0
  16. package/dist/commands/logs-all.d.ts.map +1 -0
  17. package/dist/commands/logs-all.js +209 -0
  18. package/dist/commands/logs-all.js.map +1 -0
  19. package/dist/commands/logs.d.ts +4 -0
  20. package/dist/commands/logs.d.ts.map +1 -1
  21. package/dist/commands/logs.js +108 -2
  22. package/dist/commands/logs.js.map +1 -1
  23. package/dist/commands/monitor.d.ts.map +1 -1
  24. package/dist/commands/monitor.js +51 -1
  25. package/dist/commands/monitor.js.map +1 -1
  26. package/dist/commands/ps.d.ts +3 -1
  27. package/dist/commands/ps.d.ts.map +1 -1
  28. package/dist/commands/ps.js +75 -5
  29. package/dist/commands/ps.js.map +1 -1
  30. package/dist/commands/rm.d.ts.map +1 -1
  31. package/dist/commands/rm.js +5 -12
  32. package/dist/commands/rm.js.map +1 -1
  33. package/dist/commands/server-show.d.ts.map +1 -1
  34. package/dist/commands/server-show.js +30 -3
  35. package/dist/commands/server-show.js.map +1 -1
  36. package/dist/commands/start.d.ts.map +1 -1
  37. package/dist/commands/start.js +34 -7
  38. package/dist/commands/start.js.map +1 -1
  39. package/dist/commands/stop.js +3 -3
  40. package/dist/commands/stop.js.map +1 -1
  41. package/dist/lib/history-manager.d.ts +46 -0
  42. package/dist/lib/history-manager.d.ts.map +1 -0
  43. package/dist/lib/history-manager.js +157 -0
  44. package/dist/lib/history-manager.js.map +1 -0
  45. package/dist/lib/metrics-aggregator.d.ts +2 -1
  46. package/dist/lib/metrics-aggregator.d.ts.map +1 -1
  47. package/dist/lib/metrics-aggregator.js +15 -4
  48. package/dist/lib/metrics-aggregator.js.map +1 -1
  49. package/dist/lib/system-collector.d.ts +9 -4
  50. package/dist/lib/system-collector.d.ts.map +1 -1
  51. package/dist/lib/system-collector.js +29 -28
  52. package/dist/lib/system-collector.js.map +1 -1
  53. package/dist/tui/HistoricalMonitorApp.d.ts +5 -0
  54. package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -0
  55. package/dist/tui/HistoricalMonitorApp.js +490 -0
  56. package/dist/tui/HistoricalMonitorApp.js.map +1 -0
  57. package/dist/tui/MonitorApp.d.ts.map +1 -1
  58. package/dist/tui/MonitorApp.js +84 -62
  59. package/dist/tui/MonitorApp.js.map +1 -1
  60. package/dist/tui/MultiServerMonitorApp.d.ts +1 -1
  61. package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
  62. package/dist/tui/MultiServerMonitorApp.js +293 -77
  63. package/dist/tui/MultiServerMonitorApp.js.map +1 -1
  64. package/dist/types/history-types.d.ts +30 -0
  65. package/dist/types/history-types.d.ts.map +1 -0
  66. package/dist/types/history-types.js +11 -0
  67. package/dist/types/history-types.js.map +1 -0
  68. package/dist/types/monitor-types.d.ts +1 -0
  69. package/dist/types/monitor-types.d.ts.map +1 -1
  70. package/dist/types/server-config.d.ts +1 -0
  71. package/dist/types/server-config.d.ts.map +1 -1
  72. package/dist/types/server-config.js.map +1 -1
  73. package/dist/utils/downsample-utils.d.ts +35 -0
  74. package/dist/utils/downsample-utils.d.ts.map +1 -0
  75. package/dist/utils/downsample-utils.js +107 -0
  76. package/dist/utils/downsample-utils.js.map +1 -0
  77. package/dist/utils/file-utils.d.ts +6 -0
  78. package/dist/utils/file-utils.d.ts.map +1 -1
  79. package/dist/utils/file-utils.js +38 -0
  80. package/dist/utils/file-utils.js.map +1 -1
  81. package/dist/utils/log-utils.d.ts +43 -0
  82. package/dist/utils/log-utils.d.ts.map +1 -0
  83. package/dist/utils/log-utils.js +190 -0
  84. package/dist/utils/log-utils.js.map +1 -0
  85. package/dist/utils/process-utils.d.ts +19 -1
  86. package/dist/utils/process-utils.d.ts.map +1 -1
  87. package/dist/utils/process-utils.js +79 -1
  88. package/dist/utils/process-utils.js.map +1 -1
  89. package/docs/images/.gitkeep +1 -0
  90. package/package.json +3 -1
  91. package/src/cli.ts +32 -7
  92. package/src/commands/config.ts +15 -1
  93. package/src/commands/create.ts +14 -5
  94. package/src/commands/delete.ts +10 -10
  95. package/src/commands/logs-all.ts +251 -0
  96. package/src/commands/logs.ts +138 -2
  97. package/src/commands/monitor.ts +21 -1
  98. package/src/commands/ps.ts +88 -5
  99. package/src/commands/rm.ts +5 -12
  100. package/src/commands/server-show.ts +35 -3
  101. package/src/commands/start.ts +35 -7
  102. package/src/commands/stop.ts +3 -3
  103. package/src/lib/history-manager.ts +172 -0
  104. package/src/lib/metrics-aggregator.ts +18 -5
  105. package/src/lib/system-collector.ts +31 -28
  106. package/src/tui/HistoricalMonitorApp.ts +548 -0
  107. package/src/tui/MonitorApp.ts +89 -64
  108. package/src/tui/MultiServerMonitorApp.ts +348 -103
  109. package/src/types/history-types.ts +39 -0
  110. package/src/types/monitor-types.ts +1 -0
  111. package/src/types/server-config.ts +1 -0
  112. package/src/utils/downsample-utils.ts +128 -0
  113. package/src/utils/file-utils.ts +40 -0
  114. package/src/utils/log-utils.ts +178 -0
  115. package/src/utils/process-utils.ts +85 -1
  116. package/test-load.sh +100 -0
  117. package/dist/tui/components/ErrorState.d.ts +0 -8
  118. package/dist/tui/components/ErrorState.d.ts.map +0 -1
  119. package/dist/tui/components/ErrorState.js +0 -22
  120. package/dist/tui/components/ErrorState.js.map +0 -1
  121. package/dist/tui/components/LoadingState.d.ts +0 -8
  122. package/dist/tui/components/LoadingState.d.ts.map +0 -1
  123. package/dist/tui/components/LoadingState.js +0 -21
  124. package/dist/tui/components/LoadingState.js.map +0 -1
@@ -6,6 +6,14 @@ import { stateManager } from '../lib/state-manager';
6
6
  import { fileExists } from '../utils/file-utils';
7
7
  import { execCommand } from '../utils/process-utils';
8
8
  import { logParser } from '../utils/log-parser';
9
+ import {
10
+ getFileSize,
11
+ formatFileSize,
12
+ rotateLogFile,
13
+ clearLogFile,
14
+ getArchivedLogInfo,
15
+ deleteArchivedLogs,
16
+ } from '../utils/log-utils';
9
17
 
10
18
  interface LogsOptions {
11
19
  follow?: boolean;
@@ -15,6 +23,10 @@ interface LogsOptions {
15
23
  http?: boolean;
16
24
  stdout?: boolean;
17
25
  filter?: string;
26
+ clear?: boolean;
27
+ rotate?: boolean;
28
+ clearArchived?: boolean;
29
+ clearAll?: boolean;
18
30
  }
19
31
 
20
32
  export async function logsCommand(identifier: string, options: LogsOptions): Promise<void> {
@@ -28,6 +40,96 @@ export async function logsCommand(identifier: string, options: LogsOptions): Pro
28
40
  const logPath = options.stdout ? server.stdoutPath : server.stderrPath;
29
41
  const logType = options.stdout ? 'stdout' : 'stderr';
30
42
 
43
+ // Handle --clear-archived option (deletes only archived logs)
44
+ if (options.clearArchived) {
45
+ const archivedInfo = await deleteArchivedLogs(server.id);
46
+
47
+ if (archivedInfo.count === 0) {
48
+ console.log(chalk.yellow(`⚠️ No archived logs found for ${server.modelName}`));
49
+ console.log(chalk.dim(` Archived logs are created via --rotate or automatic rotation`));
50
+ return;
51
+ }
52
+
53
+ console.log(chalk.green(`✅ Deleted archived logs for ${server.modelName}`));
54
+ console.log(chalk.dim(` Files deleted: ${archivedInfo.count}`));
55
+ console.log(chalk.dim(` Space freed: ${formatFileSize(archivedInfo.totalSize)}`));
56
+ console.log(chalk.dim(` Current logs preserved`));
57
+ return;
58
+ }
59
+
60
+ // Handle --clear-all option (clears both current and archived logs)
61
+ if (options.clearAll) {
62
+ let totalFreed = 0;
63
+ let currentSize = 0;
64
+ let archivedSize = 0;
65
+
66
+ // Clear current stderr
67
+ if (await fileExists(server.stderrPath)) {
68
+ currentSize += await getFileSize(server.stderrPath);
69
+ await clearLogFile(server.stderrPath);
70
+ }
71
+
72
+ // Clear current stdout
73
+ if (await fileExists(server.stdoutPath)) {
74
+ currentSize += await getFileSize(server.stdoutPath);
75
+ await clearLogFile(server.stdoutPath);
76
+ }
77
+
78
+ // Delete all archived logs
79
+ const archivedInfo = await deleteArchivedLogs(server.id);
80
+ archivedSize = archivedInfo.totalSize;
81
+
82
+ totalFreed = currentSize + archivedSize;
83
+
84
+ console.log(chalk.green(`✅ Cleared all logs for ${server.modelName}`));
85
+ if (currentSize > 0) {
86
+ console.log(chalk.dim(` Current logs: ${formatFileSize(currentSize)}`));
87
+ }
88
+ if (archivedSize > 0) {
89
+ console.log(chalk.dim(` Archived logs: ${formatFileSize(archivedSize)} (${archivedInfo.count} file${archivedInfo.count > 1 ? 's' : ''})`));
90
+ }
91
+ console.log(chalk.dim(` Total freed: ${formatFileSize(totalFreed)}`));
92
+ return;
93
+ }
94
+
95
+ // Handle --clear option
96
+ if (options.clear) {
97
+ if (!(await fileExists(logPath))) {
98
+ console.log(chalk.yellow(`⚠️ No ${logType} found for ${server.modelName}`));
99
+ console.log(chalk.dim(` Log file does not exist: ${logPath}`));
100
+ return;
101
+ }
102
+
103
+ const sizeBefore = await getFileSize(logPath);
104
+ await clearLogFile(logPath);
105
+
106
+ console.log(chalk.green(`✅ Cleared ${logType} for ${server.modelName}`));
107
+ console.log(chalk.dim(` Freed: ${formatFileSize(sizeBefore)}`));
108
+ console.log(chalk.dim(` ${logPath}`));
109
+ return;
110
+ }
111
+
112
+ // Handle --rotate option
113
+ if (options.rotate) {
114
+ if (!(await fileExists(logPath))) {
115
+ console.log(chalk.yellow(`⚠️ No ${logType} found for ${server.modelName}`));
116
+ console.log(chalk.dim(` Log file does not exist: ${logPath}`));
117
+ return;
118
+ }
119
+
120
+ try {
121
+ const archivedPath = await rotateLogFile(logPath);
122
+ const size = await getFileSize(archivedPath);
123
+
124
+ console.log(chalk.green(`✅ Rotated ${logType} for ${server.modelName}`));
125
+ console.log(chalk.dim(` Archived: ${formatFileSize(size)}`));
126
+ console.log(chalk.dim(` → ${archivedPath}`));
127
+ } catch (error) {
128
+ throw new Error(`Failed to rotate log: ${(error as Error).message}`);
129
+ }
130
+ return;
131
+ }
132
+
31
133
  // Check if log file exists
32
134
  if (!(await fileExists(logPath))) {
33
135
  console.log(chalk.yellow(`⚠️ No ${logType} found for ${server.modelName}`));
@@ -65,6 +167,16 @@ export async function logsCommand(identifier: string, options: LogsOptions): Pro
65
167
  console.log(chalk.blue(`📋 Logs for ${server.modelName} (${logType}${filterDesc})`));
66
168
  console.log(chalk.dim(` ${logPath}`));
67
169
 
170
+ // Show log size information
171
+ const currentSize = await getFileSize(logPath);
172
+ const archivedInfo = await getArchivedLogInfo(server.id);
173
+
174
+ if (archivedInfo.count > 0) {
175
+ console.log(chalk.dim(` Current: ${formatFileSize(currentSize)} | Archived: ${formatFileSize(archivedInfo.totalSize)} (${archivedInfo.count} file${archivedInfo.count > 1 ? 's' : ''})`));
176
+ } else {
177
+ console.log(chalk.dim(` Current: ${formatFileSize(currentSize)}`));
178
+ }
179
+
68
180
  // Show subtle note if verbose logging is not enabled
69
181
  if (!server.verbose && !options.verbose && !options.errors && !options.http && !options.filter) {
70
182
  console.log(chalk.dim(` verbosity is disabled`));
@@ -140,10 +252,18 @@ export async function logsCommand(identifier: string, options: LogsOptions): Pro
140
252
  // Compact mode: read file and parse
141
253
  try {
142
254
  // Use large multiplier to account for verbose debug output between requests
143
- const command = `tail -n ${lines * 100} "${logPath}" | grep -E "log_server_r"`;
255
+ // Add || true to prevent grep from failing when no matches found
256
+ const command = `tail -n ${lines * 100} "${logPath}" | grep -E "log_server_r" || true`;
144
257
  const output = await execCommand(command);
145
258
  const logLines = output.split('\n').filter((l) => l.trim());
146
259
 
260
+ if (logLines.length === 0) {
261
+ console.log(chalk.dim('No HTTP request logs in compact format.'));
262
+ console.log(chalk.dim('The server may be starting up, or only simple GET requests have been made.'));
263
+ console.log(chalk.dim('\nTip: Use --http to see raw HTTP logs, or --verbose for all server logs.'));
264
+ return;
265
+ }
266
+
147
267
  const compactLines: string[] = [];
148
268
  for (const line of logLines) {
149
269
  logParser.processLine(line, (compactLine) => {
@@ -156,6 +276,14 @@ export async function logsCommand(identifier: string, options: LogsOptions): Pro
156
276
  compactLines.push(compactLine);
157
277
  });
158
278
 
279
+ // Check if we got any parsed output
280
+ if (compactLines.length === 0) {
281
+ console.log(chalk.dim('HTTP request logs found, but could not parse in compact format.'));
282
+ console.log(chalk.dim('This usually happens with simple GET requests (health checks, slots, etc.).'));
283
+ console.log(chalk.dim('\nTip: Use --http to see raw HTTP logs instead.'));
284
+ return;
285
+ }
286
+
159
287
  // Show only the last N compact lines
160
288
  const limitedLines = compactLines.slice(-lines);
161
289
  limitedLines.forEach((line) => console.log(line));
@@ -169,13 +297,21 @@ export async function logsCommand(identifier: string, options: LogsOptions): Pro
169
297
 
170
298
  if (filterPattern) {
171
299
  // Use tail piped to grep
172
- command = `tail -n ${lines} "${logPath}" | grep -E "${filterPattern}"`;
300
+ // Add || true to prevent grep from failing when no matches found
301
+ command = `tail -n ${lines} "${logPath}" | grep -E "${filterPattern}" || true`;
173
302
  } else {
174
303
  // No filter
175
304
  command = `tail -n ${lines} "${logPath}"`;
176
305
  }
177
306
 
178
307
  const output = await execCommand(command);
308
+
309
+ if (filterPattern && output.trim() === '') {
310
+ console.log(chalk.dim(`No logs matching pattern: ${filterPattern}`));
311
+ console.log(chalk.dim('\nTip: Try --verbose to see all logs, or adjust your filter pattern.'));
312
+ return;
313
+ }
314
+
179
315
  console.log(output);
180
316
  } catch (error) {
181
317
  throw new Error(`Failed to read logs: ${(error as Error).message}`);
@@ -36,6 +36,12 @@ export async function monitorCommand(identifier?: string): Promise<void> {
36
36
  );
37
37
  }
38
38
 
39
+ // Update server status to reflect actual launchctl state
40
+ const { statusChecker } = await import('../lib/status-checker.js');
41
+ const status = await statusChecker.checkServer(server);
42
+ server.status = status.isRunning ? 'running' : 'stopped';
43
+ server.pid = status.pid || undefined;
44
+
39
45
  // Check if server is running
40
46
  if (server.status !== 'running') {
41
47
  screen.destroy();
@@ -51,6 +57,12 @@ export async function monitorCommand(identifier?: string): Promise<void> {
51
57
  // Only one server - single server mode
52
58
  const server = allServers[0];
53
59
 
60
+ // Update server status to reflect actual launchctl state
61
+ const { statusChecker } = await import('../lib/status-checker.js');
62
+ const status = await statusChecker.checkServer(server);
63
+ server.status = status.isRunning ? 'running' : 'stopped';
64
+ server.pid = status.pid || undefined;
65
+
54
66
  // Check if server is running
55
67
  if (server.status !== 'running') {
56
68
  screen.destroy();
@@ -64,6 +76,14 @@ export async function monitorCommand(identifier?: string): Promise<void> {
64
76
  await createMonitorUI(screen, server);
65
77
  } else {
66
78
  // Multiple servers - multi-server mode
79
+ // Update all server statuses to reflect actual launchctl state
80
+ const { statusChecker } = await import('../lib/status-checker.js');
81
+ for (const server of allServers) {
82
+ const status = await statusChecker.checkServer(server);
83
+ server.status = status.isRunning ? 'running' : 'stopped';
84
+ server.pid = status.pid || undefined;
85
+ }
86
+
67
87
  // Filter to only running servers for monitoring
68
88
  const runningServers = allServers.filter(s => s.status === 'running');
69
89
 
@@ -78,7 +98,7 @@ export async function monitorCommand(identifier?: string): Promise<void> {
78
98
  );
79
99
  }
80
100
 
81
- // Launch multi-server TUI
101
+ // Launch multi-server TUI (pass all servers so we can see stopped ones too)
82
102
  await createMultiServerMonitorUI(screen, allServers);
83
103
  }
84
104
 
@@ -1,11 +1,14 @@
1
1
  import chalk from 'chalk';
2
2
  import Table from 'cli-table3';
3
+ import blessed from 'blessed';
3
4
  import { stateManager } from '../lib/state-manager';
4
5
  import { statusChecker } from '../lib/status-checker';
5
6
  import { formatUptime, formatBytes } from '../utils/format-utils';
6
7
  import { getProcessMemory } from '../utils/process-utils';
8
+ import { createMultiServerMonitorUI } from '../tui/MultiServerMonitorApp.js';
9
+ import { ServerConfig } from '../types/server-config.js';
7
10
 
8
- export async function psCommand(): Promise<void> {
11
+ async function showStaticTable(): Promise<void> {
9
12
  const servers = await stateManager.getAllServers();
10
13
 
11
14
  if (servers.length === 0) {
@@ -52,12 +55,14 @@ export async function psCommand(): Promise<void> {
52
55
  ? formatUptime(server.lastStarted)
53
56
  : '-';
54
57
 
55
- // Get memory usage for running servers
58
+ // Get memory usage for running servers (CPU + Metal GPU memory)
56
59
  let memoryText = '-';
57
60
  if (server.status === 'running' && server.pid) {
58
- const memoryBytes = await getProcessMemory(server.pid);
59
- if (memoryBytes !== null) {
60
- memoryText = formatBytes(memoryBytes);
61
+ const cpuMemoryBytes = await getProcessMemory(server.pid);
62
+ if (cpuMemoryBytes !== null) {
63
+ const metalMemoryBytes = server.metalMemoryMB ? server.metalMemoryMB * 1024 * 1024 : 0;
64
+ const totalMemoryBytes = cpuMemoryBytes + metalMemoryBytes;
65
+ memoryText = formatBytes(totalMemoryBytes);
61
66
  }
62
67
  }
63
68
 
@@ -88,3 +93,81 @@ export async function psCommand(): Promise<void> {
88
93
  console.log(chalk.red('\n⚠️ Some servers have crashed. Check logs with: llamacpp server logs <id> --errors'));
89
94
  }
90
95
  }
96
+
97
+ export async function psCommand(identifier?: string, options?: { table?: boolean }): Promise<void> {
98
+ // If --table flag is set, show static table (backward compatibility)
99
+ if (options?.table) {
100
+ await showStaticTable();
101
+ return;
102
+ }
103
+
104
+ // Get all servers and update their statuses
105
+ const servers = await stateManager.getAllServers();
106
+
107
+ if (servers.length === 0) {
108
+ console.log(chalk.yellow('No servers configured.'));
109
+ console.log(chalk.dim('\nCreate a server: llamacpp server create <model-filename>'));
110
+ return;
111
+ }
112
+
113
+ // Update all server statuses
114
+ const updated = await statusChecker.updateAllServerStatuses();
115
+
116
+ // If identifier is provided, find the server and jump to detail view
117
+ if (identifier) {
118
+ const server = await findServer(identifier, updated);
119
+ if (!server) {
120
+ console.log(chalk.red(`❌ Server not found: ${identifier}`));
121
+ console.log(chalk.dim('\nAvailable servers:'));
122
+ updated.forEach((s: ServerConfig) => {
123
+ console.log(chalk.dim(` - ${s.id} (port ${s.port})`));
124
+ });
125
+ process.exit(1);
126
+ }
127
+
128
+ // Find the server index for direct jump
129
+ const serverIndex = updated.findIndex(s => s.id === server.id);
130
+
131
+ // Launch multi-server TUI with direct jump to detail view
132
+ const screen = blessed.screen({
133
+ smartCSR: true,
134
+ title: 'llama.cpp Multi-Server Monitor',
135
+ fullUnicode: true,
136
+ });
137
+
138
+ await createMultiServerMonitorUI(screen, updated, true, serverIndex); // fromPs = true, directJumpIndex
139
+ return;
140
+ }
141
+
142
+ // No identifier - launch multi-server TUI
143
+ const runningServers = updated.filter((s: ServerConfig) => s.status === 'running');
144
+
145
+ // Launch multi-server TUI (shows all servers, not just running ones)
146
+ const screen = blessed.screen({
147
+ smartCSR: true,
148
+ title: 'llama.cpp Multi-Server Monitor',
149
+ fullUnicode: true,
150
+ });
151
+
152
+ await createMultiServerMonitorUI(screen, updated, true); // fromPs = true
153
+ }
154
+
155
+ // Helper function to find server by identifier
156
+ async function findServer(identifier: string, servers: ServerConfig[]): Promise<ServerConfig | null> {
157
+ // Try by port
158
+ const port = parseInt(identifier);
159
+ if (!isNaN(port)) {
160
+ const server = servers.find(s => s.port === port);
161
+ if (server) return server;
162
+ }
163
+
164
+ // Try by exact ID
165
+ const byId = servers.find(s => s.id === identifier);
166
+ if (byId) return byId;
167
+
168
+ // Try by partial model name
169
+ const byModel = servers.find(s => s.modelName.toLowerCase().includes(identifier.toLowerCase()));
170
+ if (byModel) return byModel;
171
+
172
+ return null;
173
+ }
@@ -47,21 +47,14 @@ export async function rmCommand(modelIdentifier: string): Promise<void> {
47
47
  for (const server of serversUsingModel) {
48
48
  console.log(chalk.dim(` Removing server: ${server.id}`));
49
49
 
50
- // Stop server if running
51
- if (server.status === 'running') {
52
- try {
53
- await launchctlManager.stopService(server.label);
54
- await launchctlManager.waitForServiceStop(server.label, 5000);
55
- } catch (error) {
56
- console.log(chalk.yellow(` ⚠️ Failed to stop server gracefully`));
57
- }
58
- }
59
-
60
- // Unload service
50
+ // Unload service (stops and removes from launchd)
61
51
  try {
62
52
  await launchctlManager.unloadService(server.plistPath);
53
+ if (server.status === 'running') {
54
+ await launchctlManager.waitForServiceStop(server.label, 5000);
55
+ }
63
56
  } catch (error) {
64
- // Ignore errors if service is already unloaded
57
+ console.log(chalk.yellow(` ⚠️ Failed to unload service gracefully`));
65
58
  }
66
59
 
67
60
  // Delete plist
@@ -3,6 +3,8 @@ import { stateManager } from '../lib/state-manager';
3
3
  import { statusChecker } from '../lib/status-checker';
4
4
  import { formatUptime, formatBytes } from '../utils/format-utils';
5
5
  import { getProcessMemory } from '../utils/process-utils';
6
+ import { getFileSize, formatFileSize, getArchivedLogInfo } from '../utils/log-utils';
7
+ import { fileExists } from '../utils/file-utils';
6
8
 
7
9
  export async function serverShowCommand(identifier: string): Promise<void> {
8
10
  // Find the server
@@ -68,9 +70,16 @@ export async function serverShowCommand(identifier: string): Promise<void> {
68
70
  }
69
71
 
70
72
  if (updatedServer.pid) {
71
- const memoryBytes = await getProcessMemory(updatedServer.pid);
72
- if (memoryBytes !== null) {
73
- console.log(`${chalk.bold('Memory:')} ${formatBytes(memoryBytes)}`);
73
+ const cpuMemoryBytes = await getProcessMemory(updatedServer.pid);
74
+ if (cpuMemoryBytes !== null) {
75
+ const metalMemoryBytes = updatedServer.metalMemoryMB ? updatedServer.metalMemoryMB * 1024 * 1024 : 0;
76
+ const totalMemoryBytes = cpuMemoryBytes + metalMemoryBytes;
77
+
78
+ if (metalMemoryBytes > 0) {
79
+ console.log(`${chalk.bold('Memory:')} ${formatBytes(totalMemoryBytes)} (CPU: ${formatBytes(cpuMemoryBytes)}, GPU: ${formatBytes(metalMemoryBytes)})`);
80
+ } else {
81
+ console.log(`${chalk.bold('Memory:')} ${formatBytes(cpuMemoryBytes)} (CPU only)`);
82
+ }
74
83
  }
75
84
  }
76
85
  }
@@ -89,6 +98,29 @@ export async function serverShowCommand(identifier: string): Promise<void> {
89
98
  console.log(`${chalk.bold('Custom Flags:')} ${updatedServer.customFlags.join(' ')}`);
90
99
  }
91
100
 
101
+ // Logs section
102
+ console.log('\n' + '─'.repeat(70));
103
+ console.log(chalk.bold('Logs:'));
104
+ console.log('─'.repeat(70));
105
+
106
+ // Get current log sizes
107
+ const stderrSize = (await fileExists(updatedServer.stderrPath))
108
+ ? await getFileSize(updatedServer.stderrPath)
109
+ : 0;
110
+ const stdoutSize = (await fileExists(updatedServer.stdoutPath))
111
+ ? await getFileSize(updatedServer.stdoutPath)
112
+ : 0;
113
+
114
+ // Get archived log info
115
+ const archivedInfo = await getArchivedLogInfo(updatedServer.id);
116
+
117
+ console.log(`${chalk.bold('Stderr:')} ${formatFileSize(stderrSize)} (current)`);
118
+ console.log(`${chalk.bold('Stdout:')} ${formatFileSize(stdoutSize)} (current)`);
119
+
120
+ if (archivedInfo.count > 0) {
121
+ console.log(`${chalk.bold('Archived:')} ${formatFileSize(archivedInfo.totalSize)} (${archivedInfo.count} file${archivedInfo.count > 1 ? 's' : ''})`);
122
+ }
123
+
92
124
  // Timestamps section
93
125
  console.log('\n' + '─'.repeat(70));
94
126
  console.log(chalk.bold('Timestamps:'));
@@ -2,6 +2,8 @@ import chalk from 'chalk';
2
2
  import { stateManager } from '../lib/state-manager';
3
3
  import { launchctlManager } from '../lib/launchctl-manager';
4
4
  import { statusChecker } from '../lib/status-checker';
5
+ import { parseMetalMemoryFromLog } from '../utils/file-utils';
6
+ import { autoRotateIfNeeded, formatFileSize } from '../utils/log-utils';
5
7
 
6
8
  export async function startCommand(identifier: string): Promise<void> {
7
9
  // Initialize state manager
@@ -29,28 +31,42 @@ export async function startCommand(identifier: string): Promise<void> {
29
31
 
30
32
  console.log(chalk.blue(`▶️ Starting ${server.modelName} (port ${server.port})...`));
31
33
 
32
- // 3. Ensure plist exists (recreate if missing)
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)
33
49
  try {
34
50
  await launchctlManager.createPlist(server);
35
51
  } catch (error) {
36
52
  // May already exist, that's okay
37
53
  }
38
54
 
39
- // 4. Load service if needed
55
+ // 5. Load service if needed
40
56
  try {
41
57
  await launchctlManager.loadService(server.plistPath);
42
58
  } catch (error) {
43
59
  // May already be loaded, that's okay
44
60
  }
45
61
 
46
- // 5. Start the service
62
+ // 6. Start the service
47
63
  try {
48
64
  await launchctlManager.startService(server.label);
49
65
  } catch (error) {
50
66
  throw new Error(`Failed to start service: ${(error as Error).message}`);
51
67
  }
52
68
 
53
- // 6. Wait for startup
69
+ // 7. Wait for startup
54
70
  console.log(chalk.dim('Waiting for server to start...'));
55
71
  const started = await launchctlManager.waitForServiceStart(server.label, 5000);
56
72
 
@@ -60,10 +76,22 @@ export async function startCommand(identifier: string): Promise<void> {
60
76
  );
61
77
  }
62
78
 
63
- // 7. Update server status
64
- await statusChecker.updateServerStatus(server);
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
+ }
65
93
 
66
- // 8. Display success
94
+ // 10. Display success
67
95
  console.log();
68
96
  console.log(chalk.green('✅ Server started successfully!'));
69
97
  console.log();
@@ -18,11 +18,11 @@ export async function stopCommand(identifier: string): Promise<void> {
18
18
 
19
19
  console.log(chalk.blue(`⏹️ Stopping ${server.modelName} (port ${server.port})...`));
20
20
 
21
- // Stop the service
21
+ // Unload the service (removes from launchd management - won't auto-restart)
22
22
  try {
23
- await launchctlManager.stopService(server.label);
23
+ await launchctlManager.unloadService(server.plistPath);
24
24
  } catch (error) {
25
- throw new Error(`Failed to stop service: ${(error as Error).message}`);
25
+ throw new Error(`Failed to unload service: ${(error as Error).message}`);
26
26
  }
27
27
 
28
28
  // Wait for clean shutdown