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
|
@@ -198,8 +198,12 @@ export function intersectRects(a: LayoutRect, b: LayoutRect): LayoutRect | null
|
|
|
198
198
|
*/
|
|
199
199
|
export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLintResult {
|
|
200
200
|
return (request) => {
|
|
201
|
-
const CHILD_LIMIT =
|
|
201
|
+
const CHILD_LIMIT = 1_000;
|
|
202
|
+
const TEXT_LIMIT = 1_000;
|
|
202
203
|
const ISSUE_LIMIT = 100;
|
|
204
|
+
// Chromium geometry is fractional even when authored CSS is integral.
|
|
205
|
+
// Differences below half a CSS pixel are rasterization noise, not clipping.
|
|
206
|
+
const SUBPIXEL_EPSILON = 0.5;
|
|
203
207
|
|
|
204
208
|
const rectOf = (rect: DOMRect): LayoutRect => ({
|
|
205
209
|
x: rect.x,
|
|
@@ -233,6 +237,14 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
|
|
|
233
237
|
const isHidden = (element: Element): boolean => {
|
|
234
238
|
let current: Element | null = element;
|
|
235
239
|
while (current) {
|
|
240
|
+
// A collapsed disclosure hides its content through the ::details-content
|
|
241
|
+
// pseudo, which is not in the ancestor chain, so no computed style on a
|
|
242
|
+
// real ancestor reports it. Without this, every closed accordion panel
|
|
243
|
+
// reads as text clipped out of its container.
|
|
244
|
+
const parent: Element | null = current.parentElement;
|
|
245
|
+
if (parent instanceof HTMLDetailsElement && !parent.open && current.tagName !== "SUMMARY") {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
236
248
|
const style = getComputedStyle(current);
|
|
237
249
|
const contentVisibility = (style as CSSStyleDeclaration & { contentVisibility?: string })
|
|
238
250
|
.contentVisibility;
|
|
@@ -495,6 +507,29 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
|
|
|
495
507
|
return { x: clips.has(style.overflowX), y: clips.has(style.overflowY) };
|
|
496
508
|
};
|
|
497
509
|
|
|
510
|
+
// A scroller hides content the same way a clip does, but the reader can
|
|
511
|
+
// reach it. Content below the fold of a capped table or right of a wide
|
|
512
|
+
// code block is not a layout defect, so an axis stops being checked at the
|
|
513
|
+
// first ancestor that scrolls it. Constraints from ancestors INSIDE that
|
|
514
|
+
// scroller still apply -- an overflow:hidden box within a scroller really
|
|
515
|
+
// does cut its content off, and the scroller cannot reveal it.
|
|
516
|
+
const scrollingStyle = (element: Element): { x: boolean; y: boolean } => {
|
|
517
|
+
const style = getComputedStyle(element);
|
|
518
|
+
const scrolls = new Set(["auto", "scroll"]);
|
|
519
|
+
return { x: scrolls.has(style.overflowX), y: scrolls.has(style.overflowY) };
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const unbounded = (): LayoutRect => ({
|
|
523
|
+
x: Number.NEGATIVE_INFINITY,
|
|
524
|
+
y: Number.NEGATIVE_INFINITY,
|
|
525
|
+
left: Number.NEGATIVE_INFINITY,
|
|
526
|
+
top: Number.NEGATIVE_INFINITY,
|
|
527
|
+
right: Number.POSITIVE_INFINITY,
|
|
528
|
+
bottom: Number.POSITIVE_INFINITY,
|
|
529
|
+
width: Number.POSITIVE_INFINITY,
|
|
530
|
+
height: Number.POSITIVE_INFINITY,
|
|
531
|
+
});
|
|
532
|
+
|
|
498
533
|
const paddingRect = (element: Element): LayoutRect => {
|
|
499
534
|
const rect = element.getBoundingClientRect();
|
|
500
535
|
const html = element as HTMLElement;
|
|
@@ -515,8 +550,8 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
|
|
|
515
550
|
};
|
|
516
551
|
|
|
517
552
|
const clip = request.clip.map(({ selector, tolerancePx }): ClipResult => {
|
|
518
|
-
const
|
|
519
|
-
if (
|
|
553
|
+
const containers = [...document.querySelectorAll(selector)];
|
|
554
|
+
if (containers.length === 0) {
|
|
520
555
|
return {
|
|
521
556
|
rule: "clip" as const,
|
|
522
557
|
selector,
|
|
@@ -532,31 +567,59 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
|
|
|
532
567
|
const unsupported = new Set<string>();
|
|
533
568
|
const excluded: LayoutExclusion[] = [];
|
|
534
569
|
const issues: ClipIssue[] = [];
|
|
535
|
-
|
|
536
|
-
let
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
570
|
+
let truncated = false;
|
|
571
|
+
let measurementIndex = 0;
|
|
572
|
+
|
|
573
|
+
const recordIssue = (
|
|
574
|
+
element: LayoutElementMeasurement,
|
|
575
|
+
allowed: LayoutRect,
|
|
576
|
+
clippedBy: string,
|
|
577
|
+
axes: { x: boolean; y: boolean } = { x: true, y: true },
|
|
578
|
+
): void => {
|
|
579
|
+
const rect = element.rect;
|
|
580
|
+
const overrun = {
|
|
581
|
+
top: axes.y ? Math.max(0, allowed.top - rect.top) : 0,
|
|
582
|
+
right: axes.x ? Math.max(0, rect.right - allowed.right) : 0,
|
|
583
|
+
bottom: axes.y ? Math.max(0, rect.bottom - allowed.bottom) : 0,
|
|
584
|
+
left: axes.x ? Math.max(0, allowed.left - rect.left) : 0,
|
|
585
|
+
};
|
|
586
|
+
const maxOverrunPx = Math.max(overrun.top, overrun.right, overrun.bottom, overrun.left);
|
|
587
|
+
if (maxOverrunPx > tolerancePx + SUBPIXEL_EPSILON && issues.length < ISSUE_LIMIT) {
|
|
588
|
+
issues.push({ element, clippedBy, overrun, maxOverrunPx });
|
|
546
589
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// Walk from `start` up to `container`, intersecting the allowed rect with
|
|
593
|
+
// every ancestor that genuinely clips. `free` records an axis the reader
|
|
594
|
+
// can scroll, after which nothing further constrains that axis -- the
|
|
595
|
+
// scroller's own box still has to fit its outer chain, but its contents
|
|
596
|
+
// are reachable and so are exempt.
|
|
597
|
+
const clipChain = (
|
|
598
|
+
start: Element | null,
|
|
599
|
+
container: Element,
|
|
600
|
+
seed: LayoutRect,
|
|
601
|
+
seedLabel: string,
|
|
602
|
+
): { allowed: LayoutRect; clippedBy: string } => {
|
|
603
|
+
let allowed = { ...seed };
|
|
604
|
+
let clippedBy = seedLabel;
|
|
605
|
+
const free = { x: false, y: false };
|
|
606
|
+
let current: Element | null = start;
|
|
550
607
|
while (current && container.contains(current)) {
|
|
551
608
|
const style = getComputedStyle(current);
|
|
552
609
|
if (style.clipPath && style.clipPath !== "none")
|
|
553
610
|
unsupported.add(`${labelOf(current)}:clip-path`);
|
|
554
|
-
if (style.borderRadius && !/^0(?:px)?(?: 0(?:px)?){0,3}$/.test(style.borderRadius)) {
|
|
555
|
-
unsupported.add(`${labelOf(current)}:border-radius`);
|
|
556
|
-
}
|
|
557
611
|
if (style.transform && style.transform !== "none")
|
|
558
612
|
unsupported.add(`${labelOf(current)}:transform`);
|
|
559
|
-
|
|
613
|
+
// The container bounds by default even when it does not CSS-clip,
|
|
614
|
+
// since it is the scope the caller asked about -- but a scroller
|
|
615
|
+
// hands its axis back whether it is the scope or an ancestor.
|
|
616
|
+
const scope = current === container;
|
|
617
|
+
const scrolls = scrollingStyle(current);
|
|
618
|
+
const css = scope ? { x: true, y: true } : clippingStyle(current);
|
|
619
|
+
const clips = {
|
|
620
|
+
x: css.x && !free.x && !scrolls.x,
|
|
621
|
+
y: css.y && !free.y && !scrolls.y,
|
|
622
|
+
};
|
|
560
623
|
if (clips.x || clips.y) {
|
|
561
624
|
const candidate = paddingRect(current);
|
|
562
625
|
allowed = {
|
|
@@ -573,26 +636,123 @@ export function buildLayoutLintCheck(): (request: LayoutLintRequest) => LayoutLi
|
|
|
573
636
|
allowed.height = Math.max(0, allowed.bottom - allowed.top);
|
|
574
637
|
clippedBy = labelOf(current);
|
|
575
638
|
}
|
|
576
|
-
if (
|
|
639
|
+
if (scrolls.x) free.x = true;
|
|
640
|
+
if (scrolls.y) free.y = true;
|
|
641
|
+
if (scope) break;
|
|
577
642
|
current = current.parentElement;
|
|
578
643
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
overrun,
|
|
591
|
-
maxOverrunPx,
|
|
592
|
-
});
|
|
644
|
+
return { allowed, clippedBy };
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const textContainer = (node: Node, scope: Element): Element | null => {
|
|
648
|
+
let current = node.parentElement;
|
|
649
|
+
while (current && scope.contains(current)) {
|
|
650
|
+
if (current instanceof SVGElement) return null;
|
|
651
|
+
const display = getComputedStyle(current).display;
|
|
652
|
+
if (display !== "inline" && display !== "contents") return current;
|
|
653
|
+
if (current === scope) return current;
|
|
654
|
+
current = current.parentElement;
|
|
593
655
|
}
|
|
594
|
-
|
|
656
|
+
return scope;
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
for (const container of containers) {
|
|
660
|
+
const descendants = [...container.querySelectorAll("*")];
|
|
661
|
+
if (descendants.length > CHILD_LIMIT) truncated = true;
|
|
662
|
+
descendants.slice(0, CHILD_LIMIT).forEach((element) => {
|
|
663
|
+
const index = measurementIndex++;
|
|
664
|
+
if (isHidden(element)) {
|
|
665
|
+
excluded.push({ index, tag: element.tagName.toLowerCase(), reason: "hidden" });
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const rect = element.getBoundingClientRect();
|
|
669
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
670
|
+
excluded.push({ index, tag: element.tagName.toLowerCase(), reason: "zero-area" });
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const chain = clipChain(
|
|
674
|
+
element.parentElement,
|
|
675
|
+
container,
|
|
676
|
+
unbounded(),
|
|
677
|
+
labelOf(container),
|
|
678
|
+
);
|
|
679
|
+
recordIssue(measure(element, index, false), chain.allowed, chain.clippedBy);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
683
|
+
let textCount = 0;
|
|
684
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
685
|
+
if (textCount >= TEXT_LIMIT) {
|
|
686
|
+
truncated = true;
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
const value = node.nodeValue ?? "";
|
|
690
|
+
if (!/\S/.test(value)) continue;
|
|
691
|
+
const parent = node.parentElement;
|
|
692
|
+
if (
|
|
693
|
+
!parent ||
|
|
694
|
+
["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"].includes(parent.tagName) ||
|
|
695
|
+
isHidden(parent)
|
|
696
|
+
) {
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
const owner = textContainer(node, container);
|
|
700
|
+
if (!owner) continue;
|
|
701
|
+
let transformed = false;
|
|
702
|
+
let current: Element | null = owner;
|
|
703
|
+
while (current && container.contains(current)) {
|
|
704
|
+
const transform = getComputedStyle(current).transform;
|
|
705
|
+
if (transform && transform !== "none") {
|
|
706
|
+
unsupported.add(`${labelOf(current)}:transform`);
|
|
707
|
+
transformed = true;
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
if (current === container) break;
|
|
711
|
+
current = current.parentElement;
|
|
712
|
+
}
|
|
713
|
+
if (transformed) continue;
|
|
714
|
+
textCount++;
|
|
715
|
+
// Text glyph boxes routinely extend a few pixels above/below their
|
|
716
|
+
// line box. That is normal font paint, not clipping. Constrain text
|
|
717
|
+
// horizontally to its nearest block owner, vertically to the scope,
|
|
718
|
+
// and on either axis where a CSS ancestor actually clips.
|
|
719
|
+
const seed = unbounded();
|
|
720
|
+
// Constrain text horizontally to its nearest block owner -- unless
|
|
721
|
+
// that owner scrolls sideways, where running past its edge is how a
|
|
722
|
+
// long line is meant to behave.
|
|
723
|
+
if (!scrollingStyle(owner).x) {
|
|
724
|
+
const ownerRect = paddingRect(owner);
|
|
725
|
+
seed.left = ownerRect.left;
|
|
726
|
+
seed.right = ownerRect.right;
|
|
727
|
+
seed.x = seed.left;
|
|
728
|
+
seed.width = Math.max(0, seed.right - seed.left);
|
|
729
|
+
}
|
|
730
|
+
const chain = clipChain(owner, container, seed, labelOf(owner));
|
|
731
|
+
const allowed = chain.allowed;
|
|
732
|
+
const clippedBy = chain.clippedBy;
|
|
733
|
+
const range = document.createRange();
|
|
734
|
+
range.selectNodeContents(node);
|
|
735
|
+
for (const fragment of [...range.getClientRects()]) {
|
|
736
|
+
if (fragment.width <= 0 || fragment.height <= 0) continue;
|
|
737
|
+
const index = measurementIndex++;
|
|
738
|
+
recordIssue(
|
|
739
|
+
{
|
|
740
|
+
index,
|
|
741
|
+
tag: parent.tagName.toLowerCase(),
|
|
742
|
+
label: labelOf(parent),
|
|
743
|
+
snippet: value.replace(/\s+/g, " ").trim().slice(0, 160),
|
|
744
|
+
source: "text",
|
|
745
|
+
rect: rectOf(fragment),
|
|
746
|
+
},
|
|
747
|
+
allowed,
|
|
748
|
+
clippedBy,
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
595
753
|
if (issues.length >= ISSUE_LIMIT) truncated = true;
|
|
754
|
+
// Border radius keeps the rectangular padding-box contract; only shapes
|
|
755
|
+
// the rectangle cannot certify contribute to unsupported geometry.
|
|
596
756
|
const outcome: LayoutOutcome =
|
|
597
757
|
issues.length > 0 ? "fail" : unsupported.size > 0 ? "unknown" : "pass";
|
|
598
758
|
return {
|
package/src/lib/browser/index.ts
CHANGED
|
@@ -8,6 +8,15 @@ export {
|
|
|
8
8
|
export {
|
|
9
9
|
Browser,
|
|
10
10
|
type BrowserOptions,
|
|
11
|
+
BrowserSessionActionError,
|
|
12
|
+
type BrowserSessionControl,
|
|
13
|
+
type BrowserSessionInspection,
|
|
14
|
+
type BrowserSessionLocator,
|
|
15
|
+
type BrowserSessionNavigation,
|
|
16
|
+
type BrowserSessionNavigationType,
|
|
17
|
+
type BrowserSessionScreenshot,
|
|
18
|
+
type BrowserSessionStatus,
|
|
19
|
+
type BrowserSessionTab,
|
|
11
20
|
type ConsoleEvent,
|
|
12
21
|
type Diagnostics,
|
|
13
22
|
type FailedRequest,
|
|
@@ -65,16 +74,46 @@ export type {
|
|
|
65
74
|
OverlapIssue,
|
|
66
75
|
OverlapResult,
|
|
67
76
|
} from "./geometry.js";
|
|
77
|
+
export { buildLayoutLintCheck } from "./geometry.js";
|
|
68
78
|
export { isWSL, wslHeadedLaunchArgs } from "./launch-args.js";
|
|
69
79
|
export type {
|
|
70
80
|
OverflowElement,
|
|
71
81
|
OverflowResult,
|
|
72
82
|
WidthResult,
|
|
73
83
|
} from "./layout.js";
|
|
84
|
+
export {
|
|
85
|
+
serializeNetscapeCookies,
|
|
86
|
+
writeNetscapeCookieFile,
|
|
87
|
+
} from "./netscape-cookies.js";
|
|
88
|
+
export {
|
|
89
|
+
type BrowserProxy,
|
|
90
|
+
type BrowserProxyGate,
|
|
91
|
+
browserProxyFromEnv,
|
|
92
|
+
browserProxyGateFromEnv,
|
|
93
|
+
extractObservedIp,
|
|
94
|
+
WEBRTC_PROXY_ONLY_ARG,
|
|
95
|
+
} from "./proxy.js";
|
|
74
96
|
export type {
|
|
75
97
|
RuntHit,
|
|
76
98
|
RuntsResult,
|
|
77
99
|
} from "./runts.js";
|
|
100
|
+
export {
|
|
101
|
+
BROWSER_SESSION_MAX_FILL_BYTES,
|
|
102
|
+
BROWSER_SESSION_MAX_FRAME_BYTES,
|
|
103
|
+
BROWSER_SESSION_MAX_INSPECTION_TEXT_BYTES,
|
|
104
|
+
BROWSER_SESSION_PROTOCOL_VERSION,
|
|
105
|
+
type BrowserSessionDescriptor,
|
|
106
|
+
BrowserSessionError,
|
|
107
|
+
type BrowserSessionRequest,
|
|
108
|
+
type BrowserSessionResponse,
|
|
109
|
+
type BrowserSessionResult,
|
|
110
|
+
type BrowserSessionServer,
|
|
111
|
+
type BrowserSessionTarget,
|
|
112
|
+
browserSessionTransport,
|
|
113
|
+
readBrowserSessionDescriptor,
|
|
114
|
+
sendBrowserSessionRequest,
|
|
115
|
+
startBrowserSessionServer,
|
|
116
|
+
} from "./session-control.js";
|
|
78
117
|
export type {
|
|
79
118
|
TargetSizeNode,
|
|
80
119
|
TargetSizeOutcome,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import type { Cookie } from "playwright";
|
|
4
|
+
|
|
5
|
+
function cookieField(value: string): string {
|
|
6
|
+
return value.replace(/[\t\r\n]/g, "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Serialize Playwright cookies in the Netscape cookies.txt format used by yt-dlp. */
|
|
10
|
+
export function serializeNetscapeCookies(cookies: readonly Cookie[]): string {
|
|
11
|
+
const lines = ["# Netscape HTTP Cookie File", "# Generated by harn browse. Keep private.", ""];
|
|
12
|
+
for (const cookie of [...cookies].sort((a, b) =>
|
|
13
|
+
`${a.domain}\0${a.path}\0${a.name}`.localeCompare(`${b.domain}\0${b.path}\0${b.name}`),
|
|
14
|
+
)) {
|
|
15
|
+
const domain = `${cookie.httpOnly ? "#HttpOnly_" : ""}${cookieField(cookie.domain)}`;
|
|
16
|
+
const includeSubdomains = cookie.domain.startsWith(".") ? "TRUE" : "FALSE";
|
|
17
|
+
const secure = cookie.secure ? "TRUE" : "FALSE";
|
|
18
|
+
const expires = cookie.expires > 0 ? String(Math.floor(cookie.expires)) : "0";
|
|
19
|
+
lines.push(
|
|
20
|
+
[
|
|
21
|
+
domain,
|
|
22
|
+
includeSubdomains,
|
|
23
|
+
cookieField(cookie.path || "/"),
|
|
24
|
+
secure,
|
|
25
|
+
expires,
|
|
26
|
+
cookieField(cookie.name),
|
|
27
|
+
cookieField(cookie.value),
|
|
28
|
+
].join("\t"),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return `${lines.join("\n")}\n`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Write a credential-bearing cookie file with owner-only permissions. */
|
|
35
|
+
export function writeNetscapeCookieFile(path: string, cookies: readonly Cookie[]): void {
|
|
36
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
37
|
+
writeFileSync(path, serializeNetscapeCookies(cookies), { mode: 0o600 });
|
|
38
|
+
chmodSync(path, 0o600);
|
|
39
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export interface BrowserProxy {
|
|
2
|
+
server: string;
|
|
3
|
+
username?: string;
|
|
4
|
+
password?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface BrowserProxyGate {
|
|
8
|
+
checkUrl: string;
|
|
9
|
+
expectedIp: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const PROXY_ENV_KEYS = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"] as const;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse an authenticated HTTP(S) proxy from the child environment without
|
|
16
|
+
* requiring credentials in command arguments. The returned server never
|
|
17
|
+
* contains userinfo, so Playwright owns the 407 challenge response.
|
|
18
|
+
*/
|
|
19
|
+
export function browserProxyFromEnv(env: NodeJS.ProcessEnv = process.env): BrowserProxy {
|
|
20
|
+
const raw = PROXY_ENV_KEYS.map((key) => env[key]).find(
|
|
21
|
+
(value): value is string => typeof value === "string" && value.trim().length > 0,
|
|
22
|
+
);
|
|
23
|
+
if (!raw) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"--proxy-from-env requires HTTPS_PROXY, https_proxy, HTTP_PROXY, or http_proxy.",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let url: URL;
|
|
30
|
+
try {
|
|
31
|
+
url = new URL(raw);
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error("The proxy environment contains an invalid URL.");
|
|
34
|
+
}
|
|
35
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
36
|
+
throw new Error("Browser proxy URLs must use http:// or https://.");
|
|
37
|
+
}
|
|
38
|
+
if (!url.hostname || !url.port || url.pathname !== "/" || url.search || url.hash) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"Browser proxy URLs must contain only scheme, host, port, and optional credentials.",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if ((url.username && !url.password) || (!url.username && url.password)) {
|
|
44
|
+
throw new Error("Browser proxy URLs must provide both username and password, or neither.");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const proxy: BrowserProxy = { server: `${url.protocol}//${url.host}` };
|
|
48
|
+
if (url.username) {
|
|
49
|
+
proxy.username = decodeURIComponent(url.username);
|
|
50
|
+
proxy.password = decodeURIComponent(url.password);
|
|
51
|
+
}
|
|
52
|
+
return proxy;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read the optional fixed-egress gate injected by an embedding host wrapper.
|
|
57
|
+
* Both fields are required together so a partial contract
|
|
58
|
+
* fails before Chromium visits the requested target.
|
|
59
|
+
*/
|
|
60
|
+
export function browserProxyGateFromEnv(
|
|
61
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
62
|
+
): BrowserProxyGate | null {
|
|
63
|
+
const expectedIp = env.HARNERY_BROWSER_PROXY_EXPECTED_IP?.trim();
|
|
64
|
+
const checkUrl = env.HARNERY_BROWSER_PROXY_CHECK_URL?.trim();
|
|
65
|
+
if (!expectedIp && !checkUrl) return null;
|
|
66
|
+
if (!expectedIp || !checkUrl) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"HARNERY_BROWSER_PROXY_EXPECTED_IP and HARNERY_BROWSER_PROXY_CHECK_URL must be set together.",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (!/^(?:\d{1,3}\.){3}\d{1,3}$/.test(expectedIp)) {
|
|
72
|
+
throw new Error("HARNERY_BROWSER_PROXY_EXPECTED_IP must be an IPv4 address.");
|
|
73
|
+
}
|
|
74
|
+
let parsed: URL;
|
|
75
|
+
try {
|
|
76
|
+
parsed = new URL(checkUrl);
|
|
77
|
+
} catch {
|
|
78
|
+
throw new Error("HARNERY_BROWSER_PROXY_CHECK_URL must be a valid HTTPS URL.");
|
|
79
|
+
}
|
|
80
|
+
if (parsed.protocol !== "https:") {
|
|
81
|
+
throw new Error("HARNERY_BROWSER_PROXY_CHECK_URL must use HTTPS.");
|
|
82
|
+
}
|
|
83
|
+
return { expectedIp, checkUrl: parsed.toString() };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function extractObservedIp(body: string): string | null {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(body) as Record<string, unknown>;
|
|
89
|
+
for (const key of ["ip", "query", "origin"]) {
|
|
90
|
+
const value = parsed[key];
|
|
91
|
+
if (typeof value === "string" && value.trim()) return value.trim().split(",")[0].trim();
|
|
92
|
+
}
|
|
93
|
+
const proxy = parsed.proxy;
|
|
94
|
+
if (proxy && typeof proxy === "object") {
|
|
95
|
+
const value = (proxy as Record<string, unknown>).ip;
|
|
96
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// A plain-text IP endpoint is also valid.
|
|
100
|
+
}
|
|
101
|
+
const trimmed = body.trim();
|
|
102
|
+
return /^(?:\d{1,3}\.){3}\d{1,3}$/.test(trimmed) ? trimmed : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const WEBRTC_PROXY_ONLY_ARG = "--force-webrtc-ip-handling-policy=disable_non_proxied_udp";
|
package/src/lib/browser/runts.ts
CHANGED
|
@@ -31,6 +31,12 @@ export interface RuntHit {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export interface RuntsResult {
|
|
34
|
+
/** Requested selector scope, or null for the document body. */
|
|
35
|
+
scope: string | null;
|
|
36
|
+
/** False when a requested selector matched nothing. */
|
|
37
|
+
found: boolean;
|
|
38
|
+
/** Unknown means the sweep hit a safety cap before proving the page clean. */
|
|
39
|
+
outcome: "pass" | "fail" | "unknown";
|
|
34
40
|
/** Text blocks scanned (post length/inline-children filters). */
|
|
35
41
|
scannedBlocks: number;
|
|
36
42
|
/** True when the block sweep hit the internal cap (very large page). */
|
|
@@ -71,7 +77,16 @@ export function buildRuntsCheck(): (args: {
|
|
|
71
77
|
const RUNT_CAP = 50;
|
|
72
78
|
|
|
73
79
|
const root = scope ? document.querySelector(scope) : document.body;
|
|
74
|
-
if (!root)
|
|
80
|
+
if (!root) {
|
|
81
|
+
return {
|
|
82
|
+
scope,
|
|
83
|
+
found: false,
|
|
84
|
+
outcome: "fail",
|
|
85
|
+
scannedBlocks: 0,
|
|
86
|
+
truncated: false,
|
|
87
|
+
runts: [],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
75
90
|
|
|
76
91
|
const sx = window.scrollX;
|
|
77
92
|
const sy = window.scrollY;
|
|
@@ -84,7 +99,9 @@ export function buildRuntsCheck(): (args: {
|
|
|
84
99
|
return tag;
|
|
85
100
|
};
|
|
86
101
|
|
|
87
|
-
|
|
102
|
+
// Include the scope itself. A caller commonly points directly at a <p>,
|
|
103
|
+
// caption, or blurb; querySelectorAll("*") would otherwise scan nothing.
|
|
104
|
+
const all = [root, ...root.querySelectorAll("*")];
|
|
88
105
|
const runts: RuntHit[] = [];
|
|
89
106
|
let scanned = 0;
|
|
90
107
|
let truncated = false;
|
|
@@ -158,7 +175,14 @@ export function buildRuntsCheck(): (args: {
|
|
|
158
175
|
}
|
|
159
176
|
}
|
|
160
177
|
|
|
161
|
-
return {
|
|
178
|
+
return {
|
|
179
|
+
scope,
|
|
180
|
+
found: true,
|
|
181
|
+
outcome: runts.length > 0 ? "fail" : truncated ? "unknown" : "pass",
|
|
182
|
+
scannedBlocks: scanned,
|
|
183
|
+
truncated,
|
|
184
|
+
runts,
|
|
185
|
+
};
|
|
162
186
|
};
|
|
163
187
|
}
|
|
164
188
|
|