@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
2
|
+
import { homedir, uptime } from 'node:os';
|
|
3
3
|
import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync, openSync, closeSync, readdirSync, statSync, renameSync, writeSync, } from 'node:fs';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { daemonRegistryFileName, DaemonSpawnMethod, ReticleEnv, } from '@reticlehq/core';
|
|
@@ -155,13 +155,42 @@ export function discoverDaemonPort() {
|
|
|
155
155
|
}
|
|
156
156
|
return found;
|
|
157
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* When the machine last booted, in epoch ms. A pidfile written before it names no daemon: every
|
|
160
|
+
* process from before the boot is gone, and its pid may now belong to anything.
|
|
161
|
+
*/
|
|
162
|
+
function lastBootMs() {
|
|
163
|
+
// The clock is read at this I/O boundary, as the other timestamps in this file are.
|
|
164
|
+
return Date.now() - uptime() * 1000;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Does this pidfile name a daemon that is still running?
|
|
168
|
+
*
|
|
169
|
+
* The pid must be alive AND the file must be newer than the last boot. A pid alone is not enough:
|
|
170
|
+
* the OS reuses pids, and after a reboot a pidfile named an unrelated Bluetooth service on Windows.
|
|
171
|
+
* The port was free, but serve skipped the start because that pid was alive, and the daemon never
|
|
172
|
+
* came up. A file whose age cannot be read is judged by its pid alone, as before.
|
|
173
|
+
*/
|
|
174
|
+
export function pidFileIsLive(path, pid, pidAlive, bootMs = lastBootMs) {
|
|
175
|
+
if (!pidAlive(pid)) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
let writtenMs;
|
|
179
|
+
try {
|
|
180
|
+
writtenMs = statSync(path).mtimeMs;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
return writtenMs >= bootMs();
|
|
186
|
+
}
|
|
158
187
|
/**
|
|
159
188
|
* Sweep ~/.reticle for daemon-<port>.pid files whose process is no longer alive and delete them, so a
|
|
160
189
|
* crashed daemon never leaves a stale pidfile that confuses discovery or makes a port look "taken".
|
|
161
190
|
* Returns the ports reclaimed. `home` and `pidAlive` are injectable for testing (default to the real
|
|
162
191
|
* ~/.reticle and the process.kill(pid,0) liveness probe).
|
|
163
192
|
*/
|
|
164
|
-
export function reclaimStaleDaemons(home = reticleStateHome(), pidAlive = isAlive) {
|
|
193
|
+
export function reclaimStaleDaemons(home = reticleStateHome(), pidAlive = isAlive, bootMs = lastBootMs) {
|
|
165
194
|
const reclaimed = [];
|
|
166
195
|
let files;
|
|
167
196
|
try {
|
|
@@ -184,7 +213,7 @@ export function reclaimStaleDaemons(home = reticleStateHome(), pidAlive = isAliv
|
|
|
184
213
|
catch {
|
|
185
214
|
pid = null; // unreadable pidfile counts as stale
|
|
186
215
|
}
|
|
187
|
-
if (null === pid || !
|
|
216
|
+
if (null === pid || !pidFileIsLive(path, pid, pidAlive, bootMs)) {
|
|
188
217
|
try {
|
|
189
218
|
unlinkSync(path);
|
|
190
219
|
removeDaemonRegistry(Number(match[1])); // drop the sidecar discovery entry too
|
|
@@ -287,8 +316,10 @@ export function spawnDaemon(nodeExec, scriptPath, args, port, deps = defaultSpaw
|
|
|
287
316
|
}
|
|
288
317
|
catch {
|
|
289
318
|
const existing = readPid(port, deps.home);
|
|
290
|
-
if (existing !== null &&
|
|
319
|
+
if (existing !== null &&
|
|
320
|
+
pidFileIsLive(pidFilePath, existing, (pid) => deps.pidAlive(pid), deps.bootTimeMs)) {
|
|
291
321
|
return false;
|
|
322
|
+
}
|
|
292
323
|
// This branch IS the crash detection — its own comment has always said so ("a stale pidfile from
|
|
293
324
|
// a crashed daemon is reclaimed"), it just never said it out loud. Captured before the unlink,
|
|
294
325
|
// because after it there is no trace left that the previous daemon died at all.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* counts. Never a message, because a message can carry the user's paths.
|
|
12
12
|
*/
|
|
13
13
|
import { closeSync, fstatSync, openSync, readSync } from 'node:fs';
|
|
14
|
-
import { DaemonEnd, NODE_ERROR_CODE } from '@reticlehq/core';
|
|
14
|
+
import { DaemonEnd, DaemonStartupError, NODE_ERROR_CODE } from '@reticlehq/core';
|
|
15
15
|
import { DAEMON_HEARTBEAT_EVENT, classifyDaemonLife } from './heartbeat.js';
|
|
16
16
|
import { DAEMON_START_FAILED_EVENT } from './startup-failure.js';
|
|
17
17
|
/**
|
|
@@ -45,6 +45,14 @@ function lastErrorCode(text) {
|
|
|
45
45
|
const code = codes.filter((c) => c !== undefined && !NOT_A_CODE.has(c)).at(-1);
|
|
46
46
|
return code !== undefined && NODE_ERROR_CODE.test(code) ? code : undefined;
|
|
47
47
|
}
|
|
48
|
+
/** The stage of a failed start, only when it is a member of the closed list. */
|
|
49
|
+
function startupStage(v) {
|
|
50
|
+
return Object.values(DaemonStartupError).find((stage) => stage === v);
|
|
51
|
+
}
|
|
52
|
+
/** The code field of a failed start, only when it has the shape of a Node error code. */
|
|
53
|
+
function codeField(v) {
|
|
54
|
+
return 'string' === typeof v && NODE_ERROR_CODE.test(v) ? v : undefined;
|
|
55
|
+
}
|
|
48
56
|
const count = (v) => 'number' === typeof v && Number.isInteger(v) && 0 <= v ? v : undefined;
|
|
49
57
|
/**
|
|
50
58
|
* Read the cause of death of the daemon that started at `startedMs`.
|
|
@@ -97,13 +105,24 @@ export function deathCauseFromLog(text, startedMs, nowMs) {
|
|
|
97
105
|
}
|
|
98
106
|
}
|
|
99
107
|
const failed = ours.findLast((l) => l.event === DAEMON_START_FAILED_EVENT);
|
|
100
|
-
const
|
|
108
|
+
const stage = startupStage(failed?.['stage']);
|
|
109
|
+
if (stage !== undefined) {
|
|
110
|
+
cause.startupError = stage;
|
|
111
|
+
}
|
|
112
|
+
const error = codeField(failed?.['code']) ??
|
|
113
|
+
('string' === typeof failed?.['error'] ? lastErrorCode(failed['error']) : undefined) ??
|
|
101
114
|
// Raw lines before this daemon's first line belong to the daemon before it, unless this one
|
|
102
115
|
// wrote no line at all, which is a crash before its first log line.
|
|
103
116
|
lastErrorCode((0 < ours.length ? rawInOurs : rawAfterLastLine).join('\n'));
|
|
104
117
|
if (error !== undefined) {
|
|
105
118
|
cause.error = error;
|
|
106
119
|
}
|
|
120
|
+
// No line of its own, and a Node error after the last line of the daemon before it: Node stopped
|
|
121
|
+
// this daemon before its code could write anything, for example on a module that did not load.
|
|
122
|
+
if (0 === ours.length && error !== undefined) {
|
|
123
|
+
cause.end = DaemonEnd.START_FAILED;
|
|
124
|
+
cause.startupError = DaemonStartupError.BEFORE_LOG;
|
|
125
|
+
}
|
|
107
126
|
return cause;
|
|
108
127
|
}
|
|
109
128
|
/** How much of the end of a daemon log the next start reads. The last beats and a stack fit. */
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { DaemonEnd } from '@reticlehq/core';
|
|
21
21
|
import { DAEMON_LOG_TRUNCATED_EVENT, guardDaemonLog } from './log-cap.js';
|
|
22
|
+
import { DAEMON_START_FAILED_EVENT } from './startup-failure.js';
|
|
22
23
|
/** The event name a beat carries. One `grep` separates liveness from everything else in the file. */
|
|
23
24
|
export const DAEMON_HEARTBEAT_EVENT = 'reticle_daemon_alive';
|
|
24
25
|
/** Written by installExitTrace when the process exits through Node. */
|
|
@@ -132,8 +133,12 @@ export function classifyDaemonLife(lines, nowMs) {
|
|
|
132
133
|
};
|
|
133
134
|
}
|
|
134
135
|
const last = beats.at(-1);
|
|
135
|
-
if (last === undefined)
|
|
136
|
-
|
|
136
|
+
if (last === undefined) {
|
|
137
|
+
// A daemon that logged a failed start never beat. Without this branch its death was `unknown`,
|
|
138
|
+
// which said "no evidence" about a log that held the cause (#2043).
|
|
139
|
+
const failed = lines.some((l) => l.event === DAEMON_START_FAILED_EVENT);
|
|
140
|
+
return { end: failed ? DaemonEnd.START_FAILED : DaemonEnd.UNKNOWN };
|
|
141
|
+
}
|
|
137
142
|
const everyMs = 'number' === typeof last.everyMs ? last.everyMs : DAEMON_HEARTBEAT_MS;
|
|
138
143
|
const silentForMs = nowMs - last.t;
|
|
139
144
|
return silentForMs > everyMs * MISSED_BEATS_TOLERATED
|
|
@@ -1,5 +1,33 @@
|
|
|
1
|
+
import { DaemonStartupError } from '@reticlehq/core';
|
|
1
2
|
/** The structured log event emitted when the daemon child cannot start. */
|
|
2
3
|
export declare const DAEMON_START_FAILED_EVENT = "reticle_daemon_start_failed";
|
|
4
|
+
type LogFn = (event: string, fields: Record<string, unknown>) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Write a failed start to the daemon log, before the process exits (#2043).
|
|
7
|
+
*
|
|
8
|
+
* The line has three fields. `error` is the message, for the person who reads the log and for
|
|
9
|
+
* `readDaemonStartupCause`. `code` is the Node error code, read from the error and not from the
|
|
10
|
+
* message. `stage` is where the start failed: a failed bind is always `listen`, whatever the caller
|
|
11
|
+
* gave. The next start reads `code` and `stage` into `daemon_died_unclean`. The message stays here.
|
|
12
|
+
*/
|
|
13
|
+
export declare function recordStartupFailure(log: LogFn, error: unknown, stage: DaemonStartupError): void;
|
|
14
|
+
/** The parts of `process` that the startup trap uses. */
|
|
15
|
+
export interface StartupTrapTarget {
|
|
16
|
+
prependListener(event: 'uncaughtException' | 'unhandledRejection', handler: (value: unknown) => void): void;
|
|
17
|
+
off(event: 'uncaughtException' | 'unhandledRejection', handler: (value: unknown) => void): void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Write a throw or a rejection that stops the start to the daemon log.
|
|
21
|
+
*
|
|
22
|
+
* Before the daemon is ready, the process crash guard owns these two events. It writes one sentence
|
|
23
|
+
* to stderr and exits, and that sentence has no event name and no code. So a daemon that died in
|
|
24
|
+
* its first 100 ms left a log that no reader could classify. This trap runs FIRST (it is prepended)
|
|
25
|
+
* and writes one structured line. It does not exit: the crash guard still does that.
|
|
26
|
+
*
|
|
27
|
+
* Returns a disposer. Call it when the daemon is ready, because from then on the daemon's own
|
|
28
|
+
* resilience handlers own these events, and a crash is no longer a failed start.
|
|
29
|
+
*/
|
|
30
|
+
export declare function installStartupTrap(proc: StartupTrapTarget, log: LogFn): () => void;
|
|
3
31
|
/**
|
|
4
32
|
* Return the most recent startup cause written by this launch attempt.
|
|
5
33
|
*
|
|
@@ -9,3 +37,4 @@ export declare const DAEMON_START_FAILED_EVENT = "reticle_daemon_start_failed";
|
|
|
9
37
|
* unavailable logs are best-effort misses; the caller still has its generic fallback diagnosis.
|
|
10
38
|
*/
|
|
11
39
|
export declare function readDaemonStartupCause(path: string, sinceMs: number): string | undefined;
|
|
40
|
+
export {};
|
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { DaemonStartupError, NODE_ERROR_CODE } from '@reticlehq/core';
|
|
2
3
|
/** The structured log event emitted when the daemon child cannot start. */
|
|
3
4
|
export const DAEMON_START_FAILED_EVENT = 'reticle_daemon_start_failed';
|
|
5
|
+
/** The `syscall` that Node puts on the error of a failed bind. */
|
|
6
|
+
const LISTEN_SYSCALL = 'listen';
|
|
7
|
+
/** The Node error code on `error`, or undefined. Only a value that has the shape of a code. */
|
|
8
|
+
function nodeErrorCode(error) {
|
|
9
|
+
if ('object' !== typeof error || null === error) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const code = error.code;
|
|
13
|
+
return 'string' === typeof code && NODE_ERROR_CODE.test(code) ? code : undefined;
|
|
14
|
+
}
|
|
15
|
+
function isListenError(error) {
|
|
16
|
+
if ('object' !== typeof error || null === error) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return LISTEN_SYSCALL === error.syscall;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Write a failed start to the daemon log, before the process exits (#2043).
|
|
23
|
+
*
|
|
24
|
+
* The line has three fields. `error` is the message, for the person who reads the log and for
|
|
25
|
+
* `readDaemonStartupCause`. `code` is the Node error code, read from the error and not from the
|
|
26
|
+
* message. `stage` is where the start failed: a failed bind is always `listen`, whatever the caller
|
|
27
|
+
* gave. The next start reads `code` and `stage` into `daemon_died_unclean`. The message stays here.
|
|
28
|
+
*/
|
|
29
|
+
export function recordStartupFailure(log, error, stage) {
|
|
30
|
+
const code = nodeErrorCode(error);
|
|
31
|
+
log(DAEMON_START_FAILED_EVENT, {
|
|
32
|
+
stage: isListenError(error) ? DaemonStartupError.LISTEN : stage,
|
|
33
|
+
...(code === undefined ? {} : { code }),
|
|
34
|
+
error: error instanceof Error ? error.message : String(error),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Write a throw or a rejection that stops the start to the daemon log.
|
|
39
|
+
*
|
|
40
|
+
* Before the daemon is ready, the process crash guard owns these two events. It writes one sentence
|
|
41
|
+
* to stderr and exits, and that sentence has no event name and no code. So a daemon that died in
|
|
42
|
+
* its first 100 ms left a log that no reader could classify. This trap runs FIRST (it is prepended)
|
|
43
|
+
* and writes one structured line. It does not exit: the crash guard still does that.
|
|
44
|
+
*
|
|
45
|
+
* Returns a disposer. Call it when the daemon is ready, because from then on the daemon's own
|
|
46
|
+
* resilience handlers own these events, and a crash is no longer a failed start.
|
|
47
|
+
*/
|
|
48
|
+
export function installStartupTrap(proc, log) {
|
|
49
|
+
let written = false;
|
|
50
|
+
const trap = (stage) => (value) => {
|
|
51
|
+
if (written) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
written = true;
|
|
55
|
+
recordStartupFailure(log, value, stage);
|
|
56
|
+
};
|
|
57
|
+
const onException = trap(DaemonStartupError.UNCAUGHT_EXCEPTION);
|
|
58
|
+
const onRejection = trap(DaemonStartupError.UNHANDLED_REJECTION);
|
|
59
|
+
proc.prependListener('uncaughtException', onException);
|
|
60
|
+
proc.prependListener('unhandledRejection', onRejection);
|
|
61
|
+
return () => {
|
|
62
|
+
proc.off('uncaughtException', onException);
|
|
63
|
+
proc.off('unhandledRejection', onRejection);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
4
66
|
/**
|
|
5
67
|
* Return the most recent startup cause written by this launch attempt.
|
|
6
68
|
*
|
|
@@ -13,6 +13,7 @@ declare const HandedOverSchema: z.ZodObject<{
|
|
|
13
13
|
pid: z.ZodNumber;
|
|
14
14
|
url: z.ZodString;
|
|
15
15
|
appDir: z.ZodString;
|
|
16
|
+
bridgePort: z.ZodOptional<z.ZodNumber>;
|
|
16
17
|
}, z.core.$strip>;
|
|
17
18
|
export type HandedOver = z.infer<typeof HandedOverSchema>;
|
|
18
19
|
/**
|
|
@@ -32,6 +33,14 @@ export declare function handedOverUrl(dir: string, appDir: string, deps: {
|
|
|
32
33
|
alive: (pid: number) => boolean;
|
|
33
34
|
answers: (url: string) => Promise<boolean>;
|
|
34
35
|
}): Promise<string | undefined>;
|
|
36
|
+
/**
|
|
37
|
+
* True when the live server `init` handed over for `appDir` was started for a daemon port other than
|
|
38
|
+
* `bridgePort`. Attaching to it leaves the page dialling the old port, even when no daemon on that
|
|
39
|
+
* port is left to retire. A record with no port gives false, as before.
|
|
40
|
+
*/
|
|
41
|
+
export declare function handedOverToOtherPort(dir: string, appDir: string, bridgePort: number, deps: {
|
|
42
|
+
alive: (pid: number) => boolean;
|
|
43
|
+
}): boolean;
|
|
35
44
|
/**
|
|
36
45
|
* Stop the server `init` handed over for `appDir`, and forget it. True when one was stopped.
|
|
37
46
|
*
|
|
@@ -34,6 +34,11 @@ const HandedOverSchema = z.object({
|
|
|
34
34
|
pid: z.number().int().positive(),
|
|
35
35
|
url: z.string().min(1),
|
|
36
36
|
appDir: z.string().min(1),
|
|
37
|
+
/**
|
|
38
|
+
* The daemon port the server was started for. Absent in a record from an older build. Reticle's
|
|
39
|
+
* static server adds a snippet that dials this port, so it cannot follow a port move.
|
|
40
|
+
*/
|
|
41
|
+
bridgePort: z.number().int().positive().optional(),
|
|
37
42
|
});
|
|
38
43
|
/**
|
|
39
44
|
* Written when `init` leaves a server running, so the next `init` can attach to it.
|
|
@@ -70,6 +75,18 @@ export async function handedOverUrl(dir, appDir, deps) {
|
|
|
70
75
|
return undefined;
|
|
71
76
|
return (await deps.answers(record.url)) ? record.url : undefined;
|
|
72
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* True when the live server `init` handed over for `appDir` was started for a daemon port other than
|
|
80
|
+
* `bridgePort`. Attaching to it leaves the page dialling the old port, even when no daemon on that
|
|
81
|
+
* port is left to retire. A record with no port gives false, as before.
|
|
82
|
+
*/
|
|
83
|
+
export function handedOverToOtherPort(dir, appDir, bridgePort, deps) {
|
|
84
|
+
const record = readRecord(join(dir, recordName(appDir)));
|
|
85
|
+
if (undefined === record || record.appDir !== appDir || !deps.alive(record.pid)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return undefined !== record.bridgePort && record.bridgePort !== bridgePort;
|
|
89
|
+
}
|
|
73
90
|
/**
|
|
74
91
|
* Stop the server `init` handed over for `appDir`, and forget it. True when one was stopped.
|
|
75
92
|
*
|
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
* starts `npm run dev`, and the rest of setup (wait, open, prove the connect) runs unchanged.
|
|
9
9
|
*
|
|
10
10
|
* It is run as its own process (`node static-page-server.js --dir <app>`), not inside init, because it
|
|
11
|
-
* has to outlive init exactly like any handed-over dev server. It imports
|
|
12
|
-
* built file runs on its own.
|
|
11
|
+
* has to outlive init exactly like any handed-over dev server. It imports only Node and
|
|
12
|
+
* `@reticlehq/init`, so the built file runs on its own.
|
|
13
|
+
*
|
|
14
|
+
* It adds the connect snippet to each HTML page it serves, so init does not edit the file the user
|
|
15
|
+
* hands over (#1544). The file on disk stays byte-identical.
|
|
13
16
|
*
|
|
14
17
|
* Loopback only, on both families: a page served to the LAN would publish the pairing token the
|
|
15
18
|
* snippet carries.
|
|
@@ -21,9 +24,16 @@ export interface StaticPageServer {
|
|
|
21
24
|
readonly url: string;
|
|
22
25
|
close(): Promise<void>;
|
|
23
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The connect literal for the page in `root`: the bridge port, and the project id its `.reticle.json`
|
|
29
|
+
* names. The same literal init wrote into the page before #1544.
|
|
30
|
+
*/
|
|
31
|
+
export declare function staticPageConnect(root: string, bridgePort: number): string;
|
|
24
32
|
export declare function startStaticPageServer(opts: {
|
|
25
33
|
root: string;
|
|
26
34
|
port: number;
|
|
35
|
+
/** The connect literal to add to each HTML page. Absent: pages are served as written. */
|
|
36
|
+
connect?: string | undefined;
|
|
27
37
|
}): Promise<StaticPageServer>;
|
|
28
38
|
/**
|
|
29
39
|
* The command setup runs for a page with no dev server of its own, or undefined when this is not
|
|
@@ -32,4 +42,6 @@ export declare function startStaticPageServer(opts: {
|
|
|
32
42
|
export declare function staticPageDevCommand(opts: {
|
|
33
43
|
appDir: string;
|
|
34
44
|
isStaticPage: boolean;
|
|
45
|
+
/** The daemon port the added snippet dials. Absent: the page is served as written. */
|
|
46
|
+
bridgePort?: number | undefined;
|
|
35
47
|
}): string | undefined;
|
|
@@ -8,22 +8,29 @@
|
|
|
8
8
|
* starts `npm run dev`, and the rest of setup (wait, open, prove the connect) runs unchanged.
|
|
9
9
|
*
|
|
10
10
|
* It is run as its own process (`node static-page-server.js --dir <app>`), not inside init, because it
|
|
11
|
-
* has to outlive init exactly like any handed-over dev server. It imports
|
|
12
|
-
* built file runs on its own.
|
|
11
|
+
* has to outlive init exactly like any handed-over dev server. It imports only Node and
|
|
12
|
+
* `@reticlehq/init`, so the built file runs on its own.
|
|
13
|
+
*
|
|
14
|
+
* It adds the connect snippet to each HTML page it serves, so init does not edit the file the user
|
|
15
|
+
* hands over (#1544). The file on disk stays byte-identical.
|
|
13
16
|
*
|
|
14
17
|
* Loopback only, on both families: a page served to the LAN would publish the pairing token the
|
|
15
18
|
* snippet carries.
|
|
16
19
|
*/
|
|
17
|
-
import { existsSync, readFile, statSync } from 'node:fs';
|
|
20
|
+
import { existsSync, readFile, readFileSync, statSync } from 'node:fs';
|
|
18
21
|
import { createServer } from 'node:http';
|
|
19
22
|
import { extname, join, resolve, sep } from 'node:path';
|
|
20
23
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
24
|
+
import { connectArg, projectIdOf, withStaticSnippet } from '@reticlehq/init';
|
|
21
25
|
export const STATIC_PAGE_INDEX = 'index.html';
|
|
22
26
|
/** Where the search for a free port starts when none is given — clear of Vite's 5173 run. */
|
|
23
27
|
export const STATIC_PAGE_FIRST_PORT = 5500;
|
|
24
28
|
const PORT_SEARCH_SPAN = 50;
|
|
25
29
|
const DIR_FLAG = '--dir';
|
|
26
30
|
const PORT_FLAG = '--port';
|
|
31
|
+
const BRIDGE_PORT_FLAG = '--bridge-port';
|
|
32
|
+
const PROJECT_CONFIG = '.reticle.json';
|
|
33
|
+
const HTML_EXTENSION = '.html';
|
|
27
34
|
const LOOPBACK_V4 = '127.0.0.1';
|
|
28
35
|
const LOOPBACK_V6 = '::1';
|
|
29
36
|
const ANNOUNCED_HOST = 'localhost';
|
|
@@ -54,8 +61,9 @@ function fileFor(root, rawUrl) {
|
|
|
54
61
|
return null;
|
|
55
62
|
}
|
|
56
63
|
const target = resolve(root, `.${path}`);
|
|
57
|
-
if (target !== root && !target.startsWith(`${root}${sep}`))
|
|
64
|
+
if (target !== root && !target.startsWith(`${root}${sep}`)) {
|
|
58
65
|
return null;
|
|
66
|
+
}
|
|
59
67
|
const file = path.endsWith('/') ? join(target, STATIC_PAGE_INDEX) : target;
|
|
60
68
|
try {
|
|
61
69
|
return statSync(file).isFile() ? file : null;
|
|
@@ -64,6 +72,27 @@ function fileFor(root, rawUrl) {
|
|
|
64
72
|
return null;
|
|
65
73
|
}
|
|
66
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* The connect literal for the page in `root`: the bridge port, and the project id its `.reticle.json`
|
|
77
|
+
* names. The same literal init wrote into the page before #1544.
|
|
78
|
+
*/
|
|
79
|
+
export function staticPageConnect(root, bridgePort) {
|
|
80
|
+
let config;
|
|
81
|
+
try {
|
|
82
|
+
config = readFileSync(join(root, PROJECT_CONFIG), 'utf8');
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
config = null;
|
|
86
|
+
}
|
|
87
|
+
return connectArg(bridgePort, projectIdOf(config));
|
|
88
|
+
}
|
|
89
|
+
/** The body to send: an HTML page gets the connect snippet, unless it already connects. */
|
|
90
|
+
function servedBody(file, body, connect) {
|
|
91
|
+
if (undefined === connect || HTML_EXTENSION !== extname(file).toLowerCase()) {
|
|
92
|
+
return body;
|
|
93
|
+
}
|
|
94
|
+
return withStaticSnippet(body.toString('utf8'), connect) ?? body;
|
|
95
|
+
}
|
|
67
96
|
function listen(server, port, host) {
|
|
68
97
|
return new Promise((ok, fail) => {
|
|
69
98
|
server.once('error', fail);
|
|
@@ -93,7 +122,7 @@ export async function startStaticPageServer(opts) {
|
|
|
93
122
|
'content-type': CONTENT_TYPES[extname(file).toLowerCase()] ?? FALLBACK_TYPE,
|
|
94
123
|
'cache-control': 'no-store',
|
|
95
124
|
});
|
|
96
|
-
res.end(body);
|
|
125
|
+
res.end(servedBody(file, body, opts.connect));
|
|
97
126
|
});
|
|
98
127
|
};
|
|
99
128
|
const v4 = createServer(handler);
|
|
@@ -121,8 +150,9 @@ export async function startStaticPageServer(opts) {
|
|
|
121
150
|
url: `http://${ANNOUNCED_HOST}:${String(bound)}/`,
|
|
122
151
|
close: async () => {
|
|
123
152
|
await closeServer(v4);
|
|
124
|
-
if (hasV6)
|
|
153
|
+
if (hasV6) {
|
|
125
154
|
await closeServer(v6);
|
|
155
|
+
}
|
|
126
156
|
},
|
|
127
157
|
};
|
|
128
158
|
}
|
|
@@ -131,10 +161,12 @@ export async function startStaticPageServer(opts) {
|
|
|
131
161
|
* that page. Quoted so an app path with a space survives the shell.
|
|
132
162
|
*/
|
|
133
163
|
export function staticPageDevCommand(opts) {
|
|
134
|
-
if (!opts.isStaticPage || !existsSync(join(opts.appDir, STATIC_PAGE_INDEX)))
|
|
164
|
+
if (!opts.isStaticPage || !existsSync(join(opts.appDir, STATIC_PAGE_INDEX))) {
|
|
135
165
|
return undefined;
|
|
166
|
+
}
|
|
136
167
|
const script = fileURLToPath(import.meta.url).replace(/\.ts$/, '.js');
|
|
137
|
-
|
|
168
|
+
const bridge = undefined === opts.bridgePort ? '' : ` ${BRIDGE_PORT_FLAG} ${String(opts.bridgePort)}`;
|
|
169
|
+
return `"${process.execPath}" "${script}" ${DIR_FLAG} "${opts.appDir}"${bridge}`;
|
|
138
170
|
}
|
|
139
171
|
function flag(argv, name) {
|
|
140
172
|
const at = argv.indexOf(name);
|
|
@@ -144,7 +176,9 @@ function flag(argv, name) {
|
|
|
144
176
|
if (undefined !== process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
145
177
|
const dir = flag(process.argv, DIR_FLAG) ?? process.cwd();
|
|
146
178
|
const port = Number(flag(process.argv, PORT_FLAG) ?? STATIC_PAGE_FIRST_PORT);
|
|
147
|
-
|
|
179
|
+
const bridgePort = flag(process.argv, BRIDGE_PORT_FLAG);
|
|
180
|
+
const connect = undefined === bridgePort ? undefined : staticPageConnect(dir, Number(bridgePort));
|
|
181
|
+
startStaticPageServer({ root: dir, port, connect }).then((server) => process.stdout.write(`serving ${dir}\n Local: ${server.url}\n`), (err) => {
|
|
148
182
|
process.stderr.write(`static page server: ${String(err)}\n`);
|
|
149
183
|
process.exit(1);
|
|
150
184
|
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import { InitConfirmation, OnboardingPhase, OnboardingStop, RETICLE_DEFAULT_PORT, } from '@reticlehq/core';
|
|
10
|
+
import { InitConfirmation, InitStop, OnboardingPhase, OnboardingStop, RETICLE_DEFAULT_PORT, } from '@reticlehq/core';
|
|
11
11
|
import { COMMUNITY_HINT, FEEDBACK_HINT, Framework, InitFailure, } from '@reticlehq/init';
|
|
12
12
|
import { confirmInstall, nodeConfirmDeps } from '../terminal/confirm.js';
|
|
13
13
|
import { writeLicenseKey } from '../license-key.js';
|
|
@@ -85,6 +85,11 @@ function printRelaunch(parsed, io, cwd) {
|
|
|
85
85
|
}).message);
|
|
86
86
|
io.print('');
|
|
87
87
|
}
|
|
88
|
+
/** The dev-server stops caused by the machine, not the app: the files init wrote are right. */
|
|
89
|
+
const WIRED_WHEN_STOPPED_AT = new Set([
|
|
90
|
+
InitStop.PORT_BUSY,
|
|
91
|
+
InitStop.SPLIT_LOOPBACK,
|
|
92
|
+
]);
|
|
88
93
|
export async function continueAfterInit(parsed, result, io, cwd,
|
|
89
94
|
/** init's ending once the connection is proved: coverage and the agent prompt. See `FirstFlowPort`. */
|
|
90
95
|
firstFlow, agentsBlock) {
|
|
@@ -242,14 +247,28 @@ firstFlow, agentsBlock) {
|
|
|
242
247
|
const devCommand = staticPageDevCommand({
|
|
243
248
|
appDir: context.appDir,
|
|
244
249
|
isStaticPage: Framework.HTML === context.framework,
|
|
250
|
+
// The server adds the connect snippet to the page, so init left the file alone (#1544).
|
|
251
|
+
bridgePort: port,
|
|
245
252
|
});
|
|
246
253
|
return undefined === devCommand ? {} : { devCommand, devCommandIsStaticPage: true };
|
|
247
254
|
})(),
|
|
248
255
|
...(undefined === parsed.url ? {} : { suppliedUrl: parsed.url }),
|
|
249
256
|
// So setup can tell which port the dev server will take, before it starts one (#1377).
|
|
250
257
|
framework: context.framework,
|
|
258
|
+
// So a dev-server stop names the Vite step init could not do (#1263).
|
|
259
|
+
...(undefined === context.pendingViteConfig
|
|
260
|
+
? {}
|
|
261
|
+
: { pendingViteConfig: context.pendingViteConfig }),
|
|
251
262
|
}, (line) => io.print(line))
|
|
252
263
|
.then(async (outcome) => {
|
|
264
|
+
// The files are written before the app starts. When the app could not be reached for a reason
|
|
265
|
+
// of the machine, not of the app (its port was taken, or it answered on one loopback address
|
|
266
|
+
// and not the other), the project is wired and not confirmed yet: not a failed install.
|
|
267
|
+
// A dev server that exits, hangs or does not exist, and a failure to connect, still fail.
|
|
268
|
+
const wiredNotStarted = !outcome.ok &&
|
|
269
|
+
outcome.reachedPhase === SetupPhase.DEV_SERVER &&
|
|
270
|
+
undefined !== outcome.stop &&
|
|
271
|
+
WIRED_WHEN_STOPPED_AT.has(outcome.stop);
|
|
253
272
|
if (result.outcome !== undefined) {
|
|
254
273
|
const init = {
|
|
255
274
|
...result.outcome,
|
|
@@ -259,6 +278,14 @@ firstFlow, agentsBlock) {
|
|
|
259
278
|
delete init.reason;
|
|
260
279
|
init.confirmation = InitConfirmation.CONNECTED;
|
|
261
280
|
}
|
|
281
|
+
else if (wiredNotStarted) {
|
|
282
|
+
init.ok = true;
|
|
283
|
+
delete init.reason;
|
|
284
|
+
init.confirmation = InitConfirmation.DEV_SERVER_NOT_STARTED;
|
|
285
|
+
if (outcome.stop !== undefined) {
|
|
286
|
+
init.stop = outcome.stop;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
262
289
|
else {
|
|
263
290
|
// A failed startup does not establish whether a daemon or instrumented page exists.
|
|
264
291
|
// Keep the watcher's no_daemon/no_session/no_page classifications for observed facts.
|
|
@@ -275,7 +302,11 @@ firstFlow, agentsBlock) {
|
|
|
275
302
|
}
|
|
276
303
|
// One object, so an agent reads a result instead of interpreting a report.
|
|
277
304
|
if (true === parsed.json) {
|
|
278
|
-
|
|
305
|
+
const answer = wiredNotStarted ? { ...outcome, ok: true, confirmed: false } : outcome;
|
|
306
|
+
process.stdout.write(`${JSON.stringify(answer, null, 2)}\n`);
|
|
307
|
+
if (wiredNotStarted) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
279
310
|
if (!outcome.ok) {
|
|
280
311
|
stop();
|
|
281
312
|
}
|
|
@@ -328,6 +359,16 @@ firstFlow, agentsBlock) {
|
|
|
328
359
|
io.print(COMMUNITY_HINT);
|
|
329
360
|
return;
|
|
330
361
|
}
|
|
362
|
+
if (wiredNotStarted) {
|
|
363
|
+
io.print('✓ Reticle is wired into this app. Its dev server did not start, so the connection is not ' +
|
|
364
|
+
'confirmed yet. To confirm it:');
|
|
365
|
+
for (const [i, step] of outcome.fallback.entries()) {
|
|
366
|
+
io.print(` ${String(i + 1)}. ${step}`);
|
|
367
|
+
}
|
|
368
|
+
io.print(` ${String(outcome.fallback.length + 1)}. Run \`reticle status\`: it lists the app when it connects.`);
|
|
369
|
+
ask();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
331
372
|
// A run that produced no verdict did not succeed, and the exit code is the one place a caller
|
|
332
373
|
// reads that without parsing anything.
|
|
333
374
|
io.print('⚠ setup did not finish. To carry on from here:');
|
|
@@ -56,9 +56,10 @@ export declare class OwnedDevServer {
|
|
|
56
56
|
*
|
|
57
57
|
* Only `unref` is needed: the output is the supervisor's, so no pipe of ours holds the event loop
|
|
58
58
|
* (`init` once printed "setup complete" and hung on one). `url` is recorded beside the log, so the
|
|
59
|
-
* next `init` attaches to this server rather than starting a second one beside it.
|
|
59
|
+
* next `init` attaches to this server rather than starting a second one beside it. `bridgePort`
|
|
60
|
+
* is recorded too, so the next `init` on another port restarts this server instead.
|
|
60
61
|
*/
|
|
61
|
-
handOver(url?: string): void;
|
|
62
|
+
handOver(url?: string, bridgePort?: number): void;
|
|
62
63
|
stop(): void;
|
|
63
64
|
}
|
|
64
65
|
/** Run a command and return what it printed, or '' if it failed to finish within `timeoutMs`. */
|
|
@@ -175,14 +175,20 @@ export class OwnedDevServer {
|
|
|
175
175
|
*
|
|
176
176
|
* Only `unref` is needed: the output is the supervisor's, so no pipe of ours holds the event loop
|
|
177
177
|
* (`init` once printed "setup complete" and hung on one). `url` is recorded beside the log, so the
|
|
178
|
-
* next `init` attaches to this server rather than starting a second one beside it.
|
|
178
|
+
* next `init` attaches to this server rather than starting a second one beside it. `bridgePort`
|
|
179
|
+
* is recorded too, so the next `init` on another port restarts this server instead.
|
|
179
180
|
*/
|
|
180
|
-
handOver(url) {
|
|
181
|
+
handOver(url, bridgePort) {
|
|
181
182
|
this.handedOver = true;
|
|
182
183
|
this.child?.unref();
|
|
183
184
|
const pid = this.child?.pid;
|
|
184
185
|
if (undefined !== url && undefined !== pid && undefined !== this.cwd) {
|
|
185
|
-
recordHandOver(this.logDir, {
|
|
186
|
+
recordHandOver(this.logDir, {
|
|
187
|
+
pid,
|
|
188
|
+
url,
|
|
189
|
+
appDir: this.cwd,
|
|
190
|
+
...(undefined === bridgePort ? {} : { bridgePort }),
|
|
191
|
+
});
|
|
186
192
|
}
|
|
187
193
|
}
|
|
188
194
|
stop() {
|
|
@@ -8,5 +8,13 @@ export interface Progress {
|
|
|
8
8
|
readonly urlSuppliedByCaller: boolean;
|
|
9
9
|
readonly url?: string | undefined;
|
|
10
10
|
readonly devCommand?: string | undefined;
|
|
11
|
+
/** The Vite config that init could not patch and left as a manual step. */
|
|
12
|
+
readonly pendingViteConfig?: string | undefined;
|
|
11
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Said when the dev server stops after init left the Vite config unpatched (#1263).
|
|
16
|
+
*
|
|
17
|
+
* The dev server loads that config, so the step init already reported is the first cause to check.
|
|
18
|
+
*/
|
|
19
|
+
export declare const pendingViteConfigCause: (config: string) => string;
|
|
12
20
|
export declare function remainingSteps(p: Progress): string[];
|
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
* worked — it should pick up at the step that did not. Ordered, and only ever the REMAINING steps.
|
|
7
7
|
*/
|
|
8
8
|
import { FEEDBACK_KIND_CHOICES } from '@reticlehq/core';
|
|
9
|
+
/**
|
|
10
|
+
* Said when the dev server stops after init left the Vite config unpatched (#1263).
|
|
11
|
+
*
|
|
12
|
+
* The dev server loads that config, so the step init already reported is the first cause to check.
|
|
13
|
+
*/
|
|
14
|
+
export const pendingViteConfigCause = (config) => `init could not patch ${config}, and its ⚠ Vite plugin step is not done. ` +
|
|
15
|
+
'The dev server loads that config, so that step is the likely cause. Do it first.';
|
|
9
16
|
const DOCS_INDEX = 'https://docs.reticle.sh/llms.txt';
|
|
10
17
|
/**
|
|
11
18
|
* The shell form on purpose, not `reticle_session { action: "feedback" }`.
|
|
@@ -21,6 +28,9 @@ export function remainingSteps(p) {
|
|
|
21
28
|
if (!p.initDone) {
|
|
22
29
|
steps.push('Run `npx @reticlehq/server@latest init` here, and fix every ⚠ it reports.');
|
|
23
30
|
}
|
|
31
|
+
if (undefined !== p.pendingViteConfig) {
|
|
32
|
+
steps.push(`Do the ⚠ Vite plugin step that init printed: add \`reticle()\` to the plugins in ${p.pendingViteConfig}, or to the config that the dev server loads.`);
|
|
33
|
+
}
|
|
24
34
|
if (!p.devServerUp && !p.urlSuppliedByCaller) {
|
|
25
35
|
steps.push(`Start the dev server yourself: ${p.devCommand ?? 'the dev script in package.json'}, then open the app in a browser.`);
|
|
26
36
|
}
|
|
@@ -158,6 +158,8 @@ export interface SetupInput {
|
|
|
158
158
|
readonly suppliedUrl?: string | undefined;
|
|
159
159
|
/** The port the dev command will take, when it is known. See probe/dev-port.ts. */
|
|
160
160
|
readonly devPort?: number | undefined;
|
|
161
|
+
/** The Vite config init left for a manual step. A dev-server stop names it (#1263). */
|
|
162
|
+
readonly pendingViteConfig?: string | undefined;
|
|
161
163
|
/**
|
|
162
164
|
* The caller's own budget for the connect wait, when they named one.
|
|
163
165
|
*
|