@appkit/llamacpp-cli 1.12.0 → 1.12.1

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 (114) hide show
  1. package/README.md +217 -168
  2. package/package.json +10 -2
  3. package/web/dist/assets/index-Bin89Lwr.css +1 -0
  4. package/web/dist/assets/index-CVmonw3T.js +17 -0
  5. package/web/{index.html → dist/index.html} +2 -1
  6. package/.versionrc.json +0 -16
  7. package/CHANGELOG.md +0 -213
  8. package/docs/images/.gitkeep +0 -1
  9. package/docs/images/web-ui-servers.png +0 -0
  10. package/src/cli.ts +0 -523
  11. package/src/commands/admin/config.ts +0 -121
  12. package/src/commands/admin/logs.ts +0 -91
  13. package/src/commands/admin/restart.ts +0 -26
  14. package/src/commands/admin/start.ts +0 -27
  15. package/src/commands/admin/status.ts +0 -84
  16. package/src/commands/admin/stop.ts +0 -16
  17. package/src/commands/config-global.ts +0 -38
  18. package/src/commands/config.ts +0 -323
  19. package/src/commands/create.ts +0 -183
  20. package/src/commands/delete.ts +0 -74
  21. package/src/commands/list.ts +0 -37
  22. package/src/commands/logs-all.ts +0 -251
  23. package/src/commands/logs.ts +0 -345
  24. package/src/commands/monitor.ts +0 -110
  25. package/src/commands/ps.ts +0 -84
  26. package/src/commands/pull.ts +0 -44
  27. package/src/commands/rm.ts +0 -107
  28. package/src/commands/router/config.ts +0 -116
  29. package/src/commands/router/logs.ts +0 -256
  30. package/src/commands/router/restart.ts +0 -36
  31. package/src/commands/router/start.ts +0 -60
  32. package/src/commands/router/status.ts +0 -119
  33. package/src/commands/router/stop.ts +0 -33
  34. package/src/commands/run.ts +0 -233
  35. package/src/commands/search.ts +0 -107
  36. package/src/commands/server-show.ts +0 -161
  37. package/src/commands/show.ts +0 -207
  38. package/src/commands/start.ts +0 -101
  39. package/src/commands/stop.ts +0 -39
  40. package/src/commands/tui.ts +0 -25
  41. package/src/lib/admin-manager.ts +0 -435
  42. package/src/lib/admin-server.ts +0 -1243
  43. package/src/lib/config-generator.ts +0 -130
  44. package/src/lib/download-job-manager.ts +0 -213
  45. package/src/lib/history-manager.ts +0 -172
  46. package/src/lib/launchctl-manager.ts +0 -225
  47. package/src/lib/metrics-aggregator.ts +0 -257
  48. package/src/lib/model-downloader.ts +0 -328
  49. package/src/lib/model-scanner.ts +0 -157
  50. package/src/lib/model-search.ts +0 -114
  51. package/src/lib/models-dir-setup.ts +0 -46
  52. package/src/lib/port-manager.ts +0 -80
  53. package/src/lib/router-logger.ts +0 -201
  54. package/src/lib/router-manager.ts +0 -414
  55. package/src/lib/router-server.ts +0 -538
  56. package/src/lib/state-manager.ts +0 -206
  57. package/src/lib/status-checker.ts +0 -113
  58. package/src/lib/system-collector.ts +0 -315
  59. package/src/tui/ConfigApp.ts +0 -1085
  60. package/src/tui/HistoricalMonitorApp.ts +0 -587
  61. package/src/tui/ModelsApp.ts +0 -368
  62. package/src/tui/MonitorApp.ts +0 -386
  63. package/src/tui/MultiServerMonitorApp.ts +0 -1833
  64. package/src/tui/RootNavigator.ts +0 -74
  65. package/src/tui/SearchApp.ts +0 -511
  66. package/src/tui/SplashScreen.ts +0 -149
  67. package/src/types/admin-config.ts +0 -25
  68. package/src/types/global-config.ts +0 -26
  69. package/src/types/history-types.ts +0 -39
  70. package/src/types/model-info.ts +0 -8
  71. package/src/types/monitor-types.ts +0 -162
  72. package/src/types/router-config.ts +0 -25
  73. package/src/types/server-config.ts +0 -46
  74. package/src/utils/downsample-utils.ts +0 -128
  75. package/src/utils/file-utils.ts +0 -146
  76. package/src/utils/format-utils.ts +0 -98
  77. package/src/utils/log-parser.ts +0 -284
  78. package/src/utils/log-utils.ts +0 -178
  79. package/src/utils/process-utils.ts +0 -316
  80. package/src/utils/prompt-utils.ts +0 -47
  81. package/test-load.sh +0 -100
  82. package/tsconfig.json +0 -20
  83. package/web/eslint.config.js +0 -23
  84. package/web/llamacpp-web-dist.tar.gz +0 -0
  85. package/web/package-lock.json +0 -4017
  86. package/web/package.json +0 -38
  87. package/web/postcss.config.js +0 -6
  88. package/web/src/App.css +0 -42
  89. package/web/src/App.tsx +0 -86
  90. package/web/src/assets/react.svg +0 -1
  91. package/web/src/components/ApiKeyPrompt.tsx +0 -71
  92. package/web/src/components/CreateServerModal.tsx +0 -372
  93. package/web/src/components/DownloadProgress.tsx +0 -123
  94. package/web/src/components/Nav.tsx +0 -89
  95. package/web/src/components/RouterConfigModal.tsx +0 -240
  96. package/web/src/components/SearchModal.tsx +0 -306
  97. package/web/src/components/ServerConfigModal.tsx +0 -291
  98. package/web/src/hooks/useApi.ts +0 -259
  99. package/web/src/index.css +0 -42
  100. package/web/src/lib/api.ts +0 -226
  101. package/web/src/main.tsx +0 -10
  102. package/web/src/pages/Dashboard.tsx +0 -103
  103. package/web/src/pages/Models.tsx +0 -258
  104. package/web/src/pages/Router.tsx +0 -270
  105. package/web/src/pages/RouterLogs.tsx +0 -201
  106. package/web/src/pages/ServerLogs.tsx +0 -553
  107. package/web/src/pages/Servers.tsx +0 -358
  108. package/web/src/types/api.ts +0 -140
  109. package/web/tailwind.config.js +0 -31
  110. package/web/tsconfig.app.json +0 -28
  111. package/web/tsconfig.json +0 -7
  112. package/web/tsconfig.node.json +0 -26
  113. package/web/vite.config.ts +0 -25
  114. /package/web/{public → dist}/vite.svg +0 -0
@@ -1,146 +0,0 @@
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 default models directory (~/.llamacpp/models)
86
- */
87
- export function getModelsDir(): string {
88
- return path.join(getConfigDir(), '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
- }
107
-
108
- /**
109
- * Parse Metal (GPU) memory allocation from llama-server stderr logs
110
- * Looks for line: "load_tensors: Metal_Mapped model buffer size = 11120.23 MiB"
111
- * Returns memory in MB, or null if not found
112
- */
113
- export async function parseMetalMemoryFromLog(stderrPath: string): Promise<number | null> {
114
- try {
115
- // Check if log file exists
116
- if (!(await fileExists(stderrPath))) {
117
- return null;
118
- }
119
-
120
- // Open file for reading
121
- const fileHandle = await fs.open(stderrPath, 'r');
122
-
123
- try {
124
- // Read first 256KB (Metal allocation happens early during model loading)
125
- const buffer = Buffer.alloc(256 * 1024);
126
- const { bytesRead } = await fileHandle.read(buffer, 0, buffer.length, 0);
127
- const content = buffer.toString('utf-8', 0, bytesRead);
128
- const lines = content.split('\n');
129
-
130
- // Look for Metal_Mapped buffer size
131
- for (const line of lines) {
132
- const match = line.match(/Metal_Mapped model buffer size\s*=\s*([\d.]+)\s*MiB/);
133
- if (match) {
134
- const sizeInMB = parseFloat(match[1]);
135
- return isNaN(sizeInMB) ? null : sizeInMB;
136
- }
137
- }
138
-
139
- return null;
140
- } finally {
141
- await fileHandle.close();
142
- }
143
- } catch {
144
- return null;
145
- }
146
- }
@@ -1,98 +0,0 @@
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
- }
81
-
82
- /**
83
- * Format context size to human-readable format
84
- * Uses "k" suffix for clean multiples of 1024 (e.g., 32768 → "32k")
85
- * Falls back to full number with "tokens" for non-standard sizes
86
- */
87
- export function formatContextSize(tokens: number): string {
88
- // Check if it's a clean multiple of 1024
89
- if (tokens % 1024 === 0) {
90
- const k = tokens / 1024;
91
- // Only use "k" format for reasonable sizes (1k to 1024k i.e., up to 1M)
92
- if (k >= 1 && k <= 1024) {
93
- return `${k}k`;
94
- }
95
- }
96
- // For non-standard sizes or very large values, show full number
97
- return `${tokens.toLocaleString()} tokens`;
98
- }
@@ -1,284 +0,0 @@
1
- /**
2
- * Parse and consolidate verbose llama-server logs into compact single-line format
3
- */
4
-
5
- interface CompactLogEntry {
6
- timestamp: string;
7
- method: string;
8
- endpoint: string;
9
- ip: string;
10
- status: number;
11
- userMessage: string;
12
- tokensIn: number;
13
- tokensOut: number;
14
- responseTimeMs: number;
15
- }
16
-
17
- export class LogParser {
18
- private buffer: string[] = [];
19
- private isBuffering = false;
20
-
21
- /**
22
- * Health check endpoints to filter out by default
23
- * These are polled frequently by the TUI and generate excessive log noise
24
- */
25
- private static readonly HEALTH_CHECK_ENDPOINTS = ['/health', '/slots', '/props'];
26
-
27
- /**
28
- * Check if a log line represents a health check request
29
- */
30
- isHealthCheckRequest(line: string): boolean {
31
- return LogParser.HEALTH_CHECK_ENDPOINTS.some(ep => line.includes(`GET ${ep} `));
32
- }
33
-
34
- /**
35
- * Check if line is a request status line (contains method/endpoint/status, no JSON)
36
- * Handles both old and new formats:
37
- * - Old: log_server_r: request: POST /v1/chat/completions 127.0.0.1 200
38
- * - New: log_server_r: done request: POST /v1/messages 172.16.0.114 200
39
- */
40
- private isRequestStatusLine(line: string): boolean {
41
- return (
42
- (line.includes('log_server_r: request:') || line.includes('log_server_r: done request:')) &&
43
- !line.includes('{') &&
44
- /(?:done )?request: (POST|GET|PUT|DELETE)/.test(line)
45
- );
46
- }
47
-
48
- /**
49
- * Process log lines and output compact format
50
- */
51
- processLine(line: string, callback: (compactLine: string) => void): void {
52
- // Check if this is a request status line (no JSON, has method/endpoint/status)
53
- // Handles both old format (request:) and new format (done request:)
54
- if (this.isRequestStatusLine(line)) {
55
- // Check if this is the start of verbose format (status line before JSON)
56
- // or a simple single-line log
57
- if (this.isBuffering) {
58
- // We're already buffering, so this is a new request - process previous buffer
59
- const compactLine = this.consolidateRequest(this.buffer);
60
- if (compactLine) {
61
- callback(compactLine);
62
- }
63
- this.buffer = [];
64
- this.isBuffering = false;
65
- }
66
-
67
- // Start buffering (might be verbose or simple)
68
- this.isBuffering = true;
69
- this.buffer = [line];
70
- return;
71
- }
72
-
73
- // If we're buffering, collect lines
74
- if (this.isBuffering) {
75
- this.buffer.push(line);
76
-
77
- // Check if we have a complete request (found response line in verbose mode)
78
- if (line.includes('log_server_r: response:')) {
79
- const compactLine = this.consolidateRequest(this.buffer);
80
- if (compactLine) {
81
- callback(compactLine);
82
- }
83
- this.buffer = [];
84
- this.isBuffering = false;
85
- }
86
- }
87
- }
88
-
89
- /**
90
- * Flush any buffered simple format logs
91
- * Call this at the end of processing to handle simple logs that don't have response lines
92
- */
93
- flush(callback: (compactLine: string) => void): void {
94
- if (this.isBuffering && this.buffer.length > 0) {
95
- // If we only have one line, it's a simple format log
96
- if (this.buffer.length === 1) {
97
- const simpleLine = this.parseSimpleFormat(this.buffer[0]);
98
- if (simpleLine) {
99
- callback(simpleLine);
100
- }
101
- }
102
- this.buffer = [];
103
- this.isBuffering = false;
104
- }
105
- }
106
-
107
- /**
108
- * Parse simple single-line format (non-verbose mode)
109
- * Handles both old and new formats:
110
- * - Old: srv log_server_r: request: POST /v1/chat/completions 127.0.0.1 200
111
- * - New: srv log_server_r: done request: POST /v1/messages 172.16.0.114 200
112
- */
113
- private parseSimpleFormat(line: string): string | null {
114
- try {
115
- const timestamp = this.extractTimestamp(line);
116
- // Match both "request:" and "done request:" formats
117
- const requestMatch = line.match(/(?:done )?request: (POST|GET|PUT|DELETE) ([^\s]+) ([^\s]+) (\d+)/);
118
- if (!requestMatch) return null;
119
-
120
- const [, method, endpoint, ip, status] = requestMatch;
121
-
122
- // Simple format doesn't include message/token details
123
- return `${timestamp} ${method} ${endpoint} ${ip} ${status}`;
124
- } catch (error) {
125
- return null;
126
- }
127
- }
128
-
129
- /**
130
- * Consolidate buffered request/response lines into single line
131
- * Handles both old and new llama.cpp log formats
132
- */
133
- private consolidateRequest(lines: string[]): string | null {
134
- try {
135
- // Parse first line: timestamp and request info
136
- // Match both "request:" and "done request:" formats
137
- const firstLine = lines[0];
138
- const timestamp = this.extractTimestamp(firstLine);
139
- const requestMatch = firstLine.match(/(?:done )?request: (POST|GET|PUT|DELETE) (\/[^\s]+) ([^\s]+) (\d+)/);
140
- if (!requestMatch) return null;
141
-
142
- const [, method, endpoint, ip, status] = requestMatch;
143
-
144
- // Parse request JSON (line with JSON body)
145
- const requestLine = lines.find((l) => l.includes('log_server_r: request:') && l.includes('{'));
146
-
147
- let userMessage = '';
148
- if (requestLine) {
149
- const requestJson = this.extractJson(requestLine);
150
- if (requestJson) {
151
- userMessage = this.extractUserMessage(requestJson);
152
- }
153
- }
154
-
155
- // Parse response JSON (may be empty in new format)
156
- const responseLine = lines.find((l) => l.includes('log_server_r: response:'));
157
- let tokensIn = 0;
158
- let tokensOut = 0;
159
- let responseTimeMs = 0;
160
-
161
- if (responseLine) {
162
- const responseJson = this.extractJson(responseLine);
163
- if (responseJson) {
164
- tokensIn = responseJson.usage?.prompt_tokens || 0;
165
- tokensOut = responseJson.usage?.completion_tokens || 0;
166
- responseTimeMs = this.extractResponseTime(responseJson);
167
- }
168
- }
169
-
170
- // Format compact line (works even without response data)
171
- return this.formatCompactLine({
172
- timestamp,
173
- method,
174
- endpoint,
175
- ip,
176
- status: parseInt(status, 10),
177
- userMessage,
178
- tokensIn,
179
- tokensOut,
180
- responseTimeMs,
181
- });
182
- } catch (error) {
183
- return null;
184
- }
185
- }
186
-
187
- /**
188
- * Extract timestamp from log line
189
- */
190
- private extractTimestamp(line: string): string {
191
- // Look for timestamp format like [2025-12-09 10:13:45]
192
- const match = line.match(/\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/);
193
- if (match) {
194
- return match[1]; // Return as-is: 2025-12-09 10:13:45
195
- }
196
- // If no timestamp in logs, use current time in same format
197
- const now = new Date();
198
- return now.toISOString().substring(0, 19).replace('T', ' '); // 2025-12-09 10:13:45
199
- }
200
-
201
- /**
202
- * Extract JSON from log line
203
- */
204
- private extractJson(line: string): any {
205
- const jsonStart = line.indexOf('{');
206
- if (jsonStart === -1) return null;
207
-
208
- try {
209
- const jsonStr = line.substring(jsonStart);
210
- return JSON.parse(jsonStr);
211
- } catch {
212
- return null;
213
- }
214
- }
215
-
216
- /**
217
- * Extract first user message from request JSON
218
- * Handles both string content and array content formats:
219
- * - String: {"role":"user","content":"Hello"}
220
- * - Array: {"role":"user","content":[{"type":"text","text":"Hello"}]}
221
- */
222
- private extractUserMessage(requestJson: any): string {
223
- const messages = requestJson.messages || [];
224
- const userMsg = messages.find((m: any) => m.role === 'user');
225
- if (!userMsg || !userMsg.content) return '';
226
-
227
- let content: string;
228
-
229
- // Handle array format (e.g., Claude/Anthropic API style)
230
- if (Array.isArray(userMsg.content)) {
231
- const textPart = userMsg.content.find((p: any) => p.type === 'text');
232
- content = textPart?.text || '';
233
- } else {
234
- content = userMsg.content;
235
- }
236
-
237
- // Clean and truncate to first 50 characters
238
- content = content.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
239
- return content.length > 50 ? content.substring(0, 47) + '...' : content;
240
- }
241
-
242
- /**
243
- * Extract response time from response JSON
244
- */
245
- private extractResponseTime(responseJson: any): number {
246
- // Check __verbose.timings first (has total time)
247
- const verboseTimings = responseJson.__verbose?.timings;
248
- if (verboseTimings) {
249
- const promptMs = verboseTimings.prompt_ms || 0;
250
- const predictedMs = verboseTimings.predicted_ms || 0;
251
- return Math.round(promptMs + predictedMs);
252
- }
253
-
254
- // Fallback to top-level timings
255
- const timings = responseJson.timings;
256
- if (timings) {
257
- const promptMs = timings.prompt_ms || 0;
258
- const predictedMs = timings.predicted_ms || 0;
259
- return Math.round(promptMs + predictedMs);
260
- }
261
-
262
- return 0;
263
- }
264
-
265
- /**
266
- * Format compact log line
267
- */
268
- private formatCompactLine(entry: CompactLogEntry): string {
269
- return [
270
- entry.timestamp,
271
- entry.method,
272
- entry.endpoint,
273
- entry.ip,
274
- entry.status,
275
- `"${entry.userMessage}"`,
276
- entry.tokensIn,
277
- entry.tokensOut,
278
- entry.responseTimeMs,
279
- ].join(' ');
280
- }
281
- }
282
-
283
- // Export singleton instance
284
- export const logParser = new LogParser();
@@ -1,178 +0,0 @@
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
- }