@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.
- package/CHANGELOG.md +7 -0
- package/README.md +71 -1
- package/dist/cli.js +24 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +182 -13
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +0 -1
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/delete.js +12 -10
- package/dist/commands/delete.js.map +1 -1
- package/dist/commands/logs-all.d.ts +9 -0
- package/dist/commands/logs-all.d.ts.map +1 -0
- package/dist/commands/logs-all.js +209 -0
- package/dist/commands/logs-all.js.map +1 -0
- package/dist/commands/logs.d.ts +4 -0
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +108 -2
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +5 -12
- package/dist/commands/rm.js.map +1 -1
- package/dist/commands/server-show.d.ts.map +1 -1
- package/dist/commands/server-show.js +20 -0
- package/dist/commands/server-show.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +22 -7
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/stop.js +3 -3
- package/dist/commands/stop.js.map +1 -1
- package/dist/utils/log-utils.d.ts +43 -0
- package/dist/utils/log-utils.d.ts.map +1 -0
- package/dist/utils/log-utils.js +190 -0
- package/dist/utils/log-utils.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +24 -1
- package/src/commands/config.ts +161 -15
- package/src/commands/create.ts +0 -1
- package/src/commands/delete.ts +10 -10
- package/src/commands/logs-all.ts +251 -0
- package/src/commands/logs.ts +138 -2
- package/src/commands/rm.ts +5 -12
- package/src/commands/server-show.ts +25 -0
- package/src/commands/start.ts +22 -7
- package/src/commands/stop.ts +3 -3
- package/src/utils/log-utils.ts +178 -0
package/src/commands/logs.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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}`);
|
package/src/commands/rm.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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:'));
|
package/src/commands/start.ts
CHANGED
|
@@ -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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
79
|
+
// 8. Update server status
|
|
65
80
|
let updatedServer = await statusChecker.updateServerStatus(server);
|
|
66
81
|
|
|
67
|
-
//
|
|
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
|
-
//
|
|
94
|
+
// 10. Display success
|
|
80
95
|
console.log();
|
|
81
96
|
console.log(chalk.green('✅ Server started successfully!'));
|
|
82
97
|
console.log();
|
package/src/commands/stop.ts
CHANGED
|
@@ -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
|
-
//
|
|
21
|
+
// Unload the service (removes from launchd management - won't auto-restart)
|
|
22
22
|
try {
|
|
23
|
-
await launchctlManager.
|
|
23
|
+
await launchctlManager.unloadService(server.plistPath);
|
|
24
24
|
} catch (error) {
|
|
25
|
-
throw new Error(`Failed to
|
|
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
|
+
}
|