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,249 +1,249 @@
1
- import { createWriteStream, createReadStream, existsSync, mkdirSync, renameSync, rmSync, readdirSync, statSync } from 'fs';
2
- import { resolve, join } from 'path';
3
- import { createHash } from 'crypto';
4
- import { pipeline } from 'stream/promises';
5
- import http from 'http';
6
- import https from 'https';
7
- import { execFileSync } from 'child_process';
8
- import type { Logger } from '../lib/logger.js';
9
-
10
- interface UpdatePaths {
11
- current: string; // /opt/lightman/agent/
12
- staging: string; // /opt/lightman/agent-staging/
13
- backup: string; // /opt/lightman/agent-backup/
14
- downloads: string; // /opt/lightman/agent-downloads/
15
- }
16
-
17
- interface UpdateStatus {
18
- phase: 'idle' | 'downloading' | 'verifying' | 'installing' | 'restarting' | 'error';
19
- version?: string;
20
- progress?: number;
21
- error?: string;
22
- }
23
-
24
- export class Updater {
25
- private readonly logger: Logger;
26
- private readonly paths: UpdatePaths;
27
- private status: UpdateStatus = { phase: 'idle' };
28
-
29
- constructor(logger: Logger, basePath?: string) {
30
- this.logger = logger;
31
- const base = basePath || '/opt/lightman';
32
- this.paths = {
33
- current: resolve(base, 'agent'),
34
- staging: resolve(base, 'agent-staging'),
35
- backup: resolve(base, 'agent-backup'),
36
- downloads: resolve(base, 'agent-downloads'),
37
- };
38
- }
39
-
40
- getStatus(): UpdateStatus {
41
- return { ...this.status };
42
- }
43
-
44
- /**
45
- * Returns true if the updater is in the middle of an install or download.
46
- */
47
- isBusy(): boolean {
48
- return this.status.phase === 'downloading' ||
49
- this.status.phase === 'verifying' ||
50
- this.status.phase === 'installing';
51
- }
52
-
53
- /**
54
- * Download a file from URL to a local temp path.
55
- * Returns the local file path.
56
- */
57
- async download(url: string): Promise<string> {
58
- // Validate URL protocol
59
- const parsed = new URL(url);
60
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
61
- throw new Error('Only http and https URLs are supported');
62
- }
63
-
64
- this.status = { ...this.status, phase: 'downloading' };
65
- this.logger.info(`Downloading update from: ${url}`);
66
-
67
- // Ensure downloads dir exists
68
- if (!existsSync(this.paths.downloads)) {
69
- mkdirSync(this.paths.downloads, { recursive: true });
70
- }
71
-
72
- const filename = `update-${Date.now()}.tar.gz`;
73
- const filePath = join(this.paths.downloads, filename);
74
-
75
- return new Promise<string>((resolvePromise, reject) => {
76
- const client = parsed.protocol === 'https:' ? https : http;
77
- const req = client.get(url, (res) => {
78
- if (res.statusCode !== 200) {
79
- reject(new Error(`Download failed with status ${res.statusCode}`));
80
- return;
81
- }
82
-
83
- const ws = createWriteStream(filePath);
84
- pipeline(res, ws)
85
- .then(() => {
86
- this.logger.info(`Download complete: ${filePath}`);
87
- resolvePromise(filePath);
88
- })
89
- .catch(reject);
90
- });
91
-
92
- req.on('error', reject);
93
- req.setTimeout(5 * 60 * 1000, () => {
94
- req.destroy(new Error('Download timeout (5 minutes)'));
95
- });
96
- });
97
- }
98
-
99
- /**
100
- * Verify SHA256 checksum of a file.
101
- */
102
- async verify(filePath: string, expectedChecksum: string): Promise<boolean> {
103
- this.status = { ...this.status, phase: 'verifying' };
104
- this.logger.info(`Verifying checksum for: ${filePath}`);
105
-
106
- const hash = createHash('sha256');
107
- const stream = createReadStream(filePath);
108
-
109
- await pipeline(stream, hash);
110
- const actual = hash.digest('hex');
111
-
112
- const match = actual === expectedChecksum.toLowerCase();
113
- if (!match) {
114
- this.logger.error(`Checksum mismatch: expected ${expectedChecksum}, got ${actual}`);
115
- } else {
116
- this.logger.info('Checksum verified OK');
117
- }
118
- return match;
119
- }
120
-
121
- /**
122
- * Install update: extract tarball to staging, swap current -> backup, staging -> current, restart.
123
- */
124
- async install(tarballPath: string, version: string): Promise<void> {
125
- this.status = { phase: 'installing', version };
126
- this.logger.info(`Installing update v${version}...`);
127
-
128
- // Clean staging dir
129
- if (existsSync(this.paths.staging)) {
130
- rmSync(this.paths.staging, { recursive: true, force: true });
131
- }
132
- mkdirSync(this.paths.staging, { recursive: true });
133
-
134
- // Extract tarball to staging
135
- try {
136
- execFileSync('tar', ['-xzf', tarballPath, '-C', this.paths.staging], {
137
- timeout: 60_000,
138
- });
139
- } catch (err) {
140
- this.status = { phase: 'error', version, error: 'Extraction failed' };
141
- // Clean up staging directory on extraction failure
142
- try {
143
- if (existsSync(this.paths.staging)) {
144
- rmSync(this.paths.staging, { recursive: true, force: true });
145
- }
146
- } catch {
147
- // Non-critical cleanup error
148
- }
149
- throw new Error(`Failed to extract tarball: ${err instanceof Error ? err.message : String(err)}`);
150
- }
151
-
152
- // Atomic swap: current -> backup, staging -> current
153
- try {
154
- // Remove old backup if it exists
155
- if (existsSync(this.paths.backup)) {
156
- rmSync(this.paths.backup, { recursive: true, force: true });
157
- }
158
-
159
- // Move current -> backup (preserve for rollback)
160
- if (existsSync(this.paths.current)) {
161
- renameSync(this.paths.current, this.paths.backup);
162
- }
163
-
164
- // Move staging -> current
165
- renameSync(this.paths.staging, this.paths.current);
166
-
167
- this.logger.info(`Update v${version} installed successfully`);
168
- } catch (err) {
169
- this.status = { phase: 'error', version, error: 'Swap failed' };
170
- // Attempt recovery: if backup exists and current doesn't, restore backup
171
- if (!existsSync(this.paths.current) && existsSync(this.paths.backup)) {
172
- try {
173
- renameSync(this.paths.backup, this.paths.current);
174
- this.logger.warn('Recovered from failed swap using backup');
175
- } catch {
176
- this.logger.error('CRITICAL: Failed to recover from swap failure');
177
- }
178
- }
179
- throw new Error(`Install swap failed: ${err instanceof Error ? err.message : String(err)}`);
180
- }
181
-
182
- // Clean up downloaded tarball
183
- try {
184
- rmSync(tarballPath, { force: true });
185
- } catch {
186
- // Non-critical
187
- }
188
-
189
- this.status = { phase: 'restarting', version };
190
- }
191
-
192
- /**
193
- * Rollback to backup version.
194
- */
195
- async rollback(): Promise<void> {
196
- this.logger.info('Rolling back to backup version...');
197
-
198
- if (!existsSync(this.paths.backup)) {
199
- throw new Error('No backup version available for rollback');
200
- }
201
-
202
- // Move current -> staging (temporary)
203
- if (existsSync(this.paths.staging)) {
204
- rmSync(this.paths.staging, { recursive: true, force: true });
205
- }
206
- if (existsSync(this.paths.current)) {
207
- renameSync(this.paths.current, this.paths.staging);
208
- }
209
-
210
- // Move backup -> current
211
- renameSync(this.paths.backup, this.paths.current);
212
-
213
- // Clean up old current (now in staging)
214
- if (existsSync(this.paths.staging)) {
215
- rmSync(this.paths.staging, { recursive: true, force: true });
216
- }
217
-
218
- this.logger.info('Rollback complete');
219
- }
220
-
221
- /**
222
- * Clean old downloads, keeping only the most recent 3.
223
- */
224
- cleanDownloads(): void {
225
- try {
226
- if (!existsSync(this.paths.downloads)) return;
227
-
228
- const files = readdirSync(this.paths.downloads)
229
- .filter(f => f.endsWith('.tar.gz'))
230
- .map(f => ({
231
- name: f,
232
- path: join(this.paths.downloads, f),
233
- mtime: statSync(join(this.paths.downloads, f)).mtimeMs,
234
- }))
235
- .sort((a, b) => b.mtime - a.mtime);
236
-
237
- // Keep only 3 most recent
238
- for (const file of files.slice(3)) {
239
- try {
240
- rmSync(file.path, { force: true });
241
- } catch {
242
- // Ignore cleanup errors
243
- }
244
- }
245
- } catch {
246
- // Non-critical
247
- }
248
- }
249
- }
1
+ import { createWriteStream, createReadStream, existsSync, mkdirSync, renameSync, rmSync, readdirSync, statSync } from 'fs';
2
+ import { resolve, join } from 'path';
3
+ import { createHash } from 'crypto';
4
+ import { pipeline } from 'stream/promises';
5
+ import http from 'http';
6
+ import https from 'https';
7
+ import { execFileSync } from 'child_process';
8
+ import type { Logger } from '../lib/logger.js';
9
+
10
+ interface UpdatePaths {
11
+ current: string; // /opt/lightman/agent/
12
+ staging: string; // /opt/lightman/agent-staging/
13
+ backup: string; // /opt/lightman/agent-backup/
14
+ downloads: string; // /opt/lightman/agent-downloads/
15
+ }
16
+
17
+ interface UpdateStatus {
18
+ phase: 'idle' | 'downloading' | 'verifying' | 'installing' | 'restarting' | 'error';
19
+ version?: string;
20
+ progress?: number;
21
+ error?: string;
22
+ }
23
+
24
+ export class Updater {
25
+ private readonly logger: Logger;
26
+ private readonly paths: UpdatePaths;
27
+ private status: UpdateStatus = { phase: 'idle' };
28
+
29
+ constructor(logger: Logger, basePath?: string) {
30
+ this.logger = logger;
31
+ const base = basePath || '/opt/lightman';
32
+ this.paths = {
33
+ current: resolve(base, 'agent'),
34
+ staging: resolve(base, 'agent-staging'),
35
+ backup: resolve(base, 'agent-backup'),
36
+ downloads: resolve(base, 'agent-downloads'),
37
+ };
38
+ }
39
+
40
+ getStatus(): UpdateStatus {
41
+ return { ...this.status };
42
+ }
43
+
44
+ /**
45
+ * Returns true if the updater is in the middle of an install or download.
46
+ */
47
+ isBusy(): boolean {
48
+ return this.status.phase === 'downloading' ||
49
+ this.status.phase === 'verifying' ||
50
+ this.status.phase === 'installing';
51
+ }
52
+
53
+ /**
54
+ * Download a file from URL to a local temp path.
55
+ * Returns the local file path.
56
+ */
57
+ async download(url: string): Promise<string> {
58
+ // Validate URL protocol
59
+ const parsed = new URL(url);
60
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
61
+ throw new Error('Only http and https URLs are supported');
62
+ }
63
+
64
+ this.status = { ...this.status, phase: 'downloading' };
65
+ this.logger.info(`Downloading update from: ${url}`);
66
+
67
+ // Ensure downloads dir exists
68
+ if (!existsSync(this.paths.downloads)) {
69
+ mkdirSync(this.paths.downloads, { recursive: true });
70
+ }
71
+
72
+ const filename = `update-${Date.now()}.tar.gz`;
73
+ const filePath = join(this.paths.downloads, filename);
74
+
75
+ return new Promise<string>((resolvePromise, reject) => {
76
+ const client = parsed.protocol === 'https:' ? https : http;
77
+ const req = client.get(url, (res) => {
78
+ if (res.statusCode !== 200) {
79
+ reject(new Error(`Download failed with status ${res.statusCode}`));
80
+ return;
81
+ }
82
+
83
+ const ws = createWriteStream(filePath);
84
+ pipeline(res, ws)
85
+ .then(() => {
86
+ this.logger.info(`Download complete: ${filePath}`);
87
+ resolvePromise(filePath);
88
+ })
89
+ .catch(reject);
90
+ });
91
+
92
+ req.on('error', reject);
93
+ req.setTimeout(5 * 60 * 1000, () => {
94
+ req.destroy(new Error('Download timeout (5 minutes)'));
95
+ });
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Verify SHA256 checksum of a file.
101
+ */
102
+ async verify(filePath: string, expectedChecksum: string): Promise<boolean> {
103
+ this.status = { ...this.status, phase: 'verifying' };
104
+ this.logger.info(`Verifying checksum for: ${filePath}`);
105
+
106
+ const hash = createHash('sha256');
107
+ const stream = createReadStream(filePath);
108
+
109
+ await pipeline(stream, hash);
110
+ const actual = hash.digest('hex');
111
+
112
+ const match = actual === expectedChecksum.toLowerCase();
113
+ if (!match) {
114
+ this.logger.error(`Checksum mismatch: expected ${expectedChecksum}, got ${actual}`);
115
+ } else {
116
+ this.logger.info('Checksum verified OK');
117
+ }
118
+ return match;
119
+ }
120
+
121
+ /**
122
+ * Install update: extract tarball to staging, swap current -> backup, staging -> current, restart.
123
+ */
124
+ async install(tarballPath: string, version: string): Promise<void> {
125
+ this.status = { phase: 'installing', version };
126
+ this.logger.info(`Installing update v${version}...`);
127
+
128
+ // Clean staging dir
129
+ if (existsSync(this.paths.staging)) {
130
+ rmSync(this.paths.staging, { recursive: true, force: true });
131
+ }
132
+ mkdirSync(this.paths.staging, { recursive: true });
133
+
134
+ // Extract tarball to staging
135
+ try {
136
+ execFileSync('tar', ['-xzf', tarballPath, '-C', this.paths.staging], {
137
+ timeout: 60_000,
138
+ });
139
+ } catch (err) {
140
+ this.status = { phase: 'error', version, error: 'Extraction failed' };
141
+ // Clean up staging directory on extraction failure
142
+ try {
143
+ if (existsSync(this.paths.staging)) {
144
+ rmSync(this.paths.staging, { recursive: true, force: true });
145
+ }
146
+ } catch {
147
+ // Non-critical cleanup error
148
+ }
149
+ throw new Error(`Failed to extract tarball: ${err instanceof Error ? err.message : String(err)}`);
150
+ }
151
+
152
+ // Atomic swap: current -> backup, staging -> current
153
+ try {
154
+ // Remove old backup if it exists
155
+ if (existsSync(this.paths.backup)) {
156
+ rmSync(this.paths.backup, { recursive: true, force: true });
157
+ }
158
+
159
+ // Move current -> backup (preserve for rollback)
160
+ if (existsSync(this.paths.current)) {
161
+ renameSync(this.paths.current, this.paths.backup);
162
+ }
163
+
164
+ // Move staging -> current
165
+ renameSync(this.paths.staging, this.paths.current);
166
+
167
+ this.logger.info(`Update v${version} installed successfully`);
168
+ } catch (err) {
169
+ this.status = { phase: 'error', version, error: 'Swap failed' };
170
+ // Attempt recovery: if backup exists and current doesn't, restore backup
171
+ if (!existsSync(this.paths.current) && existsSync(this.paths.backup)) {
172
+ try {
173
+ renameSync(this.paths.backup, this.paths.current);
174
+ this.logger.warn('Recovered from failed swap using backup');
175
+ } catch {
176
+ this.logger.error('CRITICAL: Failed to recover from swap failure');
177
+ }
178
+ }
179
+ throw new Error(`Install swap failed: ${err instanceof Error ? err.message : String(err)}`);
180
+ }
181
+
182
+ // Clean up downloaded tarball
183
+ try {
184
+ rmSync(tarballPath, { force: true });
185
+ } catch {
186
+ // Non-critical
187
+ }
188
+
189
+ this.status = { phase: 'restarting', version };
190
+ }
191
+
192
+ /**
193
+ * Rollback to backup version.
194
+ */
195
+ async rollback(): Promise<void> {
196
+ this.logger.info('Rolling back to backup version...');
197
+
198
+ if (!existsSync(this.paths.backup)) {
199
+ throw new Error('No backup version available for rollback');
200
+ }
201
+
202
+ // Move current -> staging (temporary)
203
+ if (existsSync(this.paths.staging)) {
204
+ rmSync(this.paths.staging, { recursive: true, force: true });
205
+ }
206
+ if (existsSync(this.paths.current)) {
207
+ renameSync(this.paths.current, this.paths.staging);
208
+ }
209
+
210
+ // Move backup -> current
211
+ renameSync(this.paths.backup, this.paths.current);
212
+
213
+ // Clean up old current (now in staging)
214
+ if (existsSync(this.paths.staging)) {
215
+ rmSync(this.paths.staging, { recursive: true, force: true });
216
+ }
217
+
218
+ this.logger.info('Rollback complete');
219
+ }
220
+
221
+ /**
222
+ * Clean old downloads, keeping only the most recent 3.
223
+ */
224
+ cleanDownloads(): void {
225
+ try {
226
+ if (!existsSync(this.paths.downloads)) return;
227
+
228
+ const files = readdirSync(this.paths.downloads)
229
+ .filter(f => f.endsWith('.tar.gz'))
230
+ .map(f => ({
231
+ name: f,
232
+ path: join(this.paths.downloads, f),
233
+ mtime: statSync(join(this.paths.downloads, f)).mtimeMs,
234
+ }))
235
+ .sort((a, b) => b.mtime - a.mtime);
236
+
237
+ // Keep only 3 most recent
238
+ for (const file of files.slice(3)) {
239
+ try {
240
+ rmSync(file.path, { force: true });
241
+ } catch {
242
+ // Ignore cleanup errors
243
+ }
244
+ }
245
+ } catch {
246
+ // Non-critical
247
+ }
248
+ }
249
+ }