@appkit/llamacpp-cli 1.6.0 → 1.8.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 (48) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +71 -1
  3. package/dist/cli.js +24 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/config.d.ts +1 -0
  6. package/dist/commands/config.d.ts.map +1 -1
  7. package/dist/commands/config.js +182 -13
  8. package/dist/commands/config.js.map +1 -1
  9. package/dist/commands/create.d.ts.map +1 -1
  10. package/dist/commands/create.js +0 -1
  11. package/dist/commands/create.js.map +1 -1
  12. package/dist/commands/delete.js +12 -10
  13. package/dist/commands/delete.js.map +1 -1
  14. package/dist/commands/logs-all.d.ts +9 -0
  15. package/dist/commands/logs-all.d.ts.map +1 -0
  16. package/dist/commands/logs-all.js +209 -0
  17. package/dist/commands/logs-all.js.map +1 -0
  18. package/dist/commands/logs.d.ts +4 -0
  19. package/dist/commands/logs.d.ts.map +1 -1
  20. package/dist/commands/logs.js +108 -2
  21. package/dist/commands/logs.js.map +1 -1
  22. package/dist/commands/rm.d.ts.map +1 -1
  23. package/dist/commands/rm.js +5 -12
  24. package/dist/commands/rm.js.map +1 -1
  25. package/dist/commands/server-show.d.ts.map +1 -1
  26. package/dist/commands/server-show.js +20 -0
  27. package/dist/commands/server-show.js.map +1 -1
  28. package/dist/commands/start.d.ts.map +1 -1
  29. package/dist/commands/start.js +22 -7
  30. package/dist/commands/start.js.map +1 -1
  31. package/dist/commands/stop.js +3 -3
  32. package/dist/commands/stop.js.map +1 -1
  33. package/dist/utils/log-utils.d.ts +43 -0
  34. package/dist/utils/log-utils.d.ts.map +1 -0
  35. package/dist/utils/log-utils.js +190 -0
  36. package/dist/utils/log-utils.js.map +1 -0
  37. package/package.json +1 -1
  38. package/src/cli.ts +24 -1
  39. package/src/commands/config.ts +161 -15
  40. package/src/commands/create.ts +0 -1
  41. package/src/commands/delete.ts +10 -10
  42. package/src/commands/logs-all.ts +251 -0
  43. package/src/commands/logs.ts +138 -2
  44. package/src/commands/rm.ts +5 -12
  45. package/src/commands/server-show.ts +25 -0
  46. package/src/commands/start.ts +22 -7
  47. package/src/commands/stop.ts +3 -3
  48. package/src/utils/log-utils.ts +178 -0
@@ -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}`);
@@ -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
@@ -96,6 +98,29 @@ export async function serverShowCommand(identifier: string): Promise<void> {
96
98
  console.log(`${chalk.bold('Custom Flags:')} ${updatedServer.customFlags.join(' ')}`);
97
99
  }
98
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
+
99
124
  // Timestamps section
100
125
  console.log('\n' + '─'.repeat(70));
101
126
  console.log(chalk.bold('Timestamps:'));
@@ -3,6 +3,7 @@ import { stateManager } from '../lib/state-manager';
3
3
  import { launchctlManager } from '../lib/launchctl-manager';
4
4
  import { statusChecker } from '../lib/status-checker';
5
5
  import { parseMetalMemoryFromLog } from '../utils/file-utils';
6
+ import { autoRotateIfNeeded, formatFileSize } from '../utils/log-utils';
6
7
 
7
8
  export async function startCommand(identifier: string): Promise<void> {
8
9
  // Initialize state manager
@@ -30,28 +31,42 @@ export async function startCommand(identifier: string): Promise<void> {
30
31
 
31
32
  console.log(chalk.blue(`▶️ Starting ${server.modelName} (port ${server.port})...`));
32
33
 
33
- // 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)
34
49
  try {
35
50
  await launchctlManager.createPlist(server);
36
51
  } catch (error) {
37
52
  // May already exist, that's okay
38
53
  }
39
54
 
40
- // 4. Load service if needed
55
+ // 5. Load service if needed
41
56
  try {
42
57
  await launchctlManager.loadService(server.plistPath);
43
58
  } catch (error) {
44
59
  // May already be loaded, that's okay
45
60
  }
46
61
 
47
- // 5. Start the service
62
+ // 6. Start the service
48
63
  try {
49
64
  await launchctlManager.startService(server.label);
50
65
  } catch (error) {
51
66
  throw new Error(`Failed to start service: ${(error as Error).message}`);
52
67
  }
53
68
 
54
- // 6. Wait for startup
69
+ // 7. Wait for startup
55
70
  console.log(chalk.dim('Waiting for server to start...'));
56
71
  const started = await launchctlManager.waitForServiceStart(server.label, 5000);
57
72
 
@@ -61,10 +76,10 @@ export async function startCommand(identifier: string): Promise<void> {
61
76
  );
62
77
  }
63
78
 
64
- // 7. Update server status
79
+ // 8. Update server status
65
80
  let updatedServer = await statusChecker.updateServerStatus(server);
66
81
 
67
- // 8. Parse Metal (GPU) memory allocation if not already captured
82
+ // 9. Parse Metal (GPU) memory allocation if not already captured
68
83
  if (!updatedServer.metalMemoryMB) {
69
84
  console.log(chalk.dim('Detecting Metal (GPU) memory allocation...'));
70
85
  await new Promise(resolve => setTimeout(resolve, 8000)); // 8 second delay
@@ -76,7 +91,7 @@ export async function startCommand(identifier: string): Promise<void> {
76
91
  }
77
92
  }
78
93
 
79
- // 9. Display success
94
+ // 10. Display success
80
95
  console.log();
81
96
  console.log(chalk.green('✅ Server started successfully!'));
82
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
@@ -0,0 +1,178 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { fileExists, getLogsDir } from './file-utils';
4
+
5
+ /**
6
+ * Get the size of a file in bytes
7
+ */
8
+ export async function getFileSize(filePath: string): Promise<number> {
9
+ try {
10
+ const stats = await fs.stat(filePath);
11
+ return stats.size;
12
+ } catch {
13
+ return 0;
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Format bytes to human-readable size
19
+ */
20
+ export function formatFileSize(bytes: number): string {
21
+ if (bytes === 0) return '0 B';
22
+ const k = 1024;
23
+ const sizes = ['B', 'KB', 'MB', 'GB'];
24
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
25
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
26
+ }
27
+
28
+ /**
29
+ * Rotate a log file with timestamp
30
+ * Renames current log to <name>.YYYY-MM-DD-HH-MM-SS.<ext>
31
+ * Returns the new archived filename
32
+ */
33
+ export async function rotateLogFile(logPath: string): Promise<string> {
34
+ if (!(await fileExists(logPath))) {
35
+ throw new Error(`Log file does not exist: ${logPath}`);
36
+ }
37
+
38
+ // Get file size before rotation
39
+ const size = await getFileSize(logPath);
40
+ if (size === 0) {
41
+ throw new Error('Log file is empty, nothing to rotate');
42
+ }
43
+
44
+ // Generate timestamp
45
+ const timestamp = new Date()
46
+ .toISOString()
47
+ .replace(/T/, '-')
48
+ .replace(/:/g, '-')
49
+ .replace(/\..+/, '');
50
+
51
+ // Parse path components
52
+ const dir = path.dirname(logPath);
53
+ const ext = path.extname(logPath);
54
+ const basename = path.basename(logPath, ext);
55
+
56
+ // New archived filename
57
+ const archivedPath = path.join(dir, `${basename}.${timestamp}${ext}`);
58
+
59
+ // Rename current log to archived version
60
+ await fs.rename(logPath, archivedPath);
61
+
62
+ return archivedPath;
63
+ }
64
+
65
+ /**
66
+ * Clear (truncate) a log file to zero bytes
67
+ */
68
+ export async function clearLogFile(logPath: string): Promise<void> {
69
+ if (!(await fileExists(logPath))) {
70
+ throw new Error(`Log file does not exist: ${logPath}`);
71
+ }
72
+
73
+ // Truncate file to 0 bytes
74
+ await fs.truncate(logPath, 0);
75
+ }
76
+
77
+ /**
78
+ * Auto-rotate log files if they exceed threshold
79
+ * Returns true if rotation occurred, false otherwise
80
+ */
81
+ export async function autoRotateIfNeeded(
82
+ stdoutPath: string,
83
+ stderrPath: string,
84
+ thresholdMB: number = 100
85
+ ): Promise<{ rotated: boolean; files: string[] }> {
86
+ const thresholdBytes = thresholdMB * 1024 * 1024;
87
+ const rotatedFiles: string[] = [];
88
+
89
+ // Check stdout
90
+ if (await fileExists(stdoutPath)) {
91
+ const stdoutSize = await getFileSize(stdoutPath);
92
+ if (stdoutSize > thresholdBytes) {
93
+ const archived = await rotateLogFile(stdoutPath);
94
+ rotatedFiles.push(archived);
95
+ }
96
+ }
97
+
98
+ // Check stderr
99
+ if (await fileExists(stderrPath)) {
100
+ const stderrSize = await getFileSize(stderrPath);
101
+ if (stderrSize > thresholdBytes) {
102
+ const archived = await rotateLogFile(stderrPath);
103
+ rotatedFiles.push(archived);
104
+ }
105
+ }
106
+
107
+ return {
108
+ rotated: rotatedFiles.length > 0,
109
+ files: rotatedFiles,
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Get information about archived log files for a server
115
+ * Returns count and total size of timestamped archived logs
116
+ */
117
+ export async function getArchivedLogInfo(serverId: string): Promise<{
118
+ count: number;
119
+ totalSize: number;
120
+ }> {
121
+ const logsDir = getLogsDir();
122
+ let count = 0;
123
+ let totalSize = 0;
124
+
125
+ try {
126
+ const files = await fs.readdir(logsDir);
127
+
128
+ // Pattern matches: server-id.YYYY-MM-DD-HH-MM-SS.{stdout,stderr}
129
+ const pattern = new RegExp(`^${serverId}\\.(\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-\\d{2})\\.(stdout|stderr)$`);
130
+
131
+ for (const file of files) {
132
+ if (pattern.test(file)) {
133
+ count++;
134
+ const filePath = path.join(logsDir, file);
135
+ totalSize += await getFileSize(filePath);
136
+ }
137
+ }
138
+ } catch {
139
+ // Directory doesn't exist or can't be read
140
+ return { count: 0, totalSize: 0 };
141
+ }
142
+
143
+ return { count, totalSize };
144
+ }
145
+
146
+ /**
147
+ * Delete all archived log files for a server
148
+ * Returns count and total size of deleted files
149
+ */
150
+ export async function deleteArchivedLogs(serverId: string): Promise<{
151
+ count: number;
152
+ totalSize: number;
153
+ }> {
154
+ const logsDir = getLogsDir();
155
+ let count = 0;
156
+ let totalSize = 0;
157
+
158
+ try {
159
+ const files = await fs.readdir(logsDir);
160
+
161
+ // Pattern matches: server-id.YYYY-MM-DD-HH-MM-SS.{stdout,stderr}
162
+ const pattern = new RegExp(`^${serverId}\\.(\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-\\d{2})\\.(stdout|stderr)$`);
163
+
164
+ for (const file of files) {
165
+ if (pattern.test(file)) {
166
+ const filePath = path.join(logsDir, file);
167
+ const size = await getFileSize(filePath);
168
+ await fs.unlink(filePath);
169
+ count++;
170
+ totalSize += size;
171
+ }
172
+ }
173
+ } catch (error) {
174
+ throw new Error(`Failed to delete archived logs: ${(error as Error).message}`);
175
+ }
176
+
177
+ return { count, totalSize };
178
+ }