@nocville/npc 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -17
- package/dist/cjs/cli.js +94 -39
- package/dist/cjs/core/base-npc.js +15 -0
- package/dist/cjs/core/config.js +19 -2
- package/dist/cjs/core/errors.js +23 -0
- package/dist/cjs/core/http.js +121 -0
- package/dist/cjs/core/pixel-art.js +110 -0
- package/dist/cjs/core/polling-npc.js +99 -14
- package/dist/cjs/core/text.js +13 -0
- package/dist/cjs/core/thresholds.js +29 -0
- package/dist/cjs/index.js +21 -2
- package/dist/cjs/npcs/agent-common/session.js +1 -0
- package/dist/cjs/npcs/agent-common/shim.js +8 -2
- package/dist/cjs/npcs/github-watcher/defaults.js +7 -1
- package/dist/cjs/npcs/github-watcher/github-api.js +82 -45
- package/dist/cjs/npcs/github-watcher/index.js +82 -36
- package/dist/cjs/npcs/internet-connectivity/checks.js +70 -67
- package/dist/cjs/npcs/internet-connectivity/defaults.js +5 -2
- package/dist/cjs/npcs/internet-connectivity/index.js +134 -131
- package/dist/cjs/npcs/log-watcher/buffer.js +61 -0
- package/dist/cjs/npcs/log-watcher/index.js +109 -103
- package/dist/cjs/npcs/log-watcher/tail.js +100 -53
- package/dist/cjs/npcs/office-characters/index.js +92 -0
- package/dist/cjs/npcs/process-watcher/index.js +60 -51
- package/dist/cjs/npcs/process-watcher/process-scanner.js +100 -68
- package/dist/cjs/npcs/receptionist/defaults.js +4 -0
- package/dist/cjs/npcs/receptionist/index.js +6 -93
- package/dist/cjs/npcs/receptionist/weather.js +42 -47
- package/dist/cjs/npcs/security/index.js +26 -0
- package/dist/cjs/npcs/system-monitor/collectors.js +66 -17
- package/dist/cjs/npcs/system-monitor/index.js +52 -28
- package/dist/cjs/npcs/web-page-watcher/basic-mode.js +30 -14
- package/dist/cjs/npcs/web-page-watcher/capture.js +23 -1
- package/dist/cjs/npcs/web-page-watcher/defaults.js +44 -2
- package/dist/cjs/npcs/web-page-watcher/index.js +119 -68
- package/dist/cjs/npcs/web-page-watcher/pixelate.js +2 -1
- package/dist/cli.js +95 -40
- package/dist/core/base-npc.d.ts +5 -0
- package/dist/core/base-npc.js +15 -0
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.js +18 -2
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +18 -0
- package/dist/core/http.d.ts +52 -0
- package/dist/core/http.js +111 -0
- package/dist/core/pixel-art.d.ts +38 -0
- package/dist/core/pixel-art.js +105 -0
- package/dist/core/polling-npc.d.ts +22 -1
- package/dist/core/polling-npc.js +98 -13
- package/dist/core/text.d.ts +6 -0
- package/dist/core/text.js +12 -0
- package/dist/core/thresholds.d.ts +9 -1
- package/dist/core/thresholds.js +27 -0
- package/dist/core/types.d.ts +5 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +6 -2
- package/dist/npcs/agent-common/session.js +1 -0
- package/dist/npcs/agent-common/shim.js +8 -2
- package/dist/npcs/github-watcher/defaults.d.ts +6 -0
- package/dist/npcs/github-watcher/defaults.js +7 -1
- package/dist/npcs/github-watcher/github-api.d.ts +21 -7
- package/dist/npcs/github-watcher/github-api.js +80 -45
- package/dist/npcs/github-watcher/index.d.ts +6 -0
- package/dist/npcs/github-watcher/index.js +83 -37
- package/dist/npcs/internet-connectivity/checks.d.ts +15 -18
- package/dist/npcs/internet-connectivity/checks.js +70 -67
- package/dist/npcs/internet-connectivity/defaults.d.ts +3 -0
- package/dist/npcs/internet-connectivity/defaults.js +5 -2
- package/dist/npcs/internet-connectivity/index.d.ts +19 -6
- package/dist/npcs/internet-connectivity/index.js +135 -133
- package/dist/npcs/log-watcher/buffer.d.ts +22 -0
- package/dist/npcs/log-watcher/buffer.js +58 -0
- package/dist/npcs/log-watcher/index.d.ts +22 -21
- package/dist/npcs/log-watcher/index.js +112 -106
- package/dist/npcs/log-watcher/tail.d.ts +23 -3
- package/dist/npcs/log-watcher/tail.js +101 -54
- package/dist/npcs/office-characters/index.d.ts +50 -0
- package/dist/npcs/office-characters/index.js +82 -0
- package/dist/npcs/process-watcher/index.d.ts +1 -1
- package/dist/npcs/process-watcher/index.js +61 -52
- package/dist/npcs/process-watcher/process-scanner.d.ts +31 -1
- package/dist/npcs/process-watcher/process-scanner.js +97 -70
- package/dist/npcs/receptionist/defaults.d.ts +4 -0
- package/dist/npcs/receptionist/defaults.js +4 -0
- package/dist/npcs/receptionist/index.d.ts +3 -12
- package/dist/npcs/receptionist/index.js +6 -93
- package/dist/npcs/receptionist/weather.d.ts +8 -6
- package/dist/npcs/receptionist/weather.js +39 -45
- package/dist/npcs/security/index.d.ts +12 -0
- package/dist/npcs/security/index.js +22 -0
- package/dist/npcs/system-monitor/collectors.d.ts +15 -3
- package/dist/npcs/system-monitor/collectors.js +64 -17
- package/dist/npcs/system-monitor/defaults.d.ts +1 -1
- package/dist/npcs/system-monitor/index.d.ts +2 -1
- package/dist/npcs/system-monitor/index.js +54 -30
- package/dist/npcs/web-page-watcher/basic-mode.d.ts +2 -2
- package/dist/npcs/web-page-watcher/basic-mode.js +30 -14
- package/dist/npcs/web-page-watcher/capture.d.ts +18 -0
- package/dist/npcs/web-page-watcher/capture.js +21 -1
- package/dist/npcs/web-page-watcher/defaults.d.ts +23 -0
- package/dist/npcs/web-page-watcher/defaults.js +41 -1
- package/dist/npcs/web-page-watcher/index.d.ts +18 -1
- package/dist/npcs/web-page-watcher/index.js +121 -70
- package/dist/npcs/web-page-watcher/pixelate.js +2 -1
- package/package.json +2 -2
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SecurityNPC = void 0;
|
|
4
|
+
const polling_npc_js_1 = require("../../core/polling-npc.js");
|
|
5
|
+
class SecurityNPC extends polling_npc_js_1.PollingNPC {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
super({ ...config, pollInterval: config.pollInterval ?? 60_000 });
|
|
8
|
+
}
|
|
9
|
+
get name() {
|
|
10
|
+
return 'security';
|
|
11
|
+
}
|
|
12
|
+
get defaultDisplayName() {
|
|
13
|
+
return 'Security';
|
|
14
|
+
}
|
|
15
|
+
get archetype() {
|
|
16
|
+
return 'custom';
|
|
17
|
+
}
|
|
18
|
+
get behavior() {
|
|
19
|
+
return { role: 'security', activity: 'idle', idlePose: 'stand' };
|
|
20
|
+
}
|
|
21
|
+
async collect() {
|
|
22
|
+
return { status: 'healthy', mood: 'happy', message: '', metrics: {} };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.SecurityNPC = SecurityNPC;
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -4,10 +4,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createCpuSampler = createCpuSampler;
|
|
7
|
+
exports.parseVmStat = parseVmStat;
|
|
8
|
+
exports.readAvailableMemory = readAvailableMemory;
|
|
7
9
|
exports.collectMemory = collectMemory;
|
|
8
10
|
exports.collectDisk = collectDisk;
|
|
9
11
|
const node_os_1 = __importDefault(require("node:os"));
|
|
10
12
|
const node_fs_1 = require("node:fs");
|
|
13
|
+
const node_child_process_1 = require("node:child_process");
|
|
14
|
+
const node_util_1 = require("node:util");
|
|
15
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
11
16
|
function aggregate(cpus) {
|
|
12
17
|
let idle = 0;
|
|
13
18
|
let total = 0;
|
|
@@ -18,6 +23,12 @@ function aggregate(cpus) {
|
|
|
18
23
|
}
|
|
19
24
|
return { idle, total };
|
|
20
25
|
}
|
|
26
|
+
function gb(bytes) {
|
|
27
|
+
return Math.round((bytes / 1024 ** 3) * 100) / 100;
|
|
28
|
+
}
|
|
29
|
+
function percentOf(part, whole) {
|
|
30
|
+
return whole > 0 ? Math.min(100, Math.max(0, Math.round((part / whole) * 100))) : 0;
|
|
31
|
+
}
|
|
21
32
|
/**
|
|
22
33
|
* Stateful CPU sampler that computes usage from the delta between consecutive
|
|
23
34
|
* `os.cpus()` snapshots — no blocking sleep. The first sample reports usage
|
|
@@ -35,34 +46,72 @@ function createCpuSampler(readCpus = () => node_os_1.default.cpus(), readLoad =
|
|
|
35
46
|
const idleDiff = cur.idle - base.idle;
|
|
36
47
|
const totalDiff = cur.total - base.total;
|
|
37
48
|
prev = cur;
|
|
38
|
-
const percent = totalDiff > 0 ?
|
|
49
|
+
const percent = totalDiff > 0 ? percentOf(totalDiff - idleDiff, totalDiff) : 0;
|
|
39
50
|
return { percent, loadAvg1m };
|
|
40
51
|
},
|
|
41
52
|
};
|
|
42
53
|
}
|
|
43
54
|
/**
|
|
44
|
-
*
|
|
55
|
+
* Parse `vm_stat` output into reclaimable ("available") bytes: free +
|
|
56
|
+
* inactive + speculative pages, the same estimate psutil uses on macOS.
|
|
57
|
+
* Returns null when the output is unrecognised.
|
|
58
|
+
*/
|
|
59
|
+
function parseVmStat(stdout) {
|
|
60
|
+
const pageSize = Number(stdout.match(/page size of (\d+) bytes/)?.[1]);
|
|
61
|
+
if (!Number.isFinite(pageSize) || pageSize <= 0)
|
|
62
|
+
return null;
|
|
63
|
+
const pages = (label) => {
|
|
64
|
+
const m = stdout.match(new RegExp(`^Pages ${label}:\\s+(\\d+)`, 'm'));
|
|
65
|
+
return m ? Number(m[1]) : null;
|
|
66
|
+
};
|
|
67
|
+
const free = pages('free');
|
|
68
|
+
const inactive = pages('inactive');
|
|
69
|
+
if (free === null || inactive === null)
|
|
70
|
+
return null;
|
|
71
|
+
return (free + inactive + (pages('speculative') ?? 0)) * pageSize;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Bytes of memory available to new work. On macOS `os.freemem()` counts only
|
|
75
|
+
* never-used pages (the file cache is "used"), which reads ~95% full on any
|
|
76
|
+
* healthy Mac, so the reclaimable estimate comes from `vm_stat` instead. Linux
|
|
77
|
+
* (`MemAvailable`) and Windows report a meaningful value through Node.
|
|
45
78
|
*/
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
79
|
+
async function readAvailableMemory() {
|
|
80
|
+
if (process.platform === 'darwin') {
|
|
81
|
+
try {
|
|
82
|
+
const { stdout } = await execFileAsync('vm_stat', [], { timeout: 2000 });
|
|
83
|
+
const available = parseVmStat(stdout);
|
|
84
|
+
if (available !== null)
|
|
85
|
+
return Math.min(available, node_os_1.default.totalmem());
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Fall through to Node's figure.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return node_os_1.default.freemem();
|
|
92
|
+
}
|
|
93
|
+
/** Memory usage from the total and the available byte count. */
|
|
94
|
+
function collectMemory(availableBytes = node_os_1.default.freemem(), totalBytes = node_os_1.default.totalmem()) {
|
|
95
|
+
const usedBytes = Math.max(0, totalBytes - availableBytes);
|
|
96
|
+
return {
|
|
97
|
+
percent: percentOf(usedBytes, totalBytes),
|
|
98
|
+
usedGb: gb(usedBytes),
|
|
99
|
+
totalGb: gb(totalBytes),
|
|
100
|
+
};
|
|
54
101
|
}
|
|
55
102
|
/**
|
|
56
|
-
*
|
|
103
|
+
* Disk usage via fs.statfs, computed like `df`: used / (used + available to
|
|
104
|
+
* unprivileged users), so blocks reserved for root don't hide a full disk.
|
|
57
105
|
*/
|
|
58
106
|
async function collectDisk(path) {
|
|
59
107
|
const stats = await node_fs_1.promises.statfs(path);
|
|
60
108
|
const totalBytes = stats.blocks * stats.bsize;
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
109
|
+
const usedBytes = (stats.blocks - stats.bfree) * stats.bsize;
|
|
110
|
+
const availBytes = stats.bavail * stats.bsize;
|
|
111
|
+
return {
|
|
112
|
+
percent: percentOf(usedBytes, usedBytes + availBytes),
|
|
113
|
+
usedGb: gb(usedBytes),
|
|
114
|
+
totalGb: gb(totalBytes),
|
|
115
|
+
};
|
|
67
116
|
}
|
|
68
117
|
//# sourceMappingURL=collectors.js.map
|
|
@@ -4,6 +4,7 @@ exports.SystemMonitorNPC = void 0;
|
|
|
4
4
|
const polling_npc_js_1 = require("../../core/polling-npc.js");
|
|
5
5
|
const thresholds_js_1 = require("../../core/thresholds.js");
|
|
6
6
|
const screen_js_1 = require("../../core/screen.js");
|
|
7
|
+
const errors_js_1 = require("../../core/errors.js");
|
|
7
8
|
const collectors_js_1 = require("./collectors.js");
|
|
8
9
|
const defaults_js_1 = require("./defaults.js");
|
|
9
10
|
class SystemMonitorNPC extends polling_npc_js_1.PollingNPC {
|
|
@@ -14,15 +15,21 @@ class SystemMonitorNPC extends polling_npc_js_1.PollingNPC {
|
|
|
14
15
|
cpuSampler = (0, collectors_js_1.createCpuSampler)();
|
|
15
16
|
cpuHistory = [];
|
|
16
17
|
memHistory = [];
|
|
18
|
+
diskErrorLogged = false;
|
|
17
19
|
constructor(config) {
|
|
18
20
|
super({
|
|
19
21
|
...config,
|
|
20
22
|
pollInterval: config.pollInterval ?? defaults_js_1.DEFAULTS.pollInterval,
|
|
21
23
|
});
|
|
22
24
|
this.diskPath = config.diskPath ?? defaults_js_1.DEFAULTS.diskPath;
|
|
23
|
-
this.cpuThreshold = config.cpuThreshold ?? defaults_js_1.DEFAULT_THRESHOLDS
|
|
24
|
-
this.memThreshold = config.memThreshold ?? defaults_js_1.DEFAULT_THRESHOLDS
|
|
25
|
-
this.diskThreshold = config.diskThreshold ?? defaults_js_1.DEFAULT_THRESHOLDS
|
|
25
|
+
this.cpuThreshold = config.cpuThreshold ?? defaults_js_1.DEFAULT_THRESHOLDS.cpu;
|
|
26
|
+
this.memThreshold = config.memThreshold ?? defaults_js_1.DEFAULT_THRESHOLDS.mem;
|
|
27
|
+
this.diskThreshold = config.diskThreshold ?? defaults_js_1.DEFAULT_THRESHOLDS.disk;
|
|
28
|
+
const problem = (0, thresholds_js_1.thresholdProblem)('CPU', this.cpuThreshold) ??
|
|
29
|
+
(0, thresholds_js_1.thresholdProblem)('Memory', this.memThreshold) ??
|
|
30
|
+
(0, thresholds_js_1.thresholdProblem)('Disk', this.diskThreshold);
|
|
31
|
+
if (problem)
|
|
32
|
+
throw new errors_js_1.NpcConfigError(problem);
|
|
26
33
|
}
|
|
27
34
|
get name() {
|
|
28
35
|
return 'system-monitor';
|
|
@@ -33,9 +40,6 @@ class SystemMonitorNPC extends polling_npc_js_1.PollingNPC {
|
|
|
33
40
|
get archetype() {
|
|
34
41
|
return 'service';
|
|
35
42
|
}
|
|
36
|
-
get heartbeatSeconds() {
|
|
37
|
-
return 30;
|
|
38
|
-
}
|
|
39
43
|
get behavior() {
|
|
40
44
|
return { idle: 'wander' };
|
|
41
45
|
}
|
|
@@ -49,19 +53,20 @@ class SystemMonitorNPC extends polling_npc_js_1.PollingNPC {
|
|
|
49
53
|
}
|
|
50
54
|
async collect() {
|
|
51
55
|
const cpu = this.cpuSampler.sample();
|
|
52
|
-
const mem = (0, collectors_js_1.collectMemory)();
|
|
53
|
-
const disk = await
|
|
56
|
+
const mem = (0, collectors_js_1.collectMemory)(await (0, collectors_js_1.readAvailableMemory)());
|
|
57
|
+
const disk = await this.readDisk();
|
|
54
58
|
const cpuStatus = (0, thresholds_js_1.evaluateThreshold)(cpu.percent, this.cpuThreshold);
|
|
55
59
|
const memStatus = (0, thresholds_js_1.evaluateThreshold)(mem.percent, this.memThreshold);
|
|
56
|
-
|
|
60
|
+
// An unreadable disk (unmounted volume, bad --disk-path) is a warning, not a crash.
|
|
61
|
+
const diskStatus = disk
|
|
62
|
+
? (0, thresholds_js_1.evaluateThreshold)(disk.percent, this.diskThreshold)
|
|
63
|
+
: 'warning';
|
|
57
64
|
const status = (0, thresholds_js_1.worstStatus)([cpuStatus, memStatus, diskStatus]);
|
|
58
|
-
const mood = status === 'critical'
|
|
59
|
-
? 'stressed'
|
|
60
|
-
: status === 'warning'
|
|
61
|
-
? 'concerned'
|
|
62
|
-
: 'happy';
|
|
63
65
|
(0, screen_js_1.pushHistory)(this.cpuHistory, cpu.percent);
|
|
64
66
|
(0, screen_js_1.pushHistory)(this.memHistory, mem.percent);
|
|
67
|
+
const diskText = disk ? `${disk.percent}%` : 'n/a';
|
|
68
|
+
// Windows has no load average (os.loadavg() is always zeros there).
|
|
69
|
+
const loadText = process.platform === 'win32' ? 'n/a' : cpu.loadAvg1m.toFixed(2);
|
|
65
70
|
const screenData = {
|
|
66
71
|
layout: 'multi-metric',
|
|
67
72
|
values: [
|
|
@@ -77,29 +82,48 @@ class SystemMonitorNPC extends polling_npc_js_1.PollingNPC {
|
|
|
77
82
|
color: (0, screen_js_1.statusColor)(memStatus),
|
|
78
83
|
sparkline: [...this.memHistory],
|
|
79
84
|
},
|
|
80
|
-
{ label: 'DISK', value:
|
|
81
|
-
{ label: 'LOAD', value:
|
|
85
|
+
{ label: 'DISK', value: diskText, color: (0, screen_js_1.statusColor)(diskStatus) },
|
|
86
|
+
{ label: 'LOAD', value: loadText, color: 'gray' },
|
|
82
87
|
],
|
|
83
88
|
};
|
|
89
|
+
const metrics = {
|
|
90
|
+
cpu_percent: cpu.percent,
|
|
91
|
+
mem_percent: mem.percent,
|
|
92
|
+
mem_used_gb: mem.usedGb,
|
|
93
|
+
mem_total_gb: mem.totalGb,
|
|
94
|
+
load_avg_1m: cpu.loadAvg1m,
|
|
95
|
+
};
|
|
96
|
+
if (disk) {
|
|
97
|
+
metrics['disk_percent'] = disk.percent;
|
|
98
|
+
metrics['disk_used_gb'] = disk.usedGb;
|
|
99
|
+
metrics['disk_total_gb'] = disk.totalGb;
|
|
100
|
+
}
|
|
84
101
|
return {
|
|
85
102
|
status,
|
|
86
|
-
mood,
|
|
87
|
-
message: `CPU: ${cpu.percent}% | Mem: ${mem.percent}% | Disk: ${disk
|
|
103
|
+
mood: (0, thresholds_js_1.moodForStatus)(status),
|
|
104
|
+
message: `CPU: ${cpu.percent}% | Mem: ${mem.percent}% | Disk: ${disk ? diskText : 'unavailable'}`,
|
|
88
105
|
screenData,
|
|
89
106
|
// When strained, go find a desk and get to work; otherwise wander.
|
|
90
107
|
behavior: status !== 'healthy' ? { busy: 'seek-desk' } : undefined,
|
|
91
|
-
metrics
|
|
92
|
-
cpu_percent: cpu.percent,
|
|
93
|
-
mem_percent: mem.percent,
|
|
94
|
-
mem_used_gb: mem.usedGb,
|
|
95
|
-
mem_total_gb: mem.totalGb,
|
|
96
|
-
disk_percent: disk.percent,
|
|
97
|
-
disk_used_gb: disk.usedGb,
|
|
98
|
-
disk_total_gb: disk.totalGb,
|
|
99
|
-
load_avg_1m: cpu.loadAvg1m,
|
|
100
|
-
},
|
|
108
|
+
metrics,
|
|
101
109
|
};
|
|
102
110
|
}
|
|
111
|
+
async readDisk() {
|
|
112
|
+
try {
|
|
113
|
+
const disk = await (0, collectors_js_1.collectDisk)(this.diskPath);
|
|
114
|
+
this.diskErrorLogged = false;
|
|
115
|
+
return disk;
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
if (!this.diskErrorLogged) {
|
|
119
|
+
this.diskErrorLogged = true;
|
|
120
|
+
this.logger.warn(`Cannot read disk usage for ${this.diskPath}`, {
|
|
121
|
+
error: err instanceof Error ? err.message : String(err),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
103
127
|
}
|
|
104
128
|
exports.SystemMonitorNPC = SystemMonitorNPC;
|
|
105
129
|
//# sourceMappingURL=index.js.map
|
|
@@ -4,10 +4,31 @@ exports.extractTitle = extractTitle;
|
|
|
4
4
|
exports.statusFromHttp = statusFromHttp;
|
|
5
5
|
exports.basicCheck = basicCheck;
|
|
6
6
|
exports.hostnameOf = hostnameOf;
|
|
7
|
-
|
|
7
|
+
const http_js_1 = require("../../core/http.js");
|
|
8
|
+
const text_js_1 = require("../../core/text.js");
|
|
9
|
+
/** Bytes of HTML read to find the <title> (it lives in <head>). */
|
|
10
|
+
const MAX_HTML_BYTES = 512 * 1024;
|
|
11
|
+
const ENTITIES = {
|
|
12
|
+
amp: '&',
|
|
13
|
+
lt: '<',
|
|
14
|
+
gt: '>',
|
|
15
|
+
quot: '"',
|
|
16
|
+
apos: "'",
|
|
17
|
+
nbsp: ' ',
|
|
18
|
+
};
|
|
19
|
+
function decodeEntities(s) {
|
|
20
|
+
return s.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (m, e) => {
|
|
21
|
+
if (e[0] === '#') {
|
|
22
|
+
const code = e[1] === 'x' || e[1] === 'X' ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10);
|
|
23
|
+
return Number.isFinite(code) && code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : m;
|
|
24
|
+
}
|
|
25
|
+
return ENTITIES[e.toLowerCase()] ?? m;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Extract the <title> text from (untrusted) HTML: entities decoded, sanitized, ≤80 chars. */
|
|
8
29
|
function extractTitle(html) {
|
|
9
30
|
const m = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
10
|
-
return m ?
|
|
31
|
+
return m ? (0, text_js_1.sanitizeText)(decodeEntities(m[1]), 80) : '';
|
|
11
32
|
}
|
|
12
33
|
/** Map an HTTP status to NPC status: 2xx/3xx ok, 4xx/timeout warn, 5xx/refused crit. */
|
|
13
34
|
function statusFromHttp(code, timedOut = false) {
|
|
@@ -21,17 +42,15 @@ function statusFromHttp(code, timedOut = false) {
|
|
|
21
42
|
}
|
|
22
43
|
/** Fetch a page and extract HTTP status + <title> (basic, no browser). */
|
|
23
44
|
async function basicCheck(url, timeoutMs = 8000) {
|
|
24
|
-
const controller = new AbortController();
|
|
25
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
26
45
|
try {
|
|
27
46
|
const res = await fetch(url, {
|
|
28
|
-
signal:
|
|
29
|
-
headers: { 'User-Agent':
|
|
47
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
48
|
+
headers: { 'User-Agent': http_js_1.NPC_USER_AGENT },
|
|
30
49
|
redirect: 'follow',
|
|
31
50
|
});
|
|
32
51
|
let title = '';
|
|
33
52
|
try {
|
|
34
|
-
title = extractTitle(await res.text
|
|
53
|
+
title = extractTitle((await (0, http_js_1.readTextCapped)(res, MAX_HTML_BYTES)).text);
|
|
35
54
|
}
|
|
36
55
|
catch {
|
|
37
56
|
// body unavailable; leave title blank
|
|
@@ -39,20 +58,17 @@ async function basicCheck(url, timeoutMs = 8000) {
|
|
|
39
58
|
return { status: res.status, title, ok: res.ok, timedOut: false };
|
|
40
59
|
}
|
|
41
60
|
catch (err) {
|
|
42
|
-
const timedOut = err instanceof Error && err.name === 'AbortError';
|
|
61
|
+
const timedOut = err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError');
|
|
43
62
|
return { status: 0, title: '', ok: false, timedOut };
|
|
44
63
|
}
|
|
45
|
-
finally {
|
|
46
|
-
clearTimeout(timer);
|
|
47
|
-
}
|
|
48
64
|
}
|
|
49
|
-
/**
|
|
65
|
+
/** Hostname of a URL for labels and service ids (never credentials or path). */
|
|
50
66
|
function hostnameOf(url) {
|
|
51
67
|
try {
|
|
52
|
-
return new URL(url).hostname;
|
|
68
|
+
return new URL(url).hostname || 'page';
|
|
53
69
|
}
|
|
54
70
|
catch {
|
|
55
|
-
return url
|
|
71
|
+
return 'invalid-url';
|
|
56
72
|
}
|
|
57
73
|
}
|
|
58
74
|
//# sourceMappingURL=basic-mode.js.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.findChrome = findChrome;
|
|
4
|
+
exports.chromeArgs = chromeArgs;
|
|
5
|
+
exports.runningAsRoot = runningAsRoot;
|
|
4
6
|
exports.loadPuppeteer = loadPuppeteer;
|
|
5
7
|
exports.browserConnected = browserConnected;
|
|
6
8
|
const node_fs_1 = require("node:fs");
|
|
@@ -21,7 +23,11 @@ const LINUX_BINARIES = [
|
|
|
21
23
|
];
|
|
22
24
|
function which(bin) {
|
|
23
25
|
try {
|
|
24
|
-
const out = (0, node_child_process_1.execFileSync)('which', [bin], {
|
|
26
|
+
const out = (0, node_child_process_1.execFileSync)('which', [bin], {
|
|
27
|
+
encoding: 'utf-8',
|
|
28
|
+
timeout: 2000,
|
|
29
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
30
|
+
}).trim();
|
|
25
31
|
return out || null;
|
|
26
32
|
}
|
|
27
33
|
catch {
|
|
@@ -54,6 +60,22 @@ function findChrome(explicit) {
|
|
|
54
60
|
}
|
|
55
61
|
return null;
|
|
56
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Chrome launch flags. The watched page is untrusted content, so Chrome's
|
|
65
|
+
* sandbox stays on unless it can't work: as root on Linux (Chrome refuses to
|
|
66
|
+
* start sandboxed, typical in containers) or when explicitly disabled.
|
|
67
|
+
*/
|
|
68
|
+
function chromeArgs(opts) {
|
|
69
|
+
const args = ['--disable-gpu', '--hide-scrollbars', '--mute-audio', '--no-first-run'];
|
|
70
|
+
const sandboxed = opts.sandbox && !opts.isRoot;
|
|
71
|
+
if (!sandboxed)
|
|
72
|
+
args.push('--no-sandbox', '--disable-setuid-sandbox');
|
|
73
|
+
return { args, sandboxed };
|
|
74
|
+
}
|
|
75
|
+
/** True when this process runs as root (POSIX only). */
|
|
76
|
+
function runningAsRoot() {
|
|
77
|
+
return typeof process.getuid === 'function' && process.getuid() === 0;
|
|
78
|
+
}
|
|
57
79
|
/**
|
|
58
80
|
* Load the optional `puppeteer-core` peer dependency without a static import,
|
|
59
81
|
* so the package builds and runs when it isn't installed. Returns null if the
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULTS = void 0;
|
|
3
|
+
exports.WAIT_UNTIL = exports.WATCH_MODES = exports.LIMITS = exports.DEFAULTS = void 0;
|
|
4
4
|
exports.parseDimensions = parseDimensions;
|
|
5
|
+
exports.screenSizeProblem = screenSizeProblem;
|
|
6
|
+
exports.viewportProblem = viewportProblem;
|
|
5
7
|
exports.DEFAULTS = {
|
|
6
8
|
pollInterval: 60000, // 60s
|
|
7
9
|
minPollInterval: 15000, // floor
|
|
8
10
|
navTimeoutMs: 15000,
|
|
11
|
+
/** Extra time for screenshot + encode before a capture counts as hung. */
|
|
12
|
+
captureGraceMs: 15000,
|
|
13
|
+
/** Wait before relaunching Chrome after it failed to start. */
|
|
14
|
+
relaunchBackoffMs: 10 * 60 * 1000,
|
|
9
15
|
maxColors: 32,
|
|
10
16
|
/** Polls to keep the 'changed' sub-status active after a page change. */
|
|
11
17
|
changedPolls: 2,
|
|
@@ -18,6 +24,21 @@ exports.DEFAULTS = {
|
|
|
18
24
|
scaleDivisor: 8,
|
|
19
25
|
waitUntil: 'networkidle2',
|
|
20
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* Size limits. The pixel payload is base64(W×H) and the API caps a screen value
|
|
29
|
+
* at 220 KB, so W×H must stay under ~165k; 320×240 leaves headroom. The
|
|
30
|
+
* viewport bound keeps Chrome's raster (and the PNG decode) to sane memory.
|
|
31
|
+
*/
|
|
32
|
+
exports.LIMITS = {
|
|
33
|
+
screenMin: 16,
|
|
34
|
+
screenMaxSide: 320,
|
|
35
|
+
screenMaxPixels: 320 * 240,
|
|
36
|
+
viewportMin: 64,
|
|
37
|
+
viewportMax: 4096,
|
|
38
|
+
};
|
|
39
|
+
exports.WATCH_MODES = ['auto', 'screenshot', 'basic'];
|
|
40
|
+
/** puppeteer `waitUntil` values. */
|
|
41
|
+
exports.WAIT_UNTIL = ['load', 'domcontentloaded', 'networkidle0', 'networkidle2'];
|
|
21
42
|
/** Parse a "WxH" string into a {width, height} pair, or null if malformed. */
|
|
22
43
|
function parseDimensions(s) {
|
|
23
44
|
const m = s
|
|
@@ -26,6 +47,27 @@ function parseDimensions(s) {
|
|
|
26
47
|
.match(/^(\d+)x(\d+)$/);
|
|
27
48
|
if (!m)
|
|
28
49
|
return null;
|
|
29
|
-
|
|
50
|
+
const width = parseInt(m[1], 10);
|
|
51
|
+
const height = parseInt(m[2], 10);
|
|
52
|
+
return width > 0 && height > 0 ? { width, height } : null;
|
|
53
|
+
}
|
|
54
|
+
/** Why a TV resolution is unusable, or null when it is fine. */
|
|
55
|
+
function screenSizeProblem(w, h) {
|
|
56
|
+
const { screenMin, screenMaxSide, screenMaxPixels } = exports.LIMITS;
|
|
57
|
+
if (!Number.isInteger(w) || !Number.isInteger(h) || w < screenMin || h < screenMin) {
|
|
58
|
+
return `--screen must be at least ${screenMin}x${screenMin}`;
|
|
59
|
+
}
|
|
60
|
+
if (w > screenMaxSide || h > screenMaxSide || w * h > screenMaxPixels) {
|
|
61
|
+
return `--screen must fit within ${screenMaxSide}x${screenMaxSide} and ${screenMaxPixels} pixels (e.g. 320x240)`;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/** Why a browser viewport is unusable, or null when it is fine. */
|
|
66
|
+
function viewportProblem(w, h) {
|
|
67
|
+
const { viewportMin, viewportMax } = exports.LIMITS;
|
|
68
|
+
const ok = (n) => Number.isInteger(n) && n >= viewportMin && n <= viewportMax;
|
|
69
|
+
return ok(w) && ok(h)
|
|
70
|
+
? null
|
|
71
|
+
: `--viewport sides must be between ${viewportMin} and ${viewportMax} pixels`;
|
|
30
72
|
}
|
|
31
73
|
//# sourceMappingURL=defaults.js.map
|