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/dist/commands/browse.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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 { resolveBinName } from "../core/config.js";
|
|
5
|
-
import { Browser, captureDevOverlay, DEFAULT_CRITIQUE_RUBRIC, parseAssertSpec, runCritique, tilesFromFullPage, wslHeadedLaunchArgs, } from "../lib/browser/index.js";
|
|
5
|
+
import { Browser, browserProxyFromEnv, browserProxyGateFromEnv, captureDevOverlay, DEFAULT_CRITIQUE_RUBRIC, extractObservedIp, parseAssertSpec, runCritique, tilesFromFullPage, WEBRTC_PROXY_ONLY_ARG, writeNetscapeCookieFile, wslHeadedLaunchArgs, } from "../lib/browser/index.js";
|
|
6
|
+
import { startBrowserSessionServer, } from "../lib/browser/session-control.js";
|
|
6
7
|
import { diffAgainstBaseline, saveBaseline, } from "../lib/browser/visual-diff.js";
|
|
7
8
|
import { CookieJar } from "../lib/cookies/index.js";
|
|
8
9
|
/**
|
|
@@ -44,7 +45,6 @@ export function registerBrowseCommand(program, emitParam, context) {
|
|
|
44
45
|
.description("Headless Chromium with persistent profile + cookie jar. Default writes a trio of files (last.png, last.html, last.json) for the LLM iteration loop; --snapshot/--html/--json switch to stdout-print mode.")
|
|
45
46
|
.option("--out <prefix>", "Output prefix for the trio (writes <prefix>.png, .html, .json). Defaults to ~/.cache/harnery/browse/last.")
|
|
46
47
|
.option("--no-screenshot", "Skip the .png in the trio (DOM + JSON only)")
|
|
47
|
-
.option("--dom-only", "Alias for --no-screenshot")
|
|
48
48
|
.option("--no-full-page", "Capture only the viewport, not the full scrollable page")
|
|
49
49
|
.option("--snapshot", "Print body innerText to stdout (skips file writes)")
|
|
50
50
|
.option("--html", `Print raw outer HTML to stdout (skips file writes; pair with \`${resolveBinName()} read -\`)`)
|
|
@@ -62,15 +62,22 @@ export function registerBrowseCommand(program, emitParam, context) {
|
|
|
62
62
|
.option("--network-har <path>", "Record network traffic to a HAR file (finalized on close)")
|
|
63
63
|
.option("--viewport <preset|WxH>", "Viewport: mobile (390x844), tablet (820x1180), desktop (1280x800), hd (1920x1080), or explicit '1920x1080'", "desktop")
|
|
64
64
|
.option("--login", "Headed mode for one-time auth flow (cookies persist in profile)")
|
|
65
|
+
.option("--login-close-file <path>", "With --login, wait for this file instead of terminal Enter, then remove it and close cleanly")
|
|
66
|
+
.option("--control-file <path>", "With --login, publish an owner-only descriptor for repeated browse-session control")
|
|
65
67
|
.option("--headed", "Headed mode for one-off (no auth-flow framing)")
|
|
66
68
|
.option("--browser-arg <flag>", "Extra Chromium launch flag, passed straight to the browser (repeatable). " +
|
|
67
69
|
"e.g. --browser-arg --disable-gpu. Also settable machine-wide via the " +
|
|
68
70
|
"HARNERY_BROWSER_ARGS env var (whitespace-separated). Under WSL, headed " +
|
|
69
71
|
"launches auto-add --disable-gpu to fix blank-window paint; opt out with " +
|
|
70
72
|
"HARNERY_BROWSER_NO_WSL_DEFAULTS=1.", (value, prev = []) => [...prev, value], [])
|
|
73
|
+
.option("--proxy-from-env", "Pass HTTP(S)_PROXY to Playwright as an authenticated browser proxy. " +
|
|
74
|
+
"Credentials stay in the child environment, never command arguments. " +
|
|
75
|
+
"When the host also injects the HARNERY_BROWSER_PROXY_* gate variables, " +
|
|
76
|
+
"the expected IP is verified before the requested URL opens.")
|
|
71
77
|
.option("--no-cookies", "Skip cookie-jar attach and persist")
|
|
72
78
|
.option("--store <path>", `Cookie store path (default ${DEFAULT_STORE})`)
|
|
73
79
|
.option("--profile <dir>", `Persistent Chromium profile dir (default ${DEFAULT_PROFILE})`)
|
|
80
|
+
.option("--export-cookies <path>", "Write live profile cookies as an owner-only Netscape cookies.txt file on close")
|
|
74
81
|
.option("--wait-until <strategy>", "Navigation wait strategy: load | domcontentloaded | networkidle | commit", "load")
|
|
75
82
|
.option("--timeout <ms>", "Navigation timeout in milliseconds", "30000")
|
|
76
83
|
.option("--check-visible <selector>", "Run an occlusion check on this selector after navigation + batch. " +
|
|
@@ -96,7 +103,7 @@ export function registerBrowseCommand(program, emitParam, context) {
|
|
|
96
103
|
.option("--check-overflow", "Assert the document has no horizontal overflow (document.scrollWidth <= window.innerWidth). " +
|
|
97
104
|
"Surfaces protruding elements in the JSON envelope and annotates them on the screenshot. " +
|
|
98
105
|
"Catches the class of mobile-layout bug where a nav/table overflows the viewport edge.")
|
|
99
|
-
.option("--check-overflow-fail", "
|
|
106
|
+
.option("--check-overflow-fail", "Run --check-overflow and exit 2 if horizontal overflow is detected.")
|
|
100
107
|
.option("--no-check-overflow-annotate", "Skip drawing overflow annotations on the screenshot (JSON still emitted).")
|
|
101
108
|
.option("--check-runts [selector]", "Scan text blocks for runts (a single word alone on a block's last visual line) " +
|
|
102
109
|
"by counting words on the last line via per-word Range rects — width thresholds " +
|
|
@@ -104,26 +111,27 @@ export function registerBrowseCommand(program, emitParam, context) {
|
|
|
104
111
|
"Atomic tokens (URLs, emails, phone numbers) are excluded. Reports hits in the JSON " +
|
|
105
112
|
"envelope under `runts` and annotates them on the screenshot.")
|
|
106
113
|
.option("--check-runts-min-chars <n>", "Minimum block text length to scan (default 40; smaller labels can't meaningfully wrap).", "40")
|
|
107
|
-
.option("--check-runts-fail", "
|
|
114
|
+
.option("--check-runts-fail", "Run a document-wide runt check when needed; exit 2 on a runt, missing scope, or incomplete sweep.")
|
|
108
115
|
.option("--no-check-runts-annotate", "Skip drawing runt boxes on the screenshot (JSON still emitted).")
|
|
109
116
|
.option("--check-align <selector>", "Check rendered child-content alignment inside this container (repeatable).", (value, previous = []) => [...previous, value], [])
|
|
110
117
|
.option("--check-align-axis <axis>", "Alignment axis: auto | x | y (default auto).", "auto")
|
|
111
118
|
.option("--check-align-threshold <px>", "Maximum rendered-center drift in CSS pixels (default 2).", "2")
|
|
112
|
-
.option("--check-align-fail", "Exit 2 when an alignment target fails or is missing.")
|
|
119
|
+
.option("--check-align-fail", "Exit 2 when an alignment target fails, is unknown, or is missing.")
|
|
113
120
|
.option("--no-check-align-annotate", "Skip alignment screenshot annotations.")
|
|
114
121
|
.option("--check-gap <selector>", "Check adjacent rendered gaps inside this container (repeatable).", (value, previous = []) => [...previous, value], [])
|
|
115
122
|
.option("--check-gap-axis <axis>", "Gap axis: auto | x | y (default auto).", "auto")
|
|
116
123
|
.option("--check-gap-expected <px>", "Expected gap in CSS pixels. Without it, groups of 3+ infer the median gap.")
|
|
117
124
|
.option("--check-gap-threshold <px>", "Maximum gap deviation in CSS pixels (default 2).", "2")
|
|
118
|
-
.option("--check-gap-fail", "Exit 2 when a gap target fails or is missing.")
|
|
125
|
+
.option("--check-gap-fail", "Exit 2 when a gap target fails, is unknown, or is missing.")
|
|
119
126
|
.option("--no-check-gap-annotate", "Skip gap screenshot annotations.")
|
|
120
|
-
.option("--check-clip <selector>", "Check
|
|
127
|
+
.option("--check-clip <selector>", "Check every matching container for descendant boxes or text paint leaving its " +
|
|
128
|
+
"rectangular clipping chain or nearest block parent (repeatable).", (value, previous = []) => [...previous, value], [])
|
|
121
129
|
.option("--check-clip-threshold <px>", "Allowed rectangular overrun in CSS pixels (default 0).", "0")
|
|
122
|
-
.option("--check-clip-fail", "Exit 2 when a clip target fails or is missing.")
|
|
130
|
+
.option("--check-clip-fail", "Exit 2 when a clip target fails, is unknown, or is missing.")
|
|
123
131
|
.option("--no-check-clip-annotate", "Skip clip screenshot annotations.")
|
|
124
132
|
.option("--check-overlap <selector>", "Check measurable in-flow siblings for unintended overlap (repeatable).", (value, previous = []) => [...previous, value], [])
|
|
125
133
|
.option("--check-overlap-threshold <px>", "Allowed overlap depth on either axis in CSS pixels (default 0).", "0")
|
|
126
|
-
.option("--check-overlap-fail", "Exit 2 when an overlap target fails or is missing.")
|
|
134
|
+
.option("--check-overlap-fail", "Exit 2 when an overlap target fails, is unknown, or is missing.")
|
|
127
135
|
.option("--no-check-overlap-annotate", "Skip overlap screenshot annotations.")
|
|
128
136
|
.option("--check-crowd <selector>", "Flag adjacent card panels (full border, modest corner radius, or box-shadow) " +
|
|
129
137
|
"that touch or nearly touch. Also treats wrappers that contain panels as " +
|
|
@@ -134,7 +142,7 @@ export function registerBrowseCommand(program, emitParam, context) {
|
|
|
134
142
|
"uneven spacing, so a uniformly-flush stack passes). Repeatable.", (value, previous = []) => [...previous, value], [])
|
|
135
143
|
.option("--check-crowd-min <px>", "Minimum acceptable edge gap between adjacent panel faces in CSS px (default 6). " +
|
|
136
144
|
"Pairs closer than this are flagged; negative separation (overlap) always flags.", "6")
|
|
137
|
-
.option("--check-crowd-fail", "Exit 2 when a crowd target fails or is missing.")
|
|
145
|
+
.option("--check-crowd-fail", "Exit 2 when a crowd target fails, is unknown, or is missing.")
|
|
138
146
|
.option("--no-check-crowd-annotate", "Skip crowd screenshot annotations.")
|
|
139
147
|
.option("--check-placeholder [selector]", "Scan rendered text for unrendered template tokens and bound-value tells: " +
|
|
140
148
|
"JS template literals, `{{x}}` handlebars, `[object Object]`, `Invalid Date`, " +
|
|
@@ -142,26 +150,26 @@ export function registerBrowseCommand(program, emitParam, context) {
|
|
|
142
150
|
"scopes the sweep (default: whole body). Catches the most common generation " +
|
|
143
151
|
"bug on funnels and reports. Do not point it at a page that documents template " +
|
|
144
152
|
"syntax in prose.")
|
|
145
|
-
.option("--check-placeholder-fail", "
|
|
153
|
+
.option("--check-placeholder-fail", "Run a document-wide placeholder check when needed; exit 2 if the result does not pass.")
|
|
146
154
|
.option("--no-check-placeholder-annotate", "Skip placeholder screenshot annotations.")
|
|
147
155
|
.option("--check-images [selector]", "Audit <img> elements for load + aspect health: a failed load (naturalWidth 0), " +
|
|
148
156
|
"a still-loading image, or a stretched image (rendered aspect ratio far from the " +
|
|
149
157
|
"intrinsic one, with an object-fit that does not correct it). Optional selector " +
|
|
150
158
|
"scopes the sweep (default: whole body).")
|
|
151
159
|
.option("--check-images-tolerance <ratio>", "Aspect-ratio deviation above which an image counts as stretched (0–1, default 0.1 = 10%).", "0.1")
|
|
152
|
-
.option("--check-images-fail", "
|
|
160
|
+
.option("--check-images-fail", "Run a document-wide image check when needed; exit 2 if the result does not pass.")
|
|
153
161
|
.option("--no-check-images-annotate", "Skip image screenshot annotations.")
|
|
154
162
|
.option("--check-truncation [selector]", "Flag text actively cut off by an ellipsis or -webkit-line-clamp — the author asked " +
|
|
155
163
|
"to truncate AND the content overflows. High precision: it does not flag plain " +
|
|
156
164
|
"overflow:hidden clips. Optional selector scopes the sweep (default: whole body).")
|
|
157
165
|
.option("--check-truncation-tolerance <px>", "Overflow past the clip box, in CSS px, above which truncation counts (default 2).", "2")
|
|
158
|
-
.option("--check-truncation-fail", "
|
|
166
|
+
.option("--check-truncation-fail", "Run a document-wide truncation check when needed; exit 2 if the result does not pass.")
|
|
159
167
|
.option("--no-check-truncation-annotate", "Skip truncation screenshot annotations.")
|
|
160
168
|
.option("--check-contrast [selector]", "Flag rendered text below the WCAG AA contrast ratio (4.5:1 normal, 3:1 large) against " +
|
|
161
169
|
"its effective background. Runs at whatever theme the page is in, so toggle the theme " +
|
|
162
170
|
"with --batch to cover light and dark. Text over an image or gradient is reported as " +
|
|
163
171
|
"`unknown`, not failed. Optional selector scopes the sweep (default: whole body).")
|
|
164
|
-
.option("--check-contrast-fail", "
|
|
172
|
+
.option("--check-contrast-fail", "Run a document-wide contrast check when needed; exit 2 on fail, unknown, or a missing scope.")
|
|
165
173
|
.option("--no-check-contrast-annotate", "Skip contrast screenshot annotations.")
|
|
166
174
|
.option("--check-critique [selector]", "Hand the rendered page to a vision model for a visual-defect review, tile by " +
|
|
167
175
|
"tile. Catches the long tail heuristic checks can't enumerate. Default tiling " +
|
|
@@ -173,18 +181,18 @@ export function registerBrowseCommand(program, emitParam, context) {
|
|
|
173
181
|
.option("--check-critique-overlap <px>", "Vertical overlap between bands so a finding at a seam stays visible (default 120).", "120")
|
|
174
182
|
.option("--check-critique-max-tiles <n>", "Cap on tiles sent to the model, to bound cost (default 24).", "24")
|
|
175
183
|
.option("--check-critique-rubric <text>", "Override the default critique rubric.")
|
|
176
|
-
.option("--check-critique-fail", "
|
|
184
|
+
.option("--check-critique-fail", "Run a whole-page critique when needed; exit 2 unless the critique conclusively passes.")
|
|
177
185
|
.option("--assert <expr>", "Assert a page value (repeatable). Grammar: '<op> <selector> => <expected>', where op is " +
|
|
178
186
|
"text (trimmed text equals), contains (text includes), matches (text matches a regex), " +
|
|
179
187
|
"count (match count vs a number or >=/<=/>/< comparator), exists / absent (no => needed). " +
|
|
180
188
|
"e.g. --assert 'text h1 => Welcome' --assert 'count .card => >=3' --assert 'absent .error'.", (value, previous = []) => [...previous, value], [])
|
|
181
|
-
.option("--assert-fail", "Exit 2 if any --assert fails.")
|
|
189
|
+
.option("--assert-fail", "Exit 2 if any --assert fails; requires at least one --assert.")
|
|
182
190
|
.option("--check-hit [selector]", "Check pointer-target size and spacing in the document or optional scope (repeatable).", (value, previous = []) => [
|
|
183
191
|
...previous,
|
|
184
192
|
typeof value === "string" ? value : null,
|
|
185
193
|
], [])
|
|
186
194
|
.option("--check-hit-profile <profile>", "Target profile: wcag-aa (24px) | comfortable (44px).", "wcag-aa")
|
|
187
|
-
.option("--check-hit-fail", "
|
|
195
|
+
.option("--check-hit-fail", "Run a document-wide target-size check when needed; exit 2 when it fails or is incomplete.")
|
|
188
196
|
.option("--no-check-hit-annotate", "Skip target-size screenshot annotations.")
|
|
189
197
|
.option("--baseline <name>", "Save the captured screenshot as a named baseline at " +
|
|
190
198
|
"~/.cache/harnery/visual-baselines/<name>.png. Use --diff <name> later to " +
|
|
@@ -194,7 +202,7 @@ export function registerBrowseCommand(program, emitParam, context) {
|
|
|
194
202
|
"similarity in the JSON envelope.")
|
|
195
203
|
.option("--diff-threshold <ratio>", "mismatchRatio (mismatchedPixels / totalPixels) below which the diff is " +
|
|
196
204
|
"considered a match (0–1, default 0.01 = 1%).", "0.01")
|
|
197
|
-
.option("--diff-fail", "Exit non-zero if --diff mismatchRatio exceeds --diff-threshold.")
|
|
205
|
+
.option("--diff-fail", "Exit non-zero if --diff mismatchRatio exceeds --diff-threshold; requires --diff.")
|
|
198
206
|
.option("--no-dev-overlay", "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`.")
|
|
199
207
|
.action(async (url, opts) => {
|
|
200
208
|
try {
|
|
@@ -208,6 +216,7 @@ export function registerBrowseCommand(program, emitParam, context) {
|
|
|
208
216
|
});
|
|
209
217
|
}
|
|
210
218
|
async function runBrowse(url, opts, context) {
|
|
219
|
+
normalizeCheckFailOptions(opts);
|
|
211
220
|
const alignAxis = parseLayoutAxis(opts.checkAlignAxis ?? "auto", "--check-align-axis");
|
|
212
221
|
const gapAxis = parseLayoutAxis(opts.checkGapAxis ?? "auto", "--check-gap-axis");
|
|
213
222
|
const alignThreshold = parseNonNegativeNumber(opts.checkAlignThreshold ?? "2", "--check-align-threshold");
|
|
@@ -225,6 +234,18 @@ async function runBrowse(url, opts, context) {
|
|
|
225
234
|
: new CookieJar({ path: opts.store ?? DEFAULT_STORE, source: "harn-browse" });
|
|
226
235
|
const headed = opts.login || opts.headed;
|
|
227
236
|
const viewport = parseViewport(opts.viewport ?? "desktop");
|
|
237
|
+
const proxy = opts.proxyFromEnv ? browserProxyFromEnv() : undefined;
|
|
238
|
+
const proxyGate = opts.proxyFromEnv ? browserProxyGateFromEnv() : null;
|
|
239
|
+
if (opts.loginCloseFile && !opts.login) {
|
|
240
|
+
throw new Error("--login-close-file requires --login.");
|
|
241
|
+
}
|
|
242
|
+
if (opts.controlFile && !opts.login) {
|
|
243
|
+
throw new Error("--control-file requires --login.");
|
|
244
|
+
}
|
|
245
|
+
const loginCloseFile = opts.loginCloseFile ? resolve(opts.loginCloseFile) : null;
|
|
246
|
+
if (loginCloseFile && existsSync(loginCloseFile)) {
|
|
247
|
+
throw new Error(`Login close signal already exists at ${loginCloseFile}. Remove the stale file before launching.`);
|
|
248
|
+
}
|
|
228
249
|
const browser = new Browser({
|
|
229
250
|
profileDir: opts.profile ?? DEFAULT_PROFILE,
|
|
230
251
|
headed,
|
|
@@ -234,12 +255,27 @@ async function runBrowse(url, opts, context) {
|
|
|
234
255
|
waitUntil: opts.waitUntil,
|
|
235
256
|
recordHarPath: opts.networkHar ? resolve(opts.networkHar) : undefined,
|
|
236
257
|
extraHeaders: context?.extraHeaders,
|
|
237
|
-
launchArgs: resolveLaunchArgs(opts, Boolean(headed)),
|
|
258
|
+
launchArgs: resolveLaunchArgs(opts, Boolean(headed), Boolean(proxy)),
|
|
259
|
+
proxy,
|
|
238
260
|
});
|
|
239
261
|
// Print mode: --snapshot / --html / --json all suppress file writes.
|
|
240
262
|
const printMode = opts.snapshot || opts.html || opts.json;
|
|
263
|
+
let controlServer = null;
|
|
264
|
+
let receivedSignal = null;
|
|
265
|
+
let resolveSignal = null;
|
|
266
|
+
const signalPromise = new Promise((resolveSignalPromise) => {
|
|
267
|
+
resolveSignal = resolveSignalPromise;
|
|
268
|
+
});
|
|
269
|
+
const handleSignal = (signal) => {
|
|
270
|
+
receivedSignal = signal;
|
|
271
|
+
resolveSignal?.();
|
|
272
|
+
};
|
|
273
|
+
const handleSigint = () => handleSignal("SIGINT");
|
|
274
|
+
const handleSigterm = () => handleSignal("SIGTERM");
|
|
241
275
|
try {
|
|
242
276
|
await browser.open();
|
|
277
|
+
if (proxyGate)
|
|
278
|
+
await verifyBrowserProxyGate(browser, proxyGate);
|
|
243
279
|
const navResult = await browser.navigate(url);
|
|
244
280
|
if (opts.fill) {
|
|
245
281
|
const sep = opts.fill.indexOf("=>");
|
|
@@ -408,11 +444,35 @@ async function runBrowse(url, opts, context) {
|
|
|
408
444
|
if (contentBoxes.length > 0) {
|
|
409
445
|
await browser.annotateContent(contentBoxes);
|
|
410
446
|
}
|
|
447
|
+
if (opts.controlFile) {
|
|
448
|
+
controlServer = await startBrowserSessionServer(opts.controlFile, browser);
|
|
449
|
+
process.once("SIGINT", handleSigint);
|
|
450
|
+
process.once("SIGTERM", handleSigterm);
|
|
451
|
+
emit.log(`browser session ready: ${controlServer.descriptorPath}`, "info");
|
|
452
|
+
}
|
|
411
453
|
if (opts.login) {
|
|
412
|
-
|
|
454
|
+
const lifecycleAbort = new AbortController();
|
|
455
|
+
const closeWaits = [];
|
|
456
|
+
if (loginCloseFile) {
|
|
457
|
+
emit.log(`[--login] Headed Chromium is open. Drive the visible window now. Create ${loginCloseFile} to close cleanly.`, "info");
|
|
458
|
+
closeWaits.push(waitForLoginCloseFile(loginCloseFile, lifecycleAbort.signal));
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
413
461
|
emit.log("[--login] Headed Chromium is open. Walk through your auth flow now. Press Enter here to close + persist cookies into the profile.", "info");
|
|
414
|
-
|
|
415
|
-
}
|
|
462
|
+
closeWaits.push(waitForTerminalEnter(lifecycleAbort.signal));
|
|
463
|
+
}
|
|
464
|
+
if (controlServer) {
|
|
465
|
+
closeWaits.push(controlServer.closeRequested, signalPromise);
|
|
466
|
+
}
|
|
467
|
+
await Promise.race(closeWaits);
|
|
468
|
+
lifecycleAbort.abort();
|
|
469
|
+
if (controlServer)
|
|
470
|
+
await controlServer.stopAccepting();
|
|
471
|
+
}
|
|
472
|
+
if (opts.exportCookies) {
|
|
473
|
+
const exportPath = resolve(opts.exportCookies);
|
|
474
|
+
writeNetscapeCookieFile(exportPath, await browser.cookies());
|
|
475
|
+
emit.log(`exported live profile cookies to ${exportPath} (mode 600)`, "info");
|
|
416
476
|
}
|
|
417
477
|
// Auto-capture Next.js dev-overlay issues unless --no-dev-overlay was passed.
|
|
418
478
|
// Cheap no-op when no <nextjs-portal> shadow host is present (non-Next.js page).
|
|
@@ -485,9 +545,17 @@ async function runBrowse(url, opts, context) {
|
|
|
485
545
|
process.exitCode = 2;
|
|
486
546
|
}
|
|
487
547
|
}
|
|
488
|
-
if (opts.checkRuntsFail && runts && runts.
|
|
489
|
-
|
|
490
|
-
emit.log(`check-runts FAIL ${
|
|
548
|
+
if (opts.checkRuntsFail && runts && runts.outcome !== "pass") {
|
|
549
|
+
if (!runts.found) {
|
|
550
|
+
emit.log(`check-runts FAIL ${runts.scope}: scope not found`, "warn");
|
|
551
|
+
}
|
|
552
|
+
else if (runts.outcome === "unknown") {
|
|
553
|
+
emit.log(`check-runts FAIL ${runts.scope ?? "document"}: sweep incomplete after ${runts.scannedBlocks} blocks`, "warn");
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
for (const hit of runts.runts) {
|
|
557
|
+
emit.log(`check-runts FAIL ${hit.block}: last line is a lone "${hit.word}" ("…${hit.snippet.slice(-40)}")`, "warn");
|
|
558
|
+
}
|
|
491
559
|
}
|
|
492
560
|
process.exitCode = 2;
|
|
493
561
|
}
|
|
@@ -501,9 +569,11 @@ async function runBrowse(url, opts, context) {
|
|
|
501
569
|
}
|
|
502
570
|
applyLayoutLintFailGates(opts, layoutLint, hit);
|
|
503
571
|
applyContentFailGates(opts, content);
|
|
504
|
-
if (opts.checkCritiqueFail && critique && critique.outcome
|
|
505
|
-
const
|
|
506
|
-
|
|
572
|
+
if (opts.checkCritiqueFail && critique && critique.outcome !== "pass") {
|
|
573
|
+
const detail = critique.outcome === "skipped"
|
|
574
|
+
? (critique.error ?? "critique was skipped")
|
|
575
|
+
: `${critique.findings.filter((f) => f.severity === "high").length} high-severity finding(s)`;
|
|
576
|
+
emit.log(`check-critique FAIL: ${detail}`, "warn");
|
|
507
577
|
process.exitCode = 2;
|
|
508
578
|
}
|
|
509
579
|
if (opts.assertFail && asserts) {
|
|
@@ -516,7 +586,12 @@ async function runBrowse(url, opts, context) {
|
|
|
516
586
|
}
|
|
517
587
|
}
|
|
518
588
|
finally {
|
|
589
|
+
process.removeListener("SIGINT", handleSigint);
|
|
590
|
+
process.removeListener("SIGTERM", handleSigterm);
|
|
591
|
+
await controlServer?.cleanup();
|
|
519
592
|
await browser.close();
|
|
593
|
+
if (receivedSignal)
|
|
594
|
+
process.exitCode = receivedSignal === "SIGINT" ? 130 : 143;
|
|
520
595
|
}
|
|
521
596
|
}
|
|
522
597
|
async function runBatch(browser, batch, defaultTimeoutMs) {
|
|
@@ -684,7 +759,7 @@ async function runTrioMode(browser, navResult, opts, evalResult, visibility, wid
|
|
|
684
759
|
const prefix = resolveOutPrefix(opts.out);
|
|
685
760
|
mkdirSync(dirname(prefix), { recursive: true });
|
|
686
761
|
const written = [];
|
|
687
|
-
const skipScreenshot = opts.screenshot === false
|
|
762
|
+
const skipScreenshot = opts.screenshot === false;
|
|
688
763
|
let pngPath;
|
|
689
764
|
let pngBytes;
|
|
690
765
|
let captureEvalResult;
|
|
@@ -707,7 +782,7 @@ async function runTrioMode(browser, navResult, opts, evalResult, visibility, wid
|
|
|
707
782
|
written.push(pngPath);
|
|
708
783
|
}
|
|
709
784
|
else if (opts.captureEvaluate) {
|
|
710
|
-
throw new Error("--capture-evaluate requires a screenshot; remove --no-screenshot
|
|
785
|
+
throw new Error("--capture-evaluate requires a screenshot; remove --no-screenshot.");
|
|
711
786
|
}
|
|
712
787
|
const htmlPath = `${prefix}.html`;
|
|
713
788
|
const html = await browser.htmlContent(opts.selector);
|
|
@@ -770,7 +845,7 @@ async function runTrioMode(browser, navResult, opts, evalResult, visibility, wid
|
|
|
770
845
|
let savedBaseline;
|
|
771
846
|
let diff;
|
|
772
847
|
if ((opts.baseline || opts.diff) && !pngPath) {
|
|
773
|
-
throw new Error("--baseline / --diff require a screenshot; --no-screenshot
|
|
848
|
+
throw new Error("--baseline / --diff require a screenshot; --no-screenshot disables it.");
|
|
774
849
|
}
|
|
775
850
|
if (opts.baseline && pngPath) {
|
|
776
851
|
savedBaseline = saveBaseline(pngPath, opts.baseline);
|
|
@@ -847,6 +922,9 @@ async function runTrioMode(browser, navResult, opts, evalResult, visibility, wid
|
|
|
847
922
|
: ""}`;
|
|
848
923
|
emit.log(`check-overflow: [${ok ? "OK" : "FAIL"}] ${detail}`, ok ? "info" : "warn");
|
|
849
924
|
}
|
|
925
|
+
if (runts) {
|
|
926
|
+
emit.log(`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)" : ""}`, runts.outcome === "pass" ? "info" : "warn");
|
|
927
|
+
}
|
|
850
928
|
if (layoutLint) {
|
|
851
929
|
logLayoutLintSummary(layoutLint);
|
|
852
930
|
}
|
|
@@ -902,6 +980,46 @@ function parseNonNegativeNumber(value, flag) {
|
|
|
902
980
|
}
|
|
903
981
|
return parsed;
|
|
904
982
|
}
|
|
983
|
+
function normalizeCheckFailOptions(opts) {
|
|
984
|
+
// Whole-document checks can be enabled by their fail flag. Selector-based
|
|
985
|
+
// checks cannot invent a scope, so reject the invocation before Chromium
|
|
986
|
+
// opens instead of returning a misleading zero.
|
|
987
|
+
if (opts.checkOverflowFail)
|
|
988
|
+
opts.checkOverflow = true;
|
|
989
|
+
if (opts.checkRuntsFail && opts.checkRunts === undefined)
|
|
990
|
+
opts.checkRunts = true;
|
|
991
|
+
if (opts.checkPlaceholderFail && opts.checkPlaceholder === undefined) {
|
|
992
|
+
opts.checkPlaceholder = true;
|
|
993
|
+
}
|
|
994
|
+
if (opts.checkImagesFail && opts.checkImages === undefined)
|
|
995
|
+
opts.checkImages = true;
|
|
996
|
+
if (opts.checkTruncationFail && opts.checkTruncation === undefined) {
|
|
997
|
+
opts.checkTruncation = true;
|
|
998
|
+
}
|
|
999
|
+
if (opts.checkContrastFail && opts.checkContrast === undefined)
|
|
1000
|
+
opts.checkContrast = true;
|
|
1001
|
+
if (opts.checkCritiqueFail && opts.checkCritique === undefined)
|
|
1002
|
+
opts.checkCritique = true;
|
|
1003
|
+
if (opts.checkHitFail && (!opts.checkHit || opts.checkHit.length === 0)) {
|
|
1004
|
+
opts.checkHit = [null];
|
|
1005
|
+
}
|
|
1006
|
+
const requireTargets = (failEnabled, targets, failFlag, checkFlag) => {
|
|
1007
|
+
if (failEnabled && (!targets || targets.length === 0)) {
|
|
1008
|
+
throw new Error(`${failFlag} requires ${checkFlag}.`);
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
requireTargets(opts.checkVisibleFail, opts.checkVisible, "--check-visible-fail", "--check-visible");
|
|
1012
|
+
requireTargets(opts.checkWidthFail, opts.checkWidth, "--check-width-fail", "--check-width");
|
|
1013
|
+
requireTargets(opts.checkAlignFail, opts.checkAlign, "--check-align-fail", "--check-align");
|
|
1014
|
+
requireTargets(opts.checkGapFail, opts.checkGap, "--check-gap-fail", "--check-gap");
|
|
1015
|
+
requireTargets(opts.checkClipFail, opts.checkClip, "--check-clip-fail", "--check-clip");
|
|
1016
|
+
requireTargets(opts.checkOverlapFail, opts.checkOverlap, "--check-overlap-fail", "--check-overlap");
|
|
1017
|
+
requireTargets(opts.checkCrowdFail, opts.checkCrowd, "--check-crowd-fail", "--check-crowd");
|
|
1018
|
+
requireTargets(opts.assertFail, opts.assert, "--assert-fail", "--assert");
|
|
1019
|
+
if (opts.diffFail && !opts.diff) {
|
|
1020
|
+
throw new Error("--diff-fail requires --diff.");
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
905
1023
|
function parseLayoutAxis(value, flag) {
|
|
906
1024
|
if (value === "auto" || value === "x" || value === "y")
|
|
907
1025
|
return value;
|
|
@@ -943,9 +1061,14 @@ function applyLayoutLintFailGates(opts, layoutLint, hit) {
|
|
|
943
1061
|
const gate = (enabled, rule, results) => {
|
|
944
1062
|
if (!enabled || !results)
|
|
945
1063
|
return;
|
|
946
|
-
const failed = results.filter((result) => !result.found || result.outcome === "fail");
|
|
1064
|
+
const failed = results.filter((result) => !result.found || result.outcome === "fail" || result.outcome === "unknown");
|
|
947
1065
|
for (const result of failed) {
|
|
948
|
-
|
|
1066
|
+
const reason = !result.found
|
|
1067
|
+
? "element not found"
|
|
1068
|
+
: result.outcome === "unknown"
|
|
1069
|
+
? "result unknown"
|
|
1070
|
+
: "rendered relation failed";
|
|
1071
|
+
emit.log(`check-${rule} FAIL ${result.selector}: ${reason}`, "warn");
|
|
949
1072
|
}
|
|
950
1073
|
if (failed.length > 0)
|
|
951
1074
|
process.exitCode = 2;
|
|
@@ -1057,8 +1180,13 @@ function applyContentFailGates(opts, content) {
|
|
|
1057
1180
|
const gate = (enabled, rule, result) => {
|
|
1058
1181
|
if (!enabled || !result)
|
|
1059
1182
|
return;
|
|
1060
|
-
if (!result.found || result.outcome
|
|
1061
|
-
|
|
1183
|
+
if (!result.found || result.outcome !== "pass") {
|
|
1184
|
+
const reason = !result.found
|
|
1185
|
+
? "scope not found"
|
|
1186
|
+
: result.outcome === "unknown"
|
|
1187
|
+
? "result unknown"
|
|
1188
|
+
: "issues found";
|
|
1189
|
+
emit.log(`check-${rule} FAIL: ${reason}`, "warn");
|
|
1062
1190
|
process.exitCode = 2;
|
|
1063
1191
|
}
|
|
1064
1192
|
};
|
|
@@ -1122,7 +1250,7 @@ function summarizeDiagnostics(diag) {
|
|
|
1122
1250
|
* 2. HARNERY_BROWSER_ARGS env (whitespace-separated) — a machine-wide default.
|
|
1123
1251
|
* 3. --browser-arg flags on this invocation (repeatable).
|
|
1124
1252
|
*/
|
|
1125
|
-
function resolveLaunchArgs(opts, headed) {
|
|
1253
|
+
function resolveLaunchArgs(opts, headed, proxyEnabled) {
|
|
1126
1254
|
const args = [];
|
|
1127
1255
|
if (headed && !process.env.HARNERY_BROWSER_NO_WSL_DEFAULTS) {
|
|
1128
1256
|
args.push(...wslHeadedLaunchArgs());
|
|
@@ -1132,8 +1260,48 @@ function resolveLaunchArgs(opts, headed) {
|
|
|
1132
1260
|
args.push(...envArgs.split(/\s+/));
|
|
1133
1261
|
if (opts.browserArg && opts.browserArg.length > 0)
|
|
1134
1262
|
args.push(...opts.browserArg);
|
|
1263
|
+
if (proxyEnabled)
|
|
1264
|
+
args.push(WEBRTC_PROXY_ONLY_ARG);
|
|
1135
1265
|
return [...new Set(args)];
|
|
1136
1266
|
}
|
|
1267
|
+
async function verifyBrowserProxyGate(browser, gate) {
|
|
1268
|
+
const result = await browser.navigate(gate.checkUrl);
|
|
1269
|
+
if (result.status === null || result.status < 200 || result.status >= 300) {
|
|
1270
|
+
throw new Error(`Browser proxy gate failed before target navigation (HTTP ${result.status ?? "unknown"}).`);
|
|
1271
|
+
}
|
|
1272
|
+
const body = await browser.evaluate("document.body?.innerText ?? ''");
|
|
1273
|
+
const observedIp = extractObservedIp(body);
|
|
1274
|
+
if (observedIp !== gate.expectedIp) {
|
|
1275
|
+
throw new Error(`Browser proxy gate returned ${observedIp ?? "no IP"}; expected ${gate.expectedIp}. Target navigation was blocked.`);
|
|
1276
|
+
}
|
|
1277
|
+
emit.log(`browser proxy gate: ${observedIp} · expected IP confirmed`, "info");
|
|
1278
|
+
}
|
|
1279
|
+
async function waitForLoginCloseFile(path, signal) {
|
|
1280
|
+
while (!existsSync(path) && !signal?.aborted) {
|
|
1281
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
1282
|
+
}
|
|
1283
|
+
if (signal?.aborted)
|
|
1284
|
+
return;
|
|
1285
|
+
unlinkSync(path);
|
|
1286
|
+
}
|
|
1287
|
+
async function waitForTerminalEnter(signal) {
|
|
1288
|
+
await new Promise((resolveWait) => {
|
|
1289
|
+
const onData = () => {
|
|
1290
|
+
cleanup();
|
|
1291
|
+
resolveWait();
|
|
1292
|
+
};
|
|
1293
|
+
const onAbort = () => {
|
|
1294
|
+
cleanup();
|
|
1295
|
+
resolveWait();
|
|
1296
|
+
};
|
|
1297
|
+
const cleanup = () => {
|
|
1298
|
+
process.stdin.removeListener("data", onData);
|
|
1299
|
+
signal.removeEventListener("abort", onAbort);
|
|
1300
|
+
};
|
|
1301
|
+
process.stdin.once("data", onData);
|
|
1302
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1137
1305
|
function parseViewport(spec) {
|
|
1138
1306
|
const preset = VIEWPORT_PRESETS[spec.toLowerCase()];
|
|
1139
1307
|
if (preset)
|
|
@@ -134,7 +134,7 @@ function resolveContinuityIdentity(context, opts) {
|
|
|
134
134
|
return { coordRoot, instanceId, sessionId, adapter };
|
|
135
135
|
}
|
|
136
136
|
function adapterFromPlatform(platform) {
|
|
137
|
-
if (platform === "
|
|
137
|
+
if (platform === "claude-code")
|
|
138
138
|
return "claude-code";
|
|
139
139
|
if (platform === "cursor")
|
|
140
140
|
return "cursor";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deinit.d.ts","sourceRoot":"","sources":["../../src/commands/deinit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"deinit.d.ts","sourceRoot":"","sources":["../../src/commands/deinit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAenD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAmHjG;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE;IACtC,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;CACtB,GAAG,OAAO,CAEV;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAM1C"}
|
package/dist/commands/deinit.js
CHANGED
|
@@ -32,6 +32,7 @@ import { createInterface } from "node:readline";
|
|
|
32
32
|
import { DEFAULT_BIN_NAME } from "../core/config.js";
|
|
33
33
|
import { ADAPTER_SPECS } from "../core/hooks/adapter/events.js";
|
|
34
34
|
import { removeInstructions } from "../lib/instructions/apply.js";
|
|
35
|
+
import { removeGitHooks } from "../lib/instructions/git-hooks.js";
|
|
35
36
|
import { unwireHooks } from "./init.js";
|
|
36
37
|
export function registerDeinitCommand(program, emit, binName) {
|
|
37
38
|
// The interactive prompt + engine-removal hint are standalone-harn niceties.
|
|
@@ -117,6 +118,9 @@ export function registerDeinitCommand(program, emit, binName) {
|
|
|
117
118
|
// ── 1b. agent-facing instructions block + skills ───────────────────────
|
|
118
119
|
const removed = removeInstructions(projectRoot, { adapter, dryRun });
|
|
119
120
|
actions.push(...removed.actions, ...removed.warnings.map((w) => `! ${w}`));
|
|
121
|
+
// ── 1c. git-hook managed regions ───────────────────────────────────────
|
|
122
|
+
const gitHooks = removeGitHooks(projectRoot, { dryRun });
|
|
123
|
+
actions.push(...gitHooks.actions, ...gitHooks.warnings.map((w) => `! ${w}`));
|
|
120
124
|
// ── 2. coord root (opt-in; destructive) ────────────────────────────────
|
|
121
125
|
if (purgeState) {
|
|
122
126
|
if (!coordExists) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/commands/docs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/commands/docs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAmC1E,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,WAAW,EACtB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CA0IN"}
|
package/dist/commands/docs.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { initDocsContext as initDocs, scanDocs } from "../lib/docs.js";
|
|
2
2
|
import { initDocsMigrationContext, runFrontmatterMigration, } from "../lib/docs-frontmatter-migrate.js";
|
|
3
3
|
import { initDocsContext as initDocsIndex, runIndex } from "../lib/docs-index.js";
|
|
4
|
+
import { initDocsContext as initDocsLinks, runLinks } from "../lib/docs-links.js";
|
|
4
5
|
import { initDocsContext as initDocsLint, runLint } from "../lib/docs-lint.js";
|
|
5
6
|
import { readDocsMetadata, readDocsMetadataKey } from "../lib/docs-meta.js";
|
|
6
7
|
import { countColdHandoffs, initDocsContext as initDocsSweep, runSweep, } from "../lib/docs-sweep.js";
|
|
@@ -17,6 +18,7 @@ function ensureContext(context) {
|
|
|
17
18
|
extraExcludedPrefixes: context.extraDocsExcludedPrefixes,
|
|
18
19
|
docsRootAllowlist: context.docsRootAllowlist,
|
|
19
20
|
});
|
|
21
|
+
initDocsLinks({ ...opts, extraExcludedPrefixes: context.extraDocsExcludedPrefixes });
|
|
20
22
|
initDocsSweep(opts);
|
|
21
23
|
}
|
|
22
24
|
let emit;
|
|
@@ -89,6 +91,25 @@ export function registerDocsCommand(program, emitParam, context) {
|
|
|
89
91
|
emit.error({ code: "docs_error", message: msg });
|
|
90
92
|
}
|
|
91
93
|
});
|
|
94
|
+
docs
|
|
95
|
+
.command("links")
|
|
96
|
+
.description("Check internal Markdown link targets and heading fragments")
|
|
97
|
+
.option("--repo <name>", "Limit to one submodule or '.' for parent")
|
|
98
|
+
.option("--no-fragments", "Check target existence only; skip heading-fragment validation")
|
|
99
|
+
.option("--strict", "Report findings in archive/audit/changelog docs as errors, not warnings")
|
|
100
|
+
.option("--check-escapes", "Also flag links that resolve outside their own repo root")
|
|
101
|
+
.option("--fail", "Exit non-zero when errors are found (advisory by default)")
|
|
102
|
+
.option("--format <type>", "Output format: human, json", "human")
|
|
103
|
+
.action(async (opts) => {
|
|
104
|
+
try {
|
|
105
|
+
ensureContext(context);
|
|
106
|
+
await handleLinks(opts);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
110
|
+
emit.error({ code: "docs_error", message: msg });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
92
113
|
docs
|
|
93
114
|
.command("sweep")
|
|
94
115
|
.description("Surface stalled lifecycle states: stalled plans, cold issues, unverified runbooks")
|
|
@@ -185,6 +206,25 @@ async function handleLint(opts) {
|
|
|
185
206
|
if (errors.length > 0)
|
|
186
207
|
emit.setExitCode(1);
|
|
187
208
|
}
|
|
209
|
+
// --- `harn docs links` ---
|
|
210
|
+
/**
|
|
211
|
+
* Advisory by default: a clean exit code even with findings, because turning
|
|
212
|
+
* link health into a gate is an enforcement policy decision for the host, not
|
|
213
|
+
* something this command should assume. `--fail` is the opt-in for CI or a hook.
|
|
214
|
+
*/
|
|
215
|
+
async function handleLinks(opts) {
|
|
216
|
+
if (opts.format === "json")
|
|
217
|
+
emit.config({ format: "json" });
|
|
218
|
+
const report = await runLinks({
|
|
219
|
+
repo: opts.repo,
|
|
220
|
+
noFragments: opts.fragments === false,
|
|
221
|
+
strict: opts.strict,
|
|
222
|
+
checkEscapes: opts.checkEscapes,
|
|
223
|
+
});
|
|
224
|
+
emit.data({ ...report, advisory: !opts.fail });
|
|
225
|
+
if (opts.fail && report.error_count > 0)
|
|
226
|
+
emit.setExitCode(1);
|
|
227
|
+
}
|
|
188
228
|
// --- `harn docs sweep` ---
|
|
189
229
|
async function handleSweep(opts) {
|
|
190
230
|
if (opts.format === "json")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAgBnD,KAAK,QAAQ,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvC,UAAU,KAAK;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAOD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAmD/E;AAED,wBAAgB,SAAS,IAAI,KAAK,EAAE,CAsBnC"}
|