peaks-loop 4.0.35 → 4.0.37
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/CHANGELOG.md +40 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/bin/peaks.js +71 -1
- package/dist/cli/cli-helpers.js +7 -0
- package/dist/cli/commands/_register.js +2 -0
- package/dist/cli/commands/best-practice-scan-command.d.ts +14 -1
- package/dist/cli/commands/best-practice-scan-command.js +67 -9
- package/dist/cli/commands/code-runtime-commands.d.ts +5 -2
- package/dist/cli/commands/code-runtime-commands.js +78 -7
- package/dist/cli/commands/core/doctor-command.d.ts +8 -0
- package/dist/cli/commands/core/doctor-command.js +44 -2
- package/dist/cli/commands/core/memory-command.js +5 -1
- package/dist/cli/commands/dispatch-commands.js +15 -3
- package/dist/cli/commands/dispatch-from-dag.js +17 -0
- package/dist/cli/commands/hooks-commands.js +10 -1
- package/dist/cli/commands/job-commands.js +107 -25
- package/dist/cli/commands/memory-commands.d.ts +24 -0
- package/dist/cli/commands/memory-commands.js +77 -10
- package/dist/cli/commands/request-commands.d.ts +8 -0
- package/dist/cli/commands/request-commands.js +23 -2
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/sub-agent-commands.js +2 -0
- package/dist/cli/commands/wave-plan-commands.d.ts +24 -0
- package/dist/cli/commands/wave-plan-commands.js +93 -0
- package/dist/cli/commands/web-commands.d.ts +28 -0
- package/dist/cli/commands/web-commands.js +327 -0
- package/dist/cli/commands/web-lifecycle-commands.d.ts +49 -0
- package/dist/cli/commands/web-lifecycle-commands.js +321 -0
- package/dist/services/best-practice/scan-orchestrator.d.ts +22 -0
- package/dist/services/best-practice/scan-orchestrator.js +14 -5
- package/dist/services/code/orchestrator-can-do.js +27 -4
- package/dist/services/context/build-dispatch-system-prompt.d.ts +66 -9
- package/dist/services/context/build-dispatch-system-prompt.js +132 -17
- package/dist/services/context/context-audit-hint.d.ts +79 -0
- package/dist/services/context/context-audit-hint.js +150 -0
- package/dist/services/context/context-audit.d.ts +100 -0
- package/dist/services/context/context-audit.js +322 -0
- package/dist/services/context/summary-view.d.ts +54 -0
- package/dist/services/context/summary-view.js +114 -0
- package/dist/services/dispatch/file-overlap-wave-planner.d.ts +70 -0
- package/dist/services/dispatch/file-overlap-wave-planner.js +119 -0
- package/dist/services/dispatch/session-capsule.d.ts +23 -0
- package/dist/services/dispatch/session-capsule.js +56 -0
- package/dist/services/dispatch/slice-dag.d.ts +9 -0
- package/dist/services/dispatch/slice-dag.js +9 -1
- package/dist/services/dispatch/test-tool-detection.d.ts +12 -1
- package/dist/services/dispatch/test-tool-detection.js +14 -13
- package/dist/services/hooks/auto-compact-hook-install.js +10 -1
- package/dist/services/hooks/write-gate.js +88 -0
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +10 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +20 -1
- package/dist/services/ide/ide-types.d.ts +15 -0
- package/dist/services/lint/detect-eslint.d.ts +2 -0
- package/dist/services/lint/detect-eslint.js +23 -9
- package/dist/services/lint/npx-resolver.d.ts +6 -0
- package/dist/services/lint/npx-resolver.js +38 -14
- package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +5 -0
- package/dist/services/memory/project-memory-service/parsers/frontmatter.js +55 -5
- package/dist/services/release/version-precheck-service.js +9 -2
- package/dist/services/scan/file-size-scan.d.ts +29 -0
- package/dist/services/scan/file-size-scan.js +63 -0
- package/dist/services/session/caller-binding-service.d.ts +24 -0
- package/dist/services/session/caller-binding-service.js +34 -0
- package/dist/services/session/getSessionDir.js +15 -10
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +33 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +34 -3
- package/dist/services/skills/hooks-settings-service.d.ts +10 -0
- package/dist/services/skills/hooks-settings-service.js +152 -61
- package/dist/services/slice/slice-check-service.d.ts +14 -0
- package/dist/services/slice/slice-check-service.js +110 -50
- package/dist/services/slice/slice-check-types.d.ts +12 -7
- package/dist/services/slice/slice-check-types.js +8 -3
- package/dist/services/slice/slice-decompose-runners.js +24 -21
- package/dist/services/sop/sop-check-service.js +12 -1
- package/dist/services/web/bounded-output.d.ts +34 -0
- package/dist/services/web/bounded-output.js +68 -0
- package/dist/services/web/browser-acquire.d.ts +14 -0
- package/dist/services/web/browser-acquire.js +84 -0
- package/dist/services/web/browser-session-manager.d.ts +111 -0
- package/dist/services/web/browser-session-manager.js +413 -0
- package/dist/services/web/daemon-entry.d.ts +1 -0
- package/dist/services/web/daemon-entry.js +65 -0
- package/dist/services/web/daemon-registry.d.ts +42 -0
- package/dist/services/web/daemon-registry.js +164 -0
- package/dist/services/web/daemon-supervisor.d.ts +144 -0
- package/dist/services/web/daemon-supervisor.js +455 -0
- package/dist/services/web/playwright-loader.d.ts +89 -0
- package/dist/services/web/playwright-loader.js +253 -0
- package/dist/services/web/snapshot-pruner.d.ts +48 -0
- package/dist/services/web/snapshot-pruner.js +241 -0
- package/dist/services/web/untrusted-envelope.d.ts +27 -0
- package/dist/services/web/untrusted-envelope.js +44 -0
- package/dist/services/web/web-artifact-paths.d.ts +79 -0
- package/dist/services/web/web-artifact-paths.js +163 -0
- package/dist/services/web/web-client.d.ts +19 -0
- package/dist/services/web/web-client.js +55 -0
- package/dist/services/web/web-daemon-service.d.ts +38 -0
- package/dist/services/web/web-daemon-service.js +416 -0
- package/dist/services/web/web-fallback.d.ts +70 -0
- package/dist/services/web/web-fallback.js +121 -0
- package/dist/services/web/web-install-service.d.ts +91 -0
- package/dist/services/web/web-install-service.js +346 -0
- package/dist/services/web/web-login-profile.d.ts +89 -0
- package/dist/services/web/web-login-profile.js +612 -0
- package/dist/services/web/web-login-staging.d.ts +27 -0
- package/dist/services/web/web-login-staging.js +173 -0
- package/dist/services/web/web-protocol.d.ts +58 -0
- package/dist/services/web/web-protocol.js +58 -0
- package/dist/services/web/web-status-report.d.ts +33 -0
- package/dist/services/web/web-status-report.js +47 -0
- package/dist/services/workspace/claude-settings-template.d.ts +41 -5
- package/dist/services/workspace/claude-settings-template.js +116 -64
- package/dist/services/workspace/workspace-claude-settings-materializer.js +5 -1
- package/dist/services/workspace/workspace-service.js +33 -0
- package/package.json +5 -5
- package/scripts/copy-templates.mjs +12 -0
- package/scripts/sync-version.mjs +20 -0
- package/skills/bee/peaks-qa/SKILL.md +2 -0
- package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +12 -0
- package/skills/bee/peaks-rd/SKILL.md +2 -0
- package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +14 -0
- package/skills/bee/peaks-txt/SKILL.md +2 -0
- package/skills/bee/peaks-ui/SKILL.md +2 -0
- package/skills/peaks-code/SKILL.md +18 -0
- package/skills/peaks-code/references/browser-workflow.md +10 -1
- package/skills/peaks-code/references/context-governance.md +29 -0
- package/skills/peaks-doctor/SKILL.md +2 -0
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
import { execFileSync } from 'node:child_process';
|
|
20
20
|
import { existsSync, readFileSync } from 'node:fs';
|
|
21
21
|
import { join, relative, dirname } from 'node:path';
|
|
22
|
+
import { resolveNpxInvocation } from '../lint/npx-resolver.js';
|
|
23
|
+
// 2026-09-10: the D1 fix (`orchestrator-can-do.ts`) resolved this tree's own CLI
|
|
24
|
+
// entry so a bare `peaks` never had to be resolved through a Windows `.cmd`
|
|
25
|
+
// shim. `runCodegraph` below needs exactly that, so it reuses the same helper
|
|
26
|
+
// rather than growing a second mechanism.
|
|
27
|
+
import { cliEntryPath, interpreterArgs } from '../web/daemon-supervisor.js';
|
|
22
28
|
export function defaultCodegraphRunner() {
|
|
23
29
|
return {
|
|
24
30
|
async query(text, projectRoot) {
|
|
@@ -88,34 +94,31 @@ export function defaultCodegraphRunner() {
|
|
|
88
94
|
};
|
|
89
95
|
}
|
|
90
96
|
function runCodegraph(args, projectRoot) {
|
|
97
|
+
const execOptions = {
|
|
98
|
+
cwd: projectRoot,
|
|
99
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
100
|
+
timeout: 60_000,
|
|
101
|
+
maxBuffer: 32 * 1024 * 1024
|
|
102
|
+
};
|
|
91
103
|
// Use `peaks codegraph` (the peaks wrapper), which adds --project support.
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
//
|
|
104
|
+
// 2026-09-10: no shell. `peaks` on Windows is a `.cmd` shim, which Node >= 20
|
|
105
|
+
// refuses to spawn without `shell: true` — and `shell: true` concatenates the
|
|
106
|
+
// argv unescaped, so `--project <projectRoot>` was split at the first space in
|
|
107
|
+
// the project path (the same defect that made `peaks slice check` report a
|
|
108
|
+
// phantom failure on such a project). Resolving this tree's own CLI entry and
|
|
109
|
+
// running it through `process.execPath` needs no shim and no shell, so a
|
|
110
|
+
// spaced `projectRoot` is just an argument again.
|
|
95
111
|
try {
|
|
96
|
-
return execFileSync(
|
|
97
|
-
cwd: projectRoot,
|
|
98
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
99
|
-
shell: isWin,
|
|
100
|
-
timeout: 60_000,
|
|
101
|
-
maxBuffer: 32 * 1024 * 1024
|
|
102
|
-
}).toString('utf8');
|
|
112
|
+
return execFileSync(process.execPath, [...interpreterArgs(cliEntryPath()), 'codegraph', ...args], execOptions).toString('utf8');
|
|
103
113
|
}
|
|
104
114
|
catch (error) {
|
|
105
115
|
const err = error;
|
|
106
116
|
if (err.code === 'ENOENT') {
|
|
107
|
-
// Fallback: raw `codegraph` (won't accept --project, drop it)
|
|
117
|
+
// Fallback: raw `codegraph` (won't accept --project, drop it), reached
|
|
118
|
+
// through the npx resolver so the local `.bin` shim is never spawned.
|
|
108
119
|
const fallbackArgs = args.filter((a) => a !== '--project' && !a.startsWith('--project='));
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
const finalArgs = command === 'npx' ? ['codegraph', ...fallbackArgs] : fallbackArgs;
|
|
112
|
-
return execFileSync(command, finalArgs, {
|
|
113
|
-
cwd: projectRoot,
|
|
114
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
115
|
-
shell: isWin,
|
|
116
|
-
timeout: 60_000,
|
|
117
|
-
maxBuffer: 32 * 1024 * 1024
|
|
118
|
-
}).toString('utf8');
|
|
120
|
+
const { command, args: npxArgs, baseEnv } = resolveNpxInvocation(['codegraph', ...fallbackArgs]);
|
|
121
|
+
return execFileSync(command, npxArgs, { ...execOptions, env: baseEnv }).toString('utf8');
|
|
119
122
|
}
|
|
120
123
|
throw error;
|
|
121
124
|
}
|
|
@@ -118,7 +118,18 @@ function evaluateCommand(projectRoot, run, expectExitZero, allowCommands, timeou
|
|
|
118
118
|
}
|
|
119
119
|
let exitCode;
|
|
120
120
|
try {
|
|
121
|
-
|
|
121
|
+
// Windows: this spawn is reachable from `peaks gate enforce` (the
|
|
122
|
+
// PreToolUse hook, which forces `allowCommands: true` below), so it runs
|
|
123
|
+
// on a per-Bash-call budget. Without `windowsHide` the child gets its own
|
|
124
|
+
// visible console window on Windows — one window per guarded Bash call.
|
|
125
|
+
// The option is inert on POSIX (libuv reads it only on Windows), which is
|
|
126
|
+
// why it is set unconditionally rather than platform-branched.
|
|
127
|
+
execFileSync(bin, args, {
|
|
128
|
+
cwd: resolve(projectRoot),
|
|
129
|
+
timeout: timeoutMs,
|
|
130
|
+
stdio: 'ignore',
|
|
131
|
+
windowsHide: true
|
|
132
|
+
});
|
|
122
133
|
exitCode = 0;
|
|
123
134
|
}
|
|
124
135
|
catch (error) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte-aware output caps for `peaks web` (slice S1, AC2).
|
|
3
|
+
*
|
|
4
|
+
* `capText` is the only absolute guarantee in the snapshot pipeline: the
|
|
5
|
+
* pruner (`snapshot-pruner.ts`) bounds node count and depth, but only a byte
|
|
6
|
+
* ceiling bounds the rendered string. These are module constants, not options
|
|
7
|
+
* (tech-doc §12).
|
|
8
|
+
*/
|
|
9
|
+
/** Hard ceiling for page text returned by `peaks web text`. */
|
|
10
|
+
export declare const MAX_TEXT_BYTES = 8192;
|
|
11
|
+
/** Hard ceiling for the rendered snapshot returned by `peaks web snap`. */
|
|
12
|
+
export declare const MAX_SNAP_BYTES = 4096;
|
|
13
|
+
/** Maximum number of real (non-marker) nodes emitted by the pruner. */
|
|
14
|
+
export declare const MAX_SNAP_NODES = 120;
|
|
15
|
+
/** Maximum tree depth emitted by the pruner. */
|
|
16
|
+
export declare const MAX_SNAP_DEPTH = 6;
|
|
17
|
+
export interface CappedText {
|
|
18
|
+
readonly text: string;
|
|
19
|
+
readonly truncated: boolean;
|
|
20
|
+
/** UTF-8 bytes removed from the input. `0` when nothing was truncated. */
|
|
21
|
+
readonly droppedBytes: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Cut `text` down to at most `capBytes` UTF-8 bytes.
|
|
25
|
+
*
|
|
26
|
+
* Three properties the AC2 tests pin:
|
|
27
|
+
* - the returned string's byte length is **never** greater than `capBytes`
|
|
28
|
+
* (the truncation marker is reserved out of the budget, not added on top);
|
|
29
|
+
* - a multi-byte codepoint is never split (the cut backs off the UTF-8
|
|
30
|
+
* continuation bytes), so the result is always valid UTF-8;
|
|
31
|
+
* - the cut lands on a line boundary when one exists, so the caller never
|
|
32
|
+
* gets a half-line.
|
|
33
|
+
*/
|
|
34
|
+
export declare function capText(text: string, capBytes: number): CappedText;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte-aware output caps for `peaks web` (slice S1, AC2).
|
|
3
|
+
*
|
|
4
|
+
* `capText` is the only absolute guarantee in the snapshot pipeline: the
|
|
5
|
+
* pruner (`snapshot-pruner.ts`) bounds node count and depth, but only a byte
|
|
6
|
+
* ceiling bounds the rendered string. These are module constants, not options
|
|
7
|
+
* (tech-doc §12).
|
|
8
|
+
*/
|
|
9
|
+
/** Hard ceiling for page text returned by `peaks web text`. */
|
|
10
|
+
export const MAX_TEXT_BYTES = 8192;
|
|
11
|
+
/** Hard ceiling for the rendered snapshot returned by `peaks web snap`. */
|
|
12
|
+
export const MAX_SNAP_BYTES = 4096;
|
|
13
|
+
/** Maximum number of real (non-marker) nodes emitted by the pruner. */
|
|
14
|
+
export const MAX_SNAP_NODES = 120;
|
|
15
|
+
/** Maximum tree depth emitted by the pruner. */
|
|
16
|
+
export const MAX_SNAP_DEPTH = 6;
|
|
17
|
+
/** The marker appended to a truncated payload. Counted INSIDE the ceiling. */
|
|
18
|
+
function truncationMarker(droppedBytes) {
|
|
19
|
+
return `\n…[truncated ${droppedBytes} bytes]`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Cut `text` down to at most `capBytes` UTF-8 bytes.
|
|
23
|
+
*
|
|
24
|
+
* Three properties the AC2 tests pin:
|
|
25
|
+
* - the returned string's byte length is **never** greater than `capBytes`
|
|
26
|
+
* (the truncation marker is reserved out of the budget, not added on top);
|
|
27
|
+
* - a multi-byte codepoint is never split (the cut backs off the UTF-8
|
|
28
|
+
* continuation bytes), so the result is always valid UTF-8;
|
|
29
|
+
* - the cut lands on a line boundary when one exists, so the caller never
|
|
30
|
+
* gets a half-line.
|
|
31
|
+
*/
|
|
32
|
+
export function capText(text, capBytes) {
|
|
33
|
+
const total = Buffer.byteLength(text, 'utf8');
|
|
34
|
+
if (total <= capBytes) {
|
|
35
|
+
return { text, truncated: false, droppedBytes: 0 };
|
|
36
|
+
}
|
|
37
|
+
// Reserve the WIDEST possible marker (the one whose byte count equals the
|
|
38
|
+
// whole input) so the ceiling holds on the first pass: the reported marker
|
|
39
|
+
// can only be narrower than the reserved one.
|
|
40
|
+
const reservedMarkerBytes = Buffer.byteLength(truncationMarker(total), 'utf8');
|
|
41
|
+
if (capBytes < reservedMarkerBytes) {
|
|
42
|
+
// The marker alone would blow the ceiling. The invariant is unconditional,
|
|
43
|
+
// so for a cap too narrow to hold it the whole payload goes.
|
|
44
|
+
return { text: '', truncated: true, droppedBytes: total };
|
|
45
|
+
}
|
|
46
|
+
const kept = cutAtLineBoundary(text, capBytes - reservedMarkerBytes);
|
|
47
|
+
const droppedBytes = total - Buffer.byteLength(kept, 'utf8');
|
|
48
|
+
return { text: kept + truncationMarker(droppedBytes), truncated: true, droppedBytes };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Largest valid-UTF-8 byte prefix of `text` at or below `budget`, backed off
|
|
52
|
+
* to the last line break. `text` without any newline keeps the raw byte cut.
|
|
53
|
+
*/
|
|
54
|
+
function cutAtLineBoundary(text, budget) {
|
|
55
|
+
if (budget <= 0) {
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
const bytes = Buffer.from(text, 'utf8');
|
|
59
|
+
let end = Math.min(budget, bytes.length);
|
|
60
|
+
// A continuation byte (0b10xxxxxx) at the cut point means the codepoint that
|
|
61
|
+
// started earlier would be split, so walk back until it is not one.
|
|
62
|
+
while (end > 0 && ((bytes[end] ?? 0) & 0xc0) === 0x80) {
|
|
63
|
+
end -= 1;
|
|
64
|
+
}
|
|
65
|
+
const prefix = bytes.subarray(0, end).toString('utf8');
|
|
66
|
+
const lastBreak = prefix.lastIndexOf('\n');
|
|
67
|
+
return lastBreak > 0 ? prefix.slice(0, lastBreak) : prefix;
|
|
68
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type PwBrowser } from './playwright-loader.js';
|
|
2
|
+
export interface AcquiredChromium {
|
|
3
|
+
readonly browser: PwBrowser;
|
|
4
|
+
readonly version: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The ordered gate: disable → resolve → cache probe → launch-or-refuse.
|
|
8
|
+
*
|
|
9
|
+
* Every exit here is off the download path, by design — see the module
|
|
10
|
+
* docstring. The `CODE: detail` shape is `web-daemon-service`'s
|
|
11
|
+
* `failureResponse` convention, so the caller gets `WEB_INSTALL_REQUIRED` and
|
|
12
|
+
* not a collapsed `WEB_OP_FAILED`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function acquireChromium(): Promise<AcquiredChromium>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acquire the chromium browser (slice S1, file 10; S3 adds the ordered gate).
|
|
3
|
+
*
|
|
4
|
+
* The order is load-bearing and is the whole of AC5's first half (tech-doc §5.1):
|
|
5
|
+
*
|
|
6
|
+
* 1. `PEAKS_WEB_DISABLED=1` → refuse, BEFORE any resolve, any lock file and
|
|
7
|
+
* any cache touch. A gate evaluated after a spawn is not a gate.
|
|
8
|
+
* 2. resolve the pinned Playwright (no npx, no shell — `playwright-loader`).
|
|
9
|
+
* 3. probe the cache. This is spawn-free and download-free (R6): it answers
|
|
10
|
+
* about the artifact `launchOnce` actually starts — the headless shell —
|
|
11
|
+
* so "is an install needed" never costs 700 MB to ask, and never answers
|
|
12
|
+
* about a browser this code will not launch.
|
|
13
|
+
* 4. launch, or refuse with `WEB_INSTALL_REQUIRED`.
|
|
14
|
+
*
|
|
15
|
+
* **Step 4 is a refusal, not a download** (R3). Installing here meant a blocking
|
|
16
|
+
* `spawnSync` on the daemon's request loop for up to 20 minutes: `/health`
|
|
17
|
+
* starved, `status` reported the daemon `orphaned`, `stop` could not prove
|
|
18
|
+
* ownership and left it running, and the CLI gave up at 30 s and called the op
|
|
19
|
+
* failed while the download carried on invisibly. The download now belongs to
|
|
20
|
+
* `peaks web install`, which runs in the caller's own process, on the caller's
|
|
21
|
+
* terminal, and prints its size before it blocks. The daemon answers the op with
|
|
22
|
+
* the tier-3 envelope instead.
|
|
23
|
+
*
|
|
24
|
+
* We never delete anything in the Playwright cache; recovery is delegated to
|
|
25
|
+
* Playwright's own `install --force` (design §10.1).
|
|
26
|
+
*/
|
|
27
|
+
import { getErrorMessage } from 'peaks-loop-shared/result';
|
|
28
|
+
import { loadPlaywright } from './playwright-loader.js';
|
|
29
|
+
import { installCommandLine, isWebDisabled, probeBrowserInstalled } from './web-install-service.js';
|
|
30
|
+
/** The message Playwright raises when the browser binary was never downloaded. */
|
|
31
|
+
const MISSING_EXECUTABLE_RE = /Executable doesn't exist/i;
|
|
32
|
+
/**
|
|
33
|
+
* The ordered gate: disable → resolve → cache probe → launch-or-refuse.
|
|
34
|
+
*
|
|
35
|
+
* Every exit here is off the download path, by design — see the module
|
|
36
|
+
* docstring. The `CODE: detail` shape is `web-daemon-service`'s
|
|
37
|
+
* `failureResponse` convention, so the caller gets `WEB_INSTALL_REQUIRED` and
|
|
38
|
+
* not a collapsed `WEB_OP_FAILED`.
|
|
39
|
+
*/
|
|
40
|
+
export async function acquireChromium() {
|
|
41
|
+
assertWebEnabled();
|
|
42
|
+
const playwright = await loadPlaywright();
|
|
43
|
+
const probe = await probeBrowserInstalled();
|
|
44
|
+
if (!probe.installed) {
|
|
45
|
+
throw new Error(`WEB_INSTALL_REQUIRED: ${installRequiredDetail(probe.version)}`);
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return await launchOnce(playwright);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (!MISSING_EXECUTABLE_RE.test(getErrorMessage(error))) {
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
// The probe named an existing executable that launch still would not take —
|
|
55
|
+
// a half-written or corrupt revision. `--force` is the documented recovery
|
|
56
|
+
// (R6), and the caller has to run it: this process must not download.
|
|
57
|
+
throw new Error(`WEB_INSTALL_REQUIRED: --force ${installRequiredDetail(probe.version)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** One sentence, shared by both refusal paths, naming the command that fixes it. */
|
|
61
|
+
function installRequiredDetail(version) {
|
|
62
|
+
const held = version === null ? 'the pinned Playwright is not resolvable' : `playwright@${version} is cached`;
|
|
63
|
+
return (`${held} but its browser is not launched-ready; run \`peaks web install\` ` +
|
|
64
|
+
`(\`npx ${installCommandLine().join(' ')}\`)`);
|
|
65
|
+
}
|
|
66
|
+
/** Step 1. `PEAKS_WEB_DISABLED=1` means no browser, no spawn, no cache touch. */
|
|
67
|
+
function assertWebEnabled() {
|
|
68
|
+
if (isWebDisabled(process.env)) {
|
|
69
|
+
throw new Error('WEB_DISABLED: PEAKS_WEB_DISABLED=1 — the local browser path is switched off for this process');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Launch with no options: `headless` defaults true and Playwright resolves
|
|
74
|
+
* `chromium-headless-shell` (`registry.getExecutableName`). That is deliberate —
|
|
75
|
+
* the full `chromium` build under new-headless mode was measured on this machine
|
|
76
|
+
* at **15 101 ms to `close()`**, against **102 ms** for the shell, and S2's
|
|
77
|
+
* teardown has to finish inside `STOP_EXIT_TIMEOUT_MS = 10 s` or the daemon is
|
|
78
|
+
* SIGTERM'd mid-teardown and its browser is orphaned (AC6). `probeBrowserInstalled`
|
|
79
|
+
* is what had to change, not this.
|
|
80
|
+
*/
|
|
81
|
+
async function launchOnce(playwright) {
|
|
82
|
+
const browser = await playwright.chromium.launch();
|
|
83
|
+
return { browser, version: browser.version() };
|
|
84
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { PwBrowser, PwContext } from './playwright-loader.js';
|
|
2
|
+
export interface BrowserSessionManagerOptions {
|
|
3
|
+
readonly projectRoot: string;
|
|
4
|
+
readonly sessionId: string;
|
|
5
|
+
}
|
|
6
|
+
export interface WebTextResult {
|
|
7
|
+
readonly text: string;
|
|
8
|
+
readonly truncated: boolean;
|
|
9
|
+
readonly droppedBytes: number;
|
|
10
|
+
}
|
|
11
|
+
export interface WebSnapResult {
|
|
12
|
+
readonly snapshot: string;
|
|
13
|
+
readonly droppedNodes: number;
|
|
14
|
+
readonly depthCapped: boolean;
|
|
15
|
+
readonly nodeCapped: boolean;
|
|
16
|
+
readonly truncated: boolean;
|
|
17
|
+
readonly droppedBytes: number;
|
|
18
|
+
}
|
|
19
|
+
export interface WebMetricsResult {
|
|
20
|
+
readonly available: boolean;
|
|
21
|
+
readonly reason: string | null;
|
|
22
|
+
readonly values: Readonly<Record<string, unknown>> | null;
|
|
23
|
+
}
|
|
24
|
+
/** One dispatch whose storage state could not be persisted at teardown. */
|
|
25
|
+
export interface StateWriteFailure {
|
|
26
|
+
readonly dispatchId: string;
|
|
27
|
+
readonly reason: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve with `work`, or reject once the step has taken longer than the step
|
|
31
|
+
* budget. The late settlement of `work` is consumed here, so a step that
|
|
32
|
+
* finishes after its deadline cannot become an unhandled rejection.
|
|
33
|
+
*/
|
|
34
|
+
export declare function boundedTeardownStep<T>(work: Promise<T>, label: string): Promise<T>;
|
|
35
|
+
export interface CloseAllResult {
|
|
36
|
+
/** Contexts closed during this call. */
|
|
37
|
+
readonly closedContexts: number;
|
|
38
|
+
/** Never silent: teardown still proceeds, but the caller can report this. */
|
|
39
|
+
readonly stateWriteFailures: readonly StateWriteFailure[];
|
|
40
|
+
}
|
|
41
|
+
export declare class BrowserSessionManager {
|
|
42
|
+
private readonly browser;
|
|
43
|
+
private readonly projectRoot;
|
|
44
|
+
private readonly sessionId;
|
|
45
|
+
private readonly sessions;
|
|
46
|
+
constructor(browser: PwBrowser, options: BrowserSessionManagerOptions);
|
|
47
|
+
/**
|
|
48
|
+
* The context for a dispatch, created on first use and reused afterwards.
|
|
49
|
+
*
|
|
50
|
+
* `profile` names a user-level login profile (`peaks web login --profile`) to
|
|
51
|
+
* build the context from. When given it REPLACES the dispatch's own state: a
|
|
52
|
+
* context holds one storage state, so "start from the profile" and "start from
|
|
53
|
+
* the per-dispatch state" are alternatives, not layers.
|
|
54
|
+
*
|
|
55
|
+
* The profile is **READ-ONLY**. This module reads it into the context and
|
|
56
|
+
* never writes, refreshes or trims it — write-back is an undecided design
|
|
57
|
+
* question (it would keep a login fresh without re-running `login`, and it
|
|
58
|
+
* would also let an automated browse silently rewrite a user-level credential
|
|
59
|
+
* file), so this slice does not decide it. `closeAll` still persists the
|
|
60
|
+
* dispatch's OWN `pw-profiles/<dispatchId>/storageState.json`; nothing under
|
|
61
|
+
* `~/.peaks/web-profiles/` is ever created or modified from here.
|
|
62
|
+
*
|
|
63
|
+
* The name is validated HERE and not only in the CLI, because it arrives off
|
|
64
|
+
* the wire: `resolveProfileName` is the SAME resolver `login` uses (folds to
|
|
65
|
+
* lower case, refuses a traversing name, a leading dot, a device name), and
|
|
66
|
+
* the read is anchored with `assertUnder`. A profile that does not exist is a
|
|
67
|
+
* NAMED failure — falling through to an unauthenticated context would answer
|
|
68
|
+
* "open this with my profile" with a logged-out browser and no signal.
|
|
69
|
+
*/
|
|
70
|
+
contextFor(dispatchId: string, rawProfile?: string): Promise<PwContext>;
|
|
71
|
+
open(dispatchId: string, url: string, profile?: string): Promise<{
|
|
72
|
+
url: string;
|
|
73
|
+
title: string;
|
|
74
|
+
}>;
|
|
75
|
+
text(dispatchId: string, selector?: string): Promise<WebTextResult>;
|
|
76
|
+
/**
|
|
77
|
+
* Primary path is `locator.ariaSnapshotJSON()` (Playwright 1.63, our exact
|
|
78
|
+
* pin). We do NOT re-derive the tree from the YAML form: parsing YAML by
|
|
79
|
+
* indentation is the fragile option, and under an exact pin the JSON API is
|
|
80
|
+
* always present (tech-doc §4.1/§4.2). A missing API is an explicit error
|
|
81
|
+
* rather than a silent degradation.
|
|
82
|
+
*/
|
|
83
|
+
snap(dispatchId: string, selector?: string): Promise<WebSnapResult>;
|
|
84
|
+
click(dispatchId: string, selector: string): Promise<{
|
|
85
|
+
result: string;
|
|
86
|
+
}>;
|
|
87
|
+
/**
|
|
88
|
+
* Screenshot to an absolute path under `web/`. Always an explicit `path`:
|
|
89
|
+
* never the Playwright default, and never a Buffer we write ourselves
|
|
90
|
+
* (tech-doc §7.2 rule 3).
|
|
91
|
+
*/
|
|
92
|
+
shot(dispatchId: string, selector?: string): Promise<{
|
|
93
|
+
path: string;
|
|
94
|
+
bytes: number;
|
|
95
|
+
}>;
|
|
96
|
+
/**
|
|
97
|
+
* Core Web Vitals for the dispatch's page. Returns `available: false` with a
|
|
98
|
+
* reason — never fabricated zeros — when there is no observation window
|
|
99
|
+
* (orchestrator decision C4), and never echoes the page's object back: only
|
|
100
|
+
* the three contract keys, only finite numbers (R2).
|
|
101
|
+
*/
|
|
102
|
+
metrics(dispatchId: string): Promise<WebMetricsResult>;
|
|
103
|
+
/**
|
|
104
|
+
* Persist each dispatch's storage state, then close every context exactly
|
|
105
|
+
* once. A state file that cannot be written must not block shutdown — but it
|
|
106
|
+
* must be reported: a silent skip here kills S4's persistence with no signal
|
|
107
|
+
* anywhere (R1).
|
|
108
|
+
*/
|
|
109
|
+
closeAll(): Promise<CloseAllResult>;
|
|
110
|
+
private pageFor;
|
|
111
|
+
}
|