harnery 0.31.6 → 0.32.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/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +2 -0
- package/dist/commands/agents.d.ts +0 -9
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +70 -95
- package/dist/commands/browse-session.d.ts +21 -0
- package/dist/commands/browse-session.d.ts.map +1 -0
- package/dist/commands/browse-session.js +157 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +205 -37
- package/dist/commands/checkpoint.js +1 -1
- package/dist/commands/deinit.d.ts.map +1 -1
- package/dist/commands/deinit.js +4 -0
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +40 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +100 -17
- package/dist/commands/init.d.ts +1 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +16 -5
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +2 -3
- package/dist/core/agents/canonical-emit.d.ts +1 -2
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +1 -2
- package/dist/core/agents/cli.js +123 -88
- package/dist/core/agents/coord-client.d.ts +7 -3
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +5 -4
- package/dist/core/agents/finalization.d.ts +68 -0
- package/dist/core/agents/finalization.d.ts.map +1 -0
- package/dist/core/agents/finalization.js +443 -0
- package/dist/core/agents/git-hook.d.ts +51 -0
- package/dist/core/agents/git-hook.d.ts.map +1 -0
- package/dist/core/agents/git-hook.js +118 -0
- package/dist/core/agents/render/prompt-context.d.ts +6 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +26 -13
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +15 -2
- package/dist/core/agents/rules/claim-conflict.js +3 -3
- package/dist/core/agents/rules/commit-conflict.d.ts +15 -6
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +21 -5
- package/dist/core/agents/rules/stop-hook.d.ts +3 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +59 -23
- package/dist/core/agents/session-events.d.ts +8 -16
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +12 -26
- package/dist/core/agents/state/heartbeat-projector.d.ts +3 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +44 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +32 -5
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +54 -17
- package/dist/core/agents/state/names.d.ts +22 -1
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +40 -2
- package/dist/core/config.d.ts +30 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +74 -11
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +12 -13
- package/dist/core/hooks/adapter/detect.d.ts +2 -4
- package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
- package/dist/core/hooks/adapter/detect.js +4 -14
- package/dist/core/hooks/adapter/output.d.ts +1 -1
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +5 -4
- package/dist/core/hooks/adapter/wiring.d.ts +23 -0
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +32 -0
- package/dist/core/hooks/cli.d.ts +1 -2
- package/dist/core/hooks/cli.d.ts.map +1 -1
- package/dist/core/hooks/cli.js +169 -27
- package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
- package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
- package/dist/core/hooks/codex-wsl-bridge.js +137 -0
- package/dist/core/hooks/effects/index.d.ts +1 -1
- package/dist/core/hooks/effects/index.js +3 -3
- package/dist/core/hooks/events/schema.d.ts +27 -0
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.d.ts +7 -1
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +15 -0
- package/dist/core/hooks/resolve/transcript.d.ts +44 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +176 -1
- package/dist/core/hooks/session-name-presence.d.ts +30 -0
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
- package/dist/core/hooks/session-name-presence.js +43 -0
- package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
- package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
- package/dist/core/hooks/unsafe-cross-shell.js +151 -0
- package/dist/core/work/state.d.ts +4 -5
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +5 -8
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/proof.d.ts +2 -2
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +2 -2
- package/dist/core/workflow/run-state.d.ts +2 -2
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +2 -2
- package/dist/core/workflow/types.d.ts +3 -3
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/execution.d.ts +2 -2
- package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/execution.js +5 -0
- package/dist/core/workflow/workspaces/index.d.ts +1 -1
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +1 -2
- package/dist/core/workflow/workspaces/paths.d.ts +2 -2
- package/dist/core/workflow/workspaces/types.d.ts +0 -6
- package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/validate.d.ts +0 -2
- package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/validate.js +0 -2
- package/dist/lib/browser/client.d.ts +118 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +435 -6
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +180 -37
- package/dist/lib/browser/index.d.ts +5 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +5 -1
- package/dist/lib/browser/netscape-cookies.d.ts +6 -0
- package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
- package/dist/lib/browser/netscape-cookies.js +31 -0
- package/dist/lib/browser/proxy.d.ts +24 -0
- package/dist/lib/browser/proxy.d.ts.map +1 -0
- package/dist/lib/browser/proxy.js +84 -0
- package/dist/lib/browser/runts.d.ts +6 -0
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +21 -4
- package/dist/lib/browser/session-control.d.ts +112 -0
- package/dist/lib/browser/session-control.d.ts.map +1 -0
- package/dist/lib/browser/session-control.js +670 -0
- package/dist/lib/docs-links.d.ts +108 -0
- package/dist/lib/docs-links.d.ts.map +1 -0
- package/dist/lib/docs-links.js +555 -0
- package/dist/lib/exec.d.ts +1 -1
- package/dist/lib/exec.js +1 -1
- package/dist/lib/identities/assume.d.ts +4 -2
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +15 -2
- package/dist/lib/instructions/git-hooks.d.ts +75 -0
- package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
- package/dist/lib/instructions/git-hooks.js +238 -0
- package/dist/lib/instructions/splice.d.ts +10 -4
- package/dist/lib/instructions/splice.d.ts.map +1 -1
- package/dist/lib/instructions/splice.js +19 -11
- package/package.json +1 -1
- package/schemas/config.schema.json +32 -0
- package/src/commander.ts +2 -0
- package/src/commands/agents.ts +96 -115
- package/src/commands/browse-session.ts +245 -0
- package/src/commands/browse.ts +287 -44
- package/src/commands/checkpoint.ts +1 -1
- package/src/commands/deinit.ts +7 -1
- package/src/commands/docs.ts +56 -0
- package/src/commands/doctor.ts +100 -18
- package/src/commands/init.ts +17 -9
- package/src/commands/tunnel.ts +2 -5
- package/src/core/agents/canonical-emit.ts +1 -2
- package/src/core/agents/cli.ts +137 -88
- package/src/core/agents/coord-client.ts +10 -5
- package/src/core/agents/finalization.ts +595 -0
- package/src/core/agents/git-hook.ts +126 -0
- package/src/core/agents/render/prompt-context.ts +28 -13
- package/src/core/agents/render/session-context.ts +15 -2
- package/src/core/agents/rules/claim-conflict.ts +3 -3
- package/src/core/agents/rules/commit-conflict.ts +35 -8
- package/src/core/agents/rules/stop-hook.ts +71 -23
- package/src/core/agents/session-events.ts +14 -41
- package/src/core/agents/state/heartbeat-projector.ts +45 -1
- package/src/core/agents/state/heartbeat-writer.ts +78 -16
- package/src/core/agents/state/names.ts +54 -2
- package/src/core/config.ts +92 -12
- package/src/core/governor/planning.ts +17 -12
- package/src/core/hooks/adapter/detect.ts +4 -13
- package/src/core/hooks/adapter/output.ts +9 -4
- package/src/core/hooks/adapter/wiring.ts +43 -0
- package/src/core/hooks/cli.ts +201 -23
- package/src/core/hooks/codex-wsl-bridge.ts +188 -0
- package/src/core/hooks/effects/index.ts +5 -5
- package/src/core/hooks/events/schema.ts +27 -0
- package/src/core/hooks/resolve/owner.ts +22 -1
- package/src/core/hooks/resolve/transcript.ts +164 -1
- package/src/core/hooks/session-name-presence.ts +52 -0
- package/src/core/hooks/unsafe-cross-shell.ts +160 -0
- package/src/core/work/state.ts +8 -12
- package/src/core/workflow/engine.ts +2 -2
- package/src/core/workflow/index.ts +0 -1
- package/src/core/workflow/proof.ts +4 -4
- package/src/core/workflow/run-state.ts +5 -5
- package/src/core/workflow/types.ts +3 -3
- package/src/core/workflow/workspaces/execution.ts +11 -4
- package/src/core/workflow/workspaces/index.ts +0 -3
- package/src/core/workflow/workspaces/local-git.ts +1 -2
- package/src/core/workflow/workspaces/paths.ts +2 -2
- package/src/core/workflow/workspaces/types.ts +0 -9
- package/src/core/workflow/workspaces/validate.ts +1 -5
- package/src/lib/browser/client.ts +528 -6
- package/src/lib/browser/geometry.ts +197 -37
- package/src/lib/browser/index.ts +39 -0
- package/src/lib/browser/netscape-cookies.ts +39 -0
- package/src/lib/browser/proxy.ts +105 -0
- package/src/lib/browser/runts.ts +27 -3
- package/src/lib/browser/session-control.ts +892 -0
- package/src/lib/docs-links.ts +674 -0
- package/src/lib/exec.ts +1 -1
- package/src/lib/identities/assume.ts +22 -1
- package/src/lib/instructions/git-hooks.ts +259 -0
- package/src/lib/instructions/splice.ts +41 -11
package/src/commands/browse.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
4
4
|
import type { Command } from "commander";
|
|
@@ -7,6 +7,8 @@ import { resolveBinName } from "../core/config.ts";
|
|
|
7
7
|
import {
|
|
8
8
|
type AssertResult,
|
|
9
9
|
Browser,
|
|
10
|
+
browserProxyFromEnv,
|
|
11
|
+
browserProxyGateFromEnv,
|
|
10
12
|
type ContentAnnotationBox,
|
|
11
13
|
type ContentChecksResult,
|
|
12
14
|
type CritiqueResult,
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
DEFAULT_CRITIQUE_RUBRIC,
|
|
16
18
|
type DevOverlayResult,
|
|
17
19
|
type Diagnostics,
|
|
20
|
+
extractObservedIp,
|
|
18
21
|
type LayoutAxis,
|
|
19
22
|
type LayoutLintResult,
|
|
20
23
|
type OverflowResult,
|
|
@@ -25,9 +28,15 @@ import {
|
|
|
25
28
|
type TargetSizeResult,
|
|
26
29
|
tilesFromFullPage,
|
|
27
30
|
type VisibilityResult,
|
|
31
|
+
WEBRTC_PROXY_ONLY_ARG,
|
|
28
32
|
type WidthResult,
|
|
33
|
+
writeNetscapeCookieFile,
|
|
29
34
|
wslHeadedLaunchArgs,
|
|
30
35
|
} from "../lib/browser/index.ts";
|
|
36
|
+
import {
|
|
37
|
+
type BrowserSessionServer,
|
|
38
|
+
startBrowserSessionServer,
|
|
39
|
+
} from "../lib/browser/session-control.ts";
|
|
31
40
|
import {
|
|
32
41
|
type DiffResult,
|
|
33
42
|
diffAgainstBaseline,
|
|
@@ -66,7 +75,6 @@ interface BrowseOpts {
|
|
|
66
75
|
// `opts.noX`. So `--no-screenshot` toggles `screenshot`, `--no-full-page`
|
|
67
76
|
// toggles `fullPage`, `--no-cookies` toggles `cookies`.
|
|
68
77
|
screenshot?: boolean;
|
|
69
|
-
domOnly?: boolean;
|
|
70
78
|
fullPage?: boolean;
|
|
71
79
|
snapshot?: boolean;
|
|
72
80
|
html?: boolean;
|
|
@@ -80,8 +88,12 @@ interface BrowseOpts {
|
|
|
80
88
|
batch?: string;
|
|
81
89
|
networkHar?: string;
|
|
82
90
|
login?: boolean;
|
|
91
|
+
loginCloseFile?: string;
|
|
92
|
+
controlFile?: string;
|
|
83
93
|
headed?: boolean;
|
|
84
94
|
browserArg?: string[];
|
|
95
|
+
proxyFromEnv?: boolean;
|
|
96
|
+
exportCookies?: string;
|
|
85
97
|
cookies?: boolean;
|
|
86
98
|
store?: string;
|
|
87
99
|
profile?: string;
|
|
@@ -201,7 +213,6 @@ export function registerBrowseCommand(
|
|
|
201
213
|
"Output prefix for the trio (writes <prefix>.png, .html, .json). Defaults to ~/.cache/harnery/browse/last.",
|
|
202
214
|
)
|
|
203
215
|
.option("--no-screenshot", "Skip the .png in the trio (DOM + JSON only)")
|
|
204
|
-
.option("--dom-only", "Alias for --no-screenshot")
|
|
205
216
|
.option("--no-full-page", "Capture only the viewport, not the full scrollable page")
|
|
206
217
|
.option("--snapshot", "Print body innerText to stdout (skips file writes)")
|
|
207
218
|
.option(
|
|
@@ -235,6 +246,14 @@ export function registerBrowseCommand(
|
|
|
235
246
|
"desktop",
|
|
236
247
|
)
|
|
237
248
|
.option("--login", "Headed mode for one-time auth flow (cookies persist in profile)")
|
|
249
|
+
.option(
|
|
250
|
+
"--login-close-file <path>",
|
|
251
|
+
"With --login, wait for this file instead of terminal Enter, then remove it and close cleanly",
|
|
252
|
+
)
|
|
253
|
+
.option(
|
|
254
|
+
"--control-file <path>",
|
|
255
|
+
"With --login, publish an owner-only descriptor for repeated browse-session control",
|
|
256
|
+
)
|
|
238
257
|
.option("--headed", "Headed mode for one-off (no auth-flow framing)")
|
|
239
258
|
.option(
|
|
240
259
|
"--browser-arg <flag>",
|
|
@@ -246,9 +265,20 @@ export function registerBrowseCommand(
|
|
|
246
265
|
(value: string, prev: string[] = []) => [...prev, value],
|
|
247
266
|
[] as string[],
|
|
248
267
|
)
|
|
268
|
+
.option(
|
|
269
|
+
"--proxy-from-env",
|
|
270
|
+
"Pass HTTP(S)_PROXY to Playwright as an authenticated browser proxy. " +
|
|
271
|
+
"Credentials stay in the child environment, never command arguments. " +
|
|
272
|
+
"When the host also injects the HARNERY_BROWSER_PROXY_* gate variables, " +
|
|
273
|
+
"the expected IP is verified before the requested URL opens.",
|
|
274
|
+
)
|
|
249
275
|
.option("--no-cookies", "Skip cookie-jar attach and persist")
|
|
250
276
|
.option("--store <path>", `Cookie store path (default ${DEFAULT_STORE})`)
|
|
251
277
|
.option("--profile <dir>", `Persistent Chromium profile dir (default ${DEFAULT_PROFILE})`)
|
|
278
|
+
.option(
|
|
279
|
+
"--export-cookies <path>",
|
|
280
|
+
"Write live profile cookies as an owner-only Netscape cookies.txt file on close",
|
|
281
|
+
)
|
|
252
282
|
.option(
|
|
253
283
|
"--wait-until <strategy>",
|
|
254
284
|
"Navigation wait strategy: load | domcontentloaded | networkidle | commit",
|
|
@@ -317,7 +347,7 @@ export function registerBrowseCommand(
|
|
|
317
347
|
)
|
|
318
348
|
.option(
|
|
319
349
|
"--check-overflow-fail",
|
|
320
|
-
"
|
|
350
|
+
"Run --check-overflow and exit 2 if horizontal overflow is detected.",
|
|
321
351
|
)
|
|
322
352
|
.option(
|
|
323
353
|
"--no-check-overflow-annotate",
|
|
@@ -336,7 +366,10 @@ export function registerBrowseCommand(
|
|
|
336
366
|
"Minimum block text length to scan (default 40; smaller labels can't meaningfully wrap).",
|
|
337
367
|
"40",
|
|
338
368
|
)
|
|
339
|
-
.option(
|
|
369
|
+
.option(
|
|
370
|
+
"--check-runts-fail",
|
|
371
|
+
"Run a document-wide runt check when needed; exit 2 on a runt, missing scope, or incomplete sweep.",
|
|
372
|
+
)
|
|
340
373
|
.option(
|
|
341
374
|
"--no-check-runts-annotate",
|
|
342
375
|
"Skip drawing runt boxes on the screenshot (JSON still emitted).",
|
|
@@ -353,7 +386,10 @@ export function registerBrowseCommand(
|
|
|
353
386
|
"Maximum rendered-center drift in CSS pixels (default 2).",
|
|
354
387
|
"2",
|
|
355
388
|
)
|
|
356
|
-
.option(
|
|
389
|
+
.option(
|
|
390
|
+
"--check-align-fail",
|
|
391
|
+
"Exit 2 when an alignment target fails, is unknown, or is missing.",
|
|
392
|
+
)
|
|
357
393
|
.option("--no-check-align-annotate", "Skip alignment screenshot annotations.")
|
|
358
394
|
.option(
|
|
359
395
|
"--check-gap <selector>",
|
|
@@ -367,11 +403,12 @@ export function registerBrowseCommand(
|
|
|
367
403
|
"Expected gap in CSS pixels. Without it, groups of 3+ infer the median gap.",
|
|
368
404
|
)
|
|
369
405
|
.option("--check-gap-threshold <px>", "Maximum gap deviation in CSS pixels (default 2).", "2")
|
|
370
|
-
.option("--check-gap-fail", "Exit 2 when a gap target fails or is missing.")
|
|
406
|
+
.option("--check-gap-fail", "Exit 2 when a gap target fails, is unknown, or is missing.")
|
|
371
407
|
.option("--no-check-gap-annotate", "Skip gap screenshot annotations.")
|
|
372
408
|
.option(
|
|
373
409
|
"--check-clip <selector>",
|
|
374
|
-
"Check
|
|
410
|
+
"Check every matching container for descendant boxes or text paint leaving its " +
|
|
411
|
+
"rectangular clipping chain or nearest block parent (repeatable).",
|
|
375
412
|
(value: string, previous: string[] = []) => [...previous, value],
|
|
376
413
|
[] as string[],
|
|
377
414
|
)
|
|
@@ -380,7 +417,7 @@ export function registerBrowseCommand(
|
|
|
380
417
|
"Allowed rectangular overrun in CSS pixels (default 0).",
|
|
381
418
|
"0",
|
|
382
419
|
)
|
|
383
|
-
.option("--check-clip-fail", "Exit 2 when a clip target fails or is missing.")
|
|
420
|
+
.option("--check-clip-fail", "Exit 2 when a clip target fails, is unknown, or is missing.")
|
|
384
421
|
.option("--no-check-clip-annotate", "Skip clip screenshot annotations.")
|
|
385
422
|
.option(
|
|
386
423
|
"--check-overlap <selector>",
|
|
@@ -393,7 +430,10 @@ export function registerBrowseCommand(
|
|
|
393
430
|
"Allowed overlap depth on either axis in CSS pixels (default 0).",
|
|
394
431
|
"0",
|
|
395
432
|
)
|
|
396
|
-
.option(
|
|
433
|
+
.option(
|
|
434
|
+
"--check-overlap-fail",
|
|
435
|
+
"Exit 2 when an overlap target fails, is unknown, or is missing.",
|
|
436
|
+
)
|
|
397
437
|
.option("--no-check-overlap-annotate", "Skip overlap screenshot annotations.")
|
|
398
438
|
.option(
|
|
399
439
|
"--check-crowd <selector>",
|
|
@@ -413,7 +453,7 @@ export function registerBrowseCommand(
|
|
|
413
453
|
"Pairs closer than this are flagged; negative separation (overlap) always flags.",
|
|
414
454
|
"6",
|
|
415
455
|
)
|
|
416
|
-
.option("--check-crowd-fail", "Exit 2 when a crowd target fails or is missing.")
|
|
456
|
+
.option("--check-crowd-fail", "Exit 2 when a crowd target fails, is unknown, or is missing.")
|
|
417
457
|
.option("--no-check-crowd-annotate", "Skip crowd screenshot annotations.")
|
|
418
458
|
.option(
|
|
419
459
|
"--check-placeholder [selector]",
|
|
@@ -424,7 +464,10 @@ export function registerBrowseCommand(
|
|
|
424
464
|
"bug on funnels and reports. Do not point it at a page that documents template " +
|
|
425
465
|
"syntax in prose.",
|
|
426
466
|
)
|
|
427
|
-
.option(
|
|
467
|
+
.option(
|
|
468
|
+
"--check-placeholder-fail",
|
|
469
|
+
"Run a document-wide placeholder check when needed; exit 2 if the result does not pass.",
|
|
470
|
+
)
|
|
428
471
|
.option("--no-check-placeholder-annotate", "Skip placeholder screenshot annotations.")
|
|
429
472
|
.option(
|
|
430
473
|
"--check-images [selector]",
|
|
@@ -438,7 +481,10 @@ export function registerBrowseCommand(
|
|
|
438
481
|
"Aspect-ratio deviation above which an image counts as stretched (0–1, default 0.1 = 10%).",
|
|
439
482
|
"0.1",
|
|
440
483
|
)
|
|
441
|
-
.option(
|
|
484
|
+
.option(
|
|
485
|
+
"--check-images-fail",
|
|
486
|
+
"Run a document-wide image check when needed; exit 2 if the result does not pass.",
|
|
487
|
+
)
|
|
442
488
|
.option("--no-check-images-annotate", "Skip image screenshot annotations.")
|
|
443
489
|
.option(
|
|
444
490
|
"--check-truncation [selector]",
|
|
@@ -451,7 +497,10 @@ export function registerBrowseCommand(
|
|
|
451
497
|
"Overflow past the clip box, in CSS px, above which truncation counts (default 2).",
|
|
452
498
|
"2",
|
|
453
499
|
)
|
|
454
|
-
.option(
|
|
500
|
+
.option(
|
|
501
|
+
"--check-truncation-fail",
|
|
502
|
+
"Run a document-wide truncation check when needed; exit 2 if the result does not pass.",
|
|
503
|
+
)
|
|
455
504
|
.option("--no-check-truncation-annotate", "Skip truncation screenshot annotations.")
|
|
456
505
|
.option(
|
|
457
506
|
"--check-contrast [selector]",
|
|
@@ -460,7 +509,10 @@ export function registerBrowseCommand(
|
|
|
460
509
|
"with --batch to cover light and dark. Text over an image or gradient is reported as " +
|
|
461
510
|
"`unknown`, not failed. Optional selector scopes the sweep (default: whole body).",
|
|
462
511
|
)
|
|
463
|
-
.option(
|
|
512
|
+
.option(
|
|
513
|
+
"--check-contrast-fail",
|
|
514
|
+
"Run a document-wide contrast check when needed; exit 2 on fail, unknown, or a missing scope.",
|
|
515
|
+
)
|
|
464
516
|
.option("--no-check-contrast-annotate", "Skip contrast screenshot annotations.")
|
|
465
517
|
.option(
|
|
466
518
|
"--check-critique [selector]",
|
|
@@ -483,7 +535,10 @@ export function registerBrowseCommand(
|
|
|
483
535
|
"24",
|
|
484
536
|
)
|
|
485
537
|
.option("--check-critique-rubric <text>", "Override the default critique rubric.")
|
|
486
|
-
.option(
|
|
538
|
+
.option(
|
|
539
|
+
"--check-critique-fail",
|
|
540
|
+
"Run a whole-page critique when needed; exit 2 unless the critique conclusively passes.",
|
|
541
|
+
)
|
|
487
542
|
.option(
|
|
488
543
|
"--assert <expr>",
|
|
489
544
|
"Assert a page value (repeatable). Grammar: '<op> <selector> => <expected>', where op is " +
|
|
@@ -493,7 +548,7 @@ export function registerBrowseCommand(
|
|
|
493
548
|
(value: string, previous: string[] = []) => [...previous, value],
|
|
494
549
|
[] as string[],
|
|
495
550
|
)
|
|
496
|
-
.option("--assert-fail", "Exit 2 if any --assert fails.")
|
|
551
|
+
.option("--assert-fail", "Exit 2 if any --assert fails; requires at least one --assert.")
|
|
497
552
|
.option(
|
|
498
553
|
"--check-hit [selector]",
|
|
499
554
|
"Check pointer-target size and spacing in the document or optional scope (repeatable).",
|
|
@@ -510,7 +565,7 @@ export function registerBrowseCommand(
|
|
|
510
565
|
)
|
|
511
566
|
.option(
|
|
512
567
|
"--check-hit-fail",
|
|
513
|
-
"
|
|
568
|
+
"Run a document-wide target-size check when needed; exit 2 when it fails or is incomplete.",
|
|
514
569
|
)
|
|
515
570
|
.option("--no-check-hit-annotate", "Skip target-size screenshot annotations.")
|
|
516
571
|
.option(
|
|
@@ -531,7 +586,10 @@ export function registerBrowseCommand(
|
|
|
531
586
|
"considered a match (0–1, default 0.01 = 1%).",
|
|
532
587
|
"0.01",
|
|
533
588
|
)
|
|
534
|
-
.option(
|
|
589
|
+
.option(
|
|
590
|
+
"--diff-fail",
|
|
591
|
+
"Exit non-zero if --diff mismatchRatio exceeds --diff-threshold; requires --diff.",
|
|
592
|
+
)
|
|
535
593
|
.option(
|
|
536
594
|
"--no-dev-overlay",
|
|
537
595
|
"Skip auto-capture of Next.js dev-overlay issues. Default: capture every queued error (kind/code/message/stack) when a <nextjs-portal> shadow root is present. Necessary because Next.js 16 + React 19 route hydration errors + most React warnings through onCaughtError → next-devtools' errorQueue, NOT through console.error, so Playwright's standard listener doesn't see them. Surfaces them in the JSON envelope under `devOverlay`.",
|
|
@@ -552,6 +610,7 @@ async function runBrowse(
|
|
|
552
610
|
opts: BrowseOpts,
|
|
553
611
|
context: HarneryProgramContext | undefined,
|
|
554
612
|
): Promise<void> {
|
|
613
|
+
normalizeCheckFailOptions(opts);
|
|
555
614
|
const alignAxis = parseLayoutAxis(opts.checkAlignAxis ?? "auto", "--check-align-axis");
|
|
556
615
|
const gapAxis = parseLayoutAxis(opts.checkGapAxis ?? "auto", "--check-gap-axis");
|
|
557
616
|
const alignThreshold = parseNonNegativeNumber(
|
|
@@ -584,6 +643,20 @@ async function runBrowse(
|
|
|
584
643
|
: new CookieJar({ path: opts.store ?? DEFAULT_STORE, source: "harn-browse" });
|
|
585
644
|
const headed = opts.login || opts.headed;
|
|
586
645
|
const viewport = parseViewport(opts.viewport ?? "desktop");
|
|
646
|
+
const proxy = opts.proxyFromEnv ? browserProxyFromEnv() : undefined;
|
|
647
|
+
const proxyGate = opts.proxyFromEnv ? browserProxyGateFromEnv() : null;
|
|
648
|
+
if (opts.loginCloseFile && !opts.login) {
|
|
649
|
+
throw new Error("--login-close-file requires --login.");
|
|
650
|
+
}
|
|
651
|
+
if (opts.controlFile && !opts.login) {
|
|
652
|
+
throw new Error("--control-file requires --login.");
|
|
653
|
+
}
|
|
654
|
+
const loginCloseFile = opts.loginCloseFile ? resolve(opts.loginCloseFile) : null;
|
|
655
|
+
if (loginCloseFile && existsSync(loginCloseFile)) {
|
|
656
|
+
throw new Error(
|
|
657
|
+
`Login close signal already exists at ${loginCloseFile}. Remove the stale file before launching.`,
|
|
658
|
+
);
|
|
659
|
+
}
|
|
587
660
|
|
|
588
661
|
const browser = new Browser({
|
|
589
662
|
profileDir: opts.profile ?? DEFAULT_PROFILE,
|
|
@@ -594,14 +667,28 @@ async function runBrowse(
|
|
|
594
667
|
waitUntil: opts.waitUntil as BrowseOpts["waitUntil"] as never,
|
|
595
668
|
recordHarPath: opts.networkHar ? resolve(opts.networkHar) : undefined,
|
|
596
669
|
extraHeaders: context?.extraHeaders,
|
|
597
|
-
launchArgs: resolveLaunchArgs(opts, Boolean(headed)),
|
|
670
|
+
launchArgs: resolveLaunchArgs(opts, Boolean(headed), Boolean(proxy)),
|
|
671
|
+
proxy,
|
|
598
672
|
});
|
|
599
673
|
|
|
600
674
|
// Print mode: --snapshot / --html / --json all suppress file writes.
|
|
601
675
|
const printMode = opts.snapshot || opts.html || opts.json;
|
|
676
|
+
let controlServer: BrowserSessionServer | null = null;
|
|
677
|
+
let receivedSignal: NodeJS.Signals | null = null;
|
|
678
|
+
let resolveSignal: (() => void) | null = null;
|
|
679
|
+
const signalPromise = new Promise<void>((resolveSignalPromise) => {
|
|
680
|
+
resolveSignal = resolveSignalPromise;
|
|
681
|
+
});
|
|
682
|
+
const handleSignal = (signal: NodeJS.Signals) => {
|
|
683
|
+
receivedSignal = signal;
|
|
684
|
+
resolveSignal?.();
|
|
685
|
+
};
|
|
686
|
+
const handleSigint = () => handleSignal("SIGINT");
|
|
687
|
+
const handleSigterm = () => handleSignal("SIGTERM");
|
|
602
688
|
|
|
603
689
|
try {
|
|
604
690
|
await browser.open();
|
|
691
|
+
if (proxyGate) await verifyBrowserProxyGate(browser, proxyGate);
|
|
605
692
|
const navResult = await browser.navigate(url);
|
|
606
693
|
|
|
607
694
|
if (opts.fill) {
|
|
@@ -790,14 +877,41 @@ async function runBrowse(
|
|
|
790
877
|
await browser.annotateContent(contentBoxes);
|
|
791
878
|
}
|
|
792
879
|
|
|
880
|
+
if (opts.controlFile) {
|
|
881
|
+
controlServer = await startBrowserSessionServer(opts.controlFile, browser);
|
|
882
|
+
process.once("SIGINT", handleSigint);
|
|
883
|
+
process.once("SIGTERM", handleSigterm);
|
|
884
|
+
emit.log(`browser session ready: ${controlServer.descriptorPath}`, "info");
|
|
885
|
+
}
|
|
886
|
+
|
|
793
887
|
if (opts.login) {
|
|
794
|
-
|
|
888
|
+
const lifecycleAbort = new AbortController();
|
|
889
|
+
const closeWaits: Promise<void>[] = [];
|
|
890
|
+
if (loginCloseFile) {
|
|
891
|
+
emit.log(
|
|
892
|
+
`[--login] Headed Chromium is open. Drive the visible window now. Create ${loginCloseFile} to close cleanly.`,
|
|
893
|
+
"info",
|
|
894
|
+
);
|
|
895
|
+
closeWaits.push(waitForLoginCloseFile(loginCloseFile, lifecycleAbort.signal));
|
|
896
|
+
} else {
|
|
795
897
|
emit.log(
|
|
796
898
|
"[--login] Headed Chromium is open. Walk through your auth flow now. Press Enter here to close + persist cookies into the profile.",
|
|
797
899
|
"info",
|
|
798
900
|
);
|
|
799
|
-
|
|
800
|
-
}
|
|
901
|
+
closeWaits.push(waitForTerminalEnter(lifecycleAbort.signal));
|
|
902
|
+
}
|
|
903
|
+
if (controlServer) {
|
|
904
|
+
closeWaits.push(controlServer.closeRequested, signalPromise);
|
|
905
|
+
}
|
|
906
|
+
await Promise.race(closeWaits);
|
|
907
|
+
lifecycleAbort.abort();
|
|
908
|
+
if (controlServer) await controlServer.stopAccepting();
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (opts.exportCookies) {
|
|
912
|
+
const exportPath = resolve(opts.exportCookies);
|
|
913
|
+
writeNetscapeCookieFile(exportPath, await browser.cookies());
|
|
914
|
+
emit.log(`exported live profile cookies to ${exportPath} (mode 600)`, "info");
|
|
801
915
|
}
|
|
802
916
|
|
|
803
917
|
// Auto-capture Next.js dev-overlay issues unless --no-dev-overlay was passed.
|
|
@@ -911,12 +1025,21 @@ async function runBrowse(
|
|
|
911
1025
|
}
|
|
912
1026
|
}
|
|
913
1027
|
|
|
914
|
-
if (opts.checkRuntsFail && runts && runts.
|
|
915
|
-
|
|
1028
|
+
if (opts.checkRuntsFail && runts && runts.outcome !== "pass") {
|
|
1029
|
+
if (!runts.found) {
|
|
1030
|
+
emit.log(`check-runts FAIL ${runts.scope}: scope not found`, "warn");
|
|
1031
|
+
} else if (runts.outcome === "unknown") {
|
|
916
1032
|
emit.log(
|
|
917
|
-
`check-runts FAIL ${
|
|
1033
|
+
`check-runts FAIL ${runts.scope ?? "document"}: sweep incomplete after ${runts.scannedBlocks} blocks`,
|
|
918
1034
|
"warn",
|
|
919
1035
|
);
|
|
1036
|
+
} else {
|
|
1037
|
+
for (const hit of runts.runts) {
|
|
1038
|
+
emit.log(
|
|
1039
|
+
`check-runts FAIL ${hit.block}: last line is a lone "${hit.word}" ("…${hit.snippet.slice(-40)}")`,
|
|
1040
|
+
"warn",
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
920
1043
|
}
|
|
921
1044
|
process.exitCode = 2;
|
|
922
1045
|
}
|
|
@@ -935,9 +1058,12 @@ async function runBrowse(
|
|
|
935
1058
|
|
|
936
1059
|
applyLayoutLintFailGates(opts, layoutLint, hit);
|
|
937
1060
|
applyContentFailGates(opts, content);
|
|
938
|
-
if (opts.checkCritiqueFail && critique && critique.outcome
|
|
939
|
-
const
|
|
940
|
-
|
|
1061
|
+
if (opts.checkCritiqueFail && critique && critique.outcome !== "pass") {
|
|
1062
|
+
const detail =
|
|
1063
|
+
critique.outcome === "skipped"
|
|
1064
|
+
? (critique.error ?? "critique was skipped")
|
|
1065
|
+
: `${critique.findings.filter((f) => f.severity === "high").length} high-severity finding(s)`;
|
|
1066
|
+
emit.log(`check-critique FAIL: ${detail}`, "warn");
|
|
941
1067
|
process.exitCode = 2;
|
|
942
1068
|
}
|
|
943
1069
|
if (opts.assertFail && asserts) {
|
|
@@ -951,7 +1077,11 @@ async function runBrowse(
|
|
|
951
1077
|
if (failed.length > 0) process.exitCode = 2;
|
|
952
1078
|
}
|
|
953
1079
|
} finally {
|
|
1080
|
+
process.removeListener("SIGINT", handleSigint);
|
|
1081
|
+
process.removeListener("SIGTERM", handleSigterm);
|
|
1082
|
+
await controlServer?.cleanup();
|
|
954
1083
|
await browser.close();
|
|
1084
|
+
if (receivedSignal) process.exitCode = receivedSignal === "SIGINT" ? 130 : 143;
|
|
955
1085
|
}
|
|
956
1086
|
}
|
|
957
1087
|
|
|
@@ -1155,7 +1285,7 @@ async function runTrioMode(
|
|
|
1155
1285
|
|
|
1156
1286
|
const written: string[] = [];
|
|
1157
1287
|
|
|
1158
|
-
const skipScreenshot = opts.screenshot === false
|
|
1288
|
+
const skipScreenshot = opts.screenshot === false;
|
|
1159
1289
|
let pngPath: string | undefined;
|
|
1160
1290
|
let pngBytes: number | undefined;
|
|
1161
1291
|
let captureEvalResult: unknown;
|
|
@@ -1176,9 +1306,7 @@ async function runTrioMode(
|
|
|
1176
1306
|
}
|
|
1177
1307
|
written.push(pngPath);
|
|
1178
1308
|
} else if (opts.captureEvaluate) {
|
|
1179
|
-
throw new Error(
|
|
1180
|
-
"--capture-evaluate requires a screenshot; remove --no-screenshot / --dom-only.",
|
|
1181
|
-
);
|
|
1309
|
+
throw new Error("--capture-evaluate requires a screenshot; remove --no-screenshot.");
|
|
1182
1310
|
}
|
|
1183
1311
|
|
|
1184
1312
|
const htmlPath = `${prefix}.html`;
|
|
@@ -1232,9 +1360,7 @@ async function runTrioMode(
|
|
|
1232
1360
|
let savedBaseline: SaveBaselineResult | undefined;
|
|
1233
1361
|
let diff: DiffResult | undefined;
|
|
1234
1362
|
if ((opts.baseline || opts.diff) && !pngPath) {
|
|
1235
|
-
throw new Error(
|
|
1236
|
-
"--baseline / --diff require a screenshot; --no-screenshot / --dom-only disables it.",
|
|
1237
|
-
);
|
|
1363
|
+
throw new Error("--baseline / --diff require a screenshot; --no-screenshot disables it.");
|
|
1238
1364
|
}
|
|
1239
1365
|
if (opts.baseline && pngPath) {
|
|
1240
1366
|
savedBaseline = saveBaseline(pngPath, opts.baseline);
|
|
@@ -1328,6 +1454,13 @@ async function runTrioMode(
|
|
|
1328
1454
|
emit.log(`check-overflow: [${ok ? "OK" : "FAIL"}] ${detail}`, ok ? "info" : "warn");
|
|
1329
1455
|
}
|
|
1330
1456
|
|
|
1457
|
+
if (runts) {
|
|
1458
|
+
emit.log(
|
|
1459
|
+
`check-runts: [${runts.outcome.toUpperCase()}] ${runts.scope ?? "document"}: ${runts.runts.length} runt${runts.runts.length === 1 ? "" : "s"} in ${runts.scannedBlocks} scanned block${runts.scannedBlocks === 1 ? "" : "s"}${runts.truncated ? " (incomplete)" : ""}`,
|
|
1460
|
+
runts.outcome === "pass" ? "info" : "warn",
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1331
1464
|
if (layoutLint) {
|
|
1332
1465
|
logLayoutLintSummary(layoutLint);
|
|
1333
1466
|
}
|
|
@@ -1397,6 +1530,59 @@ function parseNonNegativeNumber(value: string, flag: string): number {
|
|
|
1397
1530
|
return parsed;
|
|
1398
1531
|
}
|
|
1399
1532
|
|
|
1533
|
+
function normalizeCheckFailOptions(opts: BrowseOpts): void {
|
|
1534
|
+
// Whole-document checks can be enabled by their fail flag. Selector-based
|
|
1535
|
+
// checks cannot invent a scope, so reject the invocation before Chromium
|
|
1536
|
+
// opens instead of returning a misleading zero.
|
|
1537
|
+
if (opts.checkOverflowFail) opts.checkOverflow = true;
|
|
1538
|
+
if (opts.checkRuntsFail && opts.checkRunts === undefined) opts.checkRunts = true;
|
|
1539
|
+
if (opts.checkPlaceholderFail && opts.checkPlaceholder === undefined) {
|
|
1540
|
+
opts.checkPlaceholder = true;
|
|
1541
|
+
}
|
|
1542
|
+
if (opts.checkImagesFail && opts.checkImages === undefined) opts.checkImages = true;
|
|
1543
|
+
if (opts.checkTruncationFail && opts.checkTruncation === undefined) {
|
|
1544
|
+
opts.checkTruncation = true;
|
|
1545
|
+
}
|
|
1546
|
+
if (opts.checkContrastFail && opts.checkContrast === undefined) opts.checkContrast = true;
|
|
1547
|
+
if (opts.checkCritiqueFail && opts.checkCritique === undefined) opts.checkCritique = true;
|
|
1548
|
+
if (opts.checkHitFail && (!opts.checkHit || opts.checkHit.length === 0)) {
|
|
1549
|
+
opts.checkHit = [null];
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
const requireTargets = (
|
|
1553
|
+
failEnabled: boolean | undefined,
|
|
1554
|
+
targets: unknown[] | undefined,
|
|
1555
|
+
failFlag: string,
|
|
1556
|
+
checkFlag: string,
|
|
1557
|
+
): void => {
|
|
1558
|
+
if (failEnabled && (!targets || targets.length === 0)) {
|
|
1559
|
+
throw new Error(`${failFlag} requires ${checkFlag}.`);
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
|
|
1563
|
+
requireTargets(
|
|
1564
|
+
opts.checkVisibleFail,
|
|
1565
|
+
opts.checkVisible,
|
|
1566
|
+
"--check-visible-fail",
|
|
1567
|
+
"--check-visible",
|
|
1568
|
+
);
|
|
1569
|
+
requireTargets(opts.checkWidthFail, opts.checkWidth, "--check-width-fail", "--check-width");
|
|
1570
|
+
requireTargets(opts.checkAlignFail, opts.checkAlign, "--check-align-fail", "--check-align");
|
|
1571
|
+
requireTargets(opts.checkGapFail, opts.checkGap, "--check-gap-fail", "--check-gap");
|
|
1572
|
+
requireTargets(opts.checkClipFail, opts.checkClip, "--check-clip-fail", "--check-clip");
|
|
1573
|
+
requireTargets(
|
|
1574
|
+
opts.checkOverlapFail,
|
|
1575
|
+
opts.checkOverlap,
|
|
1576
|
+
"--check-overlap-fail",
|
|
1577
|
+
"--check-overlap",
|
|
1578
|
+
);
|
|
1579
|
+
requireTargets(opts.checkCrowdFail, opts.checkCrowd, "--check-crowd-fail", "--check-crowd");
|
|
1580
|
+
requireTargets(opts.assertFail, opts.assert, "--assert-fail", "--assert");
|
|
1581
|
+
if (opts.diffFail && !opts.diff) {
|
|
1582
|
+
throw new Error("--diff-fail requires --diff.");
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1400
1586
|
function parseLayoutAxis(value: string, flag: string): LayoutAxis {
|
|
1401
1587
|
if (value === "auto" || value === "x" || value === "y") return value;
|
|
1402
1588
|
throw new Error(`${flag} must be auto, x, or y (got: ${value})`);
|
|
@@ -1466,12 +1652,16 @@ function applyLayoutLintFailGates(
|
|
|
1466
1652
|
results: T[] | undefined,
|
|
1467
1653
|
): void => {
|
|
1468
1654
|
if (!enabled || !results) return;
|
|
1469
|
-
const failed = results.filter(
|
|
1655
|
+
const failed = results.filter(
|
|
1656
|
+
(result) => !result.found || result.outcome === "fail" || result.outcome === "unknown",
|
|
1657
|
+
);
|
|
1470
1658
|
for (const result of failed) {
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
"
|
|
1474
|
-
|
|
1659
|
+
const reason = !result.found
|
|
1660
|
+
? "element not found"
|
|
1661
|
+
: result.outcome === "unknown"
|
|
1662
|
+
? "result unknown"
|
|
1663
|
+
: "rendered relation failed";
|
|
1664
|
+
emit.log(`check-${rule} FAIL ${result.selector}: ${reason}`, "warn");
|
|
1475
1665
|
}
|
|
1476
1666
|
if (failed.length > 0) process.exitCode = 2;
|
|
1477
1667
|
};
|
|
@@ -1602,8 +1792,13 @@ function applyContentFailGates(opts: BrowseOpts, content: ContentChecksResult |
|
|
|
1602
1792
|
result: { found: boolean; outcome: string } | undefined,
|
|
1603
1793
|
): void => {
|
|
1604
1794
|
if (!enabled || !result) return;
|
|
1605
|
-
if (!result.found || result.outcome
|
|
1606
|
-
|
|
1795
|
+
if (!result.found || result.outcome !== "pass") {
|
|
1796
|
+
const reason = !result.found
|
|
1797
|
+
? "scope not found"
|
|
1798
|
+
: result.outcome === "unknown"
|
|
1799
|
+
? "result unknown"
|
|
1800
|
+
: "issues found";
|
|
1801
|
+
emit.log(`check-${rule} FAIL: ${reason}`, "warn");
|
|
1607
1802
|
process.exitCode = 2;
|
|
1608
1803
|
}
|
|
1609
1804
|
};
|
|
@@ -1682,7 +1877,7 @@ function summarizeDiagnostics(diag: Diagnostics): Record<string, unknown> {
|
|
|
1682
1877
|
* 2. HARNERY_BROWSER_ARGS env (whitespace-separated) — a machine-wide default.
|
|
1683
1878
|
* 3. --browser-arg flags on this invocation (repeatable).
|
|
1684
1879
|
*/
|
|
1685
|
-
function resolveLaunchArgs(opts: BrowseOpts, headed: boolean): string[] {
|
|
1880
|
+
function resolveLaunchArgs(opts: BrowseOpts, headed: boolean, proxyEnabled: boolean): string[] {
|
|
1686
1881
|
const args: string[] = [];
|
|
1687
1882
|
if (headed && !process.env.HARNERY_BROWSER_NO_WSL_DEFAULTS) {
|
|
1688
1883
|
args.push(...wslHeadedLaunchArgs());
|
|
@@ -1690,9 +1885,57 @@ function resolveLaunchArgs(opts: BrowseOpts, headed: boolean): string[] {
|
|
|
1690
1885
|
const envArgs = process.env.HARNERY_BROWSER_ARGS?.trim();
|
|
1691
1886
|
if (envArgs) args.push(...envArgs.split(/\s+/));
|
|
1692
1887
|
if (opts.browserArg && opts.browserArg.length > 0) args.push(...opts.browserArg);
|
|
1888
|
+
if (proxyEnabled) args.push(WEBRTC_PROXY_ONLY_ARG);
|
|
1693
1889
|
return [...new Set(args)];
|
|
1694
1890
|
}
|
|
1695
1891
|
|
|
1892
|
+
async function verifyBrowserProxyGate(
|
|
1893
|
+
browser: Browser,
|
|
1894
|
+
gate: { checkUrl: string; expectedIp: string },
|
|
1895
|
+
): Promise<void> {
|
|
1896
|
+
const result = await browser.navigate(gate.checkUrl);
|
|
1897
|
+
if (result.status === null || result.status < 200 || result.status >= 300) {
|
|
1898
|
+
throw new Error(
|
|
1899
|
+
`Browser proxy gate failed before target navigation (HTTP ${result.status ?? "unknown"}).`,
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
const body = await browser.evaluate<string>("document.body?.innerText ?? ''");
|
|
1903
|
+
const observedIp = extractObservedIp(body);
|
|
1904
|
+
if (observedIp !== gate.expectedIp) {
|
|
1905
|
+
throw new Error(
|
|
1906
|
+
`Browser proxy gate returned ${observedIp ?? "no IP"}; expected ${gate.expectedIp}. Target navigation was blocked.`,
|
|
1907
|
+
);
|
|
1908
|
+
}
|
|
1909
|
+
emit.log(`browser proxy gate: ${observedIp} · expected IP confirmed`, "info");
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
async function waitForLoginCloseFile(path: string, signal?: AbortSignal): Promise<void> {
|
|
1913
|
+
while (!existsSync(path) && !signal?.aborted) {
|
|
1914
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
1915
|
+
}
|
|
1916
|
+
if (signal?.aborted) return;
|
|
1917
|
+
unlinkSync(path);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
async function waitForTerminalEnter(signal: AbortSignal): Promise<void> {
|
|
1921
|
+
await new Promise<void>((resolveWait) => {
|
|
1922
|
+
const onData = () => {
|
|
1923
|
+
cleanup();
|
|
1924
|
+
resolveWait();
|
|
1925
|
+
};
|
|
1926
|
+
const onAbort = () => {
|
|
1927
|
+
cleanup();
|
|
1928
|
+
resolveWait();
|
|
1929
|
+
};
|
|
1930
|
+
const cleanup = () => {
|
|
1931
|
+
process.stdin.removeListener("data", onData);
|
|
1932
|
+
signal.removeEventListener("abort", onAbort);
|
|
1933
|
+
};
|
|
1934
|
+
process.stdin.once("data", onData);
|
|
1935
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1696
1939
|
function parseViewport(spec: string): { width: number; height: number } {
|
|
1697
1940
|
const preset = VIEWPORT_PRESETS[spec.toLowerCase()];
|
|
1698
1941
|
if (preset) return preset;
|
|
@@ -165,7 +165,7 @@ function resolveContinuityIdentity(
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
function adapterFromPlatform(platform: string | undefined): Adapter | null {
|
|
168
|
-
if (platform === "
|
|
168
|
+
if (platform === "claude-code") return "claude-code";
|
|
169
169
|
if (platform === "cursor") return "cursor";
|
|
170
170
|
if (platform === "codex") return "codex";
|
|
171
171
|
return null;
|
package/src/commands/deinit.ts
CHANGED
|
@@ -34,8 +34,10 @@ import type { Command } from "commander";
|
|
|
34
34
|
import type { EmitContext } from "../commander.ts";
|
|
35
35
|
import { DEFAULT_BIN_NAME } from "../core/config.ts";
|
|
36
36
|
import { ADAPTER_SPECS, type AdapterId } from "../core/hooks/adapter/events.ts";
|
|
37
|
+
import type { SettingsFile } from "../core/hooks/adapter/wiring.ts";
|
|
37
38
|
import { removeInstructions } from "../lib/instructions/apply.ts";
|
|
38
|
-
import {
|
|
39
|
+
import { removeGitHooks } from "../lib/instructions/git-hooks.ts";
|
|
40
|
+
import { unwireHooks } from "./init.ts";
|
|
39
41
|
|
|
40
42
|
interface DeinitOpts {
|
|
41
43
|
adapter: string;
|
|
@@ -138,6 +140,10 @@ export function registerDeinitCommand(program: Command, emit: EmitContext, binNa
|
|
|
138
140
|
const removed = removeInstructions(projectRoot, { adapter, dryRun });
|
|
139
141
|
actions.push(...removed.actions, ...removed.warnings.map((w) => `! ${w}`));
|
|
140
142
|
|
|
143
|
+
// ── 1c. git-hook managed regions ───────────────────────────────────────
|
|
144
|
+
const gitHooks = removeGitHooks(projectRoot, { dryRun });
|
|
145
|
+
actions.push(...gitHooks.actions, ...gitHooks.warnings.map((w) => `! ${w}`));
|
|
146
|
+
|
|
141
147
|
// ── 2. coord root (opt-in; destructive) ────────────────────────────────
|
|
142
148
|
if (purgeState) {
|
|
143
149
|
if (!coordExists) {
|