@appkit/llamacpp-cli 1.9.0 → 1.10.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 +16 -0
- package/README.md +171 -42
- package/dist/cli.js +75 -10
- package/dist/cli.js.map +1 -1
- package/dist/commands/completion.d.ts +9 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +83 -0
- package/dist/commands/completion.js.map +1 -0
- package/dist/commands/monitor.js +1 -1
- package/dist/commands/monitor.js.map +1 -1
- package/dist/commands/ps.d.ts +1 -3
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +36 -115
- package/dist/commands/ps.js.map +1 -1
- package/dist/commands/router/config.d.ts +1 -0
- package/dist/commands/router/config.d.ts.map +1 -1
- package/dist/commands/router/config.js +7 -2
- package/dist/commands/router/config.js.map +1 -1
- package/dist/commands/router/logs.d.ts +12 -0
- package/dist/commands/router/logs.d.ts.map +1 -0
- package/dist/commands/router/logs.js +238 -0
- package/dist/commands/router/logs.js.map +1 -0
- package/dist/commands/tui.d.ts +2 -0
- package/dist/commands/tui.d.ts.map +1 -0
- package/dist/commands/tui.js +27 -0
- package/dist/commands/tui.js.map +1 -0
- package/dist/lib/completion.d.ts +5 -0
- package/dist/lib/completion.d.ts.map +1 -0
- package/dist/lib/completion.js +195 -0
- package/dist/lib/completion.js.map +1 -0
- package/dist/lib/model-downloader.d.ts +5 -1
- package/dist/lib/model-downloader.d.ts.map +1 -1
- package/dist/lib/model-downloader.js +53 -20
- package/dist/lib/model-downloader.js.map +1 -1
- package/dist/lib/router-logger.d.ts +61 -0
- package/dist/lib/router-logger.d.ts.map +1 -0
- package/dist/lib/router-logger.js +200 -0
- package/dist/lib/router-logger.js.map +1 -0
- package/dist/lib/router-manager.d.ts.map +1 -1
- package/dist/lib/router-manager.js +1 -0
- package/dist/lib/router-manager.js.map +1 -1
- package/dist/lib/router-server.d.ts +9 -0
- package/dist/lib/router-server.d.ts.map +1 -1
- package/dist/lib/router-server.js +169 -57
- package/dist/lib/router-server.js.map +1 -1
- package/dist/tui/ConfigApp.d.ts +7 -0
- package/dist/tui/ConfigApp.d.ts.map +1 -0
- package/dist/tui/ConfigApp.js +1002 -0
- package/dist/tui/ConfigApp.js.map +1 -0
- package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -1
- package/dist/tui/HistoricalMonitorApp.js +85 -49
- package/dist/tui/HistoricalMonitorApp.js.map +1 -1
- package/dist/tui/ModelsApp.d.ts +7 -0
- package/dist/tui/ModelsApp.d.ts.map +1 -0
- package/dist/tui/ModelsApp.js +362 -0
- package/dist/tui/ModelsApp.js.map +1 -0
- package/dist/tui/MultiServerMonitorApp.d.ts +6 -1
- package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
- package/dist/tui/MultiServerMonitorApp.js +1038 -122
- package/dist/tui/MultiServerMonitorApp.js.map +1 -1
- package/dist/tui/RootNavigator.d.ts +7 -0
- package/dist/tui/RootNavigator.d.ts.map +1 -0
- package/dist/tui/RootNavigator.js +55 -0
- package/dist/tui/RootNavigator.js.map +1 -0
- package/dist/tui/SearchApp.d.ts +6 -0
- package/dist/tui/SearchApp.d.ts.map +1 -0
- package/dist/tui/SearchApp.js +451 -0
- package/dist/tui/SearchApp.js.map +1 -0
- package/dist/tui/SplashScreen.d.ts +16 -0
- package/dist/tui/SplashScreen.d.ts.map +1 -0
- package/dist/tui/SplashScreen.js +129 -0
- package/dist/tui/SplashScreen.js.map +1 -0
- package/dist/types/router-config.d.ts +1 -0
- package/dist/types/router-config.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +41 -10
- package/src/commands/monitor.ts +1 -1
- package/src/commands/ps.ts +44 -133
- package/src/commands/router/config.ts +9 -2
- package/src/commands/router/logs.ts +256 -0
- package/src/commands/tui.ts +25 -0
- package/src/lib/model-downloader.ts +57 -20
- package/src/lib/router-logger.ts +201 -0
- package/src/lib/router-manager.ts +1 -0
- package/src/lib/router-server.ts +193 -62
- package/src/tui/ConfigApp.ts +1085 -0
- package/src/tui/HistoricalMonitorApp.ts +88 -49
- package/src/tui/ModelsApp.ts +368 -0
- package/src/tui/MultiServerMonitorApp.ts +1163 -122
- package/src/tui/RootNavigator.ts +74 -0
- package/src/tui/SearchApp.ts +511 -0
- package/src/tui/SplashScreen.ts +149 -0
- package/src/types/router-config.ts +1 -0
package/src/cli.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { routerStopCommand } from './commands/router/stop';
|
|
|
24
24
|
import { routerStatusCommand } from './commands/router/status';
|
|
25
25
|
import { routerRestartCommand } from './commands/router/restart';
|
|
26
26
|
import { routerConfigCommand } from './commands/router/config';
|
|
27
|
+
import { routerLogsCommand } from './commands/router/logs';
|
|
27
28
|
import packageJson from '../package.json';
|
|
28
29
|
|
|
29
30
|
const program = new Command();
|
|
@@ -31,7 +32,17 @@ const program = new Command();
|
|
|
31
32
|
program
|
|
32
33
|
.name('llamacpp')
|
|
33
34
|
.description('CLI tool to manage local llama.cpp servers on macOS')
|
|
34
|
-
.version(packageJson.version, '-v, --version', 'Output the version number')
|
|
35
|
+
.version(packageJson.version, '-v, --version', 'Output the version number')
|
|
36
|
+
.action(async () => {
|
|
37
|
+
// Default action: launch TUI when no command provided
|
|
38
|
+
try {
|
|
39
|
+
const { tuiCommand } = await import('./commands/tui');
|
|
40
|
+
await tuiCommand();
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(chalk.red('❌ Error:'), (error as Error).message);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
35
46
|
|
|
36
47
|
// List models
|
|
37
48
|
program
|
|
@@ -46,14 +57,13 @@ program
|
|
|
46
57
|
}
|
|
47
58
|
});
|
|
48
59
|
|
|
49
|
-
// List
|
|
60
|
+
// List servers (static table)
|
|
50
61
|
program
|
|
51
|
-
.command('ps
|
|
52
|
-
.description('
|
|
53
|
-
.
|
|
54
|
-
.action(async (identifier?: string, options?: { table?: boolean }) => {
|
|
62
|
+
.command('ps')
|
|
63
|
+
.description('List all servers with status (static table)')
|
|
64
|
+
.action(async () => {
|
|
55
65
|
try {
|
|
56
|
-
await psCommand(
|
|
66
|
+
await psCommand();
|
|
57
67
|
} catch (error) {
|
|
58
68
|
console.error(chalk.red('❌ Error:'), (error as Error).message);
|
|
59
69
|
process.exit(1);
|
|
@@ -298,14 +308,14 @@ server
|
|
|
298
308
|
}
|
|
299
309
|
});
|
|
300
310
|
|
|
301
|
-
// Monitor server (deprecated - redirects to
|
|
311
|
+
// Monitor server (deprecated - redirects to TUI)
|
|
302
312
|
server
|
|
303
313
|
.command('monitor [identifier]')
|
|
304
|
-
.description('Monitor server with real-time metrics TUI (deprecated: use "llamacpp
|
|
314
|
+
.description('Monitor server with real-time metrics TUI (deprecated: use "llamacpp" instead)')
|
|
305
315
|
.action(async (identifier?: string) => {
|
|
306
316
|
try {
|
|
307
317
|
console.log(chalk.yellow('⚠️ The "monitor" command is deprecated and will be removed in a future version.'));
|
|
308
|
-
console.log(chalk.dim(' Please use "llamacpp
|
|
318
|
+
console.log(chalk.dim(' Please use "llamacpp" instead for the same functionality.\n'));
|
|
309
319
|
await monitorCommand(identifier);
|
|
310
320
|
} catch (error) {
|
|
311
321
|
console.error(chalk.red('❌ Error:'), (error as Error).message);
|
|
@@ -378,6 +388,7 @@ router
|
|
|
378
388
|
.option('-h, --host <address>', 'Update bind address')
|
|
379
389
|
.option('--timeout <ms>', 'Update request timeout (milliseconds)', parseInt)
|
|
380
390
|
.option('--health-interval <ms>', 'Update health check interval (milliseconds)', parseInt)
|
|
391
|
+
.option('-v, --verbose [boolean]', 'Enable/disable verbose logging to file (true/false)', (val) => val === 'true' || val === '1')
|
|
381
392
|
.option('-r, --restart', 'Automatically restart router if running')
|
|
382
393
|
.action(async (options) => {
|
|
383
394
|
try {
|
|
@@ -388,5 +399,25 @@ router
|
|
|
388
399
|
}
|
|
389
400
|
});
|
|
390
401
|
|
|
402
|
+
// Router logs
|
|
403
|
+
router
|
|
404
|
+
.command('logs')
|
|
405
|
+
.description('View router logs')
|
|
406
|
+
.option('-f, --follow', 'Follow logs in real-time (like tail -f)')
|
|
407
|
+
.option('-n, --lines <number>', 'Number of lines to show (default: 50)', parseInt)
|
|
408
|
+
.option('--stderr', 'Show system logs (stderr) instead of activity logs (stdout)')
|
|
409
|
+
.option('-v, --verbose', 'Show verbose JSON log file (if enabled)')
|
|
410
|
+
.option('--clear', 'Clear the log file')
|
|
411
|
+
.option('--rotate', 'Rotate the log file with timestamp')
|
|
412
|
+
.option('--clear-all', 'Clear all router logs (activity, system, verbose)')
|
|
413
|
+
.action(async (options) => {
|
|
414
|
+
try {
|
|
415
|
+
await routerLogsCommand(options);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.error(chalk.red('❌ Error:'), (error as Error).message);
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
391
422
|
// Parse arguments
|
|
392
423
|
program.parse();
|
package/src/commands/monitor.ts
CHANGED
|
@@ -31,7 +31,7 @@ export async function monitorCommand(identifier?: string): Promise<void> {
|
|
|
31
31
|
screen.destroy();
|
|
32
32
|
throw new Error(
|
|
33
33
|
`Server not found: ${identifier}\n\n` +
|
|
34
|
-
`Use: llamacpp ps\n` +
|
|
34
|
+
`Use: llamacpp ps (to list servers)\n` +
|
|
35
35
|
`Or create a new server: llamacpp server create <model>`
|
|
36
36
|
);
|
|
37
37
|
}
|
package/src/commands/ps.ts
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import Table from 'cli-table3';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { getProcessMemory } from '../utils/process-utils';
|
|
8
|
-
import { createMultiServerMonitorUI } from '../tui/MultiServerMonitorApp.js';
|
|
3
|
+
import { stateManager } from '../lib/state-manager.js';
|
|
4
|
+
import { statusChecker } from '../lib/status-checker.js';
|
|
5
|
+
import { formatUptime, formatBytes } from '../utils/format-utils.js';
|
|
6
|
+
import { getProcessMemory } from '../utils/process-utils.js';
|
|
9
7
|
import { ServerConfig } from '../types/server-config.js';
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
const STATUS_CONFIG = {
|
|
10
|
+
running: { text: '✅ RUNNING', color: chalk.green },
|
|
11
|
+
crashed: { text: '❌ CRASHED', color: chalk.red },
|
|
12
|
+
stopped: { text: '⚠️ STOPPED', color: chalk.yellow },
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
async function getServerMemory(server: ServerConfig): Promise<string> {
|
|
16
|
+
if (server.status !== 'running' || !server.pid) {
|
|
17
|
+
return '-';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const cpuMemoryBytes = await getProcessMemory(server.pid);
|
|
21
|
+
if (cpuMemoryBytes === null) {
|
|
22
|
+
return '-';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const metalMemoryBytes = server.metalMemoryMB ? server.metalMemoryMB * 1024 * 1024 : 0;
|
|
26
|
+
return formatBytes(cpuMemoryBytes + metalMemoryBytes);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function psCommand(): Promise<void> {
|
|
12
30
|
const servers = await stateManager.getAllServers();
|
|
13
31
|
|
|
14
32
|
if (servers.length === 0) {
|
|
@@ -17,60 +35,31 @@ async function showStaticTable(): Promise<void> {
|
|
|
17
35
|
return;
|
|
18
36
|
}
|
|
19
37
|
|
|
20
|
-
// Update all server statuses
|
|
21
38
|
console.log(chalk.dim('Checking server statuses...\n'));
|
|
22
|
-
const
|
|
39
|
+
const serversWithStatus = await statusChecker.updateAllServerStatuses();
|
|
23
40
|
|
|
24
41
|
const table = new Table({
|
|
25
42
|
head: ['SERVER ID', 'MODEL', 'PORT', 'STATUS', 'PID', 'MEMORY', 'UPTIME'],
|
|
26
43
|
});
|
|
27
44
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
runningCount++;
|
|
41
|
-
break;
|
|
42
|
-
case 'crashed':
|
|
43
|
-
statusText = '❌ CRASHED';
|
|
44
|
-
statusColor = chalk.red;
|
|
45
|
-
crashedCount++;
|
|
46
|
-
break;
|
|
47
|
-
default:
|
|
48
|
-
statusText = '⚠️ STOPPED';
|
|
49
|
-
statusColor = chalk.yellow;
|
|
50
|
-
stoppedCount++;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const uptime =
|
|
54
|
-
server.status === 'running' && server.lastStarted
|
|
55
|
-
? formatUptime(server.lastStarted)
|
|
56
|
-
: '-';
|
|
57
|
-
|
|
58
|
-
// Get memory usage for running servers (CPU + Metal GPU memory)
|
|
59
|
-
let memoryText = '-';
|
|
60
|
-
if (server.status === 'running' && server.pid) {
|
|
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);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
45
|
+
const counts = { running: 0, stopped: 0, crashed: 0 };
|
|
46
|
+
|
|
47
|
+
for (const server of serversWithStatus) {
|
|
48
|
+
const status = server.status || 'stopped';
|
|
49
|
+
const config = STATUS_CONFIG[status] || STATUS_CONFIG.stopped;
|
|
50
|
+
counts[status]++;
|
|
51
|
+
|
|
52
|
+
const uptime = server.status === 'running' && server.lastStarted
|
|
53
|
+
? formatUptime(server.lastStarted)
|
|
54
|
+
: '-';
|
|
55
|
+
|
|
56
|
+
const memoryText = await getServerMemory(server);
|
|
68
57
|
|
|
69
58
|
table.push([
|
|
70
59
|
server.id,
|
|
71
60
|
server.modelName,
|
|
72
61
|
server.port.toString(),
|
|
73
|
-
|
|
62
|
+
config.color(config.text),
|
|
74
63
|
server.pid?.toString() || '-',
|
|
75
64
|
memoryText,
|
|
76
65
|
uptime,
|
|
@@ -80,94 +69,16 @@ async function showStaticTable(): Promise<void> {
|
|
|
80
69
|
console.log(table.toString());
|
|
81
70
|
|
|
82
71
|
const summary = [
|
|
83
|
-
chalk.green(`${
|
|
84
|
-
chalk.yellow(`${
|
|
72
|
+
chalk.green(`${counts.running} running`),
|
|
73
|
+
chalk.yellow(`${counts.stopped} stopped`),
|
|
85
74
|
];
|
|
86
|
-
if (
|
|
87
|
-
summary.push(chalk.red(`${
|
|
75
|
+
if (counts.crashed > 0) {
|
|
76
|
+
summary.push(chalk.red(`${counts.crashed} crashed`));
|
|
88
77
|
}
|
|
89
78
|
|
|
90
79
|
console.log(chalk.dim(`\nTotal: ${servers.length} servers (${summary.join(', ')})`));
|
|
91
80
|
|
|
92
|
-
if (
|
|
81
|
+
if (counts.crashed > 0) {
|
|
93
82
|
console.log(chalk.red('\n⚠️ Some servers have crashed. Check logs with: llamacpp server logs <id> --errors'));
|
|
94
83
|
}
|
|
95
84
|
}
|
|
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
|
-
}
|
|
@@ -6,6 +6,7 @@ interface ConfigOptions {
|
|
|
6
6
|
host?: string;
|
|
7
7
|
timeout?: number;
|
|
8
8
|
healthInterval?: number;
|
|
9
|
+
verbose?: boolean;
|
|
9
10
|
restart?: boolean;
|
|
10
11
|
}
|
|
11
12
|
|
|
@@ -18,9 +19,9 @@ export async function routerConfigCommand(options: ConfigOptions): Promise<void>
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
// Check if any options were provided
|
|
21
|
-
const hasOptions = options.port || options.host || options.timeout || options.healthInterval;
|
|
22
|
+
const hasOptions = options.port || options.host || options.timeout || options.healthInterval || options.verbose !== undefined;
|
|
22
23
|
if (!hasOptions) {
|
|
23
|
-
throw new Error('No configuration options provided. Use --port, --host, --timeout,
|
|
24
|
+
throw new Error('No configuration options provided. Use --port, --host, --timeout, --health-interval, or --verbose');
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const isRunning = config.status === 'running';
|
|
@@ -55,6 +56,12 @@ export async function routerConfigCommand(options: ConfigOptions): Promise<void>
|
|
|
55
56
|
updates.healthCheckInterval = options.healthInterval;
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
if (options.verbose !== undefined) {
|
|
60
|
+
const verboseStr = (val: boolean) => val ? 'enabled' : 'disabled';
|
|
61
|
+
changes.push(`Verbose Logging: ${verboseStr(config.verbose)} → ${verboseStr(options.verbose)}`);
|
|
62
|
+
updates.verbose = options.verbose;
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
// Display changes
|
|
59
66
|
console.log(chalk.blue('📝 Configuration changes:'));
|
|
60
67
|
console.log();
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import * as readline from 'readline';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import { routerManager } from '../../lib/router-manager';
|
|
6
|
+
import { fileExists } from '../../utils/file-utils';
|
|
7
|
+
import {
|
|
8
|
+
getFileSize,
|
|
9
|
+
formatFileSize,
|
|
10
|
+
rotateLogFile,
|
|
11
|
+
clearLogFile,
|
|
12
|
+
} from '../../utils/log-utils';
|
|
13
|
+
|
|
14
|
+
interface RouterLogsOptions {
|
|
15
|
+
follow?: boolean;
|
|
16
|
+
lines?: number;
|
|
17
|
+
stderr?: boolean; // View system logs (stderr) instead of activity logs (stdout)
|
|
18
|
+
verbose?: boolean;
|
|
19
|
+
clear?: boolean;
|
|
20
|
+
rotate?: boolean;
|
|
21
|
+
clearAll?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function routerLogsCommand(options: RouterLogsOptions): Promise<void> {
|
|
25
|
+
// Load router config
|
|
26
|
+
const config = await routerManager.loadConfig();
|
|
27
|
+
if (!config) {
|
|
28
|
+
throw new Error('Router configuration not found. Use "llamacpp router start" to create it.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Determine log file (default to stdout for activity logs, stderr for system logs)
|
|
32
|
+
const logPath = options.stderr ? config.stderrPath : config.stdoutPath;
|
|
33
|
+
const logType = options.stderr ? 'system' : 'activity';
|
|
34
|
+
|
|
35
|
+
// Also check for verbose JSON log file if --verbose flag is used
|
|
36
|
+
const verboseLogPath = '/Users/dweaver/.llamacpp/logs/router.log';
|
|
37
|
+
const useVerboseLog = options.verbose && (await fileExists(verboseLogPath));
|
|
38
|
+
|
|
39
|
+
// Handle --clear-all option (clears both stderr and stdout)
|
|
40
|
+
if (options.clearAll) {
|
|
41
|
+
let totalFreed = 0;
|
|
42
|
+
|
|
43
|
+
// Clear stderr
|
|
44
|
+
if (await fileExists(config.stderrPath)) {
|
|
45
|
+
totalFreed += await getFileSize(config.stderrPath);
|
|
46
|
+
await clearLogFile(config.stderrPath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Clear stdout
|
|
50
|
+
if (await fileExists(config.stdoutPath)) {
|
|
51
|
+
totalFreed += await getFileSize(config.stdoutPath);
|
|
52
|
+
await clearLogFile(config.stdoutPath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Clear verbose log file
|
|
56
|
+
if (await fileExists(verboseLogPath)) {
|
|
57
|
+
totalFreed += await getFileSize(verboseLogPath);
|
|
58
|
+
await clearLogFile(verboseLogPath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(chalk.green('✅ Cleared all router logs'));
|
|
62
|
+
console.log(chalk.dim(` Total freed: ${formatFileSize(totalFreed)}`));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Handle --clear option
|
|
67
|
+
if (options.clear) {
|
|
68
|
+
const targetPath = useVerboseLog ? verboseLogPath : logPath;
|
|
69
|
+
|
|
70
|
+
if (!(await fileExists(targetPath))) {
|
|
71
|
+
console.log(chalk.yellow(`⚠️ No ${useVerboseLog ? 'verbose log' : logType} found for router`));
|
|
72
|
+
console.log(chalk.dim(` Log file does not exist: ${targetPath}`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sizeBefore = await getFileSize(targetPath);
|
|
77
|
+
await clearLogFile(targetPath);
|
|
78
|
+
|
|
79
|
+
console.log(chalk.green(`✅ Cleared router ${useVerboseLog ? 'verbose log' : logType}`));
|
|
80
|
+
console.log(chalk.dim(` Freed: ${formatFileSize(sizeBefore)}`));
|
|
81
|
+
console.log(chalk.dim(` ${targetPath}`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Handle --rotate option
|
|
86
|
+
if (options.rotate) {
|
|
87
|
+
const targetPath = useVerboseLog ? verboseLogPath : logPath;
|
|
88
|
+
|
|
89
|
+
if (!(await fileExists(targetPath))) {
|
|
90
|
+
console.log(chalk.yellow(`⚠️ No ${useVerboseLog ? 'verbose log' : logType} found for router`));
|
|
91
|
+
console.log(chalk.dim(` Log file does not exist: ${targetPath}`));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const archivedPath = await rotateLogFile(targetPath);
|
|
97
|
+
const size = await getFileSize(archivedPath);
|
|
98
|
+
|
|
99
|
+
console.log(chalk.green(`✅ Rotated router ${useVerboseLog ? 'verbose log' : logType}`));
|
|
100
|
+
console.log(chalk.dim(` Archived: ${formatFileSize(size)}`));
|
|
101
|
+
console.log(chalk.dim(` → ${archivedPath}`));
|
|
102
|
+
} catch (error) {
|
|
103
|
+
throw new Error(`Failed to rotate log: ${(error as Error).message}`);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Determine which log to display
|
|
109
|
+
const displayPath = useVerboseLog ? verboseLogPath : logPath;
|
|
110
|
+
const displayType = useVerboseLog ? 'verbose JSON log' : logType;
|
|
111
|
+
|
|
112
|
+
// Check if log file exists
|
|
113
|
+
if (!(await fileExists(displayPath))) {
|
|
114
|
+
console.log(chalk.yellow(`⚠️ No ${displayType} found for router`));
|
|
115
|
+
console.log(chalk.dim(` Log file does not exist: ${displayPath}`));
|
|
116
|
+
|
|
117
|
+
if (useVerboseLog) {
|
|
118
|
+
console.log();
|
|
119
|
+
console.log(chalk.dim(' Verbose logging is disabled. Enable with:'));
|
|
120
|
+
console.log(chalk.dim(' llamacpp router config --verbose true --restart'));
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(chalk.blue(`📋 Router logs (${displayType})`));
|
|
126
|
+
console.log(chalk.dim(` ${displayPath}`));
|
|
127
|
+
|
|
128
|
+
// Show log size information
|
|
129
|
+
const currentSize = await getFileSize(displayPath);
|
|
130
|
+
console.log(chalk.dim(` Size: ${formatFileSize(currentSize)}`));
|
|
131
|
+
|
|
132
|
+
if (!useVerboseLog && config.verbose) {
|
|
133
|
+
console.log(chalk.dim(` Verbose logging is enabled (use --verbose to view JSON log)`));
|
|
134
|
+
} else if (!useVerboseLog && !config.verbose) {
|
|
135
|
+
console.log(chalk.dim(` Verbose logging is disabled`));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log();
|
|
139
|
+
|
|
140
|
+
if (options.follow) {
|
|
141
|
+
// Follow logs in real-time
|
|
142
|
+
if (useVerboseLog) {
|
|
143
|
+
// Pretty-print JSON logs
|
|
144
|
+
const tailProcess = spawn('tail', ['-f', displayPath]);
|
|
145
|
+
const rl = readline.createInterface({
|
|
146
|
+
input: tailProcess.stdout,
|
|
147
|
+
crlfDelay: Infinity,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
rl.on('line', (line) => {
|
|
151
|
+
try {
|
|
152
|
+
const entry = JSON.parse(line);
|
|
153
|
+
// Format timestamp
|
|
154
|
+
const timestamp = new Date(entry.timestamp).toLocaleTimeString();
|
|
155
|
+
// Color code status
|
|
156
|
+
const statusColor = entry.status === 'success' ? chalk.green : chalk.red;
|
|
157
|
+
|
|
158
|
+
console.log(
|
|
159
|
+
chalk.dim(`[${timestamp}]`),
|
|
160
|
+
statusColor(entry.statusCode),
|
|
161
|
+
entry.method,
|
|
162
|
+
entry.endpoint,
|
|
163
|
+
'→',
|
|
164
|
+
chalk.cyan(entry.model),
|
|
165
|
+
chalk.dim(`(${entry.backend || 'N/A'})`),
|
|
166
|
+
chalk.yellow(`${entry.durationMs}ms`)
|
|
167
|
+
);
|
|
168
|
+
if (entry.prompt) {
|
|
169
|
+
console.log(chalk.dim(` Prompt: "${entry.prompt}"`));
|
|
170
|
+
}
|
|
171
|
+
if (entry.error) {
|
|
172
|
+
console.log(chalk.red(` Error: ${entry.error}`));
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// Not JSON, just print raw line
|
|
176
|
+
console.log(line);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
tailProcess.on('close', () => {
|
|
181
|
+
process.exit(0);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Handle Ctrl+C gracefully
|
|
185
|
+
process.on('SIGINT', () => {
|
|
186
|
+
tailProcess.kill();
|
|
187
|
+
process.exit(0);
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
// Standard tail for stderr/stdout
|
|
191
|
+
const tailProcess = spawn('tail', ['-f', displayPath]);
|
|
192
|
+
tailProcess.stdout.pipe(process.stdout);
|
|
193
|
+
tailProcess.stderr.pipe(process.stderr);
|
|
194
|
+
|
|
195
|
+
tailProcess.on('close', () => {
|
|
196
|
+
process.exit(0);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Handle Ctrl+C gracefully
|
|
200
|
+
process.on('SIGINT', () => {
|
|
201
|
+
tailProcess.kill();
|
|
202
|
+
process.exit(0);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
// Show last N lines (default 50)
|
|
207
|
+
const linesToShow = options.lines || 50;
|
|
208
|
+
|
|
209
|
+
if (useVerboseLog) {
|
|
210
|
+
// Pretty-print JSON logs
|
|
211
|
+
const lines = fs.readFileSync(displayPath, 'utf-8')
|
|
212
|
+
.split('\n')
|
|
213
|
+
.filter(line => line.trim())
|
|
214
|
+
.slice(-linesToShow);
|
|
215
|
+
|
|
216
|
+
for (const line of lines) {
|
|
217
|
+
try {
|
|
218
|
+
const entry = JSON.parse(line);
|
|
219
|
+
// Format timestamp
|
|
220
|
+
const timestamp = new Date(entry.timestamp).toLocaleTimeString();
|
|
221
|
+
// Color code status
|
|
222
|
+
const statusColor = entry.status === 'success' ? chalk.green : chalk.red;
|
|
223
|
+
|
|
224
|
+
console.log(
|
|
225
|
+
chalk.dim(`[${timestamp}]`),
|
|
226
|
+
statusColor(entry.statusCode),
|
|
227
|
+
entry.method,
|
|
228
|
+
entry.endpoint,
|
|
229
|
+
'→',
|
|
230
|
+
chalk.cyan(entry.model),
|
|
231
|
+
chalk.dim(`(${entry.backend || 'N/A'})`),
|
|
232
|
+
chalk.yellow(`${entry.durationMs}ms`)
|
|
233
|
+
);
|
|
234
|
+
if (entry.prompt) {
|
|
235
|
+
console.log(chalk.dim(` Prompt: "${entry.prompt}"`));
|
|
236
|
+
}
|
|
237
|
+
if (entry.error) {
|
|
238
|
+
console.log(chalk.red(` Error: ${entry.error}`));
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
// Not JSON, just print raw line
|
|
242
|
+
console.log(line);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
// Standard tail for stderr/stdout
|
|
247
|
+
const { execSync } = require('child_process');
|
|
248
|
+
try {
|
|
249
|
+
const output = execSync(`tail -n ${linesToShow} "${displayPath}"`, { encoding: 'utf-8' });
|
|
250
|
+
process.stdout.write(output);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
throw new Error(`Failed to read log file: ${(error as Error).message}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import blessed from 'blessed';
|
|
3
|
+
import { stateManager } from '../lib/state-manager.js';
|
|
4
|
+
import { statusChecker } from '../lib/status-checker.js';
|
|
5
|
+
import { createRootNavigator } from '../tui/RootNavigator.js';
|
|
6
|
+
|
|
7
|
+
export async function tuiCommand(): Promise<void> {
|
|
8
|
+
const servers = await stateManager.getAllServers();
|
|
9
|
+
|
|
10
|
+
if (servers.length === 0) {
|
|
11
|
+
console.log(chalk.yellow('No servers configured.'));
|
|
12
|
+
console.log(chalk.dim('\nCreate a server: llamacpp server create <model-filename>'));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const serversWithStatus = await statusChecker.updateAllServerStatuses();
|
|
17
|
+
|
|
18
|
+
const screen = blessed.screen({
|
|
19
|
+
smartCSR: true,
|
|
20
|
+
title: 'llama.cpp Server Monitor',
|
|
21
|
+
fullUnicode: true,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await createRootNavigator(screen, serversWithStatus);
|
|
25
|
+
}
|