@page-scanner/cli 0.1.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/LICENSE +201 -0
- package/README.md +217 -0
- package/dist/api.d.ts +87 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +132 -0
- package/dist/api.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +49 -0
- package/dist/bin.js.map +1 -0
- package/dist/bridge/server.d.ts +97 -0
- package/dist/bridge/server.d.ts.map +1 -0
- package/dist/bridge/server.js +329 -0
- package/dist/bridge/server.js.map +1 -0
- package/dist/bridge/tokens.d.ts +10 -0
- package/dist/bridge/tokens.d.ts.map +1 -0
- package/dist/bridge/tokens.js +15 -0
- package/dist/bridge/tokens.js.map +1 -0
- package/dist/cli/args.d.ts +63 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +388 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/commands.d.ts +8 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +234 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/format.d.ts +22 -0
- package/dist/cli/format.d.ts.map +1 -0
- package/dist/cli/format.js +152 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +54 -0
- package/dist/config.js.map +1 -0
- package/dist/daemon/client.d.ts +28 -0
- package/dist/daemon/client.d.ts.map +1 -0
- package/dist/daemon/client.js +139 -0
- package/dist/daemon/client.js.map +1 -0
- package/dist/daemon/rpc-server.d.ts +31 -0
- package/dist/daemon/rpc-server.d.ts.map +1 -0
- package/dist/daemon/rpc-server.js +254 -0
- package/dist/daemon/rpc-server.js.map +1 -0
- package/dist/daemon/run.d.ts +20 -0
- package/dist/daemon/run.d.ts.map +1 -0
- package/dist/daemon/run.js +129 -0
- package/dist/daemon/run.js.map +1 -0
- package/dist/daemon/self.d.ts +25 -0
- package/dist/daemon/self.d.ts.map +1 -0
- package/dist/daemon/self.js +113 -0
- package/dist/daemon/self.js.map +1 -0
- package/dist/daemon/state.d.ts +25 -0
- package/dist/daemon/state.d.ts.map +1 -0
- package/dist/daemon/state.js +116 -0
- package/dist/daemon/state.js.map +1 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +83 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +13 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +71 -0
- package/dist/output.js.map +1 -0
- package/dist/protocol.d.ts +144 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +128 -0
- package/dist/protocol.js.map +1 -0
- package/dist/version.d.ts +11 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +11 -0
- package/dist/version.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning results into the text a person reads.
|
|
3
|
+
*
|
|
4
|
+
* Every function here is pure: data in, a string out, no writing and no
|
|
5
|
+
* colour. A command can then be tested on what it computed rather than on what
|
|
6
|
+
* it printed, and the `--json` path skips this file entirely.
|
|
7
|
+
*
|
|
8
|
+
* The house style for the tables is "plain enough to pipe". No box drawing, no
|
|
9
|
+
* separator row, no pipes, and no trailing whitespace on any line, because the
|
|
10
|
+
* next thing to read this output is as likely to be `awk` as a person.
|
|
11
|
+
*/
|
|
12
|
+
import type { ConnectedBrowser } from '../bridge/server.js';
|
|
13
|
+
import type { TabSummary, TruncationReport, WindowSummary } from '../protocol.js';
|
|
14
|
+
/** A plain column-aligned table. Returns '' for an empty rows array. */
|
|
15
|
+
export declare function table(headers: string[], rows: string[][]): string;
|
|
16
|
+
export declare function formatBrowsers(browsers: ConnectedBrowser[]): string;
|
|
17
|
+
export declare function formatTabs(windows: WindowSummary[], tabs: TabSummary[]): string;
|
|
18
|
+
/** One line, for stderr, when a capture stopped short of the whole page. */
|
|
19
|
+
export declare function formatTruncation(report: TruncationReport): string;
|
|
20
|
+
/** e.g. "3s", "2m 04s". */
|
|
21
|
+
export declare function formatDuration(ms: number): string;
|
|
22
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/cli/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAiBlF,wEAAwE;AACxE,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,MAAM,CA+BjE;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAWnE;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,MAAM,CAmC/E;AAED,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CASjE;AAED,2BAA2B;AAC3B,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CASjD"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much room a tab title gets.
|
|
3
|
+
*
|
|
4
|
+
* The terminal's real width is deliberately not consulted. A piped run has no
|
|
5
|
+
* width to read (`process.stdout.columns` is undefined when stdout is not a
|
|
6
|
+
* TTY), so the choice would be between one layout when a person looks at it
|
|
7
|
+
* and another when a script does, which is both harder to read and harder to
|
|
8
|
+
* assert on. 60 is a budget in code points, not in terminal columns: a title
|
|
9
|
+
* of CJK or emoji occupies up to twice its length on screen, so the 80-column
|
|
10
|
+
* claim below holds for a Latin title and is a guideline for anything else.
|
|
11
|
+
* Measuring real display width would mean carrying an East Asian Width table,
|
|
12
|
+
* which is not worth it to keep a tab listing tidy.
|
|
13
|
+
*/
|
|
14
|
+
const TITLE_WIDTH = 60;
|
|
15
|
+
/** A plain column-aligned table. Returns '' for an empty rows array. */
|
|
16
|
+
export function table(headers, rows) {
|
|
17
|
+
if (rows.length === 0)
|
|
18
|
+
return '';
|
|
19
|
+
// A row is allowed to be shorter or longer than the header row, so the
|
|
20
|
+
// column count is whatever the widest thing passed in has.
|
|
21
|
+
const columns = Math.max(headers.length, ...rows.map((row) => row.length));
|
|
22
|
+
const widths = [];
|
|
23
|
+
for (let index = 0; index < columns; index += 1) {
|
|
24
|
+
let width = headers[index]?.length ?? 0;
|
|
25
|
+
for (const row of rows)
|
|
26
|
+
width = Math.max(width, row[index]?.length ?? 0);
|
|
27
|
+
widths[index] = width;
|
|
28
|
+
}
|
|
29
|
+
const line = (cells) => {
|
|
30
|
+
const padded = [];
|
|
31
|
+
for (let index = 0; index < columns; index += 1) {
|
|
32
|
+
const cell = cells[index] ?? '';
|
|
33
|
+
// The last column is never padded, and the line is trimmed at the end on
|
|
34
|
+
// top of that: a row whose tail cells are empty must not leave invisible
|
|
35
|
+
// whitespace behind for whatever is reading the pipe.
|
|
36
|
+
padded.push(index === columns - 1 ? cell : cell.padEnd(widths[index] ?? 0));
|
|
37
|
+
}
|
|
38
|
+
return padded.join(' ').trimEnd();
|
|
39
|
+
};
|
|
40
|
+
// A caller that passes no headers, or only blank ones, wants rows alone
|
|
41
|
+
// rather than a leading empty line.
|
|
42
|
+
const body = rows.map(line);
|
|
43
|
+
return headers.some((header) => header.length > 0)
|
|
44
|
+
? [line(headers), ...body].join('\n')
|
|
45
|
+
: body.join('\n');
|
|
46
|
+
}
|
|
47
|
+
export function formatBrowsers(browsers) {
|
|
48
|
+
if (browsers.length === 0)
|
|
49
|
+
return 'No browser is connected.';
|
|
50
|
+
return table(['LABEL', 'BROWSER ID', 'VERSION', 'CONNECTED'], browsers.map((browser) => [
|
|
51
|
+
browser.label,
|
|
52
|
+
browser.browserId,
|
|
53
|
+
browser.extensionVersion,
|
|
54
|
+
formatDuration(browser.connectedForMs),
|
|
55
|
+
]));
|
|
56
|
+
}
|
|
57
|
+
export function formatTabs(windows, tabs) {
|
|
58
|
+
if (windows.length === 0 && tabs.length === 0)
|
|
59
|
+
return 'No tabs are open.';
|
|
60
|
+
// Seed the groups from the window list so a window with nothing in it still
|
|
61
|
+
// shows up, then file each tab. A tab whose window was not listed gets a
|
|
62
|
+
// group of its own rather than being dropped: the two lists come off the
|
|
63
|
+
// wire separately and the tab is the thing the user asked for.
|
|
64
|
+
const grouped = new Map();
|
|
65
|
+
for (const window of windows)
|
|
66
|
+
grouped.set(window.windowId, []);
|
|
67
|
+
for (const tab of tabs) {
|
|
68
|
+
const group = grouped.get(tab.windowId);
|
|
69
|
+
if (group)
|
|
70
|
+
group.push(tab);
|
|
71
|
+
else
|
|
72
|
+
grouped.set(tab.windowId, [tab]);
|
|
73
|
+
}
|
|
74
|
+
const summaries = new Map(windows.map((window) => [window.windowId, window]));
|
|
75
|
+
const blocks = [];
|
|
76
|
+
for (const [windowId, group] of grouped) {
|
|
77
|
+
const lines = [windowHeading(windowId, summaries.get(windowId), group.length)];
|
|
78
|
+
const body = table(['', 'TAB', 'TITLE', 'URL'], group.map((tab) => [
|
|
79
|
+
tab.active ? '*' : '',
|
|
80
|
+
String(tab.tabId),
|
|
81
|
+
truncate(tab.title.trim() || '(untitled)', TITLE_WIDTH),
|
|
82
|
+
// The URL is the last column and is never cut. It is the one cell a
|
|
83
|
+
// person is likely to copy, and a shortened one is worse than a long
|
|
84
|
+
// line the terminal wraps itself.
|
|
85
|
+
tab.url,
|
|
86
|
+
]));
|
|
87
|
+
if (body)
|
|
88
|
+
lines.push(body);
|
|
89
|
+
blocks.push(lines.join('\n'));
|
|
90
|
+
}
|
|
91
|
+
return blocks.join('\n\n');
|
|
92
|
+
}
|
|
93
|
+
/** One line, for stderr, when a capture stopped short of the whole page. */
|
|
94
|
+
export function formatTruncation(report) {
|
|
95
|
+
const measured = `${px(report.requestedWidth)}x${px(report.requestedHeight)}`;
|
|
96
|
+
const captured = `${px(report.width)}x${px(report.height)}`;
|
|
97
|
+
const head = `Truncated: the page measured ${measured} CSS px, captured ${captured}.`;
|
|
98
|
+
// The sentence is quoted, not paraphrased. The extension owns the ceiling
|
|
99
|
+
// that caused this and is the only side that knows what it is, so restating
|
|
100
|
+
// it here would be a second copy of a limit that lives over there.
|
|
101
|
+
const message = report.message.trim();
|
|
102
|
+
return message ? `${head} ${message}` : head;
|
|
103
|
+
}
|
|
104
|
+
/** e.g. "3s", "2m 04s". */
|
|
105
|
+
export function formatDuration(ms) {
|
|
106
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
107
|
+
return '0s';
|
|
108
|
+
const total = Math.round(ms / 1000);
|
|
109
|
+
if (total < 60)
|
|
110
|
+
return `${total}s`;
|
|
111
|
+
const minutes = Math.floor(total / 60);
|
|
112
|
+
if (minutes < 60)
|
|
113
|
+
return `${minutes}m ${pad2(total % 60)}s`;
|
|
114
|
+
// A browser left connected overnight would otherwise read as "734m 09s".
|
|
115
|
+
// Past an hour the seconds carry no information anyone acts on.
|
|
116
|
+
return `${Math.floor(minutes / 60)}h ${pad2(minutes % 60)}m`;
|
|
117
|
+
}
|
|
118
|
+
function windowHeading(windowId, summary, shown) {
|
|
119
|
+
const parts = [];
|
|
120
|
+
if (summary?.focused)
|
|
121
|
+
parts.push('focused');
|
|
122
|
+
if (summary && summary.windowType)
|
|
123
|
+
parts.push(summary.windowType);
|
|
124
|
+
// The window's own figure, which the extension builds in the same loop that
|
|
125
|
+
// produces the rows, so it should equal them. Printing it rather than the
|
|
126
|
+
// row count means a disagreement shows up here instead of being hidden.
|
|
127
|
+
const count = summary?.tabCount ?? shown;
|
|
128
|
+
parts.push(count === 1 ? '1 tab' : `${count} tabs`);
|
|
129
|
+
return `Window ${windowId} (${parts.join(', ')})`;
|
|
130
|
+
}
|
|
131
|
+
function truncate(text, width) {
|
|
132
|
+
// Cut by code point, not by `slice`. A page title with an emoji in it is
|
|
133
|
+
// ordinary, and slicing UTF-16 code units can land between a surrogate pair
|
|
134
|
+
// and emit a lone surrogate, which a terminal draws as a replacement glyph.
|
|
135
|
+
const characters = Array.from(text);
|
|
136
|
+
if (characters.length <= width)
|
|
137
|
+
return text;
|
|
138
|
+
// One character short of the width, so the ellipsis lands inside it, and
|
|
139
|
+
// trimmed so it is not hung off the end of a space.
|
|
140
|
+
return `${characters
|
|
141
|
+
.slice(0, width - 1)
|
|
142
|
+
.join('')
|
|
143
|
+
.trimEnd()}…`;
|
|
144
|
+
}
|
|
145
|
+
/** A CSS pixel measurement can be fractional; nobody wants to read that. */
|
|
146
|
+
function px(value) {
|
|
147
|
+
return String(Math.round(value));
|
|
148
|
+
}
|
|
149
|
+
function pad2(value) {
|
|
150
|
+
return String(value).padStart(2, '0');
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/cli/format.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const DEFAULT_PORT = 45711;
|
|
2
|
+
export interface PairingConfig {
|
|
3
|
+
port: number;
|
|
4
|
+
token: string;
|
|
5
|
+
}
|
|
6
|
+
/** `PAGE_SCANNER_HOME` exists so a test can point the whole thing at a tmpdir. */
|
|
7
|
+
export declare function configDir(): string;
|
|
8
|
+
export declare function configPath(): string;
|
|
9
|
+
export declare function readConfig(): PairingConfig;
|
|
10
|
+
/** Creates a pairing if there is none, and returns whatever is now on disk. */
|
|
11
|
+
export declare function ensureConfig({ port, rotate, }?: {
|
|
12
|
+
port?: number;
|
|
13
|
+
rotate?: boolean;
|
|
14
|
+
}): PairingConfig;
|
|
15
|
+
/** True on every platform that actually enforces the 0600 above. */
|
|
16
|
+
export declare function configFileIsOwnerOnly(): boolean;
|
|
17
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,YAAY,QAAQ,CAAC;AAElC,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,kFAAkF;AAClF,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED,wBAAgB,UAAU,IAAI,aAAa,CAW1C;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,EAC3B,IAAI,EACJ,MAAc,GACf,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,aAAa,CAU1D;AAED,oEAAoE;AACpE,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the pairing lives: `~/.page-scanner/config.json`, mode 0600.
|
|
3
|
+
*
|
|
4
|
+
* The token is the only thing standing between a local agent and a scan of any
|
|
5
|
+
* tab the user has open, so the file is owner-readable and nothing else. On
|
|
6
|
+
* Windows that mode is a no-op: NTFS does not implement it, and the file is
|
|
7
|
+
* readable by anything running as the user. The README says so rather than the
|
|
8
|
+
* `pair` output claiming a guarantee the platform does not give.
|
|
9
|
+
*
|
|
10
|
+
* The shape is the one page-scanner-mcp 0.1 wrote, so a browser paired against
|
|
11
|
+
* that version keeps working.
|
|
12
|
+
*/
|
|
13
|
+
import { randomBytes } from 'node:crypto';
|
|
14
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
export const DEFAULT_PORT = 45711;
|
|
18
|
+
/** `PAGE_SCANNER_HOME` exists so a test can point the whole thing at a tmpdir. */
|
|
19
|
+
export function configDir() {
|
|
20
|
+
return process.env.PAGE_SCANNER_HOME ?? join(homedir(), '.page-scanner');
|
|
21
|
+
}
|
|
22
|
+
export function configPath() {
|
|
23
|
+
return join(configDir(), 'config.json');
|
|
24
|
+
}
|
|
25
|
+
export function readConfig() {
|
|
26
|
+
try {
|
|
27
|
+
const raw = JSON.parse(readFileSync(configPath(), 'utf8'));
|
|
28
|
+
const record = typeof raw === 'object' && raw !== null ? raw : {};
|
|
29
|
+
return {
|
|
30
|
+
port: Number(record.port) || DEFAULT_PORT,
|
|
31
|
+
token: typeof record.token === 'string' ? record.token : '',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { port: DEFAULT_PORT, token: '' };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Creates a pairing if there is none, and returns whatever is now on disk. */
|
|
39
|
+
export function ensureConfig({ port, rotate = false, } = {}) {
|
|
40
|
+
const current = readConfig();
|
|
41
|
+
const next = {
|
|
42
|
+
port: port ?? current.port,
|
|
43
|
+
token: rotate || !current.token ? randomBytes(32).toString('base64url') : current.token,
|
|
44
|
+
};
|
|
45
|
+
mkdirSync(configDir(), { recursive: true, mode: 0o700 });
|
|
46
|
+
writeFileSync(configPath(), `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
47
|
+
chmodSync(configPath(), 0o600);
|
|
48
|
+
return next;
|
|
49
|
+
}
|
|
50
|
+
/** True on every platform that actually enforces the 0600 above. */
|
|
51
|
+
export function configFileIsOwnerOnly() {
|
|
52
|
+
return process.platform !== 'win32';
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC5E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,CAAC;AAOlC,kFAAkF;AAClF,MAAM,UAAU,SAAS;IACvB,OAAO,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,eAAe,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,IAAI,CAAC,SAAS,EAAE,EAAE,aAAa,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,IAAI,CAAC;QACH,MAAM,GAAG,GAAY,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,UAAU,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;QACpE,MAAM,MAAM,GAAG,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC,CAAE,GAA+B,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/F,OAAO;YACL,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,YAAY;YACzC,KAAK,EAAE,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;SAC5D,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAC3C,CAAC;AACH,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,YAAY,CAAC,EAC3B,IAAI,EACJ,MAAM,GAAG,KAAK,MACyB,EAAE;IACzC,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,IAAI,GAAkB;QAC1B,IAAI,EAAE,IAAI,IAAI,OAAO,CAAC,IAAI;QAC1B,KAAK,EAAE,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK;KACxF,CAAC;IACF,SAAS,CAAC,SAAS,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzD,aAAa,CAAC,UAAU,EAAE,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACnF,SAAS,CAAC,UAAU,EAAE,EAAE,KAAK,CAAC,CAAC;IAC/B,OAAO,IAAI,CAAC;AACd,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,qBAAqB;IACnC,OAAO,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC;AACtC,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type DaemonState } from './state.js';
|
|
2
|
+
/** How long to wait for a freshly spawned daemon to publish its state and answer. */
|
|
3
|
+
export declare const DAEMON_START_TIMEOUT_MS = 5000;
|
|
4
|
+
export interface DaemonClient {
|
|
5
|
+
state: DaemonState;
|
|
6
|
+
call<T>(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<T>;
|
|
7
|
+
}
|
|
8
|
+
export interface ConnectOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Starts the daemon. Injectable so a test can assert the spawn decision
|
|
11
|
+
* without putting a process on the machine.
|
|
12
|
+
*/
|
|
13
|
+
startDaemon?: () => void;
|
|
14
|
+
startTimeoutMs?: number;
|
|
15
|
+
/** Injectable so a test does not wait in real time. */
|
|
16
|
+
sleep?: (ms: number) => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Returns a client for a running daemon, starting one if necessary.
|
|
20
|
+
*
|
|
21
|
+
* A daemon left behind by an older install is shut down rather than reused:
|
|
22
|
+
* its RPC surface may not be the one this process expects, and the failure
|
|
23
|
+
* that produces is much harder to read than a restart.
|
|
24
|
+
*/
|
|
25
|
+
export declare function connectDaemon(options?: ConnectOptions): Promise<DaemonClient>;
|
|
26
|
+
/** Returns true when a daemon was running and has been told to stop. */
|
|
27
|
+
export declare function stopDaemon(): Promise<boolean>;
|
|
28
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/daemon/client.ts"],"names":[],"mappings":"AAeA,OAAO,EAKL,KAAK,WAAW,EACjB,MAAM,YAAY,CAAC;AAEpB,qFAAqF;AACrF,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AAG7C,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,WAAW,CAAC;IACnB,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC3F;AAED,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uDAAuD;IACvD,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AA4FD;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,YAAY,CAAC,CAoCvF;AAED,wEAAwE;AACxE,wBAAsB,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,CAkBnD"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Talking to the daemon, and starting one if there is none.
|
|
3
|
+
*
|
|
4
|
+
* Every command except `serve` goes through here. The daemon exists because
|
|
5
|
+
* the extension is the WebSocket *client*: something has to be listening
|
|
6
|
+
* before Chrome can dial in, and a one-shot listener per command would mean
|
|
7
|
+
* the browser reconnecting for every invocation, which takes seconds and
|
|
8
|
+
* wakes a service worker that has just been retired.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { openSync } from 'node:fs';
|
|
12
|
+
import { PageScannerError, toPageScannerError } from '../errors.js';
|
|
13
|
+
import { CLI_VERSION } from '../version.js';
|
|
14
|
+
import { selfCommand } from './self.js';
|
|
15
|
+
import { clearDaemonState, daemonLogPath, processAlive, readDaemonState, } from './state.js';
|
|
16
|
+
/** How long to wait for a freshly spawned daemon to publish its state and answer. */
|
|
17
|
+
export const DAEMON_START_TIMEOUT_MS = 5_000;
|
|
18
|
+
const POLL_INTERVAL_MS = 100;
|
|
19
|
+
const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
|
+
function clientFor(state) {
|
|
21
|
+
return {
|
|
22
|
+
state,
|
|
23
|
+
async call(method, params = {}, timeoutMs = 130_000) {
|
|
24
|
+
let response;
|
|
25
|
+
try {
|
|
26
|
+
response = await fetch(`http://127.0.0.1:${state.rpcPort}/rpc`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
'content-type': 'application/json',
|
|
30
|
+
authorization: `Bearer ${state.secret}`,
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify({ method, params }),
|
|
33
|
+
// The daemon long-polls a scan, so the client's deadline is the real
|
|
34
|
+
// one. Five seconds of slack over the daemon's own timeout, so its
|
|
35
|
+
// considered answer wins the race against this abort.
|
|
36
|
+
signal: AbortSignal.timeout(timeoutMs + 5_000),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new PageScannerError('DAEMON_FAILED', `The daemon on port ${state.rpcPort} did not answer: ${error instanceof Error ? error.message : String(error)}`, 'Run `page-scanner stop` and try again.');
|
|
41
|
+
}
|
|
42
|
+
if (response.status === 401) {
|
|
43
|
+
throw new PageScannerError('DAEMON_FAILED', 'The daemon rejected this process. Its secret changed under us.', 'Run `page-scanner stop` and try again.');
|
|
44
|
+
}
|
|
45
|
+
const envelope = (await response.json());
|
|
46
|
+
if (envelope.ok)
|
|
47
|
+
return envelope.result;
|
|
48
|
+
throw new PageScannerError(envelope.error?.code ?? 'SCAN_FAILED', envelope.error?.message ?? 'The daemon failed without saying why.', envelope.error?.hint);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** A cheap liveness probe that never throws. */
|
|
53
|
+
async function healthy(state) {
|
|
54
|
+
try {
|
|
55
|
+
return await clientFor(state).call('health', {}, 2_000);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Spawns this same program as `serve --daemon`, detached, and lets go of it. */
|
|
62
|
+
function spawnDaemon() {
|
|
63
|
+
const { command, args } = selfCommand();
|
|
64
|
+
let log;
|
|
65
|
+
try {
|
|
66
|
+
log = openSync(daemonLogPath(), 'a');
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw new PageScannerError('DAEMON_FAILED', `Could not open the daemon log at ${daemonLogPath()}: ${error instanceof Error ? error.message : String(error)}`);
|
|
70
|
+
}
|
|
71
|
+
const child = spawn(command, [...args, 'serve', '--daemon'], {
|
|
72
|
+
detached: true,
|
|
73
|
+
stdio: ['ignore', log, log],
|
|
74
|
+
windowsHide: true,
|
|
75
|
+
});
|
|
76
|
+
child.unref();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Returns a client for a running daemon, starting one if necessary.
|
|
80
|
+
*
|
|
81
|
+
* A daemon left behind by an older install is shut down rather than reused:
|
|
82
|
+
* its RPC surface may not be the one this process expects, and the failure
|
|
83
|
+
* that produces is much harder to read than a restart.
|
|
84
|
+
*/
|
|
85
|
+
export async function connectDaemon(options = {}) {
|
|
86
|
+
const sleep = options.sleep ?? realSleep;
|
|
87
|
+
const start = options.startDaemon ?? spawnDaemon;
|
|
88
|
+
const deadlineMs = options.startTimeoutMs ?? DAEMON_START_TIMEOUT_MS;
|
|
89
|
+
const existing = readDaemonState();
|
|
90
|
+
if (existing) {
|
|
91
|
+
const health = await healthy(existing);
|
|
92
|
+
if (health && health.version === CLI_VERSION)
|
|
93
|
+
return clientFor(existing);
|
|
94
|
+
if (health) {
|
|
95
|
+
// Same machine, different version of this CLI. Replace it.
|
|
96
|
+
try {
|
|
97
|
+
await clientFor(existing).call('shutdown', {}, 2_000);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// It is going away either way.
|
|
101
|
+
}
|
|
102
|
+
await sleep(POLL_INTERVAL_MS);
|
|
103
|
+
}
|
|
104
|
+
clearDaemonState();
|
|
105
|
+
}
|
|
106
|
+
start();
|
|
107
|
+
for (let waited = 0; waited < deadlineMs; waited += POLL_INTERVAL_MS) {
|
|
108
|
+
await sleep(POLL_INTERVAL_MS);
|
|
109
|
+
const state = readDaemonState();
|
|
110
|
+
if (!state)
|
|
111
|
+
continue;
|
|
112
|
+
const health = await healthy(state);
|
|
113
|
+
if (health)
|
|
114
|
+
return clientFor(state);
|
|
115
|
+
}
|
|
116
|
+
throw new PageScannerError('DAEMON_FAILED', `The daemon did not come up within ${Math.round(deadlineMs / 1000)}s.`, `Run \`page-scanner serve\` in another terminal to see why, or read ${daemonLogPath()}.`);
|
|
117
|
+
}
|
|
118
|
+
/** Returns true when a daemon was running and has been told to stop. */
|
|
119
|
+
export async function stopDaemon() {
|
|
120
|
+
const state = readDaemonState();
|
|
121
|
+
if (!state) {
|
|
122
|
+
clearDaemonState();
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
await clientFor(state).call('shutdown', {}, 2_000);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
// It may have died without cleaning up after itself. Only report a
|
|
131
|
+
// failure if the process is genuinely still there.
|
|
132
|
+
if (!processAlive(state.pid)) {
|
|
133
|
+
clearDaemonState();
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
throw toPageScannerError(error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/daemon/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ErrorCode } from '../errors.js';
|
|
2
|
+
import type { BridgeServer } from '../bridge/server.js';
|
|
3
|
+
export interface RpcSuccess {
|
|
4
|
+
ok: true;
|
|
5
|
+
result: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface RpcFailure {
|
|
8
|
+
ok: false;
|
|
9
|
+
error: {
|
|
10
|
+
code: ErrorCode;
|
|
11
|
+
message: string;
|
|
12
|
+
hint?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export type RpcResponse = RpcSuccess | RpcFailure;
|
|
16
|
+
export interface RpcServerHandle {
|
|
17
|
+
port: number;
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export interface RpcServerOptions {
|
|
21
|
+
bridge: BridgeServer;
|
|
22
|
+
secret: string;
|
|
23
|
+
/** Called when a `shutdown` request arrives, after it has been answered. */
|
|
24
|
+
onShutdown: () => void;
|
|
25
|
+
/** Called on every authenticated request, so the daemon can stay awake. */
|
|
26
|
+
onActivity?: () => void;
|
|
27
|
+
/** 0 asks the OS for a free port, which is the normal case. */
|
|
28
|
+
port?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function startRpcServer(options: RpcServerOptions): Promise<RpcServerHandle>;
|
|
31
|
+
//# sourceMappingURL=rpc-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc-server.d.ts","sourceRoot":"","sources":["../../src/daemon/rpc-server.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAUlE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAMxD,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,IAAI,CAAC;IACT,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5D;AAED,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,UAAU,CAAC;AAElD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAoMD,wBAAgB,cAAc,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CA4BlF"}
|