lightman-agent 1.0.5 → 1.0.7
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/lib/config.ts
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
import { resolve } from 'path';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import type { AgentConfig } from './types.js';
|
|
5
|
-
|
|
6
|
-
const kioskSchema = z.object({
|
|
7
|
-
browserPath: z.string().default('chromium-browser'),
|
|
8
|
-
defaultUrl: z.string().url().default('http://localhost:3401/display'),
|
|
9
|
-
extraArgs: z.array(z.string()).default([]),
|
|
10
|
-
pollIntervalMs: z.number().int().min(1000).default(10_000),
|
|
11
|
-
maxCrashesInWindow: z.number().int().min(1).default(10),
|
|
12
|
-
crashWindowMs: z.number().int().min(10_000).default(300_000),
|
|
13
|
-
shellMode: z.boolean().default(false),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
const screenshotSchema = z.object({
|
|
17
|
-
captureCommand: z.string().default('scrot'),
|
|
18
|
-
quality: z.number().int().min(1).max(100).default(80),
|
|
19
|
-
uploadEndpoint: z.string().default('/api/devices/{deviceId}/screenshot'),
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const powerScheduleSchema = z.object({
|
|
23
|
-
shutdownCron: z.string().optional(),
|
|
24
|
-
startupCron: z.string().optional(),
|
|
25
|
-
timezone: z.string().default(Intl.DateTimeFormat().resolvedOptions().timeZone),
|
|
26
|
-
shutdownWarningSeconds: z.number().int().min(0).max(600).default(60),
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const configSchema = z.object({
|
|
30
|
-
serverUrl: z.string().url(),
|
|
31
|
-
deviceSlug: z.string().min(1),
|
|
32
|
-
healthIntervalMs: z.number().int().min(5000).default(60000),
|
|
33
|
-
logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
|
34
|
-
logFile: z.string().default('agent.log'),
|
|
35
|
-
identityFile: z.string().default('.lightman-identity.json'),
|
|
36
|
-
localServices: z.boolean().default(true),
|
|
37
|
-
kiosk: kioskSchema.optional(),
|
|
38
|
-
screenshot: screenshotSchema.optional(),
|
|
39
|
-
powerSchedule: powerScheduleSchema.optional(),
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
export function loadConfig(configPath?: string): AgentConfig {
|
|
43
|
-
const filePath = configPath || resolve(process.cwd(), 'agent.config.json');
|
|
44
|
-
|
|
45
|
-
if (!existsSync(filePath)) {
|
|
46
|
-
throw new Error(`Config file not found: ${filePath}`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
let raw: Record<string, unknown>;
|
|
50
|
-
try {
|
|
51
|
-
raw = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
52
|
-
} catch (err) {
|
|
53
|
-
throw new Error(`Invalid JSON in config file ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Apply environment overrides
|
|
57
|
-
const merged = {
|
|
58
|
-
...raw,
|
|
59
|
-
...(process.env.LIGHTMAN_SERVER_URL && { serverUrl: process.env.LIGHTMAN_SERVER_URL }),
|
|
60
|
-
...(process.env.LIGHTMAN_DEVICE_SLUG && { deviceSlug: process.env.LIGHTMAN_DEVICE_SLUG }),
|
|
61
|
-
...(process.env.LIGHTMAN_HEALTH_INTERVAL && { healthIntervalMs: parseInt(process.env.LIGHTMAN_HEALTH_INTERVAL, 10) }),
|
|
62
|
-
...(process.env.LIGHTMAN_LOG_LEVEL && { logLevel: process.env.LIGHTMAN_LOG_LEVEL }),
|
|
63
|
-
...(process.env.LIGHTMAN_LOG_FILE && { logFile: process.env.LIGHTMAN_LOG_FILE }),
|
|
64
|
-
...(process.env.LIGHTMAN_IDENTITY_FILE && { identityFile: process.env.LIGHTMAN_IDENTITY_FILE }),
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const result = configSchema.parse(merged);
|
|
68
|
-
return result as AgentConfig;
|
|
69
|
-
}
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import type { AgentConfig } from './types.js';
|
|
5
|
+
|
|
6
|
+
const kioskSchema = z.object({
|
|
7
|
+
browserPath: z.string().default('chromium-browser'),
|
|
8
|
+
defaultUrl: z.string().url().default('http://localhost:3401/display'),
|
|
9
|
+
extraArgs: z.array(z.string()).default([]),
|
|
10
|
+
pollIntervalMs: z.number().int().min(1000).default(10_000),
|
|
11
|
+
maxCrashesInWindow: z.number().int().min(1).default(10),
|
|
12
|
+
crashWindowMs: z.number().int().min(10_000).default(300_000),
|
|
13
|
+
shellMode: z.boolean().default(false),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const screenshotSchema = z.object({
|
|
17
|
+
captureCommand: z.string().default('scrot'),
|
|
18
|
+
quality: z.number().int().min(1).max(100).default(80),
|
|
19
|
+
uploadEndpoint: z.string().default('/api/devices/{deviceId}/screenshot'),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const powerScheduleSchema = z.object({
|
|
23
|
+
shutdownCron: z.string().optional(),
|
|
24
|
+
startupCron: z.string().optional(),
|
|
25
|
+
timezone: z.string().default(Intl.DateTimeFormat().resolvedOptions().timeZone),
|
|
26
|
+
shutdownWarningSeconds: z.number().int().min(0).max(600).default(60),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const configSchema = z.object({
|
|
30
|
+
serverUrl: z.string().url(),
|
|
31
|
+
deviceSlug: z.string().min(1),
|
|
32
|
+
healthIntervalMs: z.number().int().min(5000).default(60000),
|
|
33
|
+
logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
|
34
|
+
logFile: z.string().default('agent.log'),
|
|
35
|
+
identityFile: z.string().default('.lightman-identity.json'),
|
|
36
|
+
localServices: z.boolean().default(true),
|
|
37
|
+
kiosk: kioskSchema.optional(),
|
|
38
|
+
screenshot: screenshotSchema.optional(),
|
|
39
|
+
powerSchedule: powerScheduleSchema.optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export function loadConfig(configPath?: string): AgentConfig {
|
|
43
|
+
const filePath = configPath || resolve(process.cwd(), 'agent.config.json');
|
|
44
|
+
|
|
45
|
+
if (!existsSync(filePath)) {
|
|
46
|
+
throw new Error(`Config file not found: ${filePath}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let raw: Record<string, unknown>;
|
|
50
|
+
try {
|
|
51
|
+
raw = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
52
|
+
} catch (err) {
|
|
53
|
+
throw new Error(`Invalid JSON in config file ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Apply environment overrides
|
|
57
|
+
const merged = {
|
|
58
|
+
...raw,
|
|
59
|
+
...(process.env.LIGHTMAN_SERVER_URL && { serverUrl: process.env.LIGHTMAN_SERVER_URL }),
|
|
60
|
+
...(process.env.LIGHTMAN_DEVICE_SLUG && { deviceSlug: process.env.LIGHTMAN_DEVICE_SLUG }),
|
|
61
|
+
...(process.env.LIGHTMAN_HEALTH_INTERVAL && { healthIntervalMs: parseInt(process.env.LIGHTMAN_HEALTH_INTERVAL, 10) }),
|
|
62
|
+
...(process.env.LIGHTMAN_LOG_LEVEL && { logLevel: process.env.LIGHTMAN_LOG_LEVEL }),
|
|
63
|
+
...(process.env.LIGHTMAN_LOG_FILE && { logFile: process.env.LIGHTMAN_LOG_FILE }),
|
|
64
|
+
...(process.env.LIGHTMAN_IDENTITY_FILE && { identityFile: process.env.LIGHTMAN_IDENTITY_FILE }),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const result = configSchema.parse(merged);
|
|
68
|
+
return result as AgentConfig;
|
|
69
|
+
}
|
package/src/lib/identity.ts
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, chmodSync } from 'fs';
|
|
2
|
-
import { resolve, dirname } from 'path';
|
|
3
|
-
import { mkdirSync } from 'fs';
|
|
4
|
-
import type { Identity } from './types.js';
|
|
5
|
-
|
|
6
|
-
export function readIdentity(filePath: string): Identity | null {
|
|
7
|
-
const fullPath = resolve(process.cwd(), filePath);
|
|
8
|
-
|
|
9
|
-
if (!existsSync(fullPath)) {
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
const raw = JSON.parse(readFileSync(fullPath, 'utf-8'));
|
|
15
|
-
if (raw.deviceId && raw.apiKey) {
|
|
16
|
-
return { deviceId: raw.deviceId, apiKey: raw.apiKey };
|
|
17
|
-
}
|
|
18
|
-
return null;
|
|
19
|
-
} catch {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function writeIdentity(filePath: string, identity: Identity): void {
|
|
25
|
-
const fullPath = resolve(process.cwd(), filePath);
|
|
26
|
-
const dir = dirname(fullPath);
|
|
27
|
-
|
|
28
|
-
if (!existsSync(dir)) {
|
|
29
|
-
mkdirSync(dir, { recursive: true });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
writeFileSync(fullPath, JSON.stringify(identity, null, 2), { mode: 0o600 });
|
|
33
|
-
|
|
34
|
-
// Ensure permissions are correct even if file already existed
|
|
35
|
-
try {
|
|
36
|
-
chmodSync(fullPath, 0o600);
|
|
37
|
-
} catch {
|
|
38
|
-
// Ignore permission errors on Windows
|
|
39
|
-
}
|
|
40
|
-
}
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, chmodSync } from 'fs';
|
|
2
|
+
import { resolve, dirname } from 'path';
|
|
3
|
+
import { mkdirSync } from 'fs';
|
|
4
|
+
import type { Identity } from './types.js';
|
|
5
|
+
|
|
6
|
+
export function readIdentity(filePath: string): Identity | null {
|
|
7
|
+
const fullPath = resolve(process.cwd(), filePath);
|
|
8
|
+
|
|
9
|
+
if (!existsSync(fullPath)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const raw = JSON.parse(readFileSync(fullPath, 'utf-8'));
|
|
15
|
+
if (raw.deviceId && raw.apiKey) {
|
|
16
|
+
return { deviceId: raw.deviceId, apiKey: raw.apiKey };
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function writeIdentity(filePath: string, identity: Identity): void {
|
|
25
|
+
const fullPath = resolve(process.cwd(), filePath);
|
|
26
|
+
const dir = dirname(fullPath);
|
|
27
|
+
|
|
28
|
+
if (!existsSync(dir)) {
|
|
29
|
+
mkdirSync(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
writeFileSync(fullPath, JSON.stringify(identity, null, 2), { mode: 0o600 });
|
|
33
|
+
|
|
34
|
+
// Ensure permissions are correct even if file already existed
|
|
35
|
+
try {
|
|
36
|
+
chmodSync(fullPath, 0o600);
|
|
37
|
+
} catch {
|
|
38
|
+
// Ignore permission errors on Windows
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/lib/logger.ts
CHANGED
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
import { appendFileSync, existsSync, mkdirSync, statSync, renameSync, unlinkSync } from 'fs';
|
|
2
|
-
import { dirname } from 'path';
|
|
3
|
-
import type { LogEntry } from './types.js';
|
|
4
|
-
|
|
5
|
-
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
6
|
-
|
|
7
|
-
const LEVEL_ORDER: Record<LogLevel, number> = {
|
|
8
|
-
debug: 0,
|
|
9
|
-
info: 1,
|
|
10
|
-
warn: 2,
|
|
11
|
-
error: 3,
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const LEVEL_LABELS: Record<LogLevel, string> = {
|
|
15
|
-
debug: 'DEBUG',
|
|
16
|
-
info: 'INFO ',
|
|
17
|
-
warn: 'WARN ',
|
|
18
|
-
error: 'ERROR',
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const MAX_LOG_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
22
|
-
const MAX_ROTATED_FILES = 3; // Keep agent.log.1, .2, .3
|
|
23
|
-
|
|
24
|
-
export class Logger {
|
|
25
|
-
private level: LogLevel;
|
|
26
|
-
private logFile: string | null;
|
|
27
|
-
private listeners: Array<(entry: LogEntry) => void> = [];
|
|
28
|
-
private writesSinceRotateCheck = 0;
|
|
29
|
-
|
|
30
|
-
constructor(level: LogLevel = 'info', logFile?: string) {
|
|
31
|
-
this.level = level;
|
|
32
|
-
this.logFile = logFile || null;
|
|
33
|
-
|
|
34
|
-
if (this.logFile) {
|
|
35
|
-
const dir = dirname(this.logFile);
|
|
36
|
-
if (!existsSync(dir)) {
|
|
37
|
-
mkdirSync(dir, { recursive: true });
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
onLog(fn: (entry: LogEntry) => void): void {
|
|
43
|
-
this.listeners = [...this.listeners, fn];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
setLevel(level: LogLevel): void {
|
|
47
|
-
this.level = level;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
debug(message: string, ...args: unknown[]): void {
|
|
51
|
-
this.log('debug', message, ...args);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
info(message: string, ...args: unknown[]): void {
|
|
55
|
-
this.log('info', message, ...args);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
warn(message: string, ...args: unknown[]): void {
|
|
59
|
-
this.log('warn', message, ...args);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
error(message: string, ...args: unknown[]): void {
|
|
63
|
-
this.log('error', message, ...args);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
private log(level: LogLevel, message: string, ...args: unknown[]): void {
|
|
67
|
-
if (LEVEL_ORDER[level] < LEVEL_ORDER[this.level]) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const timestamp = new Date().toISOString();
|
|
72
|
-
const label = LEVEL_LABELS[level];
|
|
73
|
-
const formatted = `[${timestamp}] [${label}] ${message}`;
|
|
74
|
-
|
|
75
|
-
// Console output
|
|
76
|
-
if (level === 'error') {
|
|
77
|
-
console.error(formatted, ...args);
|
|
78
|
-
} else if (level === 'warn') {
|
|
79
|
-
console.warn(formatted, ...args);
|
|
80
|
-
} else {
|
|
81
|
-
console.log(formatted, ...args);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// File output (with rotation)
|
|
85
|
-
if (this.logFile) {
|
|
86
|
-
try {
|
|
87
|
-
const extra = args.length > 0 ? ' ' + args.map(a => JSON.stringify(a)).join(' ') : '';
|
|
88
|
-
appendFileSync(this.logFile, formatted + extra + '\n');
|
|
89
|
-
|
|
90
|
-
// Check rotation every 100 writes to avoid stat() on every log line
|
|
91
|
-
this.writesSinceRotateCheck++;
|
|
92
|
-
if (this.writesSinceRotateCheck >= 100) {
|
|
93
|
-
this.writesSinceRotateCheck = 0;
|
|
94
|
-
this.rotateIfNeeded();
|
|
95
|
-
}
|
|
96
|
-
} catch {
|
|
97
|
-
// Silently fail file writes to avoid recursive errors
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Notify listeners
|
|
102
|
-
const entry: LogEntry = { timestamp, level, message, source: 'agent' };
|
|
103
|
-
for (const listener of this.listeners) {
|
|
104
|
-
try {
|
|
105
|
-
listener(entry);
|
|
106
|
-
} catch {
|
|
107
|
-
// Prevent listener errors from breaking logging
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private rotateIfNeeded(): void {
|
|
113
|
-
if (!this.logFile) return;
|
|
114
|
-
try {
|
|
115
|
-
const stat = statSync(this.logFile);
|
|
116
|
-
if (stat.size < MAX_LOG_SIZE_BYTES) return;
|
|
117
|
-
|
|
118
|
-
// Rotate: agent.log.3 → delete, agent.log.2 → .3, agent.log.1 → .2, agent.log → .1
|
|
119
|
-
for (let i = MAX_ROTATED_FILES; i >= 1; i--) {
|
|
120
|
-
const src = i === 1 ? this.logFile : `${this.logFile}.${i - 1}`;
|
|
121
|
-
const dst = `${this.logFile}.${i}`;
|
|
122
|
-
try {
|
|
123
|
-
if (i === MAX_ROTATED_FILES && existsSync(dst)) {
|
|
124
|
-
unlinkSync(dst);
|
|
125
|
-
}
|
|
126
|
-
if (existsSync(src)) {
|
|
127
|
-
renameSync(src, dst);
|
|
128
|
-
}
|
|
129
|
-
} catch {
|
|
130
|
-
// Best effort rotation
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
} catch {
|
|
134
|
-
// stat failed, skip rotation
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, statSync, renameSync, unlinkSync } from 'fs';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
import type { LogEntry } from './types.js';
|
|
4
|
+
|
|
5
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
6
|
+
|
|
7
|
+
const LEVEL_ORDER: Record<LogLevel, number> = {
|
|
8
|
+
debug: 0,
|
|
9
|
+
info: 1,
|
|
10
|
+
warn: 2,
|
|
11
|
+
error: 3,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const LEVEL_LABELS: Record<LogLevel, string> = {
|
|
15
|
+
debug: 'DEBUG',
|
|
16
|
+
info: 'INFO ',
|
|
17
|
+
warn: 'WARN ',
|
|
18
|
+
error: 'ERROR',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const MAX_LOG_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
22
|
+
const MAX_ROTATED_FILES = 3; // Keep agent.log.1, .2, .3
|
|
23
|
+
|
|
24
|
+
export class Logger {
|
|
25
|
+
private level: LogLevel;
|
|
26
|
+
private logFile: string | null;
|
|
27
|
+
private listeners: Array<(entry: LogEntry) => void> = [];
|
|
28
|
+
private writesSinceRotateCheck = 0;
|
|
29
|
+
|
|
30
|
+
constructor(level: LogLevel = 'info', logFile?: string) {
|
|
31
|
+
this.level = level;
|
|
32
|
+
this.logFile = logFile || null;
|
|
33
|
+
|
|
34
|
+
if (this.logFile) {
|
|
35
|
+
const dir = dirname(this.logFile);
|
|
36
|
+
if (!existsSync(dir)) {
|
|
37
|
+
mkdirSync(dir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onLog(fn: (entry: LogEntry) => void): void {
|
|
43
|
+
this.listeners = [...this.listeners, fn];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setLevel(level: LogLevel): void {
|
|
47
|
+
this.level = level;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
debug(message: string, ...args: unknown[]): void {
|
|
51
|
+
this.log('debug', message, ...args);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
info(message: string, ...args: unknown[]): void {
|
|
55
|
+
this.log('info', message, ...args);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
warn(message: string, ...args: unknown[]): void {
|
|
59
|
+
this.log('warn', message, ...args);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
error(message: string, ...args: unknown[]): void {
|
|
63
|
+
this.log('error', message, ...args);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private log(level: LogLevel, message: string, ...args: unknown[]): void {
|
|
67
|
+
if (LEVEL_ORDER[level] < LEVEL_ORDER[this.level]) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const timestamp = new Date().toISOString();
|
|
72
|
+
const label = LEVEL_LABELS[level];
|
|
73
|
+
const formatted = `[${timestamp}] [${label}] ${message}`;
|
|
74
|
+
|
|
75
|
+
// Console output
|
|
76
|
+
if (level === 'error') {
|
|
77
|
+
console.error(formatted, ...args);
|
|
78
|
+
} else if (level === 'warn') {
|
|
79
|
+
console.warn(formatted, ...args);
|
|
80
|
+
} else {
|
|
81
|
+
console.log(formatted, ...args);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// File output (with rotation)
|
|
85
|
+
if (this.logFile) {
|
|
86
|
+
try {
|
|
87
|
+
const extra = args.length > 0 ? ' ' + args.map(a => JSON.stringify(a)).join(' ') : '';
|
|
88
|
+
appendFileSync(this.logFile, formatted + extra + '\n');
|
|
89
|
+
|
|
90
|
+
// Check rotation every 100 writes to avoid stat() on every log line
|
|
91
|
+
this.writesSinceRotateCheck++;
|
|
92
|
+
if (this.writesSinceRotateCheck >= 100) {
|
|
93
|
+
this.writesSinceRotateCheck = 0;
|
|
94
|
+
this.rotateIfNeeded();
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// Silently fail file writes to avoid recursive errors
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Notify listeners
|
|
102
|
+
const entry: LogEntry = { timestamp, level, message, source: 'agent' };
|
|
103
|
+
for (const listener of this.listeners) {
|
|
104
|
+
try {
|
|
105
|
+
listener(entry);
|
|
106
|
+
} catch {
|
|
107
|
+
// Prevent listener errors from breaking logging
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private rotateIfNeeded(): void {
|
|
113
|
+
if (!this.logFile) return;
|
|
114
|
+
try {
|
|
115
|
+
const stat = statSync(this.logFile);
|
|
116
|
+
if (stat.size < MAX_LOG_SIZE_BYTES) return;
|
|
117
|
+
|
|
118
|
+
// Rotate: agent.log.3 → delete, agent.log.2 → .3, agent.log.1 → .2, agent.log → .1
|
|
119
|
+
for (let i = MAX_ROTATED_FILES; i >= 1; i--) {
|
|
120
|
+
const src = i === 1 ? this.logFile : `${this.logFile}.${i - 1}`;
|
|
121
|
+
const dst = `${this.logFile}.${i}`;
|
|
122
|
+
try {
|
|
123
|
+
if (i === MAX_ROTATED_FILES && existsSync(dst)) {
|
|
124
|
+
unlinkSync(dst);
|
|
125
|
+
}
|
|
126
|
+
if (existsSync(src)) {
|
|
127
|
+
renameSync(src, dst);
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// Best effort rotation
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// stat failed, skip rotation
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/lib/platform.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { platform } from 'os';
|
|
2
|
-
|
|
3
|
-
export type Platform = 'linux' | 'windows' | 'darwin';
|
|
4
|
-
|
|
5
|
-
export function getPlatform(): Platform {
|
|
6
|
-
const p = platform();
|
|
7
|
-
if (p === 'win32') return 'windows';
|
|
8
|
-
if (p === 'darwin') return 'darwin';
|
|
9
|
-
return 'linux';
|
|
10
|
-
}
|
|
1
|
+
import { platform } from 'os';
|
|
2
|
+
|
|
3
|
+
export type Platform = 'linux' | 'windows' | 'darwin';
|
|
4
|
+
|
|
5
|
+
export function getPlatform(): Platform {
|
|
6
|
+
const p = platform();
|
|
7
|
+
if (p === 'win32') return 'windows';
|
|
8
|
+
if (p === 'darwin') return 'darwin';
|
|
9
|
+
return 'linux';
|
|
10
|
+
}
|