@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
|
@@ -6,19 +6,30 @@
|
|
|
6
6
|
* every command harder to read.
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
-
import { RETICLE_DEFAULT_PORT } from '@reticlehq/core';
|
|
9
|
+
import { InitConfirmation, RETICLE_DEFAULT_PORT } from '@reticlehq/core';
|
|
10
|
+
import { FEEDBACK_HINT, Framework, InitFailure } from '@reticlehq/init';
|
|
10
11
|
import { confirmInstall, nodeConfirmDeps } from '../terminal/confirm.js';
|
|
11
12
|
import { writeLicenseKey } from '../license-key.js';
|
|
12
13
|
import { registerOtherAgents, runSetupCommand } from '../setup-command.js';
|
|
13
14
|
import { bridgeOccupied } from '../bringup/bridge-port.js';
|
|
15
|
+
import { defaultPairingTokenDir, readOrCreatePairingTokenSync, } from '../../../portal/bridge/pairing-token.js';
|
|
14
16
|
import { relaunchDecision } from '../bringup/relaunch.js';
|
|
15
17
|
import { claudeTranscriptExists, codexSessionFor } from '../terminal/transcripts.js';
|
|
16
18
|
import { probePresence } from '../../daemon/binding/port-presence.js';
|
|
17
19
|
import { probeDaemon } from '../../../surface/mcp/proxy/proxy-daemon-probe.js';
|
|
18
20
|
import { fetchStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
19
21
|
import { collectEnv, DEFAULT_PHASE_TIMEOUT_MS } from '../setup-options.js';
|
|
22
|
+
import { staticPageDevCommand } from '../bringup/static-page-server.js';
|
|
23
|
+
import { SetupPhase } from '../run-setup.js';
|
|
24
|
+
import { reportInitOutcome } from '../../../telemetry/init-telemetry.js';
|
|
20
25
|
/** How often the runtime phases look again: fast enough not to be the wait, slow enough to be free. */
|
|
21
26
|
const POLL_MS = 250;
|
|
27
|
+
/**
|
|
28
|
+
* The shapes whose served HTML carries the SDK marker: Vite injects it into index.html, and a plain
|
|
29
|
+
* HTML page has the snippet pasted in. Everything else connects from the JS bundle, where a fetch
|
|
30
|
+
* of the document can never see it. See `htmlCarriesSdk` in run-setup.ts.
|
|
31
|
+
*/
|
|
32
|
+
const HTML_CARRIES_SDK = new Set([Framework.VITE, Framework.HTML]);
|
|
22
33
|
/** Registration is global MCP wiring, so both flags that disown it are read here. */
|
|
23
34
|
function wantsAgents(parsed) {
|
|
24
35
|
return false !== parsed.agents && false !== parsed.mcp;
|
|
@@ -50,9 +61,9 @@ function printRelaunch(parsed, io, cwd) {
|
|
|
50
61
|
}
|
|
51
62
|
export async function continueAfterInit(parsed, result, io, cwd) {
|
|
52
63
|
const port = parsed.port ?? RETICLE_DEFAULT_PORT;
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
if (undefined !== parsed.licenseKey) {
|
|
64
|
+
// Real installs persist the key whichever way setup ends. A preview must leave .env and
|
|
65
|
+
// .gitignore untouched, just as it leaves project wiring and agent configuration untouched.
|
|
66
|
+
if (!parsed.dryRun && undefined !== parsed.licenseKey) {
|
|
56
67
|
const written = writeLicenseKey(cwd, parsed.licenseKey, licenseIo);
|
|
57
68
|
io.print(written.message);
|
|
58
69
|
}
|
|
@@ -70,18 +81,35 @@ export async function continueAfterInit(parsed, result, io, cwd) {
|
|
|
70
81
|
// has: nothing reaches back into a machine that installed Reticle a version ago, so the
|
|
71
82
|
// upgrade path is re-running init, and the light form of init has to be enough to carry it.
|
|
72
83
|
// A dry run writes nothing anywhere, including here.
|
|
73
|
-
if (true === parsed.filesOnly && wantsAgents(parsed))
|
|
84
|
+
if (!parsed.dryRun && true === parsed.filesOnly && wantsAgents(parsed)) {
|
|
74
85
|
registerOtherAgents(io.print);
|
|
86
|
+
}
|
|
75
87
|
return confirmInstall(result, io, nodeConfirmDeps(port)).then(() => {
|
|
88
|
+
if (true === parsed.json)
|
|
89
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
76
90
|
if (!result.ok)
|
|
77
91
|
process.exit(1);
|
|
78
92
|
});
|
|
79
93
|
}
|
|
94
|
+
// From here on init left the ask to us, so it is the last thing on every exit below.
|
|
95
|
+
const ask = (spaced = true) => {
|
|
96
|
+
if (spaced)
|
|
97
|
+
io.print('');
|
|
98
|
+
io.print(FEEDBACK_HINT);
|
|
99
|
+
};
|
|
80
100
|
const context = result.context;
|
|
81
101
|
if (context === undefined) {
|
|
82
102
|
// Nothing was established, so there is nothing to run against. init has already said why.
|
|
103
|
+
ask();
|
|
104
|
+
if (true === parsed.json)
|
|
105
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
83
106
|
process.exit(1);
|
|
84
107
|
}
|
|
108
|
+
const reportFailure = (reason) => {
|
|
109
|
+
if (result.outcome !== undefined) {
|
|
110
|
+
reportInitOutcome({ ...result.outcome, ok: false, reason });
|
|
111
|
+
}
|
|
112
|
+
};
|
|
85
113
|
// A PENDING connect step is not a reason to skip the runtime phase, and treating it as one made
|
|
86
114
|
// `init` stop with "paste this snippet" while never looking at the app.
|
|
87
115
|
//
|
|
@@ -97,15 +125,26 @@ export async function continueAfterInit(parsed, result, io, cwd) {
|
|
|
97
125
|
// Before the connect wait, never after: a bridge held by a stranger makes a session impossible,
|
|
98
126
|
// so going ahead spends the entire budget and then reports what reads as an instrumentation
|
|
99
127
|
// problem — the one place that is fine. See bridge-port.ts.
|
|
100
|
-
const refusal = bridgeOccupied(await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus })
|
|
128
|
+
const refusal = bridgeOccupied(await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus }).catch((error) => {
|
|
129
|
+
reportFailure(InitFailure.RUNTIME_ERROR);
|
|
130
|
+
throw error;
|
|
131
|
+
}), port);
|
|
101
132
|
if (refusal !== undefined) {
|
|
133
|
+
reportFailure(InitFailure.BRIDGE_OCCUPIED);
|
|
102
134
|
io.print(refusal);
|
|
135
|
+
ask();
|
|
136
|
+
if (true === parsed.json) {
|
|
137
|
+
process.stdout.write(`${JSON.stringify({ ok: false, reason: InitFailure.BRIDGE_OCCUPIED }, null, 2)}\n`);
|
|
138
|
+
}
|
|
103
139
|
process.exit(1);
|
|
104
140
|
}
|
|
105
141
|
return runSetupCommand({
|
|
106
142
|
appDir: context.appDir,
|
|
107
143
|
invokedAt: cwd,
|
|
108
144
|
bridgePort: port,
|
|
145
|
+
// Read here because this is the layer that already owns the bridge: setup opens a lease over
|
|
146
|
+
// the daemon's MCP transport for `--no-open`, and that transport is gated on this token.
|
|
147
|
+
pairingToken: readOrCreatePairingTokenSync(defaultPairingTokenDir()),
|
|
109
148
|
env: collectEnv(parsed.env ?? []),
|
|
110
149
|
openBrowser: false !== parsed.open,
|
|
111
150
|
registerAgents: wantsAgents(parsed),
|
|
@@ -116,11 +155,46 @@ export async function continueAfterInit(parsed, result, io, cwd) {
|
|
|
116
155
|
// keeps deciding, so nobody who passed nothing waits less than they used to.
|
|
117
156
|
...(undefined === parsed.timeoutSeconds
|
|
118
157
|
? {}
|
|
119
|
-
: {
|
|
158
|
+
: {
|
|
159
|
+
connectBudgetMs: parsed.timeoutSeconds * 1000,
|
|
160
|
+
startupBudgetMs: parsed.timeoutSeconds * 1000,
|
|
161
|
+
}),
|
|
120
162
|
pollMs: POLL_MS,
|
|
121
|
-
|
|
163
|
+
htmlCarriesSdk: HTML_CARRIES_SDK.has(context.framework),
|
|
164
|
+
// A plain page with no dev script gets Reticle's own static server, so the page is actually
|
|
165
|
+
// served and the connect can be proved, instead of stopping at "start the app yourself".
|
|
166
|
+
...(() => {
|
|
167
|
+
const devCommand = context.devCommand ??
|
|
168
|
+
(undefined === parsed.url
|
|
169
|
+
? staticPageDevCommand({
|
|
170
|
+
appDir: context.appDir,
|
|
171
|
+
isStaticPage: Framework.HTML === context.framework,
|
|
172
|
+
})
|
|
173
|
+
: undefined);
|
|
174
|
+
return undefined === devCommand ? {} : { devCommand };
|
|
175
|
+
})(),
|
|
122
176
|
...(undefined === parsed.url ? {} : { suppliedUrl: parsed.url }),
|
|
123
177
|
}, (line) => io.print(line)).then((outcome) => {
|
|
178
|
+
if (result.outcome !== undefined) {
|
|
179
|
+
const init = {
|
|
180
|
+
...result.outcome,
|
|
181
|
+
ok: outcome.ok,
|
|
182
|
+
};
|
|
183
|
+
if (outcome.ok) {
|
|
184
|
+
delete init.reason;
|
|
185
|
+
init.confirmation = InitConfirmation.CONNECTED;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// A failed startup does not establish whether a daemon or instrumented page exists.
|
|
189
|
+
// Keep the watcher's no_daemon/no_session/no_page classifications for observed facts.
|
|
190
|
+
delete init.confirmation;
|
|
191
|
+
init.reason =
|
|
192
|
+
outcome.reachedPhase === SetupPhase.DEV_SERVER
|
|
193
|
+
? InitFailure.DEV_SERVER
|
|
194
|
+
: InitFailure.APP_CONNECTION;
|
|
195
|
+
}
|
|
196
|
+
reportInitOutcome(init);
|
|
197
|
+
}
|
|
124
198
|
// One object, so an agent reads a result instead of interpreting a report.
|
|
125
199
|
if (true === parsed.json) {
|
|
126
200
|
process.stdout.write(`${JSON.stringify(outcome, null, 2)}\n`);
|
|
@@ -145,9 +219,12 @@ export async function continueAfterInit(parsed, result, io, cwd) {
|
|
|
145
219
|
// Success, and no flow. Saying "a flow was driven" here would replace a wrong exit code with
|
|
146
220
|
// a wrong sentence, which is the worse of the two: the exit code is read by CI and the
|
|
147
221
|
// sentence is read by a person deciding whether their app is verified. It is not.
|
|
148
|
-
|
|
222
|
+
// The connect already said "connected, nothing verified yet" and listed what to do next;
|
|
223
|
+
// repeating it here was the same sentence twice, three lines apart.
|
|
149
224
|
for (const [i, step] of outcome.fallback.entries())
|
|
150
225
|
io.print(` ${String(i + 1)}. ${step}`);
|
|
226
|
+
// Already spaced by the blank line above when nothing was listed.
|
|
227
|
+
ask(0 < outcome.fallback.length);
|
|
151
228
|
return;
|
|
152
229
|
}
|
|
153
230
|
if (outcome.ok) {
|
|
@@ -156,6 +233,7 @@ export async function continueAfterInit(parsed, result, io, cwd) {
|
|
|
156
233
|
// themselves, and it deserves a line of its own rather than a paragraph that gets skimmed.
|
|
157
234
|
io.print(' Read the FINDINGS above before moving on: a flow can pass with a failed request or a ' +
|
|
158
235
|
'console error behind it, and that is the app, not the check.');
|
|
236
|
+
ask();
|
|
159
237
|
return;
|
|
160
238
|
}
|
|
161
239
|
// A run that produced no verdict did not succeed, and the exit code is the one place a caller
|
|
@@ -163,6 +241,10 @@ export async function continueAfterInit(parsed, result, io, cwd) {
|
|
|
163
241
|
io.print('⚠ setup did not finish. To carry on from here:');
|
|
164
242
|
for (const [i, step] of outcome.fallback.entries())
|
|
165
243
|
io.print(` ${i + 1}. ${step}`);
|
|
244
|
+
ask();
|
|
166
245
|
process.exit(1);
|
|
246
|
+
}, (error) => {
|
|
247
|
+
reportFailure(InitFailure.RUNTIME_ERROR);
|
|
248
|
+
throw error;
|
|
167
249
|
});
|
|
168
250
|
}
|
|
@@ -15,31 +15,42 @@ import type { CandidateSession } from './session-pick.js';
|
|
|
15
15
|
import type { PageProbe } from './probe/page-probe.js';
|
|
16
16
|
/** The dev server this process started, and the only one it will ever stop. */
|
|
17
17
|
export declare class OwnedDevServer {
|
|
18
|
+
private readonly logDir;
|
|
18
19
|
private child;
|
|
19
|
-
private
|
|
20
|
-
private
|
|
20
|
+
private log;
|
|
21
|
+
private cwd;
|
|
22
|
+
private startedAt;
|
|
21
23
|
private handedOver;
|
|
24
|
+
constructor(logDir?: string);
|
|
25
|
+
/**
|
|
26
|
+
* Start it with its output going to a bounded log, through a supervisor that outlives `init`.
|
|
27
|
+
*
|
|
28
|
+
* Not a pipe into THIS process. A pipe's read end belonged to `init`; once it exited nobody held
|
|
29
|
+
* it, and the server's next write (a compile error, an HMR line, SvelteKit's first warning) was an
|
|
30
|
+
* EPIPE that killed it: "it connected, then the app went away". And not a bare file either, which
|
|
31
|
+
* has no ceiling: an upstream echo loop grew one to 2.9 GB in five minutes. The supervisor reads
|
|
32
|
+
* the server's pipes for as long as they are open and rotates the file past a cap — see
|
|
33
|
+
* dev-server-log-cap.ts. It is the process group leader, so `pid()` and `stop()` cover both.
|
|
34
|
+
*/
|
|
22
35
|
start(command: string, cwd: string, env: Readonly<Record<string, string>>): void;
|
|
36
|
+
/** Where the server's output goes, for the line that hands it over. */
|
|
37
|
+
logPath(): string | undefined;
|
|
23
38
|
/** The process group leader, for a caller that has to clean up what it handed over. */
|
|
24
39
|
pid(): number | undefined;
|
|
25
40
|
output(): string;
|
|
26
41
|
exited(): boolean;
|
|
42
|
+
/** Since the log last grew, which is since the server last printed anything. */
|
|
27
43
|
quietForMs(): number;
|
|
28
44
|
/** Ports anything in this server's process tree is listening on. */
|
|
29
45
|
listeningPorts(): number[];
|
|
30
|
-
/** Hand the running server to the user. After this, `stop()` does nothing. */
|
|
31
46
|
/**
|
|
32
|
-
* Leave the server running and stop holding it.
|
|
33
|
-
*
|
|
34
|
-
* `unref` on the child is NOT enough: its stdout and stderr are pipes owned by this process, and
|
|
35
|
-
* an open pipe keeps the event loop alive on its own. So `init` printed "setup complete" and then
|
|
36
|
-
* hung until something killed it, turning a successful run into a non-zero exit — which is
|
|
37
|
-
* exactly what the install gate reported, on a run whose every other assertion passed.
|
|
47
|
+
* Leave the server running and stop holding it. After this, `stop()` does nothing.
|
|
38
48
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
49
|
+
* Only `unref` is needed: the output is the supervisor's, so no pipe of ours holds the event loop
|
|
50
|
+
* (`init` once printed "setup complete" and hung on one). `url` is recorded beside the log, so the
|
|
51
|
+
* next `init` attaches to this server rather than starting a second one beside it.
|
|
41
52
|
*/
|
|
42
|
-
handOver(): void;
|
|
53
|
+
handOver(url?: string): void;
|
|
43
54
|
stop(): void;
|
|
44
55
|
}
|
|
45
56
|
/**
|
|
@@ -50,9 +61,11 @@ export declare class OwnedDevServer {
|
|
|
50
61
|
* server that is already running.
|
|
51
62
|
*
|
|
52
63
|
* On loopback, the announced host is tried first and the other loopback families follow when it
|
|
53
|
-
*
|
|
64
|
+
* is REFUSED — so an IPv6-only Vite that printed `127.0.0.1` still counts as served (#884). Only
|
|
65
|
+
* refused: a timeout means the right server is slow, and the caller polls again on the same host.
|
|
66
|
+
* See `isAddressMiss` for the Next 16 first run that hopping on a timeout broke.
|
|
54
67
|
*/
|
|
55
|
-
export declare function probePage(url: string): Promise<PageProbe>;
|
|
68
|
+
export declare function probePage(url: string, timeoutMs?: number): Promise<PageProbe>;
|
|
56
69
|
/** Sessions the daemon is holding, in the shape the picker reads. */
|
|
57
70
|
export declare function listSessions(port: number): Promise<CandidateSession[]>;
|
|
58
71
|
/** Whether this project has a saved flow, wherever the project keeps one. */
|
|
@@ -12,60 +12,101 @@
|
|
|
12
12
|
* listening indefinitely.
|
|
13
13
|
*/
|
|
14
14
|
import { spawn, spawnSync } from 'node:child_process';
|
|
15
|
-
import {
|
|
16
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
15
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
17
16
|
import { join } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { isAlive } from '../daemon/daemon.js';
|
|
19
|
+
import { DEV_SERVER_LOG_CAP_BYTES, ROTATED_SUFFIX } from './bringup/dev-server-log-cap.js';
|
|
20
|
+
import { devServerLogName, recordHandOver, sweepDeadDevServers, } from './bringup/dev-server-files.js';
|
|
18
21
|
import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
|
|
22
|
+
import { reticleStateHome } from '../daemon/daemon.js';
|
|
19
23
|
import { descendants, parseLsofPorts, parseNetstatListeners, parseWmicProcesses, } from './terminal/listeners.js';
|
|
20
|
-
import { loopbackProbeUrls } from './probe/loopback-probe-urls.js';
|
|
24
|
+
import { isAddressMiss, loopbackProbeUrls } from './probe/loopback-probe-urls.js';
|
|
21
25
|
const WINDOWS = 'win32' === process.platform;
|
|
26
|
+
/** Astro otherwise daemonizes automatically when it detects an agent terminal. */
|
|
27
|
+
const ASTRO_DEV_BACKGROUND_ENV = 'ASTRO_DEV_BACKGROUND';
|
|
22
28
|
/** A page fetch that is slow is a page fetch that failed, for our purposes. */
|
|
23
29
|
const PROBE_TIMEOUT_MS = 5_000;
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
* A child's piped stdout is a Socket at runtime and a Readable in the types, which has no `unref`.
|
|
28
|
-
* Narrowing through Socket keeps that honest rather than asserting it.
|
|
29
|
-
*/
|
|
30
|
-
function releasePipe(stream) {
|
|
31
|
-
if (stream instanceof Socket)
|
|
32
|
-
stream.unref();
|
|
33
|
-
}
|
|
30
|
+
/** The supervisor that owns the server's output and keeps its log bounded. */
|
|
31
|
+
const LOG_CAP_SCRIPT = fileURLToPath(new URL('./bringup/dev-server-log-cap.js', import.meta.url));
|
|
34
32
|
/** The dev server this process started, and the only one it will ever stop. */
|
|
35
33
|
export class OwnedDevServer {
|
|
34
|
+
logDir;
|
|
36
35
|
child;
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
log;
|
|
37
|
+
cwd;
|
|
38
|
+
startedAt = Date.now();
|
|
39
39
|
handedOver = false;
|
|
40
|
+
constructor(logDir = reticleStateHome()) {
|
|
41
|
+
this.logDir = logDir;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Start it with its output going to a bounded log, through a supervisor that outlives `init`.
|
|
45
|
+
*
|
|
46
|
+
* Not a pipe into THIS process. A pipe's read end belonged to `init`; once it exited nobody held
|
|
47
|
+
* it, and the server's next write (a compile error, an HMR line, SvelteKit's first warning) was an
|
|
48
|
+
* EPIPE that killed it: "it connected, then the app went away". And not a bare file either, which
|
|
49
|
+
* has no ceiling: an upstream echo loop grew one to 2.9 GB in five minutes. The supervisor reads
|
|
50
|
+
* the server's pipes for as long as they are open and rotates the file past a cap — see
|
|
51
|
+
* dev-server-log-cap.ts. It is the process group leader, so `pid()` and `stop()` cover both.
|
|
52
|
+
*/
|
|
40
53
|
start(command, cwd, env) {
|
|
41
|
-
|
|
54
|
+
mkdirSync(this.logDir, { recursive: true });
|
|
55
|
+
// Logs of servers that are gone, from every project, so the state home stops growing a file
|
|
56
|
+
// per app ever started. This one's are replaced below anyway.
|
|
57
|
+
sweepDeadDevServers(this.logDir, { alive: isAlive, now: Date.now(), keep: cwd });
|
|
58
|
+
const log = join(this.logDir, devServerLogName(cwd));
|
|
59
|
+
// Unlinked rather than truncated: a previous server for this project may still hold the old
|
|
60
|
+
// file open, and truncating under it would interleave its lines into this run's output.
|
|
61
|
+
rmSync(log, { force: true });
|
|
62
|
+
rmSync(`${log}${ROTATED_SUFFIX}`, { force: true });
|
|
63
|
+
this.child = spawn(process.execPath, [LOG_CAP_SCRIPT, log, String(DEV_SERVER_LOG_CAP_BYTES), command], {
|
|
42
64
|
cwd,
|
|
43
|
-
shell: true,
|
|
44
65
|
// Its own process group, so stopping it stops what it started rather than only the wrapper.
|
|
45
66
|
detached: !WINDOWS,
|
|
46
|
-
|
|
47
|
-
|
|
67
|
+
// Nothing of ours: the supervisor holds the server's pipes, and init holds none of its.
|
|
68
|
+
stdio: 'ignore',
|
|
69
|
+
// Reticle already provides the detached supervisor. Astro's agent auto-backgrounding
|
|
70
|
+
// would escape its process group and leave the recorded handover PID pointing at a dead
|
|
71
|
+
// launcher, so re-init and cleanup could no longer find or stop the actual server.
|
|
72
|
+
env: { ...process.env, ...env, [ASTRO_DEV_BACKGROUND_ENV]: '1' },
|
|
48
73
|
});
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.
|
|
74
|
+
this.log = log;
|
|
75
|
+
this.cwd = cwd;
|
|
76
|
+
this.startedAt = Date.now();
|
|
77
|
+
}
|
|
78
|
+
/** Where the server's output goes, for the line that hands it over. */
|
|
79
|
+
logPath() {
|
|
80
|
+
return this.log;
|
|
56
81
|
}
|
|
57
82
|
/** The process group leader, for a caller that has to clean up what it handed over. */
|
|
58
83
|
pid() {
|
|
59
84
|
return this.child?.pid;
|
|
60
85
|
}
|
|
61
86
|
output() {
|
|
62
|
-
|
|
87
|
+
if (undefined === this.log)
|
|
88
|
+
return '';
|
|
89
|
+
try {
|
|
90
|
+
return readFileSync(this.log, 'utf8');
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return '';
|
|
94
|
+
}
|
|
63
95
|
}
|
|
64
96
|
exited() {
|
|
65
97
|
return undefined !== this.child && null !== this.child.exitCode;
|
|
66
98
|
}
|
|
99
|
+
/** Since the log last grew, which is since the server last printed anything. */
|
|
67
100
|
quietForMs() {
|
|
68
|
-
|
|
101
|
+
let lastOutputAt = this.startedAt;
|
|
102
|
+
try {
|
|
103
|
+
if (undefined !== this.log)
|
|
104
|
+
lastOutputAt = Math.max(lastOutputAt, statSync(this.log).mtimeMs);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* not written yet */
|
|
108
|
+
}
|
|
109
|
+
return Date.now() - lastOutputAt;
|
|
69
110
|
}
|
|
70
111
|
/** Ports anything in this server's process tree is listening on. */
|
|
71
112
|
listeningPorts() {
|
|
@@ -90,24 +131,20 @@ export class OwnedDevServer {
|
|
|
90
131
|
`lsof -a -p ${pids.join(',')} -iTCP -sTCP:LISTEN -P -n 2>/dev/null | awk 'NR>1{print $9}'`,
|
|
91
132
|
]));
|
|
92
133
|
}
|
|
93
|
-
/** Hand the running server to the user. After this, `stop()` does nothing. */
|
|
94
134
|
/**
|
|
95
|
-
* Leave the server running and stop holding it.
|
|
96
|
-
*
|
|
97
|
-
* `unref` on the child is NOT enough: its stdout and stderr are pipes owned by this process, and
|
|
98
|
-
* an open pipe keeps the event loop alive on its own. So `init` printed "setup complete" and then
|
|
99
|
-
* hung until something killed it, turning a successful run into a non-zero exit — which is
|
|
100
|
-
* exactly what the install gate reported, on a run whose every other assertion passed.
|
|
135
|
+
* Leave the server running and stop holding it. After this, `stop()` does nothing.
|
|
101
136
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
137
|
+
* Only `unref` is needed: the output is the supervisor's, so no pipe of ours holds the event loop
|
|
138
|
+
* (`init` once printed "setup complete" and hung on one). `url` is recorded beside the log, so the
|
|
139
|
+
* next `init` attaches to this server rather than starting a second one beside it.
|
|
104
140
|
*/
|
|
105
|
-
handOver() {
|
|
141
|
+
handOver(url) {
|
|
106
142
|
this.handedOver = true;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
143
|
+
this.child?.unref();
|
|
144
|
+
const pid = this.child?.pid;
|
|
145
|
+
if (undefined !== url && undefined !== pid && undefined !== this.cwd) {
|
|
146
|
+
recordHandOver(this.logDir, { pid, url, appDir: this.cwd });
|
|
147
|
+
}
|
|
111
148
|
}
|
|
112
149
|
stop() {
|
|
113
150
|
const child = this.child;
|
|
@@ -164,14 +201,16 @@ function windowsProcessPairs() {
|
|
|
164
201
|
* server that is already running.
|
|
165
202
|
*
|
|
166
203
|
* On loopback, the announced host is tried first and the other loopback families follow when it
|
|
167
|
-
*
|
|
204
|
+
* is REFUSED — so an IPv6-only Vite that printed `127.0.0.1` still counts as served (#884). Only
|
|
205
|
+
* refused: a timeout means the right server is slow, and the caller polls again on the same host.
|
|
206
|
+
* See `isAddressMiss` for the Next 16 first run that hopping on a timeout broke.
|
|
168
207
|
*/
|
|
169
|
-
export async function probePage(url) {
|
|
208
|
+
export async function probePage(url, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
170
209
|
const candidates = loopbackProbeUrls(url);
|
|
171
210
|
let sawTlsRefused = false;
|
|
172
211
|
for (const candidate of candidates) {
|
|
173
212
|
try {
|
|
174
|
-
const res = await fetch(candidate, { signal: AbortSignal.timeout(
|
|
213
|
+
const res = await fetch(candidate, { signal: AbortSignal.timeout(timeoutMs) });
|
|
175
214
|
const body = await res.text();
|
|
176
215
|
const sdkInPage = /@reticlehq|@reticle-connect|reticle-dev/.test(body);
|
|
177
216
|
// Only stamp reachedUrl when we had to leave the announcement — callers that already have
|
|
@@ -187,6 +226,8 @@ export async function probePage(url) {
|
|
|
187
226
|
if (/certificate|SELF_SIGNED|DEPTH_ZERO|ERR_TLS|unable to verify/i.test(message)) {
|
|
188
227
|
sawTlsRefused = true;
|
|
189
228
|
}
|
|
229
|
+
if (!isAddressMiss(err))
|
|
230
|
+
break;
|
|
190
231
|
}
|
|
191
232
|
}
|
|
192
233
|
return { served: false, sdkInPage: false, ...(sawTlsRefused ? { tlsRefused: true } : {}) };
|
|
@@ -16,3 +16,10 @@
|
|
|
16
16
|
* family-split problem, and inventing alternates would probe the wrong machine.
|
|
17
17
|
*/
|
|
18
18
|
export declare function loopbackProbeUrls(url: string): string[];
|
|
19
|
+
/**
|
|
20
|
+
* Whether a failed fetch was refused at the connection level on this address.
|
|
21
|
+
*
|
|
22
|
+
* `fetch` wraps the socket error as `cause`; with happy-eyeballs that cause is an AggregateError of
|
|
23
|
+
* one error per address tried, and it is a miss only when every one of them was.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isAddressMiss(err: unknown): boolean;
|
|
@@ -50,3 +50,43 @@ export function loopbackProbeUrls(url) {
|
|
|
50
50
|
push(host);
|
|
51
51
|
return out;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Errors that mean "nothing is listening at THIS address", so another loopback family may be.
|
|
55
|
+
*
|
|
56
|
+
* Anything else, a timeout above all, means the address was right and the server is slow. A cold
|
|
57
|
+
* Next 16 first compile takes long enough to time a probe out, and treating that as a family miss
|
|
58
|
+
* walked on to `127.0.0.1`/`[::1]`, which answered first, became the app url, and is an origin Next
|
|
59
|
+
* refuses its dev resources to: no hydration, so the SDK never ran and init blamed the page.
|
|
60
|
+
*/
|
|
61
|
+
const ADDRESS_MISS_CODES = new Set([
|
|
62
|
+
'ECONNREFUSED',
|
|
63
|
+
'EADDRNOTAVAIL',
|
|
64
|
+
'ENOTFOUND',
|
|
65
|
+
'EAI_AGAIN',
|
|
66
|
+
'ENETUNREACH',
|
|
67
|
+
'EHOSTUNREACH',
|
|
68
|
+
'EAFNOSUPPORT',
|
|
69
|
+
]);
|
|
70
|
+
function field(value, key) {
|
|
71
|
+
return 'object' === typeof value && null !== value ? Reflect.get(value, key) : undefined;
|
|
72
|
+
}
|
|
73
|
+
const codeOf = (value) => {
|
|
74
|
+
const code = field(value, 'code');
|
|
75
|
+
return 'string' === typeof code ? code : undefined;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Whether a failed fetch was refused at the connection level on this address.
|
|
79
|
+
*
|
|
80
|
+
* `fetch` wraps the socket error as `cause`; with happy-eyeballs that cause is an AggregateError of
|
|
81
|
+
* one error per address tried, and it is a miss only when every one of them was.
|
|
82
|
+
*/
|
|
83
|
+
export function isAddressMiss(err) {
|
|
84
|
+
const cause = field(err, 'cause');
|
|
85
|
+
const direct = codeOf(cause);
|
|
86
|
+
if (undefined !== direct)
|
|
87
|
+
return ADDRESS_MISS_CODES.has(direct);
|
|
88
|
+
const errors = field(cause, 'errors');
|
|
89
|
+
if (!Array.isArray(errors) || 0 === errors.length)
|
|
90
|
+
return false;
|
|
91
|
+
return errors.every((e) => ADDRESS_MISS_CODES.has(codeOf(e) ?? ''));
|
|
92
|
+
}
|
|
@@ -40,8 +40,13 @@ export declare function readPage(probe: PageProbe): PageFinding;
|
|
|
40
40
|
/**
|
|
41
41
|
* What the page adds to the daemon's account. Deliberately a sentence to APPEND, not a replacement:
|
|
42
42
|
* the daemon's diagnosis names the fix, and this says what the page looked like when we checked.
|
|
43
|
+
*
|
|
44
|
+
* `daemonSpoke`: the daemon already gave its account. Whether the page dialled is ITS fact (it
|
|
45
|
+
* sees every hello, including one it refused), so the SDK_PRESENT sentence then states only the
|
|
46
|
+
* page fact. Saying "never dialled the bridge" under "this daemon REFUSED the last page" contradicted
|
|
47
|
+
* the one piece of positive evidence in the run.
|
|
43
48
|
*/
|
|
44
|
-
export declare function describePage(finding: PageFinding, url: string): string;
|
|
49
|
+
export declare function describePage(finding: PageFinding, url: string, daemonSpoke?: boolean): string;
|
|
45
50
|
/**
|
|
46
51
|
* What the page looks like once it has had its short chance to come up.
|
|
47
52
|
*
|
|
@@ -68,4 +73,10 @@ export declare function describePage(finding: PageFinding, url: string): string;
|
|
|
68
73
|
export declare function findingBeforeOpen(probe: () => Promise<PageProbe>, clock: {
|
|
69
74
|
readonly now: () => number;
|
|
70
75
|
readonly sleep: (ms: number) => Promise<void>;
|
|
71
|
-
}, windowMs: number, pollMs: number
|
|
76
|
+
}, windowMs: number, pollMs: number,
|
|
77
|
+
/**
|
|
78
|
+
* The served HTML carries the SDK marker on this shape. When it does not (the connect is in the
|
|
79
|
+
* JS bundle), SDK_MISSING is the permanent state of a healthy page, and waiting for it to change
|
|
80
|
+
* only delays the window that would load the bundle.
|
|
81
|
+
*/
|
|
82
|
+
htmlCarriesSdk?: boolean): Promise<PageFinding>;
|
|
@@ -27,8 +27,16 @@ export function readPage(probe) {
|
|
|
27
27
|
/**
|
|
28
28
|
* What the page adds to the daemon's account. Deliberately a sentence to APPEND, not a replacement:
|
|
29
29
|
* the daemon's diagnosis names the fix, and this says what the page looked like when we checked.
|
|
30
|
+
*
|
|
31
|
+
* `daemonSpoke`: the daemon already gave its account. Whether the page dialled is ITS fact (it
|
|
32
|
+
* sees every hello, including one it refused), so the SDK_PRESENT sentence then states only the
|
|
33
|
+
* page fact. Saying "never dialled the bridge" under "this daemon REFUSED the last page" contradicted
|
|
34
|
+
* the one piece of positive evidence in the run.
|
|
30
35
|
*/
|
|
31
|
-
export function describePage(finding, url) {
|
|
36
|
+
export function describePage(finding, url, daemonSpoke = false) {
|
|
37
|
+
if (PageFinding.SDK_PRESENT === finding && daemonSpoke) {
|
|
38
|
+
return `The SDK IS in the page at ${url}; the daemon's account above says what became of it.`;
|
|
39
|
+
}
|
|
32
40
|
switch (finding) {
|
|
33
41
|
case PageFinding.NOT_SERVED:
|
|
34
42
|
// Leads with the cause. The break-matrix asserts this phrasing because it is what a reader
|
|
@@ -77,10 +85,17 @@ export function describePage(finding, url) {
|
|
|
77
85
|
* `probePage` 1,500 times before reaching this point and a test of the decision cannot get near
|
|
78
86
|
* it through that. Here it takes a probe and a clock and answers in three lines of setup.
|
|
79
87
|
*/
|
|
80
|
-
export async function findingBeforeOpen(probe, clock, windowMs, pollMs
|
|
88
|
+
export async function findingBeforeOpen(probe, clock, windowMs, pollMs,
|
|
89
|
+
/**
|
|
90
|
+
* The served HTML carries the SDK marker on this shape. When it does not (the connect is in the
|
|
91
|
+
* JS bundle), SDK_MISSING is the permanent state of a healthy page, and waiting for it to change
|
|
92
|
+
* only delays the window that would load the bundle.
|
|
93
|
+
*/
|
|
94
|
+
htmlCarriesSdk = true) {
|
|
81
95
|
const readyBy = clock.now() + windowMs;
|
|
82
96
|
let finding = readPage(await probe());
|
|
83
|
-
while ((
|
|
97
|
+
while (((htmlCarriesSdk && PageFinding.SDK_MISSING === finding) ||
|
|
98
|
+
PageFinding.NOT_SERVED === finding) &&
|
|
84
99
|
clock.now() < readyBy) {
|
|
85
100
|
await clock.sleep(pollMs);
|
|
86
101
|
finding = readPage(await probe());
|
|
@@ -51,6 +51,11 @@ export interface DaemonReason {
|
|
|
51
51
|
/** The whole differential, recorded for the agent reading `--json`. Defaults to the lead. */
|
|
52
52
|
readonly full?: string;
|
|
53
53
|
}
|
|
54
|
+
/** A lease this run opened for its connect proof, and the way to hand it back. */
|
|
55
|
+
export interface OwnedBrowser {
|
|
56
|
+
readonly sessionId: string;
|
|
57
|
+
readonly release: () => Promise<void>;
|
|
58
|
+
}
|
|
54
59
|
export interface SetupEffects {
|
|
55
60
|
/** Start the dev server. Resolves once started; the caller owns stopping it. */
|
|
56
61
|
readonly startDevServer: (command: string, cwd: string) => Promise<void>;
|
|
@@ -79,8 +84,19 @@ export interface SetupEffects {
|
|
|
79
84
|
* short (`vite-react`, `vite-vue`, `next-app-router`, `monorepo-subdir`, run 35242186243 against
|
|
80
85
|
* 35232727716). A non-zero launcher exit means UNKNOWN, not "no browser": it reports whether the
|
|
81
86
|
* COMMAND succeeded, never whether a window opened. Do not shorten a wait on it.
|
|
87
|
+
*
|
|
88
|
+
* It may resolve with the launcher's own failure, and that is used for exactly one thing: to ALSO
|
|
89
|
+
* open a Reticle-owned browser, so the proof does not depend on a window that may never appear.
|
|
90
|
+
* Adding a second source of a session is safe on a false alarm; cutting the wait was not.
|
|
91
|
+
*/
|
|
92
|
+
readonly openBrowser: (url: string) => Promise<string | void>;
|
|
93
|
+
/**
|
|
94
|
+
* Open the url in a browser Reticle owns (a pooled lease), for a run with no system browser to
|
|
95
|
+
* lean on: `--no-open`, CI, a container, an agent. Optional: absent means no lease is attempted.
|
|
82
96
|
*/
|
|
83
|
-
readonly
|
|
97
|
+
readonly openLease?: (url: string) => Promise<OwnedBrowser | {
|
|
98
|
+
readonly failed: string;
|
|
99
|
+
}>;
|
|
84
100
|
readonly listSessions: () => Promise<CandidateSession[]>;
|
|
85
101
|
/**
|
|
86
102
|
* The daemon's own account of why nothing connected, if it can be asked.
|
|
@@ -101,6 +117,8 @@ export interface SetupEffects {
|
|
|
101
117
|
}
|
|
102
118
|
export interface SetupInput {
|
|
103
119
|
readonly appDir: string;
|
|
120
|
+
/** This project's id, so a live tab of it can be told from another app's on the same url. */
|
|
121
|
+
readonly projectId?: string | undefined;
|
|
104
122
|
readonly devCommand?: string | undefined;
|
|
105
123
|
/** The app is already served here, so nothing is started. */
|
|
106
124
|
readonly suppliedUrl?: string | undefined;
|
|
@@ -114,9 +132,18 @@ export interface SetupInput {
|
|
|
114
132
|
* before setup could say what was wrong.
|
|
115
133
|
*/
|
|
116
134
|
readonly connectBudgetMs?: number | undefined;
|
|
135
|
+
/** Explicit --timeout for startup; absent preserves the adaptive build wait. */
|
|
136
|
+
readonly startupBudgetMs?: number | undefined;
|
|
117
137
|
readonly openBrowser: boolean;
|
|
118
138
|
/** Web, Electron or Tauri. Desktop changes three things; see desktop-shape.ts. */
|
|
119
139
|
readonly shape: AppShape;
|
|
140
|
+
/**
|
|
141
|
+
* The served HTML carries the SDK marker on this shape: Vite's index.html injection, or a plain
|
|
142
|
+
* HTML page with the snippet pasted in. Every other framework delivers the connect in the JS
|
|
143
|
+
* bundle, so "the SDK is NOT in the page" is the permanent state of a healthy page there, and
|
|
144
|
+
* saying it sent people to restart a dev server that was fine.
|
|
145
|
+
*/
|
|
146
|
+
readonly htmlCarriesSdk: boolean;
|
|
120
147
|
readonly phaseTimeoutMs: number;
|
|
121
148
|
readonly pollMs: number;
|
|
122
149
|
}
|