lightman-agent 1.0.5 → 1.0.6
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/agent.config.template.json +30 -30
- package/package.json +52 -52
- package/public/assets/index-CcBNCz6h.css +1 -1
- package/public/assets/index-D9QHMG8k.js +1 -0
- package/public/assets/index-H-8HDl46.js +1 -1
- package/public/assets/index-YodeiCia.css +1 -0
- package/public/assets/index-legacy-DWtNM8y7.js +41 -0
- package/public/assets/museum-map-CwVDA2z1.svg +4182 -0
- package/public/assets/polyfills-legacy-DyVYWHbW.js +4 -0
- package/public/index.html +7 -2
- package/public/templates/custom08/elements/back-button.svg +20 -0
- package/public/templates/custom08/elements/base-map-background.svg +37 -0
- package/public/templates/custom08/elements/base-map.svg +1191 -0
- package/public/templates/custom08/elements/gallery-1-2-3-info-panel.svg +236 -0
- package/public/templates/custom08/elements/gallery-4-5-6-7-info-panel.svg +266 -0
- package/public/templates/custom08/elements/gallery-8-9-info-panel.svg +274 -0
- package/public/templates/custom08/elements/gallery-labels/_nav-map-styles.css +554 -0
- package/public/templates/custom08/elements/gallery-labels/_styles.css +556 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-1.svg +35 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-2.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-3.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-4.svg +37 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-5.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-6.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-7.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-8.svg +37 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-9.svg +34 -0
- package/public/templates/custom08/elements/hand-hint.png +0 -0
- package/public/templates/custom08/elements/idle-screen-bg.svg +5 -0
- package/public/templates/custom08/elements/idle-screen-map.svg +627 -0
- package/public/templates/custom08/elements/idle-screen-text.svg +350 -0
- package/public/templates/custom08/elements/key-map-1.svg +986 -0
- package/public/templates/custom08/elements/key-map-2.svg +1018 -0
- package/public/templates/custom08/elements/key-map-3.svg +1019 -0
- package/public/templates/custom08/elements/key-map-combined.svg +1001 -0
- package/public/templates/custom08/elements/map-highlight-marker.svg +11 -0
- package/public/templates/custom08/elements/map-pin-marker.svg +15 -0
- package/public/templates/custom08/elements/map-teardrop-star-marker.svg +13 -0
- package/public/templates/custom08/elements/nav-circle-galleries-1-3.svg +21 -0
- package/public/templates/custom08/elements/nav-circle-galleries-4-7.svg +24 -0
- package/public/templates/custom08/elements/nav-circle-galleries-8-9.svg +20 -0
- package/public/templates/custom08/elements/section1-map.svg +1435 -0
- package/public/templates/custom08/elements/section2-map.svg +1724 -0
- package/public/templates/custom08/elements/section3-map.svg +1295 -0
- package/public/templates/custom08/fonts/CabinetGrotesk-Variable.ttf +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-05_at_7.23.12_PM.png +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-05_at_7.23.56_PM.png +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-05_at_7.24.24_PM.png +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.31.58_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.11_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.36_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.48_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.59_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.15_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.27_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.34_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.42_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.50_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.58_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.04_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.11_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.20_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.57_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.35.03_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.35.16_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.35.23_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/prologue-highlight.png +0 -0
- package/scripts/guardian.ps1 +75 -75
- package/scripts/install-linux.sh +134 -134
- package/scripts/install-rpi.sh +117 -117
- package/scripts/install-windows.ps1 +505 -505
- package/scripts/launch-kiosk.vbs +101 -101
- package/scripts/lightman-agent.logrotate +12 -12
- package/scripts/lightman-agent.service +38 -38
- package/scripts/lightman-shell.bat +107 -107
- package/scripts/reinstall-windows.ps1 +26 -26
- package/scripts/restore-desktop.ps1 +32 -32
- package/scripts/setup.ps1 +116 -116
- package/scripts/setup.sh +115 -115
- package/scripts/sync-display.mjs +20 -0
- package/scripts/uninstall-linux.sh +50 -50
- package/scripts/uninstall-windows.ps1 +54 -54
- package/src/commands/display.ts +177 -177
- package/src/commands/kiosk.ts +113 -113
- package/src/commands/maintenance.ts +106 -106
- package/src/commands/network.ts +129 -129
- package/src/commands/power.ts +163 -163
- package/src/commands/rpi.ts +45 -45
- package/src/commands/screenshot.ts +166 -166
- package/src/commands/serial.ts +17 -17
- package/src/commands/update.ts +124 -124
- package/src/index.ts +652 -652
- package/src/lib/config.ts +69 -69
- package/src/lib/identity.ts +40 -40
- package/src/lib/logger.ts +137 -137
- package/src/lib/platform.ts +10 -10
- package/src/lib/rpi.ts +180 -180
- package/src/lib/screens.ts +128 -128
- package/src/lib/types.ts +176 -176
- package/src/services/commands.ts +107 -107
- package/src/services/health.ts +161 -161
- package/src/services/kiosk.ts +384 -384
- package/src/services/localEvents.ts +60 -60
- package/src/services/logForwarder.ts +72 -72
- package/src/services/multiScreenKiosk.ts +324 -324
- package/src/services/oscBridge.ts +186 -186
- package/src/services/powerScheduler.ts +260 -260
- package/src/services/provisioning.ts +120 -120
- package/src/services/serialBridge.ts +230 -230
- package/src/services/serviceLauncher.ts +183 -183
- package/src/services/staticServer.ts +226 -226
- package/src/services/updater.ts +249 -249
- package/src/services/watchdog.ts +310 -310
- package/src/services/websocket.ts +152 -152
- package/tsconfig.json +28 -28
package/src/commands/rpi.ts
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
import type { CommandHandler } from '../lib/types.js';
|
|
2
|
-
import type { Logger } from '../lib/logger.js';
|
|
3
|
-
import { isRaspberryPi, getRpiInfo, getGpuTemp, getThrottled, isSdCardReadOnly, startWatchdog, stopWatchdog } from '../lib/rpi.js';
|
|
4
|
-
|
|
5
|
-
export function registerRpiCommands(
|
|
6
|
-
register: (command: string, handler: CommandHandler) => void,
|
|
7
|
-
logger: Logger
|
|
8
|
-
): void {
|
|
9
|
-
// rpi:info — returns model, serial, revision, gpuTemp, throttled, sdCardReadOnly
|
|
10
|
-
register('rpi:info', async () => {
|
|
11
|
-
if (!isRaspberryPi()) {
|
|
12
|
-
throw new Error('Not a Raspberry Pi');
|
|
13
|
-
}
|
|
14
|
-
const info = getRpiInfo();
|
|
15
|
-
return {
|
|
16
|
-
...info,
|
|
17
|
-
gpuTemp: getGpuTemp(),
|
|
18
|
-
throttled: getThrottled(),
|
|
19
|
-
sdCardReadOnly: isSdCardReadOnly(),
|
|
20
|
-
};
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// rpi:watchdog-start — start hardware watchdog
|
|
24
|
-
register('rpi:watchdog-start', async () => {
|
|
25
|
-
if (!isRaspberryPi()) {
|
|
26
|
-
throw new Error('Not a Raspberry Pi');
|
|
27
|
-
}
|
|
28
|
-
const started = startWatchdog();
|
|
29
|
-
if (!started) {
|
|
30
|
-
throw new Error('Watchdog device not available. Ensure /dev/watchdog exists and agent has permissions.');
|
|
31
|
-
}
|
|
32
|
-
logger.info('Hardware watchdog started');
|
|
33
|
-
return { started: true };
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// rpi:watchdog-stop — stop hardware watchdog gracefully
|
|
37
|
-
register('rpi:watchdog-stop', async () => {
|
|
38
|
-
if (!isRaspberryPi()) {
|
|
39
|
-
throw new Error('Not a Raspberry Pi');
|
|
40
|
-
}
|
|
41
|
-
stopWatchdog();
|
|
42
|
-
logger.info('Hardware watchdog stopped');
|
|
43
|
-
return { stopped: true };
|
|
44
|
-
});
|
|
45
|
-
}
|
|
1
|
+
import type { CommandHandler } from '../lib/types.js';
|
|
2
|
+
import type { Logger } from '../lib/logger.js';
|
|
3
|
+
import { isRaspberryPi, getRpiInfo, getGpuTemp, getThrottled, isSdCardReadOnly, startWatchdog, stopWatchdog } from '../lib/rpi.js';
|
|
4
|
+
|
|
5
|
+
export function registerRpiCommands(
|
|
6
|
+
register: (command: string, handler: CommandHandler) => void,
|
|
7
|
+
logger: Logger
|
|
8
|
+
): void {
|
|
9
|
+
// rpi:info — returns model, serial, revision, gpuTemp, throttled, sdCardReadOnly
|
|
10
|
+
register('rpi:info', async () => {
|
|
11
|
+
if (!isRaspberryPi()) {
|
|
12
|
+
throw new Error('Not a Raspberry Pi');
|
|
13
|
+
}
|
|
14
|
+
const info = getRpiInfo();
|
|
15
|
+
return {
|
|
16
|
+
...info,
|
|
17
|
+
gpuTemp: getGpuTemp(),
|
|
18
|
+
throttled: getThrottled(),
|
|
19
|
+
sdCardReadOnly: isSdCardReadOnly(),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// rpi:watchdog-start — start hardware watchdog
|
|
24
|
+
register('rpi:watchdog-start', async () => {
|
|
25
|
+
if (!isRaspberryPi()) {
|
|
26
|
+
throw new Error('Not a Raspberry Pi');
|
|
27
|
+
}
|
|
28
|
+
const started = startWatchdog();
|
|
29
|
+
if (!started) {
|
|
30
|
+
throw new Error('Watchdog device not available. Ensure /dev/watchdog exists and agent has permissions.');
|
|
31
|
+
}
|
|
32
|
+
logger.info('Hardware watchdog started');
|
|
33
|
+
return { started: true };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// rpi:watchdog-stop — stop hardware watchdog gracefully
|
|
37
|
+
register('rpi:watchdog-stop', async () => {
|
|
38
|
+
if (!isRaspberryPi()) {
|
|
39
|
+
throw new Error('Not a Raspberry Pi');
|
|
40
|
+
}
|
|
41
|
+
stopWatchdog();
|
|
42
|
+
logger.info('Hardware watchdog stopped');
|
|
43
|
+
return { stopped: true };
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -1,166 +1,166 @@
|
|
|
1
|
-
import { execFileSync } from 'child_process';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
import type { CommandHandler } from '../lib/types.js';
|
|
6
|
-
import type { Logger } from '../lib/logger.js';
|
|
7
|
-
import { getPlatform } from '../lib/platform.js';
|
|
8
|
-
|
|
9
|
-
const ALLOWED_CAPTURE_TOOLS = ['scrot', 'import', 'screencapture'] as const;
|
|
10
|
-
|
|
11
|
-
// --- Zod Schema ---
|
|
12
|
-
const ScreenshotArgsSchema = z.object({
|
|
13
|
-
serverUrl: z.string().url().optional(),
|
|
14
|
-
deviceId: z.string().uuid().optional(),
|
|
15
|
-
apiKey: z.string().min(1).optional(),
|
|
16
|
-
quality: z.number().int().min(1).max(100).optional(),
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
export function registerScreenshotCommands(
|
|
20
|
-
register: (command: string, handler: CommandHandler) => void,
|
|
21
|
-
logger: Logger
|
|
22
|
-
): void {
|
|
23
|
-
register('kiosk:screenshot', async (args) => {
|
|
24
|
-
const parsed = ScreenshotArgsSchema.safeParse(args ?? {});
|
|
25
|
-
if (!parsed.success) {
|
|
26
|
-
throw new Error(`Invalid screenshot args: ${parsed.error.issues.map((i) => i.message).join(', ')}`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const { serverUrl, deviceId, apiKey, quality: parsedQuality } = parsed.data;
|
|
30
|
-
const quality = parsedQuality ?? 75;
|
|
31
|
-
|
|
32
|
-
const tmpFile = `/tmp/lightman-screenshot-${Date.now()}.jpg`;
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
// --- Capture screenshot ---
|
|
36
|
-
const { tool, toolArgs } = resolveCapture(quality, tmpFile);
|
|
37
|
-
logger.info(`Capturing screenshot: ${tool} ${toolArgs.join(' ')}`);
|
|
38
|
-
|
|
39
|
-
execFileSync(tool, toolArgs, { timeout: 10_000, stdio: 'pipe' });
|
|
40
|
-
|
|
41
|
-
if (!fs.existsSync(tmpFile)) {
|
|
42
|
-
throw new Error('Screenshot file was not created');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const buffer = await fs.promises.readFile(tmpFile);
|
|
46
|
-
logger.info(`Screenshot captured: ${buffer.length} bytes`);
|
|
47
|
-
|
|
48
|
-
// --- Upload if server info provided ---
|
|
49
|
-
if (serverUrl && deviceId && apiKey) {
|
|
50
|
-
try {
|
|
51
|
-
const uploaded = await uploadScreenshot(
|
|
52
|
-
buffer,
|
|
53
|
-
tmpFile,
|
|
54
|
-
serverUrl,
|
|
55
|
-
deviceId,
|
|
56
|
-
apiKey,
|
|
57
|
-
logger
|
|
58
|
-
);
|
|
59
|
-
return { captured: true, size: buffer.length, uploaded };
|
|
60
|
-
} catch (uploadErr) {
|
|
61
|
-
const errMsg =
|
|
62
|
-
uploadErr instanceof Error ? uploadErr.message : String(uploadErr);
|
|
63
|
-
logger.error('Screenshot upload failed:', errMsg);
|
|
64
|
-
return { captured: true, size: buffer.length, uploaded: false, error: errMsg };
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return { captured: true, size: buffer.length, uploaded: false };
|
|
69
|
-
} catch (err) {
|
|
70
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
71
|
-
logger.error('Screenshot capture failed:', errMsg);
|
|
72
|
-
throw new Error(`Screenshot capture failed: ${errMsg}`);
|
|
73
|
-
} finally {
|
|
74
|
-
// Clean up temp file
|
|
75
|
-
try {
|
|
76
|
-
if (fs.existsSync(tmpFile)) {
|
|
77
|
-
await fs.promises.unlink(tmpFile);
|
|
78
|
-
}
|
|
79
|
-
} catch {
|
|
80
|
-
// Ignore cleanup errors
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Determine the capture tool and arguments based on platform.
|
|
88
|
-
* No user-controlled command strings — only allowlisted tools with safe args.
|
|
89
|
-
*/
|
|
90
|
-
function resolveCapture(
|
|
91
|
-
quality: number,
|
|
92
|
-
tmpFile: string
|
|
93
|
-
): { tool: string; toolArgs: string[] } {
|
|
94
|
-
const platform = getPlatform();
|
|
95
|
-
|
|
96
|
-
if (platform === 'linux') {
|
|
97
|
-
if (commandExists('scrot')) {
|
|
98
|
-
return { tool: 'scrot', toolArgs: ['-q', String(quality), tmpFile] };
|
|
99
|
-
}
|
|
100
|
-
if (commandExists('import')) {
|
|
101
|
-
return { tool: 'import', toolArgs: ['-window', 'root', '-quality', String(quality), tmpFile] };
|
|
102
|
-
}
|
|
103
|
-
throw new Error(
|
|
104
|
-
'No screenshot tool available. Install scrot or ImageMagick (import).'
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (platform === 'darwin') {
|
|
109
|
-
return { tool: 'screencapture', toolArgs: ['-x', '-t', 'jpg', tmpFile] };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
throw new Error(`Screenshot capture not supported on platform: ${platform}`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Check if a command exists on the system using execFileSync (no shell injection).
|
|
117
|
-
*/
|
|
118
|
-
function commandExists(cmd: string): boolean {
|
|
119
|
-
if (!ALLOWED_CAPTURE_TOOLS.includes(cmd as typeof ALLOWED_CAPTURE_TOOLS[number])) {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
try {
|
|
123
|
-
execFileSync('which', [cmd], { stdio: 'pipe' });
|
|
124
|
-
return true;
|
|
125
|
-
} catch {
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Upload screenshot to server via multipart/form-data.
|
|
132
|
-
*/
|
|
133
|
-
async function uploadScreenshot(
|
|
134
|
-
buffer: Buffer,
|
|
135
|
-
filePath: string,
|
|
136
|
-
serverUrl: string,
|
|
137
|
-
deviceId: string,
|
|
138
|
-
apiKey: string,
|
|
139
|
-
logger: Logger
|
|
140
|
-
): Promise<boolean> {
|
|
141
|
-
const endpoint = `/api/devices/${deviceId}/screenshot`;
|
|
142
|
-
const url = `${serverUrl}${endpoint}`;
|
|
143
|
-
const filename = path.basename(filePath);
|
|
144
|
-
|
|
145
|
-
logger.info(`Uploading screenshot to ${url}`);
|
|
146
|
-
|
|
147
|
-
const blob = new Blob([buffer], { type: 'image/jpeg' });
|
|
148
|
-
const formData = new FormData();
|
|
149
|
-
formData.append('screenshot', blob, filename);
|
|
150
|
-
|
|
151
|
-
const response = await fetch(url, {
|
|
152
|
-
method: 'POST',
|
|
153
|
-
headers: {
|
|
154
|
-
'x-api-key': apiKey,
|
|
155
|
-
},
|
|
156
|
-
body: formData,
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
if (!response.ok) {
|
|
160
|
-
const body = await response.text().catch(() => '');
|
|
161
|
-
throw new Error(`Upload failed: HTTP ${response.status} ${body}`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
logger.info('Screenshot uploaded successfully');
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import type { CommandHandler } from '../lib/types.js';
|
|
6
|
+
import type { Logger } from '../lib/logger.js';
|
|
7
|
+
import { getPlatform } from '../lib/platform.js';
|
|
8
|
+
|
|
9
|
+
const ALLOWED_CAPTURE_TOOLS = ['scrot', 'import', 'screencapture'] as const;
|
|
10
|
+
|
|
11
|
+
// --- Zod Schema ---
|
|
12
|
+
const ScreenshotArgsSchema = z.object({
|
|
13
|
+
serverUrl: z.string().url().optional(),
|
|
14
|
+
deviceId: z.string().uuid().optional(),
|
|
15
|
+
apiKey: z.string().min(1).optional(),
|
|
16
|
+
quality: z.number().int().min(1).max(100).optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export function registerScreenshotCommands(
|
|
20
|
+
register: (command: string, handler: CommandHandler) => void,
|
|
21
|
+
logger: Logger
|
|
22
|
+
): void {
|
|
23
|
+
register('kiosk:screenshot', async (args) => {
|
|
24
|
+
const parsed = ScreenshotArgsSchema.safeParse(args ?? {});
|
|
25
|
+
if (!parsed.success) {
|
|
26
|
+
throw new Error(`Invalid screenshot args: ${parsed.error.issues.map((i) => i.message).join(', ')}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { serverUrl, deviceId, apiKey, quality: parsedQuality } = parsed.data;
|
|
30
|
+
const quality = parsedQuality ?? 75;
|
|
31
|
+
|
|
32
|
+
const tmpFile = `/tmp/lightman-screenshot-${Date.now()}.jpg`;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// --- Capture screenshot ---
|
|
36
|
+
const { tool, toolArgs } = resolveCapture(quality, tmpFile);
|
|
37
|
+
logger.info(`Capturing screenshot: ${tool} ${toolArgs.join(' ')}`);
|
|
38
|
+
|
|
39
|
+
execFileSync(tool, toolArgs, { timeout: 10_000, stdio: 'pipe' });
|
|
40
|
+
|
|
41
|
+
if (!fs.existsSync(tmpFile)) {
|
|
42
|
+
throw new Error('Screenshot file was not created');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const buffer = await fs.promises.readFile(tmpFile);
|
|
46
|
+
logger.info(`Screenshot captured: ${buffer.length} bytes`);
|
|
47
|
+
|
|
48
|
+
// --- Upload if server info provided ---
|
|
49
|
+
if (serverUrl && deviceId && apiKey) {
|
|
50
|
+
try {
|
|
51
|
+
const uploaded = await uploadScreenshot(
|
|
52
|
+
buffer,
|
|
53
|
+
tmpFile,
|
|
54
|
+
serverUrl,
|
|
55
|
+
deviceId,
|
|
56
|
+
apiKey,
|
|
57
|
+
logger
|
|
58
|
+
);
|
|
59
|
+
return { captured: true, size: buffer.length, uploaded };
|
|
60
|
+
} catch (uploadErr) {
|
|
61
|
+
const errMsg =
|
|
62
|
+
uploadErr instanceof Error ? uploadErr.message : String(uploadErr);
|
|
63
|
+
logger.error('Screenshot upload failed:', errMsg);
|
|
64
|
+
return { captured: true, size: buffer.length, uploaded: false, error: errMsg };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { captured: true, size: buffer.length, uploaded: false };
|
|
69
|
+
} catch (err) {
|
|
70
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
71
|
+
logger.error('Screenshot capture failed:', errMsg);
|
|
72
|
+
throw new Error(`Screenshot capture failed: ${errMsg}`);
|
|
73
|
+
} finally {
|
|
74
|
+
// Clean up temp file
|
|
75
|
+
try {
|
|
76
|
+
if (fs.existsSync(tmpFile)) {
|
|
77
|
+
await fs.promises.unlink(tmpFile);
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// Ignore cleanup errors
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Determine the capture tool and arguments based on platform.
|
|
88
|
+
* No user-controlled command strings — only allowlisted tools with safe args.
|
|
89
|
+
*/
|
|
90
|
+
function resolveCapture(
|
|
91
|
+
quality: number,
|
|
92
|
+
tmpFile: string
|
|
93
|
+
): { tool: string; toolArgs: string[] } {
|
|
94
|
+
const platform = getPlatform();
|
|
95
|
+
|
|
96
|
+
if (platform === 'linux') {
|
|
97
|
+
if (commandExists('scrot')) {
|
|
98
|
+
return { tool: 'scrot', toolArgs: ['-q', String(quality), tmpFile] };
|
|
99
|
+
}
|
|
100
|
+
if (commandExists('import')) {
|
|
101
|
+
return { tool: 'import', toolArgs: ['-window', 'root', '-quality', String(quality), tmpFile] };
|
|
102
|
+
}
|
|
103
|
+
throw new Error(
|
|
104
|
+
'No screenshot tool available. Install scrot or ImageMagick (import).'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (platform === 'darwin') {
|
|
109
|
+
return { tool: 'screencapture', toolArgs: ['-x', '-t', 'jpg', tmpFile] };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw new Error(`Screenshot capture not supported on platform: ${platform}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if a command exists on the system using execFileSync (no shell injection).
|
|
117
|
+
*/
|
|
118
|
+
function commandExists(cmd: string): boolean {
|
|
119
|
+
if (!ALLOWED_CAPTURE_TOOLS.includes(cmd as typeof ALLOWED_CAPTURE_TOOLS[number])) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
execFileSync('which', [cmd], { stdio: 'pipe' });
|
|
124
|
+
return true;
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Upload screenshot to server via multipart/form-data.
|
|
132
|
+
*/
|
|
133
|
+
async function uploadScreenshot(
|
|
134
|
+
buffer: Buffer,
|
|
135
|
+
filePath: string,
|
|
136
|
+
serverUrl: string,
|
|
137
|
+
deviceId: string,
|
|
138
|
+
apiKey: string,
|
|
139
|
+
logger: Logger
|
|
140
|
+
): Promise<boolean> {
|
|
141
|
+
const endpoint = `/api/devices/${deviceId}/screenshot`;
|
|
142
|
+
const url = `${serverUrl}${endpoint}`;
|
|
143
|
+
const filename = path.basename(filePath);
|
|
144
|
+
|
|
145
|
+
logger.info(`Uploading screenshot to ${url}`);
|
|
146
|
+
|
|
147
|
+
const blob = new Blob([buffer], { type: 'image/jpeg' });
|
|
148
|
+
const formData = new FormData();
|
|
149
|
+
formData.append('screenshot', blob, filename);
|
|
150
|
+
|
|
151
|
+
const response = await fetch(url, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: {
|
|
154
|
+
'x-api-key': apiKey,
|
|
155
|
+
},
|
|
156
|
+
body: formData,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
const body = await response.text().catch(() => '');
|
|
161
|
+
throw new Error(`Upload failed: HTTP ${response.status} ${body}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
logger.info('Screenshot uploaded successfully');
|
|
165
|
+
return true;
|
|
166
|
+
}
|
package/src/commands/serial.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import type { CommandHandler } from '../lib/types.js';
|
|
2
|
-
import type { Logger } from '../lib/logger.js';
|
|
3
|
-
|
|
4
|
-
// --- Register Serial Commands ---
|
|
5
|
-
|
|
6
|
-
export function registerSerialCommands(
|
|
7
|
-
register: (command: string, handler: CommandHandler) => void,
|
|
8
|
-
logger: Logger
|
|
9
|
-
): void {
|
|
10
|
-
// serial:close — placeholder
|
|
11
|
-
register('serial:close', async (args) => {
|
|
12
|
-
const port = args?.port as string;
|
|
13
|
-
if (!port) throw new Error('Port path is required');
|
|
14
|
-
logger.info(`serial:close requested for ${port}`);
|
|
15
|
-
return { status: 'acknowledged', port };
|
|
16
|
-
});
|
|
17
|
-
}
|
|
1
|
+
import type { CommandHandler } from '../lib/types.js';
|
|
2
|
+
import type { Logger } from '../lib/logger.js';
|
|
3
|
+
|
|
4
|
+
// --- Register Serial Commands ---
|
|
5
|
+
|
|
6
|
+
export function registerSerialCommands(
|
|
7
|
+
register: (command: string, handler: CommandHandler) => void,
|
|
8
|
+
logger: Logger
|
|
9
|
+
): void {
|
|
10
|
+
// serial:close — placeholder
|
|
11
|
+
register('serial:close', async (args) => {
|
|
12
|
+
const port = args?.port as string;
|
|
13
|
+
if (!port) throw new Error('Port path is required');
|
|
14
|
+
logger.info(`serial:close requested for ${port}`);
|
|
15
|
+
return { status: 'acknowledged', port };
|
|
16
|
+
});
|
|
17
|
+
}
|