@reticlehq/server 3.9.0 → 3.10.1
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/SKILL.md +13 -12
- package/dist/command/cli/adhoc-suite.js +2 -0
- package/dist/command/cli/adhoc-verdict.d.ts +2 -43
- package/dist/command/cli/adhoc-verdict.js +50 -187
- package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
- package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
- package/dist/command/cli/cli-doctor.js +1 -1
- package/dist/command/cli/cli-flow-commands.js +2 -9
- package/dist/command/cli/cli-kill.js +1 -1
- package/dist/command/cli/cli-parse-compare.d.ts +22 -0
- package/dist/command/cli/cli-parse-compare.js +75 -0
- package/dist/command/cli/cli-parse-grade.d.ts +36 -0
- package/dist/command/cli/cli-parse-grade.js +75 -0
- package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
- package/dist/command/cli/cli-parse-grammar.js +18 -20
- package/dist/command/cli/cli-parse-verify.d.ts +10 -0
- package/dist/command/cli/cli-parse-verify.js +63 -14
- package/dist/command/cli/cli-parse.d.ts +17 -15
- package/dist/command/cli/cli-parse.js +44 -27
- package/dist/command/cli/cli-update-commands.d.ts +1 -1
- package/dist/command/cli/cli-update-commands.js +29 -20
- package/dist/command/cli/cli-usage.js +58 -29
- package/dist/command/cli/cli-verify.d.ts +11 -2
- package/dist/command/cli/cli-verify.js +27 -8
- package/dist/command/cli/cloud-cli.js +2 -119
- package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
- package/dist/command/cli/daemon-link/daemon-client.js +205 -0
- package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
- package/dist/command/cli/doctor-repairs.js +1 -1
- package/dist/command/cli/ensure-daemon.d.ts +26 -0
- package/dist/command/cli/ensure-daemon.js +57 -0
- package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
- package/dist/command/cli/finished-run/grade-command.js +180 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
- package/dist/command/cli/ports/port-holder.d.ts +9 -0
- package/dist/command/cli/ports/port-holder.js +27 -3
- package/dist/command/cli/status/status-lines.d.ts +1 -0
- package/dist/command/cli/status/status-lines.js +22 -0
- package/dist/command/cli/status-fields.d.ts +5 -1
- package/dist/command/cli/status-fields.js +7 -3
- package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
- package/dist/command/cli/two-builds/compare-command.js +314 -0
- package/dist/command/cli/verify-against.d.ts +61 -0
- package/dist/command/cli/verify-against.js +187 -0
- package/dist/command/cli/verify-no-session.d.ts +2 -0
- package/dist/command/cli/verify-no-session.js +13 -0
- package/dist/command/cli/verify-results.d.ts +35 -2
- package/dist/command/cli/verify-results.js +76 -10
- package/dist/command/cli.js +46 -41
- package/dist/command/daemon/daemon-resolve.d.ts +8 -1
- package/dist/command/daemon/daemon-resolve.js +27 -1
- package/dist/command/daemon/daemon.d.ts +12 -1
- package/dist/command/daemon/daemon.js +35 -4
- package/dist/command/daemon/lifetime/death-cause.js +21 -2
- package/dist/command/daemon/lifetime/heartbeat.js +7 -2
- package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
- package/dist/command/daemon/lifetime/startup-failure.js +62 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
- package/dist/command/setup/bringup/dev-server-files.js +17 -0
- package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
- package/dist/command/setup/bringup/static-page-server.js +43 -9
- package/dist/command/setup/init/init-runtime.js +43 -2
- package/dist/command/setup/node-effects.d.ts +3 -2
- package/dist/command/setup/node-effects.js +9 -3
- package/dist/command/setup/remaining-steps.d.ts +8 -0
- package/dist/command/setup/remaining-steps.js +10 -0
- package/dist/command/setup/run-setup.d.ts +2 -0
- package/dist/command/setup/run-setup.js +9 -1
- package/dist/command/setup/setup-command.js +6 -3
- package/dist/command/update/updater.js +6 -0
- package/dist/command/version/version-skew.js +1 -1
- package/dist/features/compare/action-plan.d.ts +260 -0
- package/dist/features/compare/action-plan.js +543 -0
- package/dist/features/compare/behaviour-compare.d.ts +24 -0
- package/dist/features/compare/behaviour-compare.js +85 -0
- package/dist/features/compare/behaviour-diff.d.ts +97 -0
- package/dist/features/compare/behaviour-diff.js +202 -0
- package/dist/features/compare/behaviour-report.d.ts +40 -0
- package/dist/features/compare/behaviour-report.js +128 -0
- package/dist/features/compare/build-readings.d.ts +106 -0
- package/dist/features/compare/build-readings.js +250 -0
- package/dist/features/compare/compare-diff.d.ts +62 -0
- package/dist/features/compare/compare-diff.js +177 -0
- package/dist/features/compare/compare-report-html.d.ts +6 -0
- package/dist/features/compare/compare-report-html.js +102 -0
- package/dist/features/compare/compare-report.d.ts +15 -0
- package/dist/features/compare/compare-report.js +73 -0
- package/dist/features/compare/compare-run.d.ts +60 -0
- package/dist/features/compare/compare-run.js +123 -0
- package/dist/features/compare/outside/effect-lines.d.ts +26 -0
- package/dist/features/compare/outside/effect-lines.js +103 -0
- package/dist/features/compare/outside/state-sources.d.ts +76 -0
- package/dist/features/compare/outside/state-sources.js +164 -0
- package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
- package/dist/features/compare/outside/value-dictionary.js +227 -0
- package/dist/features/compare/visible-content.d.ts +20 -0
- package/dist/features/compare/visible-content.js +125 -0
- package/dist/features/crawl/crawl-empty.js +2 -1
- package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
- package/dist/features/crawl/crawl-exhaustive.js +138 -0
- package/dist/features/crawl/crawl-tools.d.ts +9 -2
- package/dist/features/crawl/crawl-tools.js +28 -12
- package/dist/features/crawl/crawl.d.ts +8 -9
- package/dist/features/crawl/crawl.js +61 -65
- package/dist/features/exhaust/app-coverage.d.ts +5 -1
- package/dist/features/exhaust/app-coverage.js +3 -1
- package/dist/features/exhaust/code-coverage.js +8 -4
- package/dist/features/exhaust/edge-cases.d.ts +133 -0
- package/dist/features/exhaust/edge-cases.js +310 -0
- package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
- package/dist/features/exhaust/edges/edge-drive.js +364 -0
- package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
- package/dist/features/exhaust/edges/edge-run.js +169 -0
- package/dist/features/exhaust/explorer.d.ts +87 -14
- package/dist/features/exhaust/explorer.js +159 -25
- package/dist/features/exhaust/failure-branches.d.ts +5 -0
- package/dist/features/exhaust/failure-branches.js +29 -11
- package/dist/features/exhaust/invariants.d.ts +112 -0
- package/dist/features/exhaust/invariants.js +188 -0
- package/dist/features/exhaust/ledger.d.ts +68 -0
- package/dist/features/exhaust/ledger.js +121 -13
- package/dist/features/exhaust/session-fold.d.ts +10 -0
- package/dist/features/exhaust/session-fold.js +46 -3
- package/dist/features/exhaust/session-port.d.ts +3 -1
- package/dist/features/exhaust/session-port.js +97 -28
- package/dist/features/grade/grade-answer.d.ts +99 -0
- package/dist/features/grade/grade-answer.js +112 -0
- package/dist/features/grade/grade-noise.d.ts +27 -0
- package/dist/features/grade/grade-noise.js +55 -0
- package/dist/features/grade/grade-renumber.d.ts +12 -0
- package/dist/features/grade/grade-renumber.js +86 -0
- package/dist/features/grade/grade-state.d.ts +50 -0
- package/dist/features/grade/grade-state.js +93 -0
- package/dist/features/grade/grade-suggest.d.ts +33 -0
- package/dist/features/grade/grade-suggest.js +98 -0
- package/dist/features/visual/visual-tools.js +66 -19
- package/dist/index.js +7 -4
- package/dist/judgement/capsule/causal-summary.js +9 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +16 -2
- package/dist/judgement/runs/runner-port.d.ts +2 -0
- package/dist/judgement/runs/runner-port.js +23 -5
- package/dist/language/flows/affected-tools.d.ts +1 -1
- package/dist/language/flows/affected-tools.js +49 -30
- package/dist/language/flows/change/git-changed.d.ts +2 -1
- package/dist/language/flows/change/git-changed.js +19 -2
- package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
- package/dist/language/flows/fields/flow-secret-field.js +77 -21
- package/dist/language/flows/flow-anchor.d.ts +60 -2
- package/dist/language/flows/flow-anchor.js +125 -20
- package/dist/language/flows/flow-classify.d.ts +20 -0
- package/dist/language/flows/flow-classify.js +90 -12
- package/dist/language/flows/flow-cross-step.d.ts +7 -1
- package/dist/language/flows/flow-cross-step.js +25 -1
- package/dist/language/flows/flow-journey.d.ts +38 -4
- package/dist/language/flows/flow-journey.js +138 -8
- package/dist/language/flows/flow-replay-run.d.ts +0 -16
- package/dist/language/flows/flow-replay-run.js +76 -14
- package/dist/language/flows/flow-replay-types.d.ts +4 -2
- package/dist/language/flows/flow-replay.d.ts +19 -1
- package/dist/language/flows/flow-replay.js +169 -52
- package/dist/language/flows/flow-replayable.js +7 -1
- package/dist/language/flows/flow-start-path.d.ts +34 -5
- package/dist/language/flows/flow-start-path.js +202 -15
- package/dist/language/flows/flow-step-runners.d.ts +14 -13
- package/dist/language/flows/flow-step-runners.js +184 -109
- package/dist/language/flows/flow-store-for-session.d.ts +26 -25
- package/dist/language/flows/flow-store-for-session.js +44 -0
- package/dist/language/flows/flow-tools.js +46 -33
- package/dist/language/flows/flows.js +176 -76
- package/dist/language/flows/recording/tape/recordings.d.ts +19 -3
- package/dist/language/flows/recording/tape/recordings.js +71 -5
- package/dist/language/flows/replay.d.ts +19 -5
- package/dist/language/flows/replay.js +99 -7
- package/dist/language/flows/steps/page-steps.d.ts +68 -0
- package/dist/language/flows/steps/page-steps.js +198 -0
- package/dist/language/flows/suite/sequential-run.d.ts +68 -0
- package/dist/language/flows/suite/sequential-run.js +96 -0
- package/dist/language/flows/verify-change-tools.js +26 -15
- package/dist/memory/cloud/background-sync.d.ts +13 -0
- package/dist/memory/cloud/background-sync.js +24 -3
- package/dist/memory/cloud/sync-daemon.js +26 -27
- package/dist/memory/journal/attach-journal.js +2 -1
- package/dist/memory/journal/drive-flow.d.ts +7 -0
- package/dist/memory/journal/drive-flow.js +44 -13
- package/dist/memory/journal/journal-recorder.d.ts +4 -2
- package/dist/memory/journal/session-journal.d.ts +7 -0
- package/dist/memory/journal/session-journal.js +58 -10
- package/dist/memory/project/root-claims.d.ts +9 -0
- package/dist/memory/project/root-claims.js +9 -4
- package/dist/portal/input/download-capture.d.ts +7 -0
- package/dist/portal/input/download-capture.js +92 -0
- package/dist/portal/input/network-detail.d.ts +6 -0
- package/dist/portal/input/network-detail.js +12 -1
- package/dist/portal/input/network-mock-tools.js +49 -13
- package/dist/portal/input/network-mock.d.ts +8 -0
- package/dist/portal/input/network-mock.js +28 -9
- package/dist/portal/input/page-capture.d.ts +24 -0
- package/dist/portal/input/page-capture.js +33 -0
- package/dist/portal/input/real-input.d.ts +22 -20
- package/dist/portal/input/real-input.js +41 -31
- package/dist/portal/input/viewport-tools.d.ts +8 -0
- package/dist/portal/input/viewport-tools.js +75 -18
- package/dist/portal/pool/browser-pool.d.ts +11 -29
- package/dist/portal/pool/browser-pool.js +35 -17
- package/dist/portal/pool/context-permissions.d.ts +17 -3
- package/dist/portal/pool/context-permissions.js +36 -5
- package/dist/portal/pool/determinism.d.ts +52 -0
- package/dist/portal/pool/determinism.js +259 -0
- package/dist/portal/pool/lease-emulation.d.ts +34 -1
- package/dist/portal/pool/lease-emulation.js +18 -0
- package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
- package/dist/portal/pool/lease-marker-stamp.js +15 -1
- package/dist/portal/pool/lease-page-error.d.ts +44 -17
- package/dist/portal/pool/lease-page-error.js +147 -5
- package/dist/portal/pool/page-error-journal.d.ts +7 -1
- package/dist/portal/pool/page-error-journal.js +27 -0
- package/dist/portal/pool/playwright-launcher.js +51 -8
- package/dist/portal/pool/pool-contract.d.ts +27 -1
- package/dist/portal/pool/pool-lease.d.ts +36 -1
- package/dist/portal/pool/zero-install.d.ts +8 -0
- package/dist/portal/pool/zero-install.js +23 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
- package/dist/portal/session/dev-server/dev-command.js +61 -15
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
- package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
- package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
- package/dist/portal/session/dev-server/project-ports.js +62 -0
- package/dist/portal/session/facts/departed-network.d.ts +66 -0
- package/dist/portal/session/facts/departed-network.js +142 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
- package/dist/portal/session/no-session-diagnosis.js +67 -25
- package/dist/portal/session/no-session-next-action.d.ts +2 -0
- package/dist/portal/session/no-session-next-action.js +12 -3
- package/dist/portal/session/no-session-watch.d.ts +6 -0
- package/dist/portal/session/no-session-watch.js +38 -3
- package/dist/portal/session/serving-directory.d.ts +10 -0
- package/dist/portal/session/serving-directory.js +39 -8
- package/dist/portal/session/session-manager.d.ts +10 -0
- package/dist/portal/session/session-manager.js +174 -63
- package/dist/start-wiring.d.ts +2 -1
- package/dist/start-wiring.js +2 -1
- package/dist/surface/mcp/mcp.js +3 -2
- package/dist/surface/mcp/shared-params.d.ts +2 -0
- package/dist/surface/mcp/shared-params.js +31 -0
- package/dist/surface/tools/act/act-observation.d.ts +40 -2
- package/dist/surface/tools/act/act-observation.js +118 -12
- package/dist/surface/tools/act/act-preflight.js +1 -1
- package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
- package/dist/surface/tools/act/act-sequence-retry.js +61 -5
- package/dist/surface/tools/act/after-match.d.ts +4 -1
- package/dist/surface/tools/act/after-match.js +8 -1
- package/dist/surface/tools/act/already-true.js +1 -1
- package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
- package/dist/surface/tools/act/navigation/reload-result.js +25 -2
- package/dist/surface/tools/act/resolve-target.d.ts +15 -1
- package/dist/surface/tools/act/resolve-target.js +24 -1
- package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
- package/dist/surface/tools/act/sequence-grade.js +37 -26
- package/dist/surface/tools/act/settle-in-flight.js +27 -15
- package/dist/surface/tools/act-command.d.ts +23 -0
- package/dist/surface/tools/act-command.js +50 -0
- package/dist/surface/tools/act-sequence-tool.js +78 -50
- package/dist/surface/tools/act-tools.d.ts +1 -13
- package/dist/surface/tools/act-tools.js +74 -86
- package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
- package/dist/surface/tools/agent/build-staleness.js +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +16 -37
- package/dist/surface/tools/browser-tools.js +12 -6
- package/dist/surface/tools/compare-tools.d.ts +11 -0
- package/dist/surface/tools/compare-tools.js +94 -0
- package/dist/surface/tools/coverage-tools.js +1 -1
- package/dist/surface/tools/dynamic-tools.js +9 -2
- package/dist/surface/tools/error-recovery.d.ts +12 -1
- package/dist/surface/tools/error-recovery.js +91 -21
- package/dist/surface/tools/explore-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +3 -2
- package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
- package/dist/surface/tools/lease-coverage-tools.js +128 -0
- package/dist/surface/tools/lease-hint.d.ts +35 -1
- package/dist/surface/tools/lease-hint.js +42 -0
- package/dist/surface/tools/lease-readiness.d.ts +26 -0
- package/dist/surface/tools/lease-readiness.js +79 -0
- package/dist/surface/tools/lease-seed.d.ts +14 -1
- package/dist/surface/tools/lease-seed.js +35 -3
- package/dist/surface/tools/lease-tools.d.ts +3 -41
- package/dist/surface/tools/lease-tools.js +62 -110
- package/dist/surface/tools/lease-url-identity.d.ts +46 -0
- package/dist/surface/tools/lease-url-identity.js +97 -0
- package/dist/surface/tools/merge-tools.js +99 -2
- package/dist/surface/tools/merged-name-redirect.js +5 -1
- package/dist/surface/tools/observe-tools.js +15 -9
- package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
- package/dist/surface/tools/read/console-pre-attach.js +23 -0
- package/dist/surface/tools/read-tools.js +33 -30
- package/dist/surface/tools/real-input-attempt.d.ts +0 -8
- package/dist/surface/tools/real-input-attempt.js +143 -28
- package/dist/surface/tools/session-ports.d.ts +14 -0
- package/dist/surface/tools/session-ports.js +53 -0
- package/dist/surface/tools/tool-kit.d.ts +6 -0
- package/dist/surface/tools/tool-kit.js +2 -3
- package/dist/surface/tools/tool-surface.js +0 -1
- package/dist/surface/tools/tools.js +28 -5
- package/dist/surface/tools/type-spelling.d.ts +13 -0
- package/dist/surface/tools/type-spelling.js +36 -0
- package/dist/telemetry/agent-client.d.ts +6 -0
- package/dist/telemetry/agent-client.js +10 -0
- package/dist/telemetry/argument-shape.js +1 -0
- package/dist/telemetry/tool-refused.d.ts +6 -3
- package/dist/telemetry/tool-refused.js +8 -3
- package/docs/actions.mdx +3 -3
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/capabilities.mdx +61 -1
- package/docs/cli/affected.mdx +9 -7
- package/docs/cli/cloud.mdx +3 -3
- package/docs/cli/compare.mdx +94 -0
- package/docs/cli/doctor.mdx +1 -1
- package/docs/cli/grade.mdx +97 -0
- package/docs/cli/init.mdx +2 -2
- package/docs/cli/license.mdx +9 -7
- package/docs/cli/restart.mdx +7 -5
- package/docs/cli/rollback.mdx +5 -5
- package/docs/cli/stop.mdx +3 -3
- package/docs/cli/update.mdx +2 -2
- package/docs/cli/verify.mdx +23 -1
- package/docs/cli.mdx +33 -23
- package/docs/deploy-checks.md +1 -1
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +2 -2
- package/docs/environment.mdx +1 -1
- package/docs/flows.md +11 -1
- package/docs/frameworks.mdx +9 -9
- package/docs/getting-started.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/license-activation.mdx +3 -3
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +1 -1
- package/docs/quickstart.mdx +68 -49
- package/docs/skill-setup.md +1 -1
- package/docs/skill.md +14 -13
- package/docs/telemetry.md +2 -2
- package/docs/tools/navigate.mdx +1 -1
- package/docs/tools/verify.mdx +4 -1
- package/docs/troubleshooting.mdx +30 -1
- package/docs/usage.md +17 -14
- package/package.json +11 -14
- package/dist/features/exhaust/exhaust-run.d.ts +0 -33
- package/dist/features/exhaust/exhaust-run.js +0 -82
|
@@ -55,8 +55,9 @@ const URL_THEN_LEASE = 'Once you have that URL, open it with reticle_lease {acti
|
|
|
55
55
|
* diagnosed the problem.
|
|
56
56
|
*/
|
|
57
57
|
function leaseAdvice(base, facts) {
|
|
58
|
-
if (true === facts.desktop)
|
|
58
|
+
if (true === facts.desktop) {
|
|
59
59
|
return DESKTOP_NO_BROWSER;
|
|
60
|
+
}
|
|
60
61
|
const caveat = leaseCaveat(facts.leaseBrowser);
|
|
61
62
|
return caveat === undefined ? base : `${base} ${caveat}`;
|
|
62
63
|
}
|
|
@@ -87,12 +88,14 @@ const TAB_GONE_WHAT = 'The tab was closed, navigated away, or hard-reloaded.';
|
|
|
87
88
|
* page down is no longer the same dead end as "the human closed the tab".
|
|
88
89
|
*/
|
|
89
90
|
function tabGoneWhat(url) {
|
|
90
|
-
if (undefined === url || '' === url)
|
|
91
|
+
if (undefined === url || '' === url) {
|
|
91
92
|
return TAB_GONE_WHAT;
|
|
93
|
+
}
|
|
92
94
|
const torn = pageTornDownWhileOn(url);
|
|
93
95
|
const first = torn.charAt(0);
|
|
94
|
-
if ('' === first)
|
|
96
|
+
if ('' === first) {
|
|
95
97
|
return TAB_GONE_WHAT;
|
|
98
|
+
}
|
|
96
99
|
return `${first.toUpperCase()}${torn.slice(1)}. ${TAB_GONE_WHAT}`;
|
|
97
100
|
}
|
|
98
101
|
/**
|
|
@@ -158,6 +161,12 @@ const DO_NOT_REINSTALL = 'Re-running the install is the wrong move here: it cann
|
|
|
158
161
|
* evidence that was never gathered.
|
|
159
162
|
*/
|
|
160
163
|
const SCANNED_PORTS = [...DEV_SERVER_PORTS].join(', ');
|
|
164
|
+
/**
|
|
165
|
+
* The project's own sources the scan adds to the well-known list (reticle#1367, reticle#1543).
|
|
166
|
+
*
|
|
167
|
+
* Named in the "nothing else" sentences so that "the scan is partial" says what it did check.
|
|
168
|
+
*/
|
|
169
|
+
const PROJECT_PORT_SOURCES = "the port in this project's dev script and `.env` files, and the ports of earlier sessions";
|
|
161
170
|
/**
|
|
162
171
|
* How to say "open the app" to an agent that may have no `reticle` binary.
|
|
163
172
|
*
|
|
@@ -180,8 +189,9 @@ const INIT_CMD = '`npx @reticlehq/server init`';
|
|
|
180
189
|
*/
|
|
181
190
|
function slowListenerClause(facts, after = '') {
|
|
182
191
|
const slow = facts.slowListeners ?? [];
|
|
183
|
-
if (0 === slow.length)
|
|
192
|
+
if (0 === slow.length) {
|
|
184
193
|
return '';
|
|
194
|
+
}
|
|
185
195
|
const ports = slow.join(', ');
|
|
186
196
|
const subject = 1 === slow.length ? `Port ${ports} ACCEPTED` : `Ports ${ports} ACCEPTED`;
|
|
187
197
|
return (`${subject} a connection but did not answer in time, which is what a server-rendered dev ` +
|
|
@@ -216,6 +226,22 @@ function openTheApp(facts) {
|
|
|
216
226
|
function orOpenCommand(facts) {
|
|
217
227
|
return true === facts.desktop ? '' : ` (or run ${OPEN_CMD_BARE})`;
|
|
218
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* The start advice's lead when processes in this folder listen. Empty when none do.
|
|
231
|
+
*
|
|
232
|
+
* Names ports and no URL. It ends where the caller's "start it" begins, so an agent whose listeners
|
|
233
|
+
* are only its own tools still gets the app started.
|
|
234
|
+
*/
|
|
235
|
+
function folderListenerClause(facts) {
|
|
236
|
+
const ports = facts.folderListeners ?? [];
|
|
237
|
+
if (0 === ports.length) {
|
|
238
|
+
return '';
|
|
239
|
+
}
|
|
240
|
+
const named = ports.map((port) => `:${String(port)}`).join(', ');
|
|
241
|
+
return (`No port Reticle scans serves a page, but processes in this folder listen on ${named}. ` +
|
|
242
|
+
'Reticle cannot tell if one is this app, because agent tools listen here too. ' +
|
|
243
|
+
'If one of them IS this app, open its URL. If none is, ');
|
|
244
|
+
}
|
|
219
245
|
/**
|
|
220
246
|
* What a machine-wide port scan can and cannot say.
|
|
221
247
|
*
|
|
@@ -224,7 +250,14 @@ function orOpenCommand(facts) {
|
|
|
224
250
|
* is up and never dialled" is unsound: those ports can belong to anything, and the app's own port
|
|
225
251
|
* may not be in the scanned set at all.
|
|
226
252
|
*/
|
|
227
|
-
function unattributedListeners(
|
|
253
|
+
function unattributedListeners(facts) {
|
|
254
|
+
const { listening } = facts;
|
|
255
|
+
const folder = facts.folderListeners ?? [];
|
|
256
|
+
if (0 < folder.length && 0 === listening.length) {
|
|
257
|
+
const named = folder.map((port) => `:${String(port)}`).join(', ');
|
|
258
|
+
return (`No port the scan covers serves a page, but processes in this folder listen on ${named}. ` +
|
|
259
|
+
'Reticle cannot tell if one is this app, because agent tools listen here too.');
|
|
260
|
+
}
|
|
228
261
|
if (0 === listening.length) {
|
|
229
262
|
return (`No listener found that I can attribute to this project: the scan covers ${SCANNED_PORTS} ` +
|
|
230
263
|
'across the whole machine, so an app on any other port is invisible to it.');
|
|
@@ -242,8 +275,9 @@ function unattributedListeners(listening) {
|
|
|
242
275
|
*/
|
|
243
276
|
function portMismatchClause(facts) {
|
|
244
277
|
const configured = facts.projectPort;
|
|
245
|
-
if (configured === undefined || configured === facts.port)
|
|
278
|
+
if (configured === undefined || configured === facts.port) {
|
|
246
279
|
return '';
|
|
280
|
+
}
|
|
247
281
|
return (` They already disagree: \`.reticle.json\` says port ${String(configured)} and this daemon is` +
|
|
248
282
|
` on ${String(facts.port)}, so the SDK is dialling ${String(configured)} and nothing is there.` +
|
|
249
283
|
` Either restart the daemon on ${String(configured)}, or update \`.reticle.json\` to` +
|
|
@@ -258,8 +292,9 @@ function portMismatchClause(facts) {
|
|
|
258
292
|
*/
|
|
259
293
|
function siblingListenerClause(facts) {
|
|
260
294
|
const occupied = facts.siblingListeners ?? [];
|
|
261
|
-
if (0 === occupied.length)
|
|
295
|
+
if (0 === occupied.length) {
|
|
262
296
|
return '';
|
|
297
|
+
}
|
|
263
298
|
const mismatch = facts.projectPort;
|
|
264
299
|
const extra = mismatch !== undefined && mismatch !== facts.port
|
|
265
300
|
? occupied.filter((port) => port !== mismatch)
|
|
@@ -291,8 +326,9 @@ function rankedCauses(facts) {
|
|
|
291
326
|
*/
|
|
292
327
|
function configsElsewhereClause(facts) {
|
|
293
328
|
const configs = facts.configsElsewhere ?? [];
|
|
294
|
-
if (0 === configs.length)
|
|
329
|
+
if (0 === configs.length) {
|
|
295
330
|
return '';
|
|
331
|
+
}
|
|
296
332
|
const named = configs
|
|
297
333
|
.map((c) => (c.projectId === undefined ? c.directory : `${c.directory} ('${c.projectId}')`))
|
|
298
334
|
.join(', ');
|
|
@@ -303,10 +339,12 @@ function configsElsewhereClause(facts) {
|
|
|
303
339
|
/** Where we looked, when we found nothing. An absence nobody can check is not evidence. */
|
|
304
340
|
function searchedClause(facts) {
|
|
305
341
|
const searched = facts.searchedDirectories ?? [];
|
|
306
|
-
if (0 === searched.length)
|
|
342
|
+
if (0 === searched.length) {
|
|
307
343
|
return '';
|
|
308
|
-
|
|
344
|
+
}
|
|
345
|
+
if ((facts.configsElsewhere ?? []).length > 0) {
|
|
309
346
|
return '';
|
|
347
|
+
}
|
|
310
348
|
return ` I looked in: ${searched.join(', ')}. If your app is not in that list, that is the answer — this daemon is standing somewhere else.`;
|
|
311
349
|
}
|
|
312
350
|
/**
|
|
@@ -318,20 +356,24 @@ function searchedClause(facts) {
|
|
|
318
356
|
*/
|
|
319
357
|
function stallClause(facts) {
|
|
320
358
|
const { daemonUpMs, initialized, everConnected } = facts;
|
|
321
|
-
if (everConnected)
|
|
359
|
+
if (everConnected) {
|
|
322
360
|
return '';
|
|
323
|
-
|
|
361
|
+
}
|
|
362
|
+
if (!initialized) {
|
|
324
363
|
return '';
|
|
325
|
-
|
|
364
|
+
}
|
|
365
|
+
if (daemonUpMs === undefined || daemonUpMs < STALL_AFTER_MS) {
|
|
326
366
|
return '';
|
|
367
|
+
}
|
|
327
368
|
const minutes = Math.round(daemonUpMs / 60_000);
|
|
328
369
|
return (`This daemon has been running for ${String(minutes)} minutes and no app has connected. ` +
|
|
329
370
|
'The most likely cause is that the dev server was not restarted after adding the Reticle ' +
|
|
330
371
|
'plugin, so the running bundle carries no SDK. Restart the dev server first. ');
|
|
331
372
|
}
|
|
332
373
|
function alreadyListeningClause(listening) {
|
|
333
|
-
if (0 === listening.length)
|
|
374
|
+
if (0 === listening.length) {
|
|
334
375
|
return '';
|
|
376
|
+
}
|
|
335
377
|
if (1 === listening.length) {
|
|
336
378
|
const port = listening[0];
|
|
337
379
|
return (` Something is serving a page on :${String(port)}; it may not be this app — just open ` +
|
|
@@ -450,9 +492,9 @@ export function explainNoSession(facts) {
|
|
|
450
492
|
// wiring is unproven, printed by a daemon holding proof that it is.
|
|
451
493
|
if (true === facts.previouslyConnected) {
|
|
452
494
|
const listeners = 0 === listening.length
|
|
453
|
-
? `${unattributedListeners(
|
|
495
|
+
? `${unattributedListeners(facts)} If the dev server is not running, start it first ` +
|
|
454
496
|
'(the command is in `next_action`).'
|
|
455
|
-
: unattributedListeners(
|
|
497
|
+
: unattributedListeners(facts);
|
|
456
498
|
return reason(NoSessionReason.APP_NOT_REOPENED, `${RESTARTED_LEAD} ${openTheApp(facts)} ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`, `${DO_NOT_REINSTALL} ${listeners} ${rankedCauses(facts)}`);
|
|
457
499
|
}
|
|
458
500
|
// A config found elsewhere outranks every "you may have no SDK" branch below: those reason from
|
|
@@ -461,7 +503,7 @@ export function explainNoSession(facts) {
|
|
|
461
503
|
if (!initialized && (facts.configsElsewhere ?? []).length > 0) {
|
|
462
504
|
return reason(NoSessionReason.CONFIG_ELSEWHERE, 'no browser session connected, and this daemon has never seen one. ' +
|
|
463
505
|
`${configsElsewhereClause(facts)} ${openTheApp(facts)} ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`, 'This daemon was started somewhere that is not the project, which is the normal outcome when ' +
|
|
464
|
-
`an editor launches it from your home directory. ${unattributedListeners(
|
|
506
|
+
`an editor launches it from your home directory. ${unattributedListeners(facts)} ` +
|
|
465
507
|
`${rankedCauses(facts)}`);
|
|
466
508
|
}
|
|
467
509
|
if (0 === listening.length) {
|
|
@@ -481,12 +523,12 @@ export function explainNoSession(facts) {
|
|
|
481
523
|
// is expected rather than diagnostic.
|
|
482
524
|
if (!initialized) {
|
|
483
525
|
return reason(NoSessionReason.NO_LISTENER_NO_CONFIG, 'no browser session connected. Two things to weigh, and neither of them is proof. ' +
|
|
484
|
-
`(1) ${slowListenerClause(facts, ' Setting that aside: ')}
|
|
485
|
-
|
|
526
|
+
`(1) ${slowListenerClause(facts, ' Setting that aside: ')}${folderListenerClause(facts) ||
|
|
527
|
+
'Nothing is listening on the ports Reticle scans, so the dev server may not be running: '}START IT YOURSELF, in the background, using the command ` +
|
|
486
528
|
'in `next_action`, and tell the human in one line that it is running. ' +
|
|
487
529
|
`(2) There is no \`.reticle.json\` in ${where}, which is the file ${INIT_CMD} writes, so ` +
|
|
488
530
|
`the app may carry no Reticle SDK. Check the app's OWN directory before re-running it. ` +
|
|
489
|
-
`${ZERO_INSTALL} ${RETRY}`, `The scan covers ${SCANNED_PORTS} and nothing else, so a server on any other port is ` +
|
|
531
|
+
`${ZERO_INSTALL} ${RETRY}`, `The scan covers ${SCANNED_PORTS}, plus ${PROJECT_PORT_SOURCES}, and nothing else, so a server on any other port is ` +
|
|
490
532
|
'invisible to it: if the app IS running, ask for its URL rather than assuming it is down, ' +
|
|
491
533
|
`and open it with ${OPEN_CMD}. The missing config is not proof either — in a monorepo the ` +
|
|
492
534
|
'daemon often runs at the root while the app lives in a subdirectory, and an app wired by ' +
|
|
@@ -501,15 +543,15 @@ export function explainNoSession(facts) {
|
|
|
501
543
|
`Wait for the first compile to finish, then open the app (the command is in \`next_action\`). ${RETRY}`, `${leaseAdvice(URL_THEN_LEASE, facts)}${siblingListenerClause(facts)}`);
|
|
502
544
|
}
|
|
503
545
|
return reason(NoSessionReason.NO_LISTENER, `${stallClause(facts)}no browser session connected, and this daemon has never seen one. ${openTheApp(facts)} ` +
|
|
504
|
-
|
|
505
|
-
|
|
546
|
+
`${folderListenerClause(facts) ||
|
|
547
|
+
'Nothing is listening on the ports Reticle scans either, and the usual reason is a dev server that is not running: '}start it yourself in the background with the command in ` +
|
|
506
548
|
'`next_action`, tell the human in one line that it is running, then open the app in a ' +
|
|
507
549
|
`browser. ${RETRY}`,
|
|
508
550
|
// The caveat is in the detail rather than omitted because the scan is NARROW: an empty result
|
|
509
551
|
// is not proof of absence. Reported twice, once against a server answering 200 on :7699 and
|
|
510
552
|
// once against one on :5000 under a custom hostname. The common defaults are in the scanned
|
|
511
553
|
// set, which narrows the gap and cannot close it — anything passed to `--port` is invisible.
|
|
512
|
-
`The scan covers ${SCANNED_PORTS} and nothing else, so it is not proof: a server on any other ` +
|
|
554
|
+
`The scan covers ${SCANNED_PORTS}, plus ${PROJECT_PORT_SOURCES}, and nothing else, so it is not proof: a server on any other ` +
|
|
513
555
|
'port is invisible to it. If the app IS running, ask the human for its URL rather than ' +
|
|
514
556
|
// Deliberately NOT offering reticle_lease here, and a test pins that: a lease opens a URL, and
|
|
515
557
|
// if nothing is listening there is nothing at any URL to open. Asking for the real one is the
|
|
@@ -526,11 +568,11 @@ export function explainNoSession(facts) {
|
|
|
526
568
|
'there and restart the dev server; if it has one, the app is wired and simply has no page ' +
|
|
527
569
|
`open — ${OPEN_CMD}. ${ZERO_INSTALL} ${RETRY}`, 'That absence is not proof: the same one is expected in a monorepo whose daemon runs at the ' +
|
|
528
570
|
'root while the app lives in a subdirectory, or in an app wired by the Vite or Babel ' +
|
|
529
|
-
`plugin. ${unattributedListeners(
|
|
571
|
+
`plugin. ${unattributedListeners(facts)}${searchedClause(facts)}${siblingListenerClause(facts)}`);
|
|
530
572
|
}
|
|
531
573
|
return reason(NoSessionReason.SDK_NOT_REACHING_DAEMON, `${stallClause(facts)}no browser session connected, and this daemon has never seen one for this project, which is ` +
|
|
532
574
|
`wired for Reticle. ${openTheApp(facts)} If the page IS open and still does not appear, the SDK ` +
|
|
533
|
-
`is not reaching this daemon (on ${String(port)}). ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`, `${unattributedListeners(
|
|
575
|
+
`is not reaching this daemon (on ${String(port)}). ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`, `${unattributedListeners(facts)} ${rankedCauses(facts)}`);
|
|
534
576
|
}
|
|
535
577
|
/**
|
|
536
578
|
* Pair a branch's verdict with its prose, so neither can be produced without the other.
|
|
@@ -30,6 +30,8 @@ interface NextActionFacts {
|
|
|
30
30
|
listening: readonly number[];
|
|
31
31
|
/** Scanned ports that serve a page from a process that cannot be tied to this project. */
|
|
32
32
|
otherListeners?: readonly number[];
|
|
33
|
+
/** Ports that processes in this folder listen on. See `NoSessionFacts.folderListeners`. */
|
|
34
|
+
folderListeners?: readonly number[];
|
|
33
35
|
dev: DevCommand | undefined;
|
|
34
36
|
/**
|
|
35
37
|
* File-existence predicate for the project root, pure for testing.
|
|
@@ -114,7 +114,15 @@ function neverWired(facts) {
|
|
|
114
114
|
* The start advice's first claim when no port is this project's. A scan that heard a page it could
|
|
115
115
|
* not tie to this project says so, because "nothing is listening" is then false.
|
|
116
116
|
*/
|
|
117
|
-
function notThisProject(others) {
|
|
117
|
+
function notThisProject(others, folder = []) {
|
|
118
|
+
// First: the folder is nearer to this app than a machine-wide scan. It is still no URL to hand
|
|
119
|
+
// over, because agent tools listen in this folder too (reticle#1543).
|
|
120
|
+
if (0 < folder.length) {
|
|
121
|
+
const ports = folder.map((port) => `:${String(port)}`).join(', ');
|
|
122
|
+
return (`processes in this folder listen on ${ports}, and Reticle cannot tell if one is this app, ` +
|
|
123
|
+
'because agent tools listen here too — if one of them IS this app, open it instead of ' +
|
|
124
|
+
'starting a second one, ');
|
|
125
|
+
}
|
|
118
126
|
if (others === undefined || 0 === others.length) {
|
|
119
127
|
return 'nothing is listening on the ports Reticle scans, so the app is probably not running — ';
|
|
120
128
|
}
|
|
@@ -279,9 +287,10 @@ export function nextActionFor(facts) {
|
|
|
279
287
|
const stoppedNote = stopped === undefined
|
|
280
288
|
? ''
|
|
281
289
|
: ` The session that was on ${redactUrl(stopped.url)} is gone and nothing listens there now.`;
|
|
282
|
-
const
|
|
290
|
+
const folder = facts.folderListeners ?? [];
|
|
291
|
+
const notRunning = departedPortIsGone && 0 === folder.length
|
|
283
292
|
? "nothing answers on the port this project's app was last on, so the app is probably not running — "
|
|
284
|
-
: notThisProject(facts.otherListeners);
|
|
293
|
+
: notThisProject(facts.otherListeners, folder);
|
|
285
294
|
if (dev === undefined) {
|
|
286
295
|
const childApps = facts.childApps ?? [];
|
|
287
296
|
if (1 < childApps.length) {
|
|
@@ -74,6 +74,12 @@ interface NoSessionWatchOptions {
|
|
|
74
74
|
* to it. Injected for tests; a test that injects `probe` and omits this observes no folder.
|
|
75
75
|
*/
|
|
76
76
|
listenerDirectory?: (port: number) => string | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* Every TCP port on this machine with a listener (reticle#1543).
|
|
79
|
+
*
|
|
80
|
+
* Injected for tests; a test that injects `probe` and omits this observes no listener.
|
|
81
|
+
*/
|
|
82
|
+
listeningPorts?: () => readonly number[];
|
|
77
83
|
/**
|
|
78
84
|
* What the route the last session was on answers right now, as an HTTP status, or undefined
|
|
79
85
|
* when there is no answer to report.
|
|
@@ -13,11 +13,12 @@ import { probeRouteStatus } from './dev-server/route-status-probe.js';
|
|
|
13
13
|
import { probeDevServerStates } from './dev-server/dev-server-probe.js';
|
|
14
14
|
import { existsSync } from 'node:fs';
|
|
15
15
|
import { homedir } from 'node:os';
|
|
16
|
-
import { join, resolve } from 'node:path';
|
|
16
|
+
import { join, parse, resolve } from 'node:path';
|
|
17
17
|
import { callingClientDirectory } from '../../hooks/client-directory.js';
|
|
18
18
|
import { registeredElsewhere } from '../../memory/recall/registered-projects.js';
|
|
19
19
|
import { explainNoSession } from './no-session-diagnosis.js';
|
|
20
20
|
import { childDevApps, detectDevCommandInProject, initCommandIn, } from './dev-server/dev-command.js';
|
|
21
|
+
import { envFilePorts } from './dev-server/project-ports.js';
|
|
21
22
|
import { nextActionFor, portOfUrl, renderNextAction } from './no-session-next-action.js';
|
|
22
23
|
import { DEV_SERVER_PORTS, readProjectFramework, readProjectId, readProjectIsDesktop, readProjectPort, } from '../../command/cli/ports/resolve/cli-port.js';
|
|
23
24
|
import { discoverProjectConfigs } from '../../command/cli/config/config-discovery.js';
|
|
@@ -27,10 +28,11 @@ import { daemonsServingProjectElsewhere, projectDaemonsElsewhere, projectlessNot
|
|
|
27
28
|
import { stallUptime } from './stall-clock.js';
|
|
28
29
|
import { probeDaemon } from '../../surface/mcp/mcp-proxy.js';
|
|
29
30
|
import { findOccupiedSiblings } from '../../command/cli/ports/sibling-ports.js';
|
|
31
|
+
import { captureLookup, findListeningPorts } from '../../command/cli/ports/port-holder.js';
|
|
30
32
|
import { isAuthRefusalReason } from '../bridge/auth-failure-reason.js';
|
|
31
33
|
import { devServersForProject } from '@reticlehq/core';
|
|
32
34
|
import { readDevServers } from '../../command/daemon/dev-servers.js';
|
|
33
|
-
import { servingDirectoryOf } from './serving-directory.js';
|
|
35
|
+
import { listenerFolders, servingDirectoryOf } from './serving-directory.js';
|
|
34
36
|
import { within } from './zero-install-first-call.js';
|
|
35
37
|
/** Slow enough to be free, fast enough that a dev server started 15s ago is already reflected. */
|
|
36
38
|
const REFRESH_MS = 15_000;
|
|
@@ -77,6 +79,16 @@ export function startNoSessionWatch(options) {
|
|
|
77
79
|
(options.probe === undefined
|
|
78
80
|
? (port) => servingDirectoryOf(`${LOCALHOST}:${String(port)}`)
|
|
79
81
|
: () => undefined);
|
|
82
|
+
const listeningPorts = options.listeningPorts ??
|
|
83
|
+
(options.probe === undefined
|
|
84
|
+
? () => findListeningPorts(captureLookup)
|
|
85
|
+
: () => []);
|
|
86
|
+
/**
|
|
87
|
+
* The folder of each listener outside `listening`, `slowListeners` and the Reticle daemons.
|
|
88
|
+
*
|
|
89
|
+
* Beside `listenerDirs` and never in `listening`: see `NoSessionFacts.folderListeners`.
|
|
90
|
+
*/
|
|
91
|
+
let unscannedFolders = new Map();
|
|
80
92
|
let siblingListeners = [];
|
|
81
93
|
/**
|
|
82
94
|
* What the last-known route answered, cached beside the port scan for the same reason the scan
|
|
@@ -163,7 +175,7 @@ export function startNoSessionWatch(options) {
|
|
|
163
175
|
if (projectId === undefined) {
|
|
164
176
|
return projectlessNote(options.port, projectDaemonsElsewhere(options.port, stateDir, isAlive));
|
|
165
177
|
}
|
|
166
|
-
return splitBrainNote(options.port, daemonsServingProjectElsewhere(projectId, options.port, stateDir, isAlive, (port) => hasProjectConnectedBefore(stateDir, port, projectId)));
|
|
178
|
+
return splitBrainNote(options.port, daemonsServingProjectElsewhere(projectId, options.port, stateDir, isAlive, (port) => hasProjectConnectedBefore(stateDir, port, projectId)), { projectId, home: stateDir });
|
|
167
179
|
};
|
|
168
180
|
// Every path that registers a session goes through SessionManager.add, so this is the one hook
|
|
169
181
|
// that makes the bit durable. Recorded per port + projectId so a shared 4400 cannot make one
|
|
@@ -208,6 +220,10 @@ export function startNoSessionWatch(options) {
|
|
|
208
220
|
if (pinned !== undefined) {
|
|
209
221
|
ports.add(pinned);
|
|
210
222
|
}
|
|
223
|
+
// A script that reads its port from `.env` pins nothing in `package.json` (reticle#1543).
|
|
224
|
+
for (const port of envFilePorts(directory)) {
|
|
225
|
+
ports.add(port);
|
|
226
|
+
}
|
|
211
227
|
const known = options.sessions.lastKnown?.();
|
|
212
228
|
const knownPort = known === undefined ? undefined : portOfUrl(known.url);
|
|
213
229
|
if (knownPort !== undefined) {
|
|
@@ -223,6 +239,19 @@ export function startNoSessionWatch(options) {
|
|
|
223
239
|
}
|
|
224
240
|
return [...ports];
|
|
225
241
|
};
|
|
242
|
+
/**
|
|
243
|
+
* The ports that processes in `dir` listen on, without the ones in `shown`.
|
|
244
|
+
*
|
|
245
|
+
* Nothing for the home folder or a filesystem root. A daemon an editor starts there holds every
|
|
246
|
+
* process on the machine inside its "folder", so the answer would prove nothing.
|
|
247
|
+
*/
|
|
248
|
+
const folderListeners = (dir, shown) => {
|
|
249
|
+
const folder = resolve(dir);
|
|
250
|
+
if (folder === homedir() || folder === parse(folder).root) {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
return [...unscannedFolders.entries()].flatMap(([port, cwd]) => within(cwd, folder) && !shown.includes(port) ? [port] : []);
|
|
254
|
+
};
|
|
226
255
|
/**
|
|
227
256
|
* Is `port` demonstrably this project's app? Either its dev server announced it, or this
|
|
228
257
|
* project's own last session was on it. Anything else is a guess, and a guess that opens a
|
|
@@ -300,6 +329,8 @@ export function startNoSessionWatch(options) {
|
|
|
300
329
|
: options.probe === undefined
|
|
301
330
|
? await findOccupiedSiblings(options.port, probeDaemon)
|
|
302
331
|
: [];
|
|
332
|
+
const reported = new Set([options.port, ...siblingListeners, ...ports, ...slowListeners]);
|
|
333
|
+
unscannedFolders = listenerFolders(listeningPorts().filter((port) => !reported.has(port)), unscannedFolders, listenerDirectory);
|
|
303
334
|
// Asked once per departed URL, on the same background cadence as the port scan, and never
|
|
304
335
|
// on the hint's own path. A url already answered is not asked again; a new departure is.
|
|
305
336
|
const known = options.sessions.lastKnown?.();
|
|
@@ -447,6 +478,7 @@ export function startNoSessionWatch(options) {
|
|
|
447
478
|
: {}),
|
|
448
479
|
listening: heard,
|
|
449
480
|
slowListeners: slow,
|
|
481
|
+
folderListeners: folderListeners(dir, heard),
|
|
450
482
|
port: options.port,
|
|
451
483
|
directory: dir,
|
|
452
484
|
previouslyConnected,
|
|
@@ -473,6 +505,7 @@ export function startNoSessionWatch(options) {
|
|
|
473
505
|
listening: heard,
|
|
474
506
|
...(0 === others.length ? {} : { otherListeners: others }),
|
|
475
507
|
slowListeners: slow,
|
|
508
|
+
folderListeners: folderListeners(dir, heard),
|
|
476
509
|
...(known === undefined
|
|
477
510
|
? {}
|
|
478
511
|
: {
|
|
@@ -526,6 +559,7 @@ export function startNoSessionWatch(options) {
|
|
|
526
559
|
...(split === undefined ? {} : { splitBrain: split }),
|
|
527
560
|
listening: observedListening(),
|
|
528
561
|
slowListeners,
|
|
562
|
+
folderListeners: folderListeners(directory, observedListening()),
|
|
529
563
|
// The url the departed tab was on — the same tombstone the prose diagnosis quotes, so the
|
|
530
564
|
// command and the sentence beside it name the same page.
|
|
531
565
|
...(() => {
|
|
@@ -570,6 +604,7 @@ export function startNoSessionWatch(options) {
|
|
|
570
604
|
...scope,
|
|
571
605
|
listening: observedListening(),
|
|
572
606
|
slowListeners,
|
|
607
|
+
folderListeners: folderListeners(directory, observedListening()),
|
|
573
608
|
port: options.port,
|
|
574
609
|
// The directory `initialized` was decided in. Named in the message because "there is no
|
|
575
610
|
// `.reticle.json`" is a claim about ONE directory, and a reader standing somewhere else
|
|
@@ -37,6 +37,16 @@ export interface DevProxy {
|
|
|
37
37
|
* only: a remote page's port says nothing about any process on this machine.
|
|
38
38
|
*/
|
|
39
39
|
export declare function servingDirectoryOf(origin: string, io?: ServingDirectoryIo): string | undefined;
|
|
40
|
+
/** How many ports one scan asks about. Each lookup blocks the daemon for two `lsof` runs. */
|
|
41
|
+
export declare const MAX_FOLDER_LOOKUPS = 16;
|
|
42
|
+
/**
|
|
43
|
+
* The folder of each port in `ports`, or undefined when it cannot be read.
|
|
44
|
+
*
|
|
45
|
+
* `known` is the last scan's answer. A port that stays open keeps it, so a machine with many
|
|
46
|
+
* listeners pays once. A port past the bound waits for the next scan. A closed port leaves the map,
|
|
47
|
+
* so the next process on it is asked about.
|
|
48
|
+
*/
|
|
49
|
+
export declare function listenerFolders(ports: readonly number[], known: ReadonlyMap<number, string | undefined>, directoryOf: (port: number) => string | undefined): Map<number, string | undefined>;
|
|
40
50
|
/**
|
|
41
51
|
* The live answers for a worktree-scoped resolve: the calling agent's checkout (from the directory
|
|
42
52
|
* its MCP proxy runs in) and each tab's checkout (from the dev server that served it).
|
|
@@ -40,13 +40,15 @@ function readOrUndefined(path) {
|
|
|
40
40
|
function readPortless() {
|
|
41
41
|
const dir = process.env[PORTLESS_STATE_DIR_ENV] ?? join(homedir(), PORTLESS_STATE_DIR);
|
|
42
42
|
const port = Number(readOrUndefined(join(dir, PORTLESS_PORT_FILE))?.trim());
|
|
43
|
-
if (!Number.isInteger(port))
|
|
43
|
+
if (!Number.isInteger(port)) {
|
|
44
44
|
return undefined;
|
|
45
|
+
}
|
|
45
46
|
let routes = [];
|
|
46
47
|
try {
|
|
47
48
|
const parsed = PortlessRoutesSchema.safeParse(JSON.parse(readOrUndefined(join(dir, PORTLESS_ROUTES_FILE)) ?? '[]'));
|
|
48
|
-
if (parsed.success)
|
|
49
|
+
if (parsed.success) {
|
|
49
50
|
routes = parsed.data;
|
|
51
|
+
}
|
|
50
52
|
}
|
|
51
53
|
catch {
|
|
52
54
|
// A table being rewritten reads as empty: the page stays unplaced, never placed wrong.
|
|
@@ -75,27 +77,55 @@ export function servingDirectoryOf(origin, io = liveIo) {
|
|
|
75
77
|
catch {
|
|
76
78
|
return undefined;
|
|
77
79
|
}
|
|
78
|
-
if (!isLoopbackHostname(url.hostname) && !isLocalhostSubdomain(url.hostname))
|
|
80
|
+
if (!isLoopbackHostname(url.hostname) && !isLocalhostSubdomain(url.hostname)) {
|
|
79
81
|
return undefined;
|
|
82
|
+
}
|
|
80
83
|
// Exact first: a dev proxy serves every worktree on one port, under the name it announced.
|
|
81
84
|
const byUrl = io.devServers().find((entry) => originOf(entry.url) === url.origin);
|
|
82
|
-
if (byUrl !== undefined)
|
|
85
|
+
if (byUrl !== undefined) {
|
|
83
86
|
return byUrl.root;
|
|
87
|
+
}
|
|
84
88
|
const pagePort = '' === url.port ? DEFAULT_PORT[url.protocol] : Number(url.port);
|
|
85
|
-
if (pagePort === undefined)
|
|
89
|
+
if (pagePort === undefined) {
|
|
86
90
|
return undefined;
|
|
91
|
+
}
|
|
87
92
|
// Behind a dev proxy the page's port is the proxy's, and `lsof` on it names the proxy's folder.
|
|
88
93
|
// The proxy's route table names the dev server's own port.
|
|
89
94
|
const proxy = isLocalhostSubdomain(url.hostname) ? io.devProxy?.() : undefined;
|
|
90
95
|
const route = proxy?.routes.find((r) => r.hostname === url.hostname.toLowerCase());
|
|
91
|
-
if (route === undefined && true === proxy?.ports.includes(pagePort))
|
|
96
|
+
if (route === undefined && true === proxy?.ports.includes(pagePort)) {
|
|
92
97
|
return undefined;
|
|
98
|
+
}
|
|
93
99
|
const port = route?.port ?? pagePort;
|
|
94
100
|
const announced = io.devServers().find((entry) => entry.port === port);
|
|
95
|
-
if (announced !== undefined)
|
|
101
|
+
if (announced !== undefined) {
|
|
96
102
|
return announced.root;
|
|
103
|
+
}
|
|
97
104
|
return io.listenerCwd(port) ?? undefined;
|
|
98
105
|
}
|
|
106
|
+
/** How many ports one scan asks about. Each lookup blocks the daemon for two `lsof` runs. */
|
|
107
|
+
export const MAX_FOLDER_LOOKUPS = 16;
|
|
108
|
+
/**
|
|
109
|
+
* The folder of each port in `ports`, or undefined when it cannot be read.
|
|
110
|
+
*
|
|
111
|
+
* `known` is the last scan's answer. A port that stays open keeps it, so a machine with many
|
|
112
|
+
* listeners pays once. A port past the bound waits for the next scan. A closed port leaves the map,
|
|
113
|
+
* so the next process on it is asked about.
|
|
114
|
+
*/
|
|
115
|
+
export function listenerFolders(ports, known, directoryOf) {
|
|
116
|
+
const folders = new Map();
|
|
117
|
+
let lookups = 0;
|
|
118
|
+
for (const port of ports) {
|
|
119
|
+
if (known.has(port)) {
|
|
120
|
+
folders.set(port, known.get(port));
|
|
121
|
+
}
|
|
122
|
+
else if (lookups < MAX_FOLDER_LOOKUPS) {
|
|
123
|
+
lookups += 1;
|
|
124
|
+
folders.set(port, directoryOf(port));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return folders;
|
|
128
|
+
}
|
|
99
129
|
/** The checkout whose dev server serves `origin` right now, or undefined. */
|
|
100
130
|
function checkoutServing(origin) {
|
|
101
131
|
const serving = servingDirectoryOf(origin);
|
|
@@ -122,8 +152,9 @@ export function liveCheckoutSource(placeOrigin = checkoutServing) {
|
|
|
122
152
|
: undefined;
|
|
123
153
|
},
|
|
124
154
|
ofTab: (tab) => {
|
|
125
|
-
if (placed.has(tab))
|
|
155
|
+
if (placed.has(tab)) {
|
|
126
156
|
return placed.get(tab);
|
|
157
|
+
}
|
|
127
158
|
const origin = originOf(tab.url);
|
|
128
159
|
const checkout = origin === undefined ? undefined : placeOrigin(origin);
|
|
129
160
|
placed.set(tab, checkout);
|
|
@@ -64,6 +64,8 @@ export declare class SessionManager {
|
|
|
64
64
|
isParked(sessionId: string): boolean;
|
|
65
65
|
remove(session: Session): boolean;
|
|
66
66
|
get(sessionId: string): Session | undefined;
|
|
67
|
+
/** The id the same tab connected as after this session's page went away, if one has. */
|
|
68
|
+
successorId(sessionId: string): string | undefined;
|
|
67
69
|
/**
|
|
68
70
|
* Did this id belong to a session that has since disconnected?
|
|
69
71
|
*
|
|
@@ -149,6 +151,14 @@ export declare class SessionManager {
|
|
|
149
151
|
/** Every departed session's URL, oldest first. The port scan has to see all of them, not only the newest. */
|
|
150
152
|
tombstoneUrls(): readonly string[];
|
|
151
153
|
resolve(sessionId?: string, scope?: ResolveScope): Session;
|
|
154
|
+
/**
|
|
155
|
+
* Resolve the session for a call that only some sessions can serve.
|
|
156
|
+
*
|
|
157
|
+
* With no sessionId, resolve() chooses by health and recency alone. A lease that could resize the
|
|
158
|
+
* page lost to a focused tab that could not, and the call was refused (#1959). Here, when exactly
|
|
159
|
+
* one candidate can serve the call, that candidate is chosen.
|
|
160
|
+
*/
|
|
161
|
+
resolveServing(sessionId: string | undefined, canServe: (session: Session) => boolean | Promise<boolean>): Promise<Session>;
|
|
152
162
|
}
|
|
153
163
|
export interface Tab {
|
|
154
164
|
id: string;
|