@trawlme/cli 3.10.0 → 3.12.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.
@@ -0,0 +1,180 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+ import type { CdpPipe } from './cdp-pipe.js';
3
+ import { type RawCdpCookie, type RawOriginLocalStorage, type StorageState } from './storage-state.js';
4
+ export interface CaptureCounts {
5
+ cookiesCaptured: number;
6
+ cookiesDroppedOutOfScope: number;
7
+ cookiesDroppedInvalid: number;
8
+ originsCaptured: number;
9
+ originsDroppedOutOfScope: number;
10
+ /** An in-scope page's localStorage could not be captured, for one of
11
+ * four reasons — never a CLI bug, which propagates as a failed capture
12
+ * instead (trawl_cli#183 post-cap review finding B, sharpened by the R4
13
+ * fix below): a `readPageOverCdp` rejection — ANY CDP-level rejection
14
+ * scoped to THIS page's `attachToTarget`/`Runtime.evaluate` round-trip,
15
+ * whatever its error class (a timeout because the page's renderer was
16
+ * blocked — a native dialog, a synchronous script, a paused debugger; or
17
+ * a plain rejection because the target closed mid-attach/mid-evaluate,
18
+ * e.g. an OAuth popup the human closed at the wrong instant); the read
19
+ * threw INSIDE the page (e.g. a `SecurityError` on partitioned/sandboxed
20
+ * storage); or `Runtime.evaluate` succeeded with no exception but
21
+ * returned a value that doesn't match the `{name,value}[]` shape asked
22
+ * for (a page tampering with a global to hand back garbage without ever
23
+ * throwing). The discriminant is PROVENANCE, not error type: did the
24
+ * rejection come out of a CDP call scoped to this one page, or out of
25
+ * this file's OWN code running on data CDP already handed back? Only the
26
+ * latter is a CLI bug, and only the latter propagates. None of these
27
+ * four is the same fact as "0 keys": CDP itself was reachable (or the
28
+ * pipe wouldn't still be open), the origin just could not be read.
29
+ * Counted, never the exception text/URL/value itself (a page controls
30
+ * that content). */
31
+ originsUnreadable: number;
32
+ /** The tracked tab was closed before Enter — cookies were still read,
33
+ * localStorage was not (the completion path the issue calls out:
34
+ * "closing the window also completes"). */
35
+ closedEarly: boolean;
36
+ }
37
+ export type CaptureFailureReason = 'non_interactive' | 'no_chrome' | 'launch_failed' | 'capture_failed' | 'process_exited_before_capture' | 'cdp_protocol_error' | 'no_cookies_in_scope';
38
+ export type CaptureResult = {
39
+ ok: true;
40
+ storageState: StorageState;
41
+ counts: CaptureCounts;
42
+ targetDomain: string;
43
+ } | {
44
+ ok: false;
45
+ reason: CaptureFailureReason;
46
+ message: string;
47
+ };
48
+ interface RawCapture {
49
+ cookies: RawCdpCookie[];
50
+ origins: RawOriginLocalStorage[];
51
+ originsUnreadable: number;
52
+ }
53
+ /** Every effectful step captureSession needs — overridable for tests. */
54
+ export interface CaptureDeps {
55
+ detectNonInteractive(): string | null;
56
+ findChrome(): string | null;
57
+ mkdtemp(): string;
58
+ rmSync(dir: string): void;
59
+ launch(chromePath: string, targetUrl: string, userDataDir: string): Promise<{
60
+ proc: ChildProcess;
61
+ cdp: CdpPipe;
62
+ }>;
63
+ /** Resolve once Chrome has opened the target URL as a page target. */
64
+ findPageTarget(cdp: CdpPipe): Promise<string>;
65
+ /**
66
+ * Race the "done" signal: Enter in the terminal, or the tracked tab
67
+ * being closed. Resolves with whether the tab closed before Enter.
68
+ * `cleanup` MUST be wired to the readline Interface's own `'SIGINT'`
69
+ * event (not just `process.on('SIGINT', …)`) — a TTY readline prompt
70
+ * puts stdin in raw mode, which stops Ctrl-C from ever generating a real
71
+ * OS SIGINT in the first place; only the Interface itself observes the
72
+ * raw 0x03 byte and re-synthesizes it as its own `'SIGINT'` event.
73
+ */
74
+ waitForDone(cdp: CdpPipe, targetId: string, cleanup: () => void): Promise<{
75
+ closedEarly: boolean;
76
+ }>;
77
+ /** Read the full (unscoped) cookie jar + open pages' localStorage. */
78
+ readCapture(cdp: CdpPipe, targetUrl: string, closedEarly: boolean): Promise<RawCapture>;
79
+ /**
80
+ * Register `cleanup` to run on every ORDINARY terminating signal this
81
+ * process can still run code for: SIGINT, SIGTERM, and SIGHUP
82
+ * (trawl_cli#183 review finding 1 — SIGTERM/SIGHUP were missing
83
+ * entirely, so `timeout` without `--signal`, a bare `kill <pid>`, most
84
+ * supervisors, and closing the terminal tab mid-login all skipped
85
+ * cleanup and left Chrome running with the profile on disk). SIGKILL is
86
+ * the only signal that cannot be handled at all, by any process, ever —
87
+ * see this function's own default implementation
88
+ * (`registerTerminationHandlers`) for that boundary spelled out.
89
+ * Returns an unregister function.
90
+ */
91
+ onSigint(cleanup: () => void): () => void;
92
+ }
93
+ /**
94
+ * The three real (non-fake) CDP orchestration steps, exported for direct
95
+ * testing against a minimal fake CdpPipe — session-capture.test.ts covers
96
+ * the OUTER guard/race/cleanup logic with these swapped out entirely;
97
+ * session-capture-defaults.test.ts covers these directly instead, since
98
+ * "test the wiring" and "test the wired thing" are two different jobs.
99
+ */
100
+ export declare function findPageTargetDefault(cdp: CdpPipe): Promise<string>;
101
+ export declare function waitForDoneDefault(cdp: CdpPipe, targetId: string, cleanup: () => void): Promise<{
102
+ closedEarly: boolean;
103
+ }>;
104
+ /**
105
+ * @desc The deadline for the ONE CDP command in this file that runs inside
106
+ * a page's own renderer rather than Chrome's browser process:
107
+ * `Runtime.evaluate` reading `window.localStorage`. Deliberately the SAME
108
+ * 30s as `CdpPipe`'s own browser-process default, not a shorter override —
109
+ * a post-cap review (trawl_cli#183) found that an earlier 5s value here cut
110
+ * off a real, terminating synchronous computation (the shape of an
111
+ * anti-bot challenge solve) at ~5s: `originsUnreadable:1` for a page that
112
+ * would have captured cleanly at 7s. There is no value that is provably
113
+ * "long enough" — a renderer truly blocked forever (a native dialog, a
114
+ * paused debugger) costs the same whether the ceiling is 5s or 30s, while
115
+ * a renderer doing bounded work of unknown-but-finite length keeps a
116
+ * chance of finishing for as long as this stays generous. 30s is a
117
+ * JUDGEMENT call on that trade-off, not a provably-correct number — kept
118
+ * equal to the pipe's own default so this file doesn't invent a second
119
+ * number to defend. Passed explicitly to `send()` anyway (rather than
120
+ * relying on the pipe's own default silently matching) so the deadline
121
+ * stays a named, assertable constant here regardless of what the pipe
122
+ * default happens to be. Whatever the outcome, it is always COUNTED and
123
+ * SURFACED (`originsUnreadable`, plus the human-facing ⚠ line in
124
+ * scraps.ts) — never silently swallowed; see `readCaptureDefault` below
125
+ * for the stderr progress line that covers the wait itself.
126
+ */
127
+ export declare const LOCALSTORAGE_READ_TIMEOUT_MS = 30000;
128
+ export declare function readCaptureDefault(cdp: CdpPipe, targetUrl: string, closedEarly: boolean): Promise<RawCapture>;
129
+ /**
130
+ * @desc `rmSync` with a short, bounded, SYNCHRONOUS retry (max ~200ms
131
+ * total). Verified against a real Chrome launch (trawl_cli#183's loopback
132
+ * E2E): even after killing Chrome's whole process group (see
133
+ * chrome-launch.ts's `detached`), a helper subprocess can hold a file
134
+ * under the profile open for a few milliseconds after the kill signal is
135
+ * delivered — a bare `rmSync` right after `kill()` lost that race and
136
+ * silently leaked the temp profile every time. Stays synchronous
137
+ * (`Atomics.wait`, no `await`) because cleanup must be callable from a
138
+ * signal handler right before `process.exit()`.
139
+ */
140
+ export declare function rmSyncWithRetry(dir: string): void;
141
+ /**
142
+ * @desc The default `onSigint` implementation, exported for direct testing
143
+ * (same pattern as `findPageTargetDefault`/`waitForDoneDefault`/
144
+ * `readCaptureDefault` above — this file's own convention keeps "test the
145
+ * wiring" and "test the wired thing" separate). Registers `cleanup` on
146
+ * SIGINT, SIGTERM, AND SIGHUP (trawl_cli#183 review finding 1 — only
147
+ * SIGINT was registered before this; `timeout` without `--signal`, a bare
148
+ * `kill <pid>`, most supervisors, and closing the terminal tab mid-login
149
+ * all default-terminate a process via SIGTERM/SIGHUP, and Node runs no
150
+ * `finally` for a default-terminated process). SIGKILL is the one signal
151
+ * this — or any handler, in any process — cannot observe: the OS tears
152
+ * the process down directly, no userspace code runs at all, so Chrome and
153
+ * the temp profile are left behind whenever that specific signal is what
154
+ * ends this process. That gap is inherent, not something a different
155
+ * signal list here could close.
156
+ */
157
+ export declare function registerTerminationHandlers(cleanup: () => void): () => void;
158
+ /**
159
+ * @desc The same guard `captureSession` runs internally as its very first
160
+ * step, exposed so the command layer can call it BEFORE printing anything
161
+ * about a Chrome window that may never open. Without this, `scraps.ts`
162
+ * printed its "a Chrome window will open" banner unconditionally, ahead of
163
+ * this exact check inside `captureSession` — so a non-interactive run saw
164
+ * that banner immediately followed by "this cannot work headless"
165
+ * (trawl_cli#183 gap). Reads live process state once; pure
166
+ * `detectNonInteractive` stays the single source of truth for both call
167
+ * sites.
168
+ * @returns {string | null} a factual reason when this command cannot
169
+ * proceed in the current environment, or null when it can.
170
+ */
171
+ export declare function checkInteractiveEnvironment(): string | null;
172
+ /**
173
+ * @desc Run the full capture flow for one scrap's target URL.
174
+ * @param {string} targetUrl
175
+ * @param {Partial<CaptureDeps>} depsOverride — for tests; real deps fill in
176
+ * the rest.
177
+ * @returns {Promise<CaptureResult>}
178
+ */
179
+ export declare function captureSession(targetUrl: string, depsOverride?: Partial<CaptureDeps>): Promise<CaptureResult>;
180
+ export {};