@reticlehq/server 3.3.0 → 3.5.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/LICENSE-ENTERPRISE +1 -1
- package/README.md +3 -3
- package/SKILL.md +12 -14
- package/dist/chromium-launch-options.d.ts +4 -3
- package/dist/chromium-launch-options.js +6 -2
- package/dist/command/cli/adhoc-suite.d.ts +22 -0
- package/dist/command/cli/adhoc-suite.js +90 -19
- package/dist/command/cli/adhoc-verdict.d.ts +35 -0
- package/dist/command/cli/adhoc-verdict.js +90 -3
- package/dist/command/cli/answers/gate-hook-message.d.ts +6 -0
- package/dist/command/cli/answers/gate-hook-message.js +6 -0
- package/dist/command/cli/auth/cloud-keystore.d.ts +0 -2
- package/dist/command/cli/auth/cloud-keystore.js +0 -3
- package/dist/command/cli/auth/cloud-session.d.ts +6 -2
- package/dist/command/cli/auth/cloud-session.js +2 -0
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-doctor.js +7 -5
- package/dist/command/cli/cli-flow-commands.d.ts +3 -6
- package/dist/command/cli/cli-flow-commands.js +67 -4
- package/dist/command/cli/cli-parse.d.ts +8 -1
- package/dist/command/cli/cli-parse.js +24 -4
- package/dist/command/cli/cli-update-commands.js +8 -1
- package/dist/command/cli/cli-verify.d.ts +24 -3
- package/dist/command/cli/cli-verify.js +54 -11
- package/dist/command/cli/cloud-cli.js +64 -2
- package/dist/command/cli/cloud-kit.js +3 -5
- package/dist/command/cli/cloud-login.d.ts +1 -1
- package/dist/command/cli/cloud-login.js +27 -8
- package/dist/command/cli/demo/demo-page.js +6 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
- package/dist/command/cli/launch/cli-launch.d.ts +8 -0
- package/dist/command/cli/launch/cli-launch.js +21 -9
- package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
- package/dist/command/cli/launch/sdk-version-match.js +206 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
- package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
- package/dist/command/cli/ports/resolve/cli-port.js +125 -15
- package/dist/command/cli/setup-mcp-cli.js +21 -13
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +21 -7
- package/dist/command/cli.js +79 -11
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
- package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
- package/dist/command/daemon/daemon-resolve.d.ts +9 -0
- package/dist/command/daemon/daemon-resolve.js +1 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
- package/dist/command/dev/stale-issue-guard.d.ts +3 -4
- package/dist/command/dev/stale-issue-guard.js +8 -8
- package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
- package/dist/command/setup/bringup/connect-lease.js +38 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
- package/dist/command/setup/bringup/dev-server-files.js +131 -0
- package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
- package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +2 -0
- package/dist/command/setup/bringup/dev-server-wait.js +1 -1
- package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
- package/dist/command/setup/bringup/ensure-daemon.js +135 -16
- package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
- package/dist/command/setup/bringup/static-page-server.js +151 -0
- package/dist/command/setup/desktop-shape.d.ts +4 -0
- package/dist/command/setup/desktop-shape.js +18 -2
- package/dist/command/setup/init/init-port.d.ts +15 -0
- package/dist/command/setup/init/init-port.js +44 -0
- package/dist/command/setup/init/init-runtime.d.ts +1 -1
- package/dist/command/setup/init/init-runtime.js +91 -9
- package/dist/command/setup/node-effects.d.ts +27 -14
- package/dist/command/setup/node-effects.js +86 -45
- package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
- package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
- package/dist/command/setup/probe/page-probe.d.ts +13 -2
- package/dist/command/setup/probe/page-probe.js +18 -3
- package/dist/command/setup/run-setup.d.ts +28 -1
- package/dist/command/setup/run-setup.js +103 -15
- package/dist/command/setup/session-pick.d.ts +13 -0
- package/dist/command/setup/session-pick.js +20 -0
- package/dist/command/setup/setup-command.d.ts +2 -14
- package/dist/command/setup/setup-command.js +105 -31
- package/dist/command/setup/setup-mcp.d.ts +2 -0
- package/dist/command/setup/setup-mcp.js +17 -8
- package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
- package/dist/command/setup/terminal/wait-progress.js +17 -0
- package/dist/command/version/peer-announce.d.ts +6 -2
- package/dist/command/version/peer-announce.js +8 -6
- package/dist/command/version/version-nudge.d.ts +17 -3
- package/dist/command/version/version-nudge.js +25 -4
- package/dist/features/crawl/crawl-empty.js +1 -1
- package/dist/features/crawl/crawl-tools.d.ts +6 -0
- package/dist/features/crawl/crawl-tools.js +57 -0
- package/dist/features/crawl/crawl.js +171 -157
- package/dist/features/exhaust/app-coverage.d.ts +33 -0
- package/dist/features/exhaust/app-coverage.js +31 -0
- package/dist/features/exhaust/code-coverage.d.ts +47 -0
- package/dist/features/exhaust/code-coverage.js +77 -0
- package/dist/features/exhaust/exhaust-run.d.ts +33 -0
- package/dist/features/exhaust/exhaust-run.js +82 -0
- package/dist/features/exhaust/explorer.d.ts +103 -0
- package/dist/features/exhaust/explorer.js +195 -0
- package/dist/features/exhaust/failure-branches.d.ts +33 -0
- package/dist/features/exhaust/failure-branches.js +92 -0
- package/dist/features/exhaust/ledger.d.ts +202 -0
- package/dist/features/exhaust/ledger.js +252 -0
- package/dist/features/exhaust/session-fold.d.ts +31 -0
- package/dist/features/exhaust/session-fold.js +79 -0
- package/dist/features/exhaust/session-port.d.ts +36 -0
- package/dist/features/exhaust/session-port.js +102 -0
- package/dist/features/harness/drive-report.d.ts +0 -9
- package/dist/features/harness/drive-report.js +18 -0
- package/dist/features/harness/driver.js +4 -7
- package/dist/features/harness/fill-values.d.ts +10 -0
- package/dist/features/harness/fill-values.js +50 -11
- package/dist/features/harness/jev-driver.js +30 -13
- package/dist/features/harness/openai-driver.d.ts +12 -0
- package/dist/features/harness/openai-driver.js +21 -12
- package/dist/features/harness/platform-config.js +5 -6
- package/dist/features/license/license.d.ts +2 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +56 -19
- package/dist/judgement/capsule/capsule-store.d.ts +4 -4
- package/dist/judgement/domain/domain-model.js +3 -1
- package/dist/judgement/oracles/self-instrument.js +4 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
- package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
- package/dist/judgement/runs/verification-sync.d.ts +3 -1
- package/dist/judgement/runs/verification-sync.js +1 -1
- package/dist/language/flows/change/affected.d.ts +9 -0
- package/dist/language/flows/change/affected.js +35 -0
- package/dist/language/flows/change/gate.d.ts +20 -0
- package/dist/language/flows/change/gate.js +12 -1
- package/dist/language/flows/flow-author.d.ts +7 -0
- package/dist/language/flows/flow-author.js +19 -0
- package/dist/language/flows/flow-expect-grammar.js +1 -1
- package/dist/language/flows/flow-journey.d.ts +54 -0
- package/dist/language/flows/flow-journey.js +119 -0
- package/dist/language/flows/flow-replay-run.d.ts +2 -3
- package/dist/language/flows/flow-replay-run.js +13 -47
- package/dist/language/flows/flow-replay.d.ts +22 -8
- package/dist/language/flows/flow-replay.js +55 -9
- package/dist/language/flows/flow-result.d.ts +2 -0
- package/dist/language/flows/flow-result.js +25 -0
- package/dist/language/flows/flow-tools.js +17 -5
- package/dist/language/flows/flows.d.ts +10 -1
- package/dist/language/flows/flows.js +90 -14
- package/dist/language/flows/recording/replay-halt.js +3 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
- package/dist/language/flows/recording/tape/recordings.js +55 -1
- package/dist/language/flows/replay.d.ts +19 -0
- package/dist/language/flows/replay.js +42 -6
- package/dist/language/flows/verify-change-tools.js +5 -2
- package/dist/launch-chromium.d.ts +80 -0
- package/dist/launch-chromium.js +218 -0
- package/dist/memory/cloud/cloud-config.d.ts +11 -2
- package/dist/memory/cloud/cloud-config.js +47 -7
- package/dist/memory/cloud/cloud-sync.d.ts +4 -1
- package/dist/memory/cloud/cloud-sync.js +6 -9
- package/dist/memory/cloud/harness-offer.js +5 -6
- package/dist/memory/cloud/harness-switch.js +5 -6
- package/dist/memory/cloud/sync-cycle.d.ts +77 -3
- package/dist/memory/cloud/sync-cycle.js +325 -55
- package/dist/memory/cloud/sync-daemon.js +24 -20
- package/dist/memory/cloud/sync-disk.js +16 -2
- package/dist/memory/intent/intent-shard.d.ts +10 -32
- package/dist/memory/intent/intent-shard.js +0 -10
- package/dist/memory/intent/intent-tools.js +8 -3
- package/dist/memory/journal/drive-flow.d.ts +18 -13
- package/dist/memory/journal/drive-flow.js +45 -37
- package/dist/memory/journal/envelope.d.ts +8 -8
- package/dist/memory/journal/journal-query.d.ts +14 -0
- package/dist/memory/journal/journal-query.js +15 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
- package/dist/memory/journal/session-end.d.ts +9 -1
- package/dist/memory/journal/session-end.js +24 -12
- package/dist/memory/project/artifact-root-resolver.d.ts +10 -0
- package/dist/memory/project/artifact-root-resolver.js +46 -19
- package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
- package/dist/memory/project/dir/reticle-dir.js +1 -0
- package/dist/memory/project/project-tools.js +3 -1
- package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
- package/dist/portal/bridge/auth-failure-reason.js +4 -2
- package/dist/portal/bridge/bridge.d.ts +12 -3
- package/dist/portal/bridge/bridge.js +49 -3
- package/dist/portal/input/js-coverage.d.ts +25 -0
- package/dist/portal/input/js-coverage.js +30 -0
- package/dist/portal/input/real-input.d.ts +11 -0
- package/dist/portal/input/real-input.js +29 -4
- package/dist/portal/input/viewport-tools.js +16 -6
- package/dist/portal/pool/playwright-launcher.d.ts +6 -0
- package/dist/portal/pool/playwright-launcher.js +20 -13
- package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
- package/dist/portal/session/dev-server/dev-command.js +30 -12
- package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
- package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
- package/dist/portal/session/dev-server/route-status-probe.js +57 -0
- package/dist/portal/session/facts/observed-state.d.ts +11 -0
- package/dist/portal/session/facts/observed-state.js +34 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
- package/dist/portal/session/no-session-diagnosis.js +62 -5
- package/dist/portal/session/no-session-next-action.d.ts +13 -0
- package/dist/portal/session/no-session-next-action.js +48 -9
- package/dist/portal/session/no-session-watch.d.ts +18 -0
- package/dist/portal/session/no-session-watch.js +88 -2
- package/dist/portal/session/session-health.js +3 -2
- package/dist/portal/session/session-manager.d.ts +20 -6
- package/dist/portal/session/session-manager.js +19 -3
- package/dist/portal/session/session-reaper.d.ts +28 -0
- package/dist/portal/session/session-reaper.js +49 -1
- package/dist/portal/session/session.d.ts +10 -8
- package/dist/portal/session/session.js +24 -32
- package/dist/surface/http-server.d.ts +3 -2
- package/dist/surface/http-server.js +8 -5
- package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
- package/dist/surface/mcp/mcp-proxy.js +2 -2
- package/dist/surface/mcp/mcp.js +5 -3
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
- package/dist/surface/mcp/proxy/proxy-log.js +3 -5
- package/dist/surface/mcp/server-instructions.js +9 -7
- package/dist/surface/tools/act/after-match.d.ts +17 -0
- package/dist/surface/tools/act/after-match.js +30 -0
- package/dist/surface/tools/act/durable.d.ts +69 -0
- package/dist/surface/tools/act/durable.js +139 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
- package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
- package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
- package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
- package/dist/surface/tools/act/proved-controls.d.ts +9 -0
- package/dist/surface/tools/act/proved-controls.js +11 -0
- package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
- package/dist/surface/tools/act/sequence-grade.js +32 -1
- package/dist/surface/tools/act-capsule.d.ts +7 -0
- package/dist/surface/tools/act-capsule.js +46 -1
- package/dist/surface/tools/act-sequence-tool.js +25 -8
- package/dist/surface/tools/act-tools.js +28 -52
- package/dist/surface/tools/assert/assert-grade.js +12 -1
- package/dist/surface/tools/assert/assert-verdict.js +2 -2
- package/dist/surface/tools/browser-tools.js +14 -2
- package/dist/surface/tools/coverage-tools.js +27 -1
- package/dist/surface/tools/dynamic-tools.js +29 -2
- package/dist/surface/tools/error-recovery.js +5 -6
- package/dist/surface/tools/feedback-invite.js +6 -4
- package/dist/surface/tools/harness-explore.d.ts +3 -2
- package/dist/surface/tools/harness-explore.js +12 -7
- package/dist/surface/tools/invoke-tool.js +12 -8
- package/dist/surface/tools/lease-tools.js +15 -6
- package/dist/surface/tools/live-call-text.js +4 -1
- package/dist/surface/tools/observe-tools.js +9 -1
- package/dist/surface/tools/tool-kit.d.ts +10 -0
- package/dist/surface/tools/tools.js +4 -0
- package/dist/telemetry/argument-shape.d.ts +0 -2
- package/dist/telemetry/argument-shape.js +1 -5
- package/dist/telemetry/error-class.d.ts +2 -3
- package/dist/telemetry/error-class.js +2 -3
- package/dist/telemetry/harness-drive.d.ts +0 -2
- package/dist/telemetry/harness-drive.js +0 -4
- package/dist/telemetry/onboarding-firsts.d.ts +2 -2
- package/dist/telemetry/onboarding-firsts.js +2 -2
- package/dist/wire-journal.d.ts +3 -0
- package/dist/wire-journal.js +4 -0
- package/dist/workspace-packages.d.ts +0 -2
- package/dist/workspace-packages.js +1 -9
- package/docs/README.md +4 -2
- package/docs/actions.mdx +3 -4
- package/docs/agent-cheatsheet.md +4 -4
- package/docs/architecture.md +6 -6
- package/docs/best-practices.mdx +1 -1
- package/docs/capabilities.mdx +32 -1
- package/docs/cli/cloud.mdx +20 -8
- package/docs/cli/doctor.mdx +4 -0
- package/docs/cli/feedback.mdx +2 -2
- package/docs/cli/gate.mdx +9 -4
- package/docs/cli/update.mdx +10 -0
- package/docs/cli.mdx +22 -7
- package/docs/desktop.mdx +14 -10
- package/docs/environment.mdx +87 -0
- package/docs/faq.mdx +18 -16
- package/docs/first-drive.md +5 -13
- package/docs/flows.md +21 -2
- package/docs/frameworks.mdx +29 -10
- package/docs/getting-started.md +15 -5
- package/docs/http-transport.md +6 -3
- package/docs/human-control.md +2 -2
- package/docs/index.mdx +16 -6
- package/docs/install-agentic.mdx +2 -2
- package/docs/install-manual.mdx +1 -1
- package/docs/integration-patterns.md +3 -3
- package/docs/multi-agent-testing.md +1 -1
- package/docs/packages/babel-plugin.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/next.mdx +3 -0
- package/docs/packages/react.mdx +2 -2
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +4 -2
- package/docs/packages.mdx +9 -9
- package/docs/predicates.mdx +50 -5
- package/docs/quickstart.mdx +12 -10
- package/docs/recipes.mdx +2 -2
- package/docs/state-management.mdx +2 -2
- package/docs/system-map.md +1 -1
- package/docs/telemetry.md +1 -1
- package/docs/token-efficiency.md +3 -3
- package/docs/tools/act-and-wait.mdx +1 -0
- package/docs/tools/act-sequence.mdx +1 -1
- package/docs/tools/act.mdx +1 -1
- package/docs/tools/console.mdx +1 -1
- package/docs/tools/inspect.mdx +1 -1
- package/docs/tools/network.mdx +1 -1
- package/docs/tools/query.mdx +1 -1
- package/docs/tools/sessions.mdx +1 -1
- package/docs/tools/snapshot.mdx +1 -1
- package/docs/tools/state.mdx +1 -1
- package/docs/tools/wait-for.mdx +1 -1
- package/docs/usage.md +7 -7
- package/docs/use-cases.mdx +157 -0
- package/docs/vs-screenshots.mdx +3 -3
- package/docs/what-is-recorded.md +2 -2
- package/docs/what-is-reticle.mdx +17 -20
- package/docs/why-reticle.mdx +14 -21
- package/package.json +10 -9
- package/dist/judgement/capsule/minimize.d.ts +0 -12
- package/dist/judgement/capsule/minimize.js +0 -22
- package/dist/judgement/phenomena/phenomena.d.ts +0 -27
- package/dist/judgement/phenomena/phenomena.js +0 -101
- package/dist/language/flows/flow-report.d.ts +0 -22
- package/dist/language/flows/flow-report.js +0 -106
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE place a Reticle-owned Chromium is launched — the pooled lease browser
|
|
3
|
+
* (`playwright-launcher`) and the drive browser (`real-input`) both come through here.
|
|
4
|
+
*
|
|
5
|
+
* Why it is more than `chromium.launch`: on a fresh machine the `playwright` package is present
|
|
6
|
+
* (npx pulled it in with the server) but its browser build is not, because that is a separate
|
|
7
|
+
* ~150 MiB download nobody told the user about. Every lease, `reticle verify`, `open` and `drive`
|
|
8
|
+
* then refused with "Chromium is not installed for Playwright. Run: npx playwright@… install
|
|
9
|
+
* chromium" — the first run stopped on a human step a machine could do. Most machines already have
|
|
10
|
+
* a Chrome or an Edge, and Playwright drives either through `channel`, so that is tried before any
|
|
11
|
+
* download; only a machine with neither gets the pinned install, run for them and announced.
|
|
12
|
+
*
|
|
13
|
+
* Announcements go to STDERR, never stdout: `reticle mcp` speaks JSON-RPC on stdout and one stray
|
|
14
|
+
* line there corrupts the client's stream.
|
|
15
|
+
*/
|
|
16
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
import { existsSync } from 'node:fs';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
import { chromiumLaunchOptions } from './chromium-launch-options.js';
|
|
21
|
+
import { bundledPlaywrightVersion, chromiumInstallCommand, probeChromium, } from './command/cli/doctor/browser/chromium-hint.js';
|
|
22
|
+
/** The installed-browser channels Playwright can drive in place of its own build. */
|
|
23
|
+
export const ChromiumChannel = {
|
|
24
|
+
CHROME: 'chrome',
|
|
25
|
+
MSEDGE: 'msedge',
|
|
26
|
+
};
|
|
27
|
+
/** Tried in this order: Chrome is the likelier install and the closer match to Playwright's build. */
|
|
28
|
+
const CHANNEL_ORDER = [ChromiumChannel.CHROME, ChromiumChannel.MSEDGE];
|
|
29
|
+
export const CHANNEL_LABEL = {
|
|
30
|
+
[ChromiumChannel.CHROME]: 'Google Chrome',
|
|
31
|
+
[ChromiumChannel.MSEDGE]: 'Microsoft Edge',
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Where each channel's executable lives, per platform — the same locations Playwright's own
|
|
35
|
+
* registry checks when it launches a channel, so "we found it" and "Playwright can launch it" agree.
|
|
36
|
+
* Windows is relative to each install root below.
|
|
37
|
+
*/
|
|
38
|
+
const CHANNEL_PATHS = {
|
|
39
|
+
[ChromiumChannel.CHROME]: {
|
|
40
|
+
darwin: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
41
|
+
linux: '/opt/google/chrome/chrome',
|
|
42
|
+
win32: '\\Google\\Chrome\\Application\\chrome.exe',
|
|
43
|
+
},
|
|
44
|
+
[ChromiumChannel.MSEDGE]: {
|
|
45
|
+
darwin: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
46
|
+
linux: '/opt/microsoft/msedge/msedge',
|
|
47
|
+
win32: '\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
const WINDOWS_INSTALL_ROOTS = ['LOCALAPPDATA', 'PROGRAMFILES', 'PROGRAMFILES(X86)'];
|
|
51
|
+
/** Candidate executable paths for a channel on this platform; empty where it is not installable. */
|
|
52
|
+
export function channelExecutableCandidates(channel, platform, env) {
|
|
53
|
+
const paths = CHANNEL_PATHS[channel];
|
|
54
|
+
if ('darwin' === platform)
|
|
55
|
+
return [paths.darwin];
|
|
56
|
+
if ('linux' === platform)
|
|
57
|
+
return [paths.linux];
|
|
58
|
+
if ('win32' === platform) {
|
|
59
|
+
return WINDOWS_INSTALL_ROOTS.flatMap((key) => {
|
|
60
|
+
const root = env[key];
|
|
61
|
+
return root === undefined || 0 === root.length ? [] : [`${root}${paths.win32}`];
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
/** The first installed Chrome/Edge on this machine, or undefined. Looks only; installs nothing. */
|
|
67
|
+
export function findInstalledChannel(deps) {
|
|
68
|
+
return CHANNEL_ORDER.find((channel) => channelExecutableCandidates(channel, deps.platform, deps.env).some((p) => deps.exists(p)));
|
|
69
|
+
}
|
|
70
|
+
const usingChannelLine = (channel) => `[reticle] Playwright's Chromium is not installed; using the installed ${CHANNEL_LABEL[channel]} instead.\n`;
|
|
71
|
+
const installingLine = (command) => `[reticle] No Chromium, Chrome or Edge found; installing Playwright's Chromium (${command}) — a one-time download…\n`;
|
|
72
|
+
const INSTALL_FAILED_LINE = '[reticle] The Chromium install did not complete.\n';
|
|
73
|
+
/**
|
|
74
|
+
* Decide which browser a launch will use, installing the pinned Chromium only as the last resort.
|
|
75
|
+
* Order: bundled build on disk → installed Chrome → installed Edge → install, then bundled.
|
|
76
|
+
*/
|
|
77
|
+
export async function resolveChromiumTarget(deps) {
|
|
78
|
+
let bundled;
|
|
79
|
+
try {
|
|
80
|
+
bundled = deps.executablePath();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// A playwright that cannot name its executable is not evidence the browser is missing. Launch
|
|
84
|
+
// as before and let Playwright's own error (translated by chromiumLaunchHint) say what is wrong,
|
|
85
|
+
// rather than download ~150 MiB on a guess.
|
|
86
|
+
return { found: true };
|
|
87
|
+
}
|
|
88
|
+
if (deps.exists(bundled))
|
|
89
|
+
return { found: true };
|
|
90
|
+
const channel = findInstalledChannel(deps);
|
|
91
|
+
if (channel !== undefined) {
|
|
92
|
+
deps.announce(usingChannelLine(channel));
|
|
93
|
+
return { found: true, channel };
|
|
94
|
+
}
|
|
95
|
+
deps.announce(installingLine(chromiumInstallCommand(bundledPlaywrightVersion())));
|
|
96
|
+
const ok = await deps.install();
|
|
97
|
+
if (ok && deps.exists(bundled))
|
|
98
|
+
return { found: true };
|
|
99
|
+
deps.announce(INSTALL_FAILED_LINE);
|
|
100
|
+
return { found: false };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The bundled playwright's own CLI, or undefined when playwright cannot be resolved from here.
|
|
104
|
+
*
|
|
105
|
+
* Run directly rather than through the `npx playwright@<version>` the hint prints. They install the
|
|
106
|
+
* same revision, but `npx` runs in the user's project and inherits everything wrong with it: in the
|
|
107
|
+
* sandbox this was written against, an `overrides` block in the app's package.json made npm refuse
|
|
108
|
+
* with EOVERRIDE before playwright ever ran. The bundled CLI is the same version by construction
|
|
109
|
+
* and needs no registry round trip to find itself.
|
|
110
|
+
*/
|
|
111
|
+
function bundledPlaywrightCli() {
|
|
112
|
+
try {
|
|
113
|
+
const pkg = createRequire(import.meta.url).resolve('playwright/package.json');
|
|
114
|
+
return join(dirname(pkg), PLAYWRIGHT_CLI);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const PLAYWRIGHT_CLI = 'cli.js';
|
|
121
|
+
const INSTALL_ARGS = ['install', 'chromium'];
|
|
122
|
+
/**
|
|
123
|
+
* Run the pinned install with its progress on stderr. Memoized per process: the pool can race
|
|
124
|
+
* several launches on the first acquire, and two concurrent installs into one browsers root would
|
|
125
|
+
* fight over the same directory. A failed install clears the memo so a later launch can retry.
|
|
126
|
+
*/
|
|
127
|
+
let installing;
|
|
128
|
+
function installPinnedChromium() {
|
|
129
|
+
if (installing === undefined) {
|
|
130
|
+
const cli = bundledPlaywrightCli();
|
|
131
|
+
installing = new Promise((resolve) => {
|
|
132
|
+
if (cli === undefined) {
|
|
133
|
+
resolve(false);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// stdout goes to fd 2 for the JSON-RPC reason in this file's header.
|
|
137
|
+
const child = spawn(process.execPath, [cli, ...INSTALL_ARGS], { stdio: ['ignore', 2, 2] });
|
|
138
|
+
child.on('error', () => resolve(false));
|
|
139
|
+
child.on('exit', (code) => resolve(0 === code));
|
|
140
|
+
}).then((ok) => {
|
|
141
|
+
if (!ok)
|
|
142
|
+
installing = undefined;
|
|
143
|
+
return ok;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return installing;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* A progress writer that says each distinct line once. The Chrome/Edge fallback line was written on
|
|
150
|
+
* every launch, and the pool launches per context, so one daemon repeated the same notice into the
|
|
151
|
+
* terminal of whoever started it.
|
|
152
|
+
*/
|
|
153
|
+
export function announceOnce(write) {
|
|
154
|
+
const said = new Set();
|
|
155
|
+
return (line) => {
|
|
156
|
+
if (said.has(line))
|
|
157
|
+
return;
|
|
158
|
+
said.add(line);
|
|
159
|
+
write(line);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** Once per PROCESS, so it is shared by every launch rather than made per call. */
|
|
163
|
+
// ponytail: an install retried after a failure is not re-announced; the install itself still runs.
|
|
164
|
+
const announceToStderr = announceOnce((line) => {
|
|
165
|
+
process.stderr.write(line);
|
|
166
|
+
});
|
|
167
|
+
function defaultDeps(chromium) {
|
|
168
|
+
return {
|
|
169
|
+
executablePath: () => chromium.executablePath(),
|
|
170
|
+
exists: existsSync,
|
|
171
|
+
platform: process.platform,
|
|
172
|
+
env: process.env,
|
|
173
|
+
install: installPinnedChromium,
|
|
174
|
+
announce: announceToStderr,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Launch a Chromium with the shared options, on whichever browser this machine can offer. When none
|
|
179
|
+
* can be found or installed, launch anyway: Playwright's own error is what `chromiumLaunchHint`
|
|
180
|
+
* turns into the pinned install command, so the user still gets the one line that fixes it.
|
|
181
|
+
*/
|
|
182
|
+
export async function launchChromium(chromium, headless, deps = defaultDeps(chromium)) {
|
|
183
|
+
const target = await resolveChromiumTarget(deps);
|
|
184
|
+
const channel = target.found ? target.channel : undefined;
|
|
185
|
+
return chromium.launch(chromiumLaunchOptions(headless, channel));
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The lease preflight's probe: "can a launch succeed?", which now includes the Chrome/Edge fallback
|
|
189
|
+
* and the automatic install. Without this the preflight would refuse a lease on a machine the
|
|
190
|
+
* launcher itself could serve. Falls back to the plain probe so a real refusal keeps its evidence.
|
|
191
|
+
*/
|
|
192
|
+
export async function probeLaunchableChromium() {
|
|
193
|
+
try {
|
|
194
|
+
const { chromium } = await import('playwright');
|
|
195
|
+
const target = await resolveChromiumTarget(defaultDeps(chromium));
|
|
196
|
+
if (target.found)
|
|
197
|
+
return { exists: true };
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// No playwright at all — probeChromium below says so, with where it looked.
|
|
201
|
+
}
|
|
202
|
+
return probeChromium();
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* `doctor`'s probe: the plain Chromium probe, plus the installed browser that stands in when the
|
|
206
|
+
* bundled build is missing. Never installs — a diagnostic that downloads 150 MiB is not one.
|
|
207
|
+
*/
|
|
208
|
+
export async function probeChromiumWithFallback() {
|
|
209
|
+
const probe = await probeChromium();
|
|
210
|
+
if (probe.exists)
|
|
211
|
+
return probe;
|
|
212
|
+
const channel = findInstalledChannel({
|
|
213
|
+
exists: existsSync,
|
|
214
|
+
platform: process.platform,
|
|
215
|
+
env: process.env,
|
|
216
|
+
});
|
|
217
|
+
return channel === undefined ? probe : { ...probe, fallback: CHANNEL_LABEL[channel] };
|
|
218
|
+
}
|
|
@@ -40,6 +40,8 @@ export interface ProjectCloud {
|
|
|
40
40
|
verify: VerifyMode;
|
|
41
41
|
/** The cloud project id this project is linked to (for display/logging); null when env-fallback. */
|
|
42
42
|
projectId: string | null;
|
|
43
|
+
/** Why `config` is null, in words a person can act on. Absent when attached. */
|
|
44
|
+
reason?: string;
|
|
43
45
|
}
|
|
44
46
|
/** Look up the API key for a cloud project id in the user credentials map. */
|
|
45
47
|
/**
|
|
@@ -63,8 +65,8 @@ export interface ProjectCloud {
|
|
|
63
65
|
export declare const credentialSlot: (url: string, projectId: string, orgId?: string) => string;
|
|
64
66
|
/**
|
|
65
67
|
* Resolve the cloud picture for a project rooted at `reticleRoot`. Reads the project's link file + the
|
|
66
|
-
* user credential store; if the project isn't linked (no cloud.json),
|
|
67
|
-
*
|
|
68
|
+
* user credential store, else the env key; if the project isn't linked (no cloud.json), the env creds
|
|
69
|
+
* are the whole story (the single-project / CI flow). `homeDir` is injected (testable; `os.homedir()` at call).
|
|
68
70
|
*/
|
|
69
71
|
export declare function resolveProjectCloud(fs: FileSystemPort, reticleRoot: string, homeDir: string, env: NodeJS.ProcessEnv): Promise<ProjectCloud>;
|
|
70
72
|
/**
|
|
@@ -76,4 +78,11 @@ export declare function resolveProjectCloud(fs: FileSystemPort, reticleRoot: str
|
|
|
76
78
|
* needs no exported key — see `withLinkedCredential`.
|
|
77
79
|
*/
|
|
78
80
|
export declare const linkedCloudPort: (fs: FileSystemPort, reticleRoot: string, homeDir: string, env: NodeJS.ProcessEnv) => () => Promise<CloudConfig | null>;
|
|
81
|
+
/**
|
|
82
|
+
* The environment with this project's resolved credential written into it, for the readers that
|
|
83
|
+
* take an env record (the panel's model config and harness switch). Handed `process.env` alone,
|
|
84
|
+
* they never saw a key `reticle link` had stored. Resolved per call, so a link made mid-session
|
|
85
|
+
* takes effect without a restart.
|
|
86
|
+
*/
|
|
87
|
+
export declare const platformEnvPort: (fs: FileSystemPort, reticleRoot: string, homeDir: string, env: NodeJS.ProcessEnv) => () => Promise<NodeJS.ProcessEnv>;
|
|
79
88
|
export {};
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* user-level credential. "Cloud attached" = a valid link file AND a key for its project id (or env creds).
|
|
11
11
|
*/
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
-
import { ReticleDir } from '@reticlehq/core';
|
|
13
|
+
import { DEFAULT_PLATFORM_URL, ReticleDir, ReticleEnv } from '@reticlehq/core';
|
|
14
14
|
import { resolveCloudConfig } from './cloud-sync.js';
|
|
15
15
|
/** `<reticleRoot>/cloud.json` — the project's cloud binding + sync policy (non-secret). */
|
|
16
16
|
/** Re-exported so the existing importers keep working; the string itself lives in core. */
|
|
@@ -121,18 +121,22 @@ function credentialFor(raw, projectId, url, orgId) {
|
|
|
121
121
|
const normalizeCloudUrl = (url) => url.replace(/\/+$/, '');
|
|
122
122
|
/**
|
|
123
123
|
* Resolve the cloud picture for a project rooted at `reticleRoot`. Reads the project's link file + the
|
|
124
|
-
* user credential store; if the project isn't linked (no cloud.json),
|
|
125
|
-
*
|
|
124
|
+
* user credential store, else the env key; if the project isn't linked (no cloud.json), the env creds
|
|
125
|
+
* are the whole story (the single-project / CI flow). `homeDir` is injected (testable; `os.homedir()` at call).
|
|
126
126
|
*/
|
|
127
127
|
export async function resolveProjectCloud(fs, reticleRoot, homeDir, env) {
|
|
128
128
|
const link = parseLink(await readJson(fs, join(reticleRoot, CLOUD_LINK_FILE)));
|
|
129
129
|
if (null === link) {
|
|
130
130
|
// No per-project link → the env vars are the whole story (legacy single-project / CI behaviour).
|
|
131
|
+
const config = resolveCloudConfig(env);
|
|
131
132
|
return {
|
|
132
|
-
config
|
|
133
|
+
config,
|
|
133
134
|
policy: DEFAULT_SYNC_POLICY,
|
|
134
135
|
verify: VerifyMode.LOCAL,
|
|
135
136
|
projectId: null,
|
|
137
|
+
...(null === config
|
|
138
|
+
? { reason: 'cloud not attached here: run `reticle link`, or set RETICLE_API_KEY' }
|
|
139
|
+
: {}),
|
|
136
140
|
};
|
|
137
141
|
}
|
|
138
142
|
const policy = {
|
|
@@ -141,9 +145,33 @@ export async function resolveProjectCloud(fs, reticleRoot, homeDir, env) {
|
|
|
141
145
|
flows: link.sync.flows ?? DEFAULT_SYNC_POLICY.flows,
|
|
142
146
|
capsules: link.sync.capsules ?? DEFAULT_SYNC_POLICY.capsules,
|
|
143
147
|
};
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
148
|
+
const stored = credentialFor(await readJson(fs, join(homeDir, ReticleDir.ROOT, CREDENTIALS_FILE)), link.projectId, link.url, link.orgId);
|
|
149
|
+
/*
|
|
150
|
+
* No stored key, so the environment's — which is the whole of CI: `cloud.json` is committed, a
|
|
151
|
+
* CI clone is therefore linked, and it has no keystore. Only when the environment's key is FOR
|
|
152
|
+
* this link's host (its URL, else the hosted default): a key exported for one cloud is not a
|
|
153
|
+
* credential for a repo linked to another, the same rule `credentialFor` applies to stored keys.
|
|
154
|
+
*/
|
|
155
|
+
const fromEnv = resolveCloudConfig(env);
|
|
156
|
+
const key = stored ??
|
|
157
|
+
(fromEnv !== null && fromEnv.url === normalizeCloudUrl(link.url) ? fromEnv.apiKey : null);
|
|
158
|
+
const linkedUrl = normalizeCloudUrl(link.url);
|
|
159
|
+
const config = key !== null ? { url: linkedUrl, apiKey: key } : null;
|
|
160
|
+
const reason = config !== null
|
|
161
|
+
? undefined
|
|
162
|
+
: fromEnv !== null
|
|
163
|
+
? `this repo is linked to ${linkedUrl}, and RETICLE_API_KEY is set for ${fromEnv.url}: ` +
|
|
164
|
+
`set RETICLE_CLOUD_URL=${linkedUrl} to use that key here, or run \`reticle login\` for it`
|
|
165
|
+
: `this repo is linked to ${linkedUrl}, and this machine has no key for it: run ` +
|
|
166
|
+
'`reticle login`, or set RETICLE_API_KEY' +
|
|
167
|
+
(DEFAULT_PLATFORM_URL === linkedUrl ? '' : ` with RETICLE_CLOUD_URL=${linkedUrl}`);
|
|
168
|
+
return {
|
|
169
|
+
config,
|
|
170
|
+
policy,
|
|
171
|
+
verify: link.verify,
|
|
172
|
+
projectId: link.projectId,
|
|
173
|
+
...(reason === undefined ? {} : { reason }),
|
|
174
|
+
};
|
|
147
175
|
}
|
|
148
176
|
/**
|
|
149
177
|
* The linked credential, as a port the tool surface can be handed.
|
|
@@ -154,3 +182,15 @@ export async function resolveProjectCloud(fs, reticleRoot, homeDir, env) {
|
|
|
154
182
|
* needs no exported key — see `withLinkedCredential`.
|
|
155
183
|
*/
|
|
156
184
|
export const linkedCloudPort = (fs, reticleRoot, homeDir, env) => async () => (await resolveProjectCloud(fs, reticleRoot, homeDir, env)).config;
|
|
185
|
+
/**
|
|
186
|
+
* The environment with this project's resolved credential written into it, for the readers that
|
|
187
|
+
* take an env record (the panel's model config and harness switch). Handed `process.env` alone,
|
|
188
|
+
* they never saw a key `reticle link` had stored. Resolved per call, so a link made mid-session
|
|
189
|
+
* takes effect without a restart.
|
|
190
|
+
*/
|
|
191
|
+
export const platformEnvPort = (fs, reticleRoot, homeDir, env) => async () => {
|
|
192
|
+
const config = (await resolveProjectCloud(fs, reticleRoot, homeDir, env)).config;
|
|
193
|
+
return null === config
|
|
194
|
+
? env
|
|
195
|
+
: { ...env, [ReticleEnv.API_KEY]: config.apiKey, [ReticleEnv.CLOUD_URL]: config.url };
|
|
196
|
+
};
|
|
@@ -24,7 +24,10 @@ export interface CloudConfig {
|
|
|
24
24
|
url: string;
|
|
25
25
|
apiKey: string;
|
|
26
26
|
}
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Resolve cloud credentials from the environment, or null without a key (sync disabled). The key
|
|
29
|
+
* alone is enough: the URL defaults to the hosted service, which is what the platform tells CI.
|
|
30
|
+
*/
|
|
28
31
|
export declare function resolveCloudConfig(env: NodeJS.ProcessEnv): CloudConfig | null;
|
|
29
32
|
export declare const CLOUD_VERIFY_TIMEOUT_MS = 120000;
|
|
30
33
|
/**
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* cloud copy is an enhancement, so a push error is logged and swallowed.
|
|
10
10
|
*/
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
-
import { VERIFY_PROGRESS_MAX_EVENTS,
|
|
12
|
+
import { VERIFY_PROGRESS_MAX_EVENTS, platformCredentialFrom, } from '@reticlehq/core';
|
|
13
13
|
/**
|
|
14
14
|
* Env var names — the presence of BOTH is what "logged in" means for cloud sync.
|
|
15
15
|
*
|
|
@@ -27,15 +27,12 @@ const CLOUD_PROJECT_RUNS_PATH = '/v1/project/runs';
|
|
|
27
27
|
const CLOUD_PROJECT_REGRESSION_PATH = '/v1/project/regression';
|
|
28
28
|
const CLOUD_VERIFICATIONS_PATH = '/v1/verifications';
|
|
29
29
|
const CLOUD_PROGRESS_PATH = '/v1/connect/progress';
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Resolve cloud credentials from the environment, or null without a key (sync disabled). The key
|
|
32
|
+
* alone is enough: the URL defaults to the hosted service, which is what the platform tells CI.
|
|
33
|
+
*/
|
|
31
34
|
export function resolveCloudConfig(env) {
|
|
32
|
-
|
|
33
|
-
const apiKey = apiKeyFrom(env);
|
|
34
|
-
if (typeof url !== 'string' || 0 === url.length)
|
|
35
|
-
return null;
|
|
36
|
-
if (apiKey === undefined)
|
|
37
|
-
return null;
|
|
38
|
-
return { url: url.replace(/\/+$/, ''), apiKey };
|
|
35
|
+
return platformCredentialFrom(env) ?? null;
|
|
39
36
|
}
|
|
40
37
|
/**
|
|
41
38
|
* Budget for a cloud call. Node's `fetch` has NO default timeout, so a connection that opens and then
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* the background is the whole design: readers get what we last heard, and a stale answer is only
|
|
16
16
|
* ever stale in the safe direction — the card the person is looking at is the one they can act on.
|
|
17
17
|
*/
|
|
18
|
-
import {
|
|
18
|
+
import { platformCredentialFrom } from '@reticlehq/core';
|
|
19
19
|
const OFFER_PATH = '/v1/harness/offer';
|
|
20
20
|
/** Same budget as the model-config read: a drive must never wait on an advert. */
|
|
21
21
|
const TIMEOUT_MS = 2_000;
|
|
@@ -65,12 +65,11 @@ function parse(body, claimUrl) {
|
|
|
65
65
|
}
|
|
66
66
|
/** Ask the platform where this workspace stands. `undefined` means "no answer", never "not claimed". */
|
|
67
67
|
export async function fetchHarnessOffer(env, claimUrl, doFetch = (url, init) => fetch(url, init), timeoutMs = TIMEOUT_MS) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
if (key === undefined || 0 === key.length)
|
|
71
|
-
return undefined;
|
|
72
|
-
if (host === undefined || 0 === host.length)
|
|
68
|
+
const cloud = platformCredentialFrom(env);
|
|
69
|
+
if (cloud === undefined)
|
|
73
70
|
return undefined;
|
|
71
|
+
const key = cloud.apiKey;
|
|
72
|
+
const host = cloud.url;
|
|
74
73
|
const controller = new AbortController();
|
|
75
74
|
const timer = setTimeout(() => {
|
|
76
75
|
controller.abort();
|
|
@@ -17,18 +17,17 @@
|
|
|
17
17
|
* that quietly did not save; the next snapshot re-reads the platform, so a lost write shows up as
|
|
18
18
|
* the switch springing back, which is a truthful thing for it to do.
|
|
19
19
|
*/
|
|
20
|
-
import {
|
|
20
|
+
import { platformCredentialFrom } from '@reticlehq/core';
|
|
21
21
|
const CONFIG_PATH = '/v1/model/config';
|
|
22
22
|
/** Same budget as the read beside it: a settings write must never hold anything up. */
|
|
23
23
|
const TIMEOUT_MS = 2_000;
|
|
24
24
|
/** `true` when the platform accepted it. Callers use this for logging, never to block a person. */
|
|
25
25
|
export async function writeHarnessSwitch(env, enabled, doFetch = (url, init) => fetch(url, init), timeoutMs = TIMEOUT_MS) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
if (key === undefined || 0 === key.length)
|
|
29
|
-
return false;
|
|
30
|
-
if (host === undefined || 0 === host.length)
|
|
26
|
+
const cloud = platformCredentialFrom(env);
|
|
27
|
+
if (cloud === undefined)
|
|
31
28
|
return false;
|
|
29
|
+
const key = cloud.apiKey;
|
|
30
|
+
const host = cloud.url;
|
|
32
31
|
const controller = new AbortController();
|
|
33
32
|
const timer = setTimeout(() => {
|
|
34
33
|
controller.abort();
|
|
@@ -13,6 +13,12 @@ declare const DERIVED_RECORDS: readonly [{
|
|
|
13
13
|
}, {
|
|
14
14
|
readonly kind: "intent";
|
|
15
15
|
readonly file: "intent.json";
|
|
16
|
+
}, {
|
|
17
|
+
readonly kind: "envelopes";
|
|
18
|
+
readonly file: "envelopes.json";
|
|
19
|
+
}, {
|
|
20
|
+
readonly kind: "assertion-tiers";
|
|
21
|
+
readonly file: "assertion-tiers.json";
|
|
16
22
|
}];
|
|
17
23
|
type DerivedKind = (typeof DERIVED_RECORDS)[number]['kind'];
|
|
18
24
|
/** What the machine reads from disk. Injected so a cycle is testable with no filesystem at all. */
|
|
@@ -69,11 +75,52 @@ export interface CloudSyncState {
|
|
|
69
75
|
* fills the gap the server declared and never overrides the server on a question it answered.
|
|
70
76
|
*/
|
|
71
77
|
sentRunIds?: string[];
|
|
78
|
+
/**
|
|
79
|
+
* The content hash of each run as the server last ACCEPTED it, for the runs still held locally.
|
|
80
|
+
*
|
|
81
|
+
* A run is not immutable: a session's live drive run is rewritten after every verdict under the
|
|
82
|
+
* same id. Sending by id alone delivered its first version and never the rest, so the server held
|
|
83
|
+
* a tab's first few verdicts and none after. The server upserts a run by id, so a changed run is
|
|
84
|
+
* simply sent again; this is how the machine tells a changed run from one it already delivered.
|
|
85
|
+
*/
|
|
86
|
+
sentRunHashes?: Record<string, string>;
|
|
87
|
+
/**
|
|
88
|
+
* Runs the server refused, by run id: its reason, and the two things that could change the answer
|
|
89
|
+
* (a hash of the run file, and of what the platform said it reads). A refused run is not offered
|
|
90
|
+
* again until one of them changes; re-offering it every cycle got the same refusal and dragged
|
|
91
|
+
* every flow and capsule along with it.
|
|
92
|
+
*/
|
|
93
|
+
refusedRuns?: Record<string, RefusedRun>;
|
|
94
|
+
/**
|
|
95
|
+
* Hashes of the flow set and the capsule set the platform last answered for. A flow saved or
|
|
96
|
+
* edited with no new run used to wait for one, because flows only rode along with other data; now
|
|
97
|
+
* a changed set is sent on its own, and an unchanged one is not sent again.
|
|
98
|
+
*/
|
|
99
|
+
sentFlowsHash?: string;
|
|
100
|
+
sentCapsulesHash?: string;
|
|
101
|
+
/**
|
|
102
|
+
* A flow or capsule set the platform refused, kept so the refusal stays visible on the cycles that
|
|
103
|
+
* do not resend it, and so the set is offered again once it changes or the platform reads more.
|
|
104
|
+
*/
|
|
105
|
+
refusedSets?: Partial<Record<SetPart, RefusedSet>>;
|
|
106
|
+
}
|
|
107
|
+
type SetPart = 'flow' | 'capsule';
|
|
108
|
+
interface RefusedSet {
|
|
109
|
+
hash: string;
|
|
110
|
+
acceptsHash: string;
|
|
111
|
+
count: number;
|
|
112
|
+
reason: string;
|
|
113
|
+
}
|
|
114
|
+
interface RefusedRun {
|
|
115
|
+
reason: string;
|
|
116
|
+
payloadHash: string;
|
|
117
|
+
acceptsHash: string;
|
|
72
118
|
}
|
|
73
119
|
export interface SyncReport {
|
|
74
120
|
/**
|
|
75
|
-
* False if the cycle failed or
|
|
76
|
-
*
|
|
121
|
+
* False if the cycle failed, the server refused anything, or a run or flow was held back for a
|
|
122
|
+
* version the platform does not read. A completed HTTP exchange alone is not a successful push.
|
|
123
|
+
* A record KIND the platform does not know yet is reported in `held` and does not fail it.
|
|
77
124
|
*/
|
|
78
125
|
ok: boolean;
|
|
79
126
|
/** Runs the server accepted this cycle. */
|
|
@@ -86,8 +133,29 @@ export interface SyncReport {
|
|
|
86
133
|
flowsSent: number;
|
|
87
134
|
/** Capsules the server accepted. Zero when it reported none, which includes not knowing the field. */
|
|
88
135
|
capsulesSent: number;
|
|
89
|
-
/**
|
|
136
|
+
/** Derived records the server ACCEPTED this cycle. Empty on a quiet cycle, the normal case. */
|
|
90
137
|
derivedSent: DerivedKind[];
|
|
138
|
+
/**
|
|
139
|
+
* Everything else the server refused: a flow, a capsule, or a derived record. Each reason is a
|
|
140
|
+
* complete sentence naming the item, because this is often read from a background daemon's log.
|
|
141
|
+
*/
|
|
142
|
+
refused: Refusal[];
|
|
143
|
+
/**
|
|
144
|
+
* What was deliberately NOT sent, and why: a record kind or a file version this platform does
|
|
145
|
+
* not read. Sending it would only be refused again on every cycle.
|
|
146
|
+
*/
|
|
147
|
+
held: string[];
|
|
148
|
+
/** Runs refused on an earlier cycle and not offered again, because nothing has changed since. */
|
|
149
|
+
notRetried: Array<{
|
|
150
|
+
runId: string;
|
|
151
|
+
reason: string;
|
|
152
|
+
}>;
|
|
153
|
+
/** Flow or capsule sets refused on an earlier cycle and not offered again. */
|
|
154
|
+
setsNotRetried: Array<{
|
|
155
|
+
part: SetPart;
|
|
156
|
+
count: number;
|
|
157
|
+
reason: string;
|
|
158
|
+
}>;
|
|
91
159
|
/** Decisions collected from the dashboard. */
|
|
92
160
|
pulled: number;
|
|
93
161
|
/** True when the pull page was full — call again now rather than waiting for the next tick. */
|
|
@@ -104,6 +172,12 @@ export interface SyncReport {
|
|
|
104
172
|
/** Set when the cycle could not complete. The local record is untouched either way. */
|
|
105
173
|
error?: string;
|
|
106
174
|
}
|
|
175
|
+
/** One item the server refused that is not a run. */
|
|
176
|
+
export interface Refusal {
|
|
177
|
+
/** `flow`, `capsule`, or the derived record's kind. */
|
|
178
|
+
part: string;
|
|
179
|
+
reason: string;
|
|
180
|
+
}
|
|
107
181
|
interface SyncDeps {
|
|
108
182
|
config: {
|
|
109
183
|
url: string;
|