lightman-agent 1.0.18 → 1.0.21

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 (52) hide show
  1. package/agent.config.json +22 -23
  2. package/agent.config.template.json +30 -31
  3. package/bin/cms-agent.js +269 -248
  4. package/package.json +1 -1
  5. package/public/assets/index-CcBNCz6h.css +1 -1
  6. package/public/assets/index-D9QHMG8k.js +1 -1
  7. package/public/assets/index-H-8HDl46.js +1 -1
  8. package/public/assets/index-YodeiCia.css +1 -1
  9. package/public/assets/index-legacy-DWtNM8y7.js +41 -41
  10. package/public/assets/polyfills-legacy-DyVYWHbW.js +4 -4
  11. package/scripts/guardian.ps1 +50 -124
  12. package/scripts/install-windows.ps1 +60 -116
  13. package/scripts/lightman-agent.logrotate +12 -12
  14. package/scripts/lightman-agent.service +38 -38
  15. package/scripts/reinstall-windows.ps1 +26 -26
  16. package/scripts/restore-desktop.ps1 +32 -32
  17. package/scripts/setup.ps1 +17 -22
  18. package/scripts/sync-display.mjs +20 -20
  19. package/scripts/uninstall-windows.ps1 +54 -54
  20. package/src/commands/display.ts +177 -177
  21. package/src/commands/kiosk.ts +113 -113
  22. package/src/commands/maintenance.ts +106 -106
  23. package/src/commands/network.ts +129 -129
  24. package/src/commands/power.ts +163 -163
  25. package/src/commands/rpi.ts +45 -45
  26. package/src/commands/screenshot.ts +166 -166
  27. package/src/commands/serial.ts +17 -17
  28. package/src/commands/update.ts +124 -124
  29. package/src/index.ts +173 -90
  30. package/src/lib/config.ts +2 -3
  31. package/src/lib/identity.ts +40 -40
  32. package/src/lib/logger.ts +137 -137
  33. package/src/lib/platform.ts +10 -10
  34. package/src/lib/rpi.ts +180 -180
  35. package/src/lib/screenMap.ts +135 -0
  36. package/src/lib/screens.ts +128 -128
  37. package/src/lib/types.ts +176 -177
  38. package/src/services/commands.ts +107 -107
  39. package/src/services/health.ts +161 -161
  40. package/src/services/localEvents.ts +60 -60
  41. package/src/services/logForwarder.ts +72 -72
  42. package/src/services/multiScreenKiosk.ts +116 -83
  43. package/src/services/oscBridge.ts +186 -186
  44. package/src/services/powerScheduler.ts +260 -260
  45. package/src/services/provisioning.ts +120 -122
  46. package/src/services/serialBridge.ts +230 -230
  47. package/src/services/serviceLauncher.ts +183 -183
  48. package/src/services/staticServer.ts +226 -226
  49. package/src/services/updater.ts +249 -249
  50. package/src/services/watchdog.ts +310 -310
  51. package/src/services/websocket.ts +152 -152
  52. package/tsconfig.json +28 -28
@@ -1,161 +1,161 @@
1
- import si from 'systeminformation';
2
- import net from 'net';
3
- import { URL } from 'url';
4
- import type { HealthReport, WsMessage } from '../lib/types.js';
5
- import type { WsClient } from './websocket.js';
6
- import type { Logger } from '../lib/logger.js';
7
- import { readFileSync } from 'fs';
8
- import { resolve } from 'path';
9
- import { isRaspberryPi, getGpuTemp, getThrottled, isSdCardReadOnly } from '../lib/rpi.js';
10
-
11
- export class HealthMonitor {
12
- private wsClient: WsClient;
13
- private logger: Logger;
14
- private intervalMs: number;
15
- private timer: NodeJS.Timeout | null = null;
16
- private agentVersion: string;
17
- private serverUrl: string;
18
-
19
- constructor(wsClient: WsClient, logger: Logger, intervalMs: number, serverUrl?: string) {
20
- this.wsClient = wsClient;
21
- this.logger = logger;
22
- this.intervalMs = intervalMs;
23
- this.serverUrl = serverUrl || '';
24
-
25
- // Read version from package.json
26
- try {
27
- const pkg = JSON.parse(
28
- readFileSync(resolve(process.cwd(), 'package.json'), 'utf-8')
29
- );
30
- this.agentVersion = pkg.version || '0.0.0';
31
- } catch {
32
- this.agentVersion = '0.0.0';
33
- }
34
- }
35
-
36
- start(): void {
37
- this.logger.info(`Health monitor started (interval: ${this.intervalMs}ms)`);
38
-
39
- // Send initial report after a short delay
40
- setTimeout(() => {
41
- this.collectAndSend();
42
- }, 5_000);
43
-
44
- this.timer = setInterval(() => {
45
- this.collectAndSend();
46
- }, this.intervalMs);
47
- }
48
-
49
- stop(): void {
50
- if (this.timer) {
51
- clearInterval(this.timer);
52
- this.timer = null;
53
- }
54
- this.logger.info('Health monitor stopped');
55
- }
56
-
57
- async collect(): Promise<HealthReport> {
58
- const [cpu, mem, disk, temp] = await Promise.all([
59
- si.currentLoad(),
60
- si.mem(),
61
- si.fsSize(),
62
- si.cpuTemperature(),
63
- ]);
64
-
65
- // Use the first/main disk
66
- const mainDisk = disk[0] || { size: 0, used: 0, use: 0 };
67
-
68
- const report: HealthReport = {
69
- cpuUsage: Math.round(cpu.currentLoad * 100) / 100,
70
- memTotal: mem.total,
71
- memUsed: mem.used,
72
- memPercent: mem.total > 0 ? Math.round((mem.used / mem.total) * 10000) / 100 : 0,
73
- diskTotal: mainDisk.size,
74
- diskUsed: mainDisk.used,
75
- diskPercent: Math.round(mainDisk.use * 100) / 100,
76
- cpuTemp: temp.main !== null ? Math.round(temp.main * 10) / 10 : null,
77
- uptime: Math.round(process.uptime()),
78
- agentVersion: this.agentVersion,
79
- };
80
-
81
- // Add RPi-specific fields when running on Raspberry Pi
82
- if (isRaspberryPi()) {
83
- report.gpuTemp = getGpuTemp();
84
- report.throttled = getThrottled();
85
- report.sdCardReadOnly = isSdCardReadOnly();
86
- }
87
-
88
- // Add network info
89
- try {
90
- const ifaces = await si.networkInterfaces();
91
- const ifaceList = Array.isArray(ifaces) ? ifaces : [ifaces];
92
- const primary = ifaceList.find((i) => !i.internal && i.ip4) || null;
93
-
94
- if (primary) {
95
- let serverLatencyMs: number | null = null;
96
- if (this.serverUrl) {
97
- try {
98
- const url = new URL(this.serverUrl);
99
- const host = url.hostname;
100
- const port = parseInt(url.port, 10) || 3001;
101
- const start = Date.now();
102
- const reachable = await this.tcpPing(host, port, 5000);
103
- serverLatencyMs = reachable ? Date.now() - start : null;
104
- } catch {
105
- // Ignore ping errors in health collection
106
- }
107
- }
108
-
109
- report.network = {
110
- interface: primary.iface,
111
- ip: primary.ip4,
112
- mac: primary.mac,
113
- serverLatencyMs,
114
- };
115
- }
116
- } catch {
117
- // Network info is optional, don't fail health collection
118
- }
119
-
120
- return report;
121
- }
122
-
123
- private tcpPing(host: string, port: number, timeoutMs: number): Promise<boolean> {
124
- return new Promise((resolve) => {
125
- const socket = new net.Socket();
126
- let resolved = false;
127
-
128
- const done = (result: boolean) => {
129
- if (resolved) return;
130
- resolved = true;
131
- socket.destroy();
132
- resolve(result);
133
- };
134
-
135
- socket.setTimeout(timeoutMs);
136
- socket.on('connect', () => done(true));
137
- socket.on('timeout', () => done(false));
138
- socket.on('error', () => done(false));
139
- socket.connect(port, host);
140
- });
141
- }
142
-
143
- private async collectAndSend(): Promise<void> {
144
- try {
145
- const report = await this.collect();
146
- const msg: WsMessage = {
147
- type: 'agent:health',
148
- payload: report as unknown as Record<string, unknown>,
149
- timestamp: Date.now(),
150
- };
151
- this.wsClient.send(msg);
152
- this.logger.debug('Health report sent', {
153
- cpu: report.cpuUsage,
154
- mem: report.memPercent,
155
- disk: report.diskPercent,
156
- });
157
- } catch (err) {
158
- this.logger.error('Failed to collect health data:', err);
159
- }
160
- }
161
- }
1
+ import si from 'systeminformation';
2
+ import net from 'net';
3
+ import { URL } from 'url';
4
+ import type { HealthReport, WsMessage } from '../lib/types.js';
5
+ import type { WsClient } from './websocket.js';
6
+ import type { Logger } from '../lib/logger.js';
7
+ import { readFileSync } from 'fs';
8
+ import { resolve } from 'path';
9
+ import { isRaspberryPi, getGpuTemp, getThrottled, isSdCardReadOnly } from '../lib/rpi.js';
10
+
11
+ export class HealthMonitor {
12
+ private wsClient: WsClient;
13
+ private logger: Logger;
14
+ private intervalMs: number;
15
+ private timer: NodeJS.Timeout | null = null;
16
+ private agentVersion: string;
17
+ private serverUrl: string;
18
+
19
+ constructor(wsClient: WsClient, logger: Logger, intervalMs: number, serverUrl?: string) {
20
+ this.wsClient = wsClient;
21
+ this.logger = logger;
22
+ this.intervalMs = intervalMs;
23
+ this.serverUrl = serverUrl || '';
24
+
25
+ // Read version from package.json
26
+ try {
27
+ const pkg = JSON.parse(
28
+ readFileSync(resolve(process.cwd(), 'package.json'), 'utf-8')
29
+ );
30
+ this.agentVersion = pkg.version || '0.0.0';
31
+ } catch {
32
+ this.agentVersion = '0.0.0';
33
+ }
34
+ }
35
+
36
+ start(): void {
37
+ this.logger.info(`Health monitor started (interval: ${this.intervalMs}ms)`);
38
+
39
+ // Send initial report after a short delay
40
+ setTimeout(() => {
41
+ this.collectAndSend();
42
+ }, 5_000);
43
+
44
+ this.timer = setInterval(() => {
45
+ this.collectAndSend();
46
+ }, this.intervalMs);
47
+ }
48
+
49
+ stop(): void {
50
+ if (this.timer) {
51
+ clearInterval(this.timer);
52
+ this.timer = null;
53
+ }
54
+ this.logger.info('Health monitor stopped');
55
+ }
56
+
57
+ async collect(): Promise<HealthReport> {
58
+ const [cpu, mem, disk, temp] = await Promise.all([
59
+ si.currentLoad(),
60
+ si.mem(),
61
+ si.fsSize(),
62
+ si.cpuTemperature(),
63
+ ]);
64
+
65
+ // Use the first/main disk
66
+ const mainDisk = disk[0] || { size: 0, used: 0, use: 0 };
67
+
68
+ const report: HealthReport = {
69
+ cpuUsage: Math.round(cpu.currentLoad * 100) / 100,
70
+ memTotal: mem.total,
71
+ memUsed: mem.used,
72
+ memPercent: mem.total > 0 ? Math.round((mem.used / mem.total) * 10000) / 100 : 0,
73
+ diskTotal: mainDisk.size,
74
+ diskUsed: mainDisk.used,
75
+ diskPercent: Math.round(mainDisk.use * 100) / 100,
76
+ cpuTemp: temp.main !== null ? Math.round(temp.main * 10) / 10 : null,
77
+ uptime: Math.round(process.uptime()),
78
+ agentVersion: this.agentVersion,
79
+ };
80
+
81
+ // Add RPi-specific fields when running on Raspberry Pi
82
+ if (isRaspberryPi()) {
83
+ report.gpuTemp = getGpuTemp();
84
+ report.throttled = getThrottled();
85
+ report.sdCardReadOnly = isSdCardReadOnly();
86
+ }
87
+
88
+ // Add network info
89
+ try {
90
+ const ifaces = await si.networkInterfaces();
91
+ const ifaceList = Array.isArray(ifaces) ? ifaces : [ifaces];
92
+ const primary = ifaceList.find((i) => !i.internal && i.ip4) || null;
93
+
94
+ if (primary) {
95
+ let serverLatencyMs: number | null = null;
96
+ if (this.serverUrl) {
97
+ try {
98
+ const url = new URL(this.serverUrl);
99
+ const host = url.hostname;
100
+ const port = parseInt(url.port, 10) || 3001;
101
+ const start = Date.now();
102
+ const reachable = await this.tcpPing(host, port, 5000);
103
+ serverLatencyMs = reachable ? Date.now() - start : null;
104
+ } catch {
105
+ // Ignore ping errors in health collection
106
+ }
107
+ }
108
+
109
+ report.network = {
110
+ interface: primary.iface,
111
+ ip: primary.ip4,
112
+ mac: primary.mac,
113
+ serverLatencyMs,
114
+ };
115
+ }
116
+ } catch {
117
+ // Network info is optional, don't fail health collection
118
+ }
119
+
120
+ return report;
121
+ }
122
+
123
+ private tcpPing(host: string, port: number, timeoutMs: number): Promise<boolean> {
124
+ return new Promise((resolve) => {
125
+ const socket = new net.Socket();
126
+ let resolved = false;
127
+
128
+ const done = (result: boolean) => {
129
+ if (resolved) return;
130
+ resolved = true;
131
+ socket.destroy();
132
+ resolve(result);
133
+ };
134
+
135
+ socket.setTimeout(timeoutMs);
136
+ socket.on('connect', () => done(true));
137
+ socket.on('timeout', () => done(false));
138
+ socket.on('error', () => done(false));
139
+ socket.connect(port, host);
140
+ });
141
+ }
142
+
143
+ private async collectAndSend(): Promise<void> {
144
+ try {
145
+ const report = await this.collect();
146
+ const msg: WsMessage = {
147
+ type: 'agent:health',
148
+ payload: report as unknown as Record<string, unknown>,
149
+ timestamp: Date.now(),
150
+ };
151
+ this.wsClient.send(msg);
152
+ this.logger.debug('Health report sent', {
153
+ cpu: report.cpuUsage,
154
+ mem: report.memPercent,
155
+ disk: report.diskPercent,
156
+ });
157
+ } catch (err) {
158
+ this.logger.error('Failed to collect health data:', err);
159
+ }
160
+ }
161
+ }
@@ -1,60 +1,60 @@
1
- import { WebSocketServer, WebSocket } from 'ws';
2
- import type { Logger } from '../lib/logger.js';
3
-
4
- /**
5
- * Local hardware event broadcaster.
6
- * Runs a WebSocket server on localhost only so the local Chrome display
7
- * can receive hardware events directly from the agent — no server round-trip.
8
- */
9
- export class LocalEventServer {
10
- private wss: WebSocketServer | null = null;
11
- private clients: Set<WebSocket> = new Set();
12
- private port: number;
13
- private logger: Logger;
14
-
15
- constructor(port: number, logger: Logger) {
16
- this.port = port;
17
- this.logger = logger;
18
- }
19
-
20
- start(): void {
21
- this.wss = new WebSocketServer({ host: '127.0.0.1', port: this.port });
22
-
23
- this.wss.on('connection', (ws) => {
24
- this.clients.add(ws);
25
- this.logger.debug(`[LocalEvents] Display connected (total: ${this.clients.size})`);
26
-
27
- ws.on('close', () => {
28
- this.clients.delete(ws);
29
- this.logger.debug(`[LocalEvents] Display disconnected (total: ${this.clients.size})`);
30
- });
31
-
32
- ws.on('error', () => {
33
- this.clients.delete(ws);
34
- });
35
- });
36
-
37
- this.wss.on('error', (err) => {
38
- this.logger.error('[LocalEvents] Server error:', err);
39
- });
40
-
41
- this.logger.info(`[LocalEvents] Hardware event server listening on ws://127.0.0.1:${this.port}`);
42
- }
43
-
44
- broadcast(event: Record<string, unknown>): void {
45
- if (this.clients.size === 0) return;
46
- const msg = JSON.stringify(event);
47
- for (const ws of this.clients) {
48
- if (ws.readyState === WebSocket.OPEN) {
49
- ws.send(msg);
50
- }
51
- }
52
- this.logger.debug(`[LocalEvents] Broadcast to ${this.clients.size} client(s): ${event.type}`);
53
- }
54
-
55
- stop(): void {
56
- this.wss?.close();
57
- this.wss = null;
58
- this.clients.clear();
59
- }
60
- }
1
+ import { WebSocketServer, WebSocket } from 'ws';
2
+ import type { Logger } from '../lib/logger.js';
3
+
4
+ /**
5
+ * Local hardware event broadcaster.
6
+ * Runs a WebSocket server on localhost only so the local Chrome display
7
+ * can receive hardware events directly from the agent — no server round-trip.
8
+ */
9
+ export class LocalEventServer {
10
+ private wss: WebSocketServer | null = null;
11
+ private clients: Set<WebSocket> = new Set();
12
+ private port: number;
13
+ private logger: Logger;
14
+
15
+ constructor(port: number, logger: Logger) {
16
+ this.port = port;
17
+ this.logger = logger;
18
+ }
19
+
20
+ start(): void {
21
+ this.wss = new WebSocketServer({ host: '127.0.0.1', port: this.port });
22
+
23
+ this.wss.on('connection', (ws) => {
24
+ this.clients.add(ws);
25
+ this.logger.debug(`[LocalEvents] Display connected (total: ${this.clients.size})`);
26
+
27
+ ws.on('close', () => {
28
+ this.clients.delete(ws);
29
+ this.logger.debug(`[LocalEvents] Display disconnected (total: ${this.clients.size})`);
30
+ });
31
+
32
+ ws.on('error', () => {
33
+ this.clients.delete(ws);
34
+ });
35
+ });
36
+
37
+ this.wss.on('error', (err) => {
38
+ this.logger.error('[LocalEvents] Server error:', err);
39
+ });
40
+
41
+ this.logger.info(`[LocalEvents] Hardware event server listening on ws://127.0.0.1:${this.port}`);
42
+ }
43
+
44
+ broadcast(event: Record<string, unknown>): void {
45
+ if (this.clients.size === 0) return;
46
+ const msg = JSON.stringify(event);
47
+ for (const ws of this.clients) {
48
+ if (ws.readyState === WebSocket.OPEN) {
49
+ ws.send(msg);
50
+ }
51
+ }
52
+ this.logger.debug(`[LocalEvents] Broadcast to ${this.clients.size} client(s): ${event.type}`);
53
+ }
54
+
55
+ stop(): void {
56
+ this.wss?.close();
57
+ this.wss = null;
58
+ this.clients.clear();
59
+ }
60
+ }
@@ -1,72 +1,72 @@
1
- import type { WsClient } from './websocket.js';
2
- import type { Logger } from '../lib/logger.js';
3
- import type { LogEntry, WsMessage } from '../lib/types.js';
4
-
5
- interface LogForwarderConfig {
6
- batchIntervalMs: number;
7
- maxBatchSize: number;
8
- }
9
-
10
- const DEFAULT_CONFIG: LogForwarderConfig = {
11
- batchIntervalMs: 30_000,
12
- maxBatchSize: 100,
13
- };
14
-
15
- export class LogForwarder {
16
- private wsClient: WsClient;
17
- private logger: Logger;
18
- private config: LogForwarderConfig;
19
- private buffer: LogEntry[] = [];
20
- private timer: NodeJS.Timeout | null = null;
21
-
22
- constructor(wsClient: WsClient, logger: Logger, config?: Partial<LogForwarderConfig>) {
23
- this.wsClient = wsClient;
24
- this.logger = logger;
25
- this.config = { ...DEFAULT_CONFIG, ...config };
26
- }
27
-
28
- onLog(entry: LogEntry): void {
29
- this.buffer = [...this.buffer, entry];
30
- // Flush if buffer exceeds max batch size
31
- if (this.buffer.length >= this.config.maxBatchSize) {
32
- this.flush();
33
- }
34
- }
35
-
36
- start(): void {
37
- if (this.timer !== null) {
38
- this.logger.warn('Log forwarder already running, ignoring duplicate start()');
39
- return;
40
- }
41
- this.timer = setInterval(() => {
42
- this.flush();
43
- }, this.config.batchIntervalMs);
44
- this.logger.info(`Log forwarder started (interval: ${this.config.batchIntervalMs}ms, maxBatch: ${this.config.maxBatchSize})`);
45
- }
46
-
47
- stop(): void {
48
- if (this.timer) {
49
- clearInterval(this.timer);
50
- this.timer = null;
51
- }
52
- // Final flush
53
- this.flush();
54
- this.logger.info('Log forwarder stopped');
55
- }
56
-
57
- flush(): void {
58
- if (this.buffer.length === 0) {
59
- return;
60
- }
61
-
62
- const entries = this.buffer;
63
- this.buffer = [];
64
-
65
- const msg: WsMessage = {
66
- type: 'agent:logs',
67
- payload: { entries: entries as unknown as Record<string, unknown>[] } as unknown as Record<string, unknown>,
68
- timestamp: Date.now(),
69
- };
70
- this.wsClient.send(msg);
71
- }
72
- }
1
+ import type { WsClient } from './websocket.js';
2
+ import type { Logger } from '../lib/logger.js';
3
+ import type { LogEntry, WsMessage } from '../lib/types.js';
4
+
5
+ interface LogForwarderConfig {
6
+ batchIntervalMs: number;
7
+ maxBatchSize: number;
8
+ }
9
+
10
+ const DEFAULT_CONFIG: LogForwarderConfig = {
11
+ batchIntervalMs: 30_000,
12
+ maxBatchSize: 100,
13
+ };
14
+
15
+ export class LogForwarder {
16
+ private wsClient: WsClient;
17
+ private logger: Logger;
18
+ private config: LogForwarderConfig;
19
+ private buffer: LogEntry[] = [];
20
+ private timer: NodeJS.Timeout | null = null;
21
+
22
+ constructor(wsClient: WsClient, logger: Logger, config?: Partial<LogForwarderConfig>) {
23
+ this.wsClient = wsClient;
24
+ this.logger = logger;
25
+ this.config = { ...DEFAULT_CONFIG, ...config };
26
+ }
27
+
28
+ onLog(entry: LogEntry): void {
29
+ this.buffer = [...this.buffer, entry];
30
+ // Flush if buffer exceeds max batch size
31
+ if (this.buffer.length >= this.config.maxBatchSize) {
32
+ this.flush();
33
+ }
34
+ }
35
+
36
+ start(): void {
37
+ if (this.timer !== null) {
38
+ this.logger.warn('Log forwarder already running, ignoring duplicate start()');
39
+ return;
40
+ }
41
+ this.timer = setInterval(() => {
42
+ this.flush();
43
+ }, this.config.batchIntervalMs);
44
+ this.logger.info(`Log forwarder started (interval: ${this.config.batchIntervalMs}ms, maxBatch: ${this.config.maxBatchSize})`);
45
+ }
46
+
47
+ stop(): void {
48
+ if (this.timer) {
49
+ clearInterval(this.timer);
50
+ this.timer = null;
51
+ }
52
+ // Final flush
53
+ this.flush();
54
+ this.logger.info('Log forwarder stopped');
55
+ }
56
+
57
+ flush(): void {
58
+ if (this.buffer.length === 0) {
59
+ return;
60
+ }
61
+
62
+ const entries = this.buffer;
63
+ this.buffer = [];
64
+
65
+ const msg: WsMessage = {
66
+ type: 'agent:logs',
67
+ payload: { entries: entries as unknown as Record<string, unknown>[] } as unknown as Record<string, unknown>,
68
+ timestamp: Date.now(),
69
+ };
70
+ this.wsClient.send(msg);
71
+ }
72
+ }