@reticlehq/server 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-ENTERPRISE +1 -1
- package/README.md +3 -3
- package/SKILL.md +12 -14
- package/dist/chromium-launch-options.d.ts +4 -3
- package/dist/chromium-launch-options.js +6 -2
- package/dist/command/cli/adhoc-suite.d.ts +22 -0
- package/dist/command/cli/adhoc-suite.js +90 -19
- package/dist/command/cli/adhoc-verdict.d.ts +35 -0
- package/dist/command/cli/adhoc-verdict.js +90 -3
- package/dist/command/cli/answers/gate-hook-message.d.ts +6 -0
- package/dist/command/cli/answers/gate-hook-message.js +6 -0
- package/dist/command/cli/auth/cloud-keystore.d.ts +0 -2
- package/dist/command/cli/auth/cloud-keystore.js +0 -3
- package/dist/command/cli/auth/cloud-session.d.ts +6 -2
- package/dist/command/cli/auth/cloud-session.js +2 -0
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-doctor.js +7 -5
- package/dist/command/cli/cli-flow-commands.d.ts +3 -6
- package/dist/command/cli/cli-flow-commands.js +67 -4
- package/dist/command/cli/cli-parse.d.ts +8 -1
- package/dist/command/cli/cli-parse.js +24 -4
- package/dist/command/cli/cli-update-commands.js +8 -1
- package/dist/command/cli/cli-verify.d.ts +24 -3
- package/dist/command/cli/cli-verify.js +54 -11
- package/dist/command/cli/cloud-cli.js +64 -2
- package/dist/command/cli/cloud-kit.js +3 -5
- package/dist/command/cli/cloud-login.d.ts +1 -1
- package/dist/command/cli/cloud-login.js +27 -8
- package/dist/command/cli/demo/demo-page.js +6 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
- package/dist/command/cli/launch/cli-launch.d.ts +8 -0
- package/dist/command/cli/launch/cli-launch.js +21 -9
- package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
- package/dist/command/cli/launch/sdk-version-match.js +206 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
- package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
- package/dist/command/cli/ports/resolve/cli-port.js +125 -15
- package/dist/command/cli/setup-mcp-cli.js +21 -13
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +21 -7
- package/dist/command/cli.js +79 -11
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
- package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
- package/dist/command/daemon/daemon-resolve.d.ts +9 -0
- package/dist/command/daemon/daemon-resolve.js +1 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
- package/dist/command/dev/stale-issue-guard.d.ts +3 -4
- package/dist/command/dev/stale-issue-guard.js +8 -8
- package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
- package/dist/command/setup/bringup/connect-lease.js +38 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
- package/dist/command/setup/bringup/dev-server-files.js +131 -0
- package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
- package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +2 -0
- package/dist/command/setup/bringup/dev-server-wait.js +1 -1
- package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
- package/dist/command/setup/bringup/ensure-daemon.js +135 -16
- package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
- package/dist/command/setup/bringup/static-page-server.js +151 -0
- package/dist/command/setup/desktop-shape.d.ts +4 -0
- package/dist/command/setup/desktop-shape.js +18 -2
- package/dist/command/setup/init/init-port.d.ts +15 -0
- package/dist/command/setup/init/init-port.js +44 -0
- package/dist/command/setup/init/init-runtime.d.ts +1 -1
- package/dist/command/setup/init/init-runtime.js +91 -9
- package/dist/command/setup/node-effects.d.ts +27 -14
- package/dist/command/setup/node-effects.js +86 -45
- package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
- package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
- package/dist/command/setup/probe/page-probe.d.ts +13 -2
- package/dist/command/setup/probe/page-probe.js +18 -3
- package/dist/command/setup/run-setup.d.ts +28 -1
- package/dist/command/setup/run-setup.js +103 -15
- package/dist/command/setup/session-pick.d.ts +13 -0
- package/dist/command/setup/session-pick.js +20 -0
- package/dist/command/setup/setup-command.d.ts +2 -14
- package/dist/command/setup/setup-command.js +105 -31
- package/dist/command/setup/setup-mcp.d.ts +2 -0
- package/dist/command/setup/setup-mcp.js +17 -8
- package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
- package/dist/command/setup/terminal/wait-progress.js +17 -0
- package/dist/command/version/peer-announce.d.ts +6 -2
- package/dist/command/version/peer-announce.js +8 -6
- package/dist/command/version/version-nudge.d.ts +17 -3
- package/dist/command/version/version-nudge.js +25 -4
- package/dist/features/crawl/crawl-empty.js +1 -1
- package/dist/features/crawl/crawl-tools.d.ts +6 -0
- package/dist/features/crawl/crawl-tools.js +57 -0
- package/dist/features/crawl/crawl.js +171 -157
- package/dist/features/exhaust/app-coverage.d.ts +33 -0
- package/dist/features/exhaust/app-coverage.js +31 -0
- package/dist/features/exhaust/code-coverage.d.ts +47 -0
- package/dist/features/exhaust/code-coverage.js +77 -0
- package/dist/features/exhaust/exhaust-run.d.ts +33 -0
- package/dist/features/exhaust/exhaust-run.js +82 -0
- package/dist/features/exhaust/explorer.d.ts +103 -0
- package/dist/features/exhaust/explorer.js +195 -0
- package/dist/features/exhaust/failure-branches.d.ts +33 -0
- package/dist/features/exhaust/failure-branches.js +92 -0
- package/dist/features/exhaust/ledger.d.ts +202 -0
- package/dist/features/exhaust/ledger.js +252 -0
- package/dist/features/exhaust/session-fold.d.ts +31 -0
- package/dist/features/exhaust/session-fold.js +79 -0
- package/dist/features/exhaust/session-port.d.ts +36 -0
- package/dist/features/exhaust/session-port.js +102 -0
- package/dist/features/harness/drive-report.d.ts +0 -9
- package/dist/features/harness/drive-report.js +18 -0
- package/dist/features/harness/driver.js +4 -7
- package/dist/features/harness/fill-values.d.ts +10 -0
- package/dist/features/harness/fill-values.js +50 -11
- package/dist/features/harness/jev-driver.js +30 -13
- package/dist/features/harness/openai-driver.d.ts +12 -0
- package/dist/features/harness/openai-driver.js +21 -12
- package/dist/features/harness/platform-config.js +5 -6
- package/dist/features/license/license.d.ts +2 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +56 -19
- package/dist/judgement/capsule/capsule-store.d.ts +4 -4
- package/dist/judgement/domain/domain-model.js +3 -1
- package/dist/judgement/oracles/self-instrument.js +4 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
- package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
- package/dist/judgement/runs/verification-sync.d.ts +3 -1
- package/dist/judgement/runs/verification-sync.js +1 -1
- package/dist/language/flows/change/affected.d.ts +9 -0
- package/dist/language/flows/change/affected.js +35 -0
- package/dist/language/flows/change/gate.d.ts +20 -0
- package/dist/language/flows/change/gate.js +12 -1
- package/dist/language/flows/flow-author.d.ts +7 -0
- package/dist/language/flows/flow-author.js +19 -0
- package/dist/language/flows/flow-expect-grammar.js +1 -1
- package/dist/language/flows/flow-journey.d.ts +54 -0
- package/dist/language/flows/flow-journey.js +119 -0
- package/dist/language/flows/flow-replay-run.d.ts +2 -3
- package/dist/language/flows/flow-replay-run.js +13 -47
- package/dist/language/flows/flow-replay.d.ts +22 -8
- package/dist/language/flows/flow-replay.js +55 -9
- package/dist/language/flows/flow-result.d.ts +2 -0
- package/dist/language/flows/flow-result.js +25 -0
- package/dist/language/flows/flow-tools.js +17 -5
- package/dist/language/flows/flows.d.ts +10 -1
- package/dist/language/flows/flows.js +90 -14
- package/dist/language/flows/recording/replay-halt.js +3 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
- package/dist/language/flows/recording/tape/recordings.js +55 -1
- package/dist/language/flows/replay.d.ts +19 -0
- package/dist/language/flows/replay.js +42 -6
- package/dist/language/flows/verify-change-tools.js +5 -2
- package/dist/launch-chromium.d.ts +80 -0
- package/dist/launch-chromium.js +218 -0
- package/dist/memory/cloud/cloud-config.d.ts +11 -2
- package/dist/memory/cloud/cloud-config.js +47 -7
- package/dist/memory/cloud/cloud-sync.d.ts +4 -1
- package/dist/memory/cloud/cloud-sync.js +6 -9
- package/dist/memory/cloud/harness-offer.js +5 -6
- package/dist/memory/cloud/harness-switch.js +5 -6
- package/dist/memory/cloud/sync-cycle.d.ts +77 -3
- package/dist/memory/cloud/sync-cycle.js +325 -55
- package/dist/memory/cloud/sync-daemon.js +24 -20
- package/dist/memory/cloud/sync-disk.js +16 -2
- package/dist/memory/intent/intent-shard.d.ts +10 -32
- package/dist/memory/intent/intent-shard.js +0 -10
- package/dist/memory/intent/intent-tools.js +8 -3
- package/dist/memory/journal/drive-flow.d.ts +18 -13
- package/dist/memory/journal/drive-flow.js +45 -37
- package/dist/memory/journal/envelope.d.ts +8 -8
- package/dist/memory/journal/journal-query.d.ts +14 -0
- package/dist/memory/journal/journal-query.js +15 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
- package/dist/memory/journal/session-end.d.ts +9 -1
- package/dist/memory/journal/session-end.js +24 -12
- package/dist/memory/project/artifact-root-resolver.d.ts +10 -0
- package/dist/memory/project/artifact-root-resolver.js +46 -19
- package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
- package/dist/memory/project/dir/reticle-dir.js +1 -0
- package/dist/memory/project/project-tools.js +3 -1
- package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
- package/dist/portal/bridge/auth-failure-reason.js +4 -2
- package/dist/portal/bridge/bridge.d.ts +12 -3
- package/dist/portal/bridge/bridge.js +49 -3
- package/dist/portal/input/js-coverage.d.ts +25 -0
- package/dist/portal/input/js-coverage.js +30 -0
- package/dist/portal/input/real-input.d.ts +11 -0
- package/dist/portal/input/real-input.js +29 -4
- package/dist/portal/input/viewport-tools.js +16 -6
- package/dist/portal/pool/playwright-launcher.d.ts +6 -0
- package/dist/portal/pool/playwright-launcher.js +20 -13
- package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
- package/dist/portal/session/dev-server/dev-command.js +30 -12
- package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
- package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
- package/dist/portal/session/dev-server/route-status-probe.js +57 -0
- package/dist/portal/session/facts/observed-state.d.ts +11 -0
- package/dist/portal/session/facts/observed-state.js +34 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
- package/dist/portal/session/no-session-diagnosis.js +62 -5
- package/dist/portal/session/no-session-next-action.d.ts +13 -0
- package/dist/portal/session/no-session-next-action.js +48 -9
- package/dist/portal/session/no-session-watch.d.ts +18 -0
- package/dist/portal/session/no-session-watch.js +88 -2
- package/dist/portal/session/session-health.js +3 -2
- package/dist/portal/session/session-manager.d.ts +20 -6
- package/dist/portal/session/session-manager.js +19 -3
- package/dist/portal/session/session-reaper.d.ts +28 -0
- package/dist/portal/session/session-reaper.js +49 -1
- package/dist/portal/session/session.d.ts +10 -8
- package/dist/portal/session/session.js +24 -32
- package/dist/surface/http-server.d.ts +3 -2
- package/dist/surface/http-server.js +8 -5
- package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
- package/dist/surface/mcp/mcp-proxy.js +2 -2
- package/dist/surface/mcp/mcp.js +5 -3
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
- package/dist/surface/mcp/proxy/proxy-log.js +3 -5
- package/dist/surface/mcp/server-instructions.js +9 -7
- package/dist/surface/tools/act/after-match.d.ts +17 -0
- package/dist/surface/tools/act/after-match.js +30 -0
- package/dist/surface/tools/act/durable.d.ts +69 -0
- package/dist/surface/tools/act/durable.js +139 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
- package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
- package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
- package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
- package/dist/surface/tools/act/proved-controls.d.ts +9 -0
- package/dist/surface/tools/act/proved-controls.js +11 -0
- package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
- package/dist/surface/tools/act/sequence-grade.js +32 -1
- package/dist/surface/tools/act-capsule.d.ts +7 -0
- package/dist/surface/tools/act-capsule.js +46 -1
- package/dist/surface/tools/act-sequence-tool.js +25 -8
- package/dist/surface/tools/act-tools.js +28 -52
- package/dist/surface/tools/assert/assert-grade.js +12 -1
- package/dist/surface/tools/assert/assert-verdict.js +2 -2
- package/dist/surface/tools/browser-tools.js +14 -2
- package/dist/surface/tools/coverage-tools.js +27 -1
- package/dist/surface/tools/dynamic-tools.js +29 -2
- package/dist/surface/tools/error-recovery.js +5 -6
- package/dist/surface/tools/feedback-invite.js +6 -4
- package/dist/surface/tools/harness-explore.d.ts +3 -2
- package/dist/surface/tools/harness-explore.js +12 -7
- package/dist/surface/tools/invoke-tool.js +12 -8
- package/dist/surface/tools/lease-tools.js +15 -6
- package/dist/surface/tools/live-call-text.js +4 -1
- package/dist/surface/tools/observe-tools.js +9 -1
- package/dist/surface/tools/tool-kit.d.ts +10 -0
- package/dist/surface/tools/tools.js +4 -0
- package/dist/telemetry/argument-shape.d.ts +0 -2
- package/dist/telemetry/argument-shape.js +1 -5
- package/dist/telemetry/error-class.d.ts +2 -3
- package/dist/telemetry/error-class.js +2 -3
- package/dist/telemetry/harness-drive.d.ts +0 -2
- package/dist/telemetry/harness-drive.js +0 -4
- package/dist/telemetry/onboarding-firsts.d.ts +2 -2
- package/dist/telemetry/onboarding-firsts.js +2 -2
- package/dist/wire-journal.d.ts +3 -0
- package/dist/wire-journal.js +4 -0
- package/dist/workspace-packages.d.ts +0 -2
- package/dist/workspace-packages.js +1 -9
- package/docs/README.md +4 -2
- package/docs/actions.mdx +3 -4
- package/docs/agent-cheatsheet.md +4 -4
- package/docs/architecture.md +6 -6
- package/docs/best-practices.mdx +1 -1
- package/docs/capabilities.mdx +32 -1
- package/docs/cli/cloud.mdx +20 -8
- package/docs/cli/doctor.mdx +4 -0
- package/docs/cli/feedback.mdx +2 -2
- package/docs/cli/gate.mdx +9 -4
- package/docs/cli/update.mdx +10 -0
- package/docs/cli.mdx +22 -7
- package/docs/desktop.mdx +14 -10
- package/docs/environment.mdx +87 -0
- package/docs/faq.mdx +18 -16
- package/docs/first-drive.md +5 -13
- package/docs/flows.md +21 -2
- package/docs/frameworks.mdx +29 -10
- package/docs/getting-started.md +15 -5
- package/docs/http-transport.md +6 -3
- package/docs/human-control.md +2 -2
- package/docs/index.mdx +16 -6
- package/docs/install-agentic.mdx +2 -2
- package/docs/install-manual.mdx +1 -1
- package/docs/integration-patterns.md +3 -3
- package/docs/multi-agent-testing.md +1 -1
- package/docs/packages/babel-plugin.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/next.mdx +3 -0
- package/docs/packages/react.mdx +2 -2
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +4 -2
- package/docs/packages.mdx +9 -9
- package/docs/predicates.mdx +50 -5
- package/docs/quickstart.mdx +12 -10
- package/docs/recipes.mdx +2 -2
- package/docs/state-management.mdx +2 -2
- package/docs/system-map.md +1 -1
- package/docs/telemetry.md +1 -1
- package/docs/token-efficiency.md +3 -3
- package/docs/tools/act-and-wait.mdx +1 -0
- package/docs/tools/act-sequence.mdx +1 -1
- package/docs/tools/act.mdx +1 -1
- package/docs/tools/console.mdx +1 -1
- package/docs/tools/inspect.mdx +1 -1
- package/docs/tools/network.mdx +1 -1
- package/docs/tools/query.mdx +1 -1
- package/docs/tools/sessions.mdx +1 -1
- package/docs/tools/snapshot.mdx +1 -1
- package/docs/tools/state.mdx +1 -1
- package/docs/tools/wait-for.mdx +1 -1
- package/docs/usage.md +7 -7
- package/docs/use-cases.mdx +157 -0
- package/docs/vs-screenshots.mdx +3 -3
- package/docs/what-is-recorded.md +2 -2
- package/docs/what-is-reticle.mdx +17 -20
- package/docs/why-reticle.mdx +14 -21
- package/package.json +10 -9
- package/dist/judgement/capsule/minimize.d.ts +0 -12
- package/dist/judgement/capsule/minimize.js +0 -22
- package/dist/judgement/phenomena/phenomena.d.ts +0 -27
- package/dist/judgement/phenomena/phenomena.js +0 -101
- package/dist/language/flows/flow-report.d.ts +0 -22
- package/dist/language/flows/flow-report.js +0 -106
|
@@ -8,51 +8,170 @@
|
|
|
8
8
|
* dial and setup ends with "the SDK is in the page and never dialled the bridge" — the SDK being
|
|
9
9
|
* blamed for the absence of the thing it was dialling.
|
|
10
10
|
*
|
|
11
|
-
* Idempotent by construction:
|
|
12
|
-
*
|
|
11
|
+
* Idempotent by construction: a daemon that speaks our wire contract is adopted. One that does NOT
|
|
12
|
+
* is not usable, however healthy its `/status`: an older Reticle turns away every hello from the page
|
|
13
|
+
* init just instrumented ("bridge refused the connection: invalid message"), and init used to adopt
|
|
14
|
+
* it anyway, wait out its budget, and blame the page. So a skewed daemon nobody is using is replaced
|
|
15
|
+
* (stopped by its LISTENER, never `lsof -ti`, which also kills the agent's MCP proxy), and one with
|
|
16
|
+
* live sessions is left alone with the exact command to run once they are done.
|
|
17
|
+
*
|
|
18
|
+
* A stranger on the port that is not a Reticle daemon at all is refused before this runs; see
|
|
19
|
+
* `bridge-port.ts`.
|
|
13
20
|
*/
|
|
14
21
|
import { log } from '../../../log.js';
|
|
15
|
-
import { spawnDaemon } from '../../daemon/daemon.js';
|
|
22
|
+
import { isAlive, readPid, reticleStateHome, spawnDaemon } from '../../daemon/daemon.js';
|
|
23
|
+
import { readDaemonRegistry } from '../../daemon/daemon-resolve.js';
|
|
24
|
+
import { LOOPBACK_HOST, STATUS_HOLD_MS } from '@reticlehq/core';
|
|
16
25
|
import { probePresence, presenceIsUsable } from '../../daemon/binding/port-presence.js';
|
|
17
|
-
import {
|
|
26
|
+
import { probeDaemon, waitForDaemonBind } from '../../../surface/mcp/mcp-proxy.js';
|
|
18
27
|
import { daemonSpawnArgs } from '../../cli/daemon-start-options.js';
|
|
19
|
-
import { fetchStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
28
|
+
import { fetchStatus, summarizeStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
29
|
+
import { runKill } from '../../cli/cli-kill.js';
|
|
20
30
|
export const EnsureDaemon = {
|
|
21
31
|
/** Something usable was already there. */
|
|
22
32
|
ADOPTED: 'adopted',
|
|
23
33
|
STARTED: 'started',
|
|
34
|
+
/** A daemon on another wire contract, serving nobody, was stopped and ours started in its place. */
|
|
35
|
+
REPLACED: 'replaced',
|
|
36
|
+
/** A daemon on another wire contract is serving live sessions, so it was left alone. */
|
|
37
|
+
SKEWED_IN_USE: 'skewed-in-use',
|
|
24
38
|
/** Could not be started, and the caller has to say so rather than blame the page. */
|
|
25
39
|
UNAVAILABLE: 'unavailable',
|
|
26
40
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
/** A string field off the /status body, or undefined on a daemon too old to report it. */
|
|
42
|
+
function statusString(payload, key) {
|
|
43
|
+
if ('object' !== typeof payload || null === payload)
|
|
44
|
+
return undefined;
|
|
45
|
+
const value = Reflect.get(payload, key);
|
|
46
|
+
return 'string' === typeof value && 0 < value.length ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
async function spawnAndWait(port, deps) {
|
|
30
49
|
if (undefined === deps.scriptPath)
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
50
|
+
return false;
|
|
51
|
+
// A lost spawn race is not a failed start: `init` printed "could not start the Reticle daemon"
|
|
52
|
+
// over a daemon that bound seconds later. Only a spawn that failed with nobody else starting one
|
|
53
|
+
// is a reason to stop without waiting.
|
|
54
|
+
if (!deps.spawn(port) && !deps.starting(port))
|
|
55
|
+
return false;
|
|
34
56
|
try {
|
|
35
57
|
// Readiness means the port ACCEPTS, not that a child was spawned: a readiness signal that
|
|
36
58
|
// precedes readiness is worse than none, because what waits on it stops waiting too early.
|
|
37
59
|
await deps.waitReady(port);
|
|
38
60
|
}
|
|
39
61
|
catch {
|
|
40
|
-
return
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return deps.usable(port);
|
|
65
|
+
}
|
|
66
|
+
export async function ensureDaemon(port, deps) {
|
|
67
|
+
if (await deps.usable(port)) {
|
|
68
|
+
const status = await deps.status(port);
|
|
69
|
+
if (!deps.skewed(status))
|
|
70
|
+
return { state: EnsureDaemon.ADOPTED };
|
|
71
|
+
const theirs = statusString(status, 'version') ?? 'an older build';
|
|
72
|
+
const { sessionCount } = summarizeStatus(status);
|
|
73
|
+
if (0 < sessionCount) {
|
|
74
|
+
return {
|
|
75
|
+
state: EnsureDaemon.SKEWED_IN_USE,
|
|
76
|
+
message: `The Reticle daemon on port ${String(port)} is ${theirs}, which speaks a different wire ` +
|
|
77
|
+
`contract than this init, so it will refuse your app — and ` +
|
|
78
|
+
`${String(sessionCount)} session(s) are still using it, so init will not stop it. When ` +
|
|
79
|
+
`they are done, run \`npx @reticlehq/server kill --port ${String(port)}\` and re-run init.`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (!(await deps.replace(port)))
|
|
83
|
+
return { state: EnsureDaemon.UNAVAILABLE };
|
|
84
|
+
return (await spawnAndWait(port, deps))
|
|
85
|
+
? {
|
|
86
|
+
state: EnsureDaemon.REPLACED,
|
|
87
|
+
message: `replaced the idle Reticle daemon on port ${String(port)} (${theirs}), which speaks a ` +
|
|
88
|
+
`different wire contract and would have refused your app, with a current one.`,
|
|
89
|
+
}
|
|
90
|
+
: { state: EnsureDaemon.UNAVAILABLE };
|
|
91
|
+
}
|
|
92
|
+
return (await spawnAndWait(port, deps))
|
|
93
|
+
? { state: EnsureDaemon.STARTED }
|
|
94
|
+
: { state: EnsureDaemon.UNAVAILABLE };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Stop this project's daemons on any port but `port`, the one init is about to use. Returns the
|
|
98
|
+
* ports stopped.
|
|
99
|
+
*
|
|
100
|
+
* `init --port <new>` started a daemon on the new port and left the old one serving: the build
|
|
101
|
+
* plugin found both, dialled the old one, and init — waiting on the new one — exited 1 with
|
|
102
|
+
* "connected to a DIFFERENT Reticle daemon". A daemon another project's tab is still on is left
|
|
103
|
+
* alone, because stopping it would take that app down to fix this one. Stopped by its listener
|
|
104
|
+
* (`runKill` without force), never `lsof -ti`, which also kills the agent's MCP proxy.
|
|
105
|
+
*/
|
|
106
|
+
export async function retireMovedDaemons(projectId, port, deps) {
|
|
107
|
+
if (undefined === projectId || 0 === projectId.length)
|
|
108
|
+
return [];
|
|
109
|
+
const stopped = [];
|
|
110
|
+
for (const entry of deps.registered()) {
|
|
111
|
+
if (entry.projectId !== projectId || entry.port === port)
|
|
112
|
+
continue;
|
|
113
|
+
const { sessions } = summarizeStatus(await deps.status(entry.port));
|
|
114
|
+
const shared = sessions.some((s) => undefined !== s.projectId && projectId !== s.projectId);
|
|
115
|
+
if (shared)
|
|
116
|
+
continue;
|
|
117
|
+
if (await deps.stop(entry.port))
|
|
118
|
+
stopped.push(entry.port);
|
|
41
119
|
}
|
|
42
|
-
return
|
|
120
|
+
return stopped;
|
|
121
|
+
}
|
|
122
|
+
/** The real registry, `/status` and listener-only stop. */
|
|
123
|
+
export function nodeRetireDeps() {
|
|
124
|
+
return {
|
|
125
|
+
registered: () => readDaemonRegistry(reticleStateHome()).filter((e) => isAlive(e.pid)),
|
|
126
|
+
status: (port) => fetchStatus(port),
|
|
127
|
+
stop: (port) => runKill(port, false),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Hold the daemon on `port` until the returned release is called, by renewing a hold on its
|
|
132
|
+
* `/status` well inside the hold window.
|
|
133
|
+
*
|
|
134
|
+
* `init` starts the daemon and can then wait on the app for longer than the idle grace — a cold
|
|
135
|
+
* desktop build, a slow first compile. None of that counted as activity, and the daemon init had
|
|
136
|
+
* just started idled out (`reticle_daemon_idle_exit`) while init was still waiting on it.
|
|
137
|
+
*/
|
|
138
|
+
export function holdDaemon(port) {
|
|
139
|
+
const renew = () => void fetchStatus(port, LOOPBACK_HOST, true);
|
|
140
|
+
renew();
|
|
141
|
+
const timer = setInterval(renew, STATUS_HOLD_MS / 3);
|
|
142
|
+
timer.unref();
|
|
143
|
+
return () => clearInterval(timer);
|
|
43
144
|
}
|
|
44
|
-
export function nodeEnsureDaemonDeps() {
|
|
145
|
+
export function nodeEnsureDaemonDeps(skewed) {
|
|
45
146
|
return {
|
|
147
|
+
skewed,
|
|
46
148
|
usable: async (port) => presenceIsUsable(await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus })),
|
|
149
|
+
status: (port) => fetchStatus(port),
|
|
150
|
+
// `reticle kill` without `--force`: it signals only the LISTENER, and only one that answers
|
|
151
|
+
// /status, so the agent's MCP proxy on the same port survives.
|
|
152
|
+
replace: (port) => runKill(port, false),
|
|
47
153
|
spawn: (port) => {
|
|
48
154
|
const scriptPath = process.argv[1];
|
|
49
155
|
if (undefined === scriptPath)
|
|
50
156
|
return false;
|
|
51
157
|
const started = spawnDaemon(process.execPath, scriptPath, daemonSpawnArgs({ port, headless: true, http: false }), port);
|
|
52
|
-
log
|
|
158
|
+
// For a log or a pipe. On a terminal it was a JSON line in the middle of `init`'s output, read
|
|
159
|
+
// by a person, about a step the next lines already report in words.
|
|
160
|
+
if (true !== process.stderr.isTTY)
|
|
161
|
+
log('reticle_setup_daemon_started', { port, started });
|
|
53
162
|
return started;
|
|
54
163
|
},
|
|
55
|
-
|
|
164
|
+
starting: (port) => {
|
|
165
|
+
const pid = readPid(port);
|
|
166
|
+
return null !== pid && isAlive(pid);
|
|
167
|
+
},
|
|
168
|
+
// The same wait `reticle restart` does: poll until `/status` answers as a daemon, bounded. A
|
|
169
|
+
// cold daemon on a fresh HOME outlived the proxy's shorter reconnect budget this used to borrow.
|
|
170
|
+
waitReady: async (port) => {
|
|
171
|
+
if (!(await waitForDaemonBind(port)))
|
|
172
|
+
throw new Error(`no daemon bound :${String(port)}`);
|
|
173
|
+
return undefined;
|
|
174
|
+
},
|
|
56
175
|
scriptPath: process.argv[1],
|
|
57
176
|
};
|
|
58
177
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dev server `init` starts for a plain HTML page that has none of its own.
|
|
3
|
+
*
|
|
4
|
+
* A directory holding only `index.html` (and a script or two) used to be wired correctly — init
|
|
5
|
+
* writes the connect snippet into the page — and then stop at "No dev command … start the app
|
|
6
|
+
* yourself and pass --url". The one thing left between that user and a connected session was a web
|
|
7
|
+
* server, which is the one thing a page like that never had. So init starts this one, the same way it
|
|
8
|
+
* starts `npm run dev`, and the rest of setup (wait, open, prove the connect) runs unchanged.
|
|
9
|
+
*
|
|
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 nothing but Node, so the
|
|
12
|
+
* built file runs on its own.
|
|
13
|
+
*
|
|
14
|
+
* Loopback only, on both families: a page served to the LAN would publish the pairing token the
|
|
15
|
+
* snippet carries.
|
|
16
|
+
*/
|
|
17
|
+
export declare const STATIC_PAGE_INDEX = "index.html";
|
|
18
|
+
/** Where the search for a free port starts when none is given — clear of Vite's 5173 run. */
|
|
19
|
+
export declare const STATIC_PAGE_FIRST_PORT = 5500;
|
|
20
|
+
export interface StaticPageServer {
|
|
21
|
+
readonly url: string;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export declare function startStaticPageServer(opts: {
|
|
25
|
+
root: string;
|
|
26
|
+
port: number;
|
|
27
|
+
}): Promise<StaticPageServer>;
|
|
28
|
+
/**
|
|
29
|
+
* The command setup runs for a page with no dev server of its own, or undefined when this is not
|
|
30
|
+
* that page. Quoted so an app path with a space survives the shell.
|
|
31
|
+
*/
|
|
32
|
+
export declare function staticPageDevCommand(opts: {
|
|
33
|
+
appDir: string;
|
|
34
|
+
isStaticPage: boolean;
|
|
35
|
+
}): string | undefined;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dev server `init` starts for a plain HTML page that has none of its own.
|
|
3
|
+
*
|
|
4
|
+
* A directory holding only `index.html` (and a script or two) used to be wired correctly — init
|
|
5
|
+
* writes the connect snippet into the page — and then stop at "No dev command … start the app
|
|
6
|
+
* yourself and pass --url". The one thing left between that user and a connected session was a web
|
|
7
|
+
* server, which is the one thing a page like that never had. So init starts this one, the same way it
|
|
8
|
+
* starts `npm run dev`, and the rest of setup (wait, open, prove the connect) runs unchanged.
|
|
9
|
+
*
|
|
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 nothing but Node, so the
|
|
12
|
+
* built file runs on its own.
|
|
13
|
+
*
|
|
14
|
+
* Loopback only, on both families: a page served to the LAN would publish the pairing token the
|
|
15
|
+
* snippet carries.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFile, statSync } from 'node:fs';
|
|
18
|
+
import { createServer } from 'node:http';
|
|
19
|
+
import { extname, join, resolve, sep } from 'node:path';
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
21
|
+
export const STATIC_PAGE_INDEX = 'index.html';
|
|
22
|
+
/** Where the search for a free port starts when none is given — clear of Vite's 5173 run. */
|
|
23
|
+
export const STATIC_PAGE_FIRST_PORT = 5500;
|
|
24
|
+
const PORT_SEARCH_SPAN = 50;
|
|
25
|
+
const DIR_FLAG = '--dir';
|
|
26
|
+
const PORT_FLAG = '--port';
|
|
27
|
+
const LOOPBACK_V4 = '127.0.0.1';
|
|
28
|
+
const LOOPBACK_V6 = '::1';
|
|
29
|
+
const ANNOUNCED_HOST = 'localhost';
|
|
30
|
+
const CONTENT_TYPES = {
|
|
31
|
+
'.html': 'text/html; charset=utf-8',
|
|
32
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
33
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
34
|
+
'.css': 'text/css; charset=utf-8',
|
|
35
|
+
'.json': 'application/json; charset=utf-8',
|
|
36
|
+
'.svg': 'image/svg+xml',
|
|
37
|
+
'.png': 'image/png',
|
|
38
|
+
'.jpg': 'image/jpeg',
|
|
39
|
+
'.jpeg': 'image/jpeg',
|
|
40
|
+
'.gif': 'image/gif',
|
|
41
|
+
'.webp': 'image/webp',
|
|
42
|
+
'.ico': 'image/x-icon',
|
|
43
|
+
'.woff2': 'font/woff2',
|
|
44
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
45
|
+
};
|
|
46
|
+
const FALLBACK_TYPE = 'application/octet-stream';
|
|
47
|
+
/** The file a request names, or null when it is missing or outside `root`. */
|
|
48
|
+
function fileFor(root, rawUrl) {
|
|
49
|
+
let path;
|
|
50
|
+
try {
|
|
51
|
+
path = decodeURIComponent(new URL(rawUrl, 'http://x').pathname);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const target = resolve(root, `.${path}`);
|
|
57
|
+
if (target !== root && !target.startsWith(`${root}${sep}`))
|
|
58
|
+
return null;
|
|
59
|
+
const file = path.endsWith('/') ? join(target, STATIC_PAGE_INDEX) : target;
|
|
60
|
+
try {
|
|
61
|
+
return statSync(file).isFile() ? file : null;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function listen(server, port, host) {
|
|
68
|
+
return new Promise((ok, fail) => {
|
|
69
|
+
server.once('error', fail);
|
|
70
|
+
server.listen(port, host, () => {
|
|
71
|
+
server.off('error', fail);
|
|
72
|
+
ok();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function closeServer(server) {
|
|
77
|
+
return new Promise((done) => server.close(() => done()));
|
|
78
|
+
}
|
|
79
|
+
export async function startStaticPageServer(opts) {
|
|
80
|
+
const root = resolve(opts.root);
|
|
81
|
+
const handler = (req, res) => {
|
|
82
|
+
const file = fileFor(root, req.url ?? '/');
|
|
83
|
+
if (null === file) {
|
|
84
|
+
res.writeHead(404).end();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
readFile(file, (err, body) => {
|
|
88
|
+
if (null !== err) {
|
|
89
|
+
res.writeHead(404).end();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
res.writeHead(200, {
|
|
93
|
+
'content-type': CONTENT_TYPES[extname(file).toLowerCase()] ?? FALLBACK_TYPE,
|
|
94
|
+
'cache-control': 'no-store',
|
|
95
|
+
});
|
|
96
|
+
res.end(body);
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
const v4 = createServer(handler);
|
|
100
|
+
// Port 0 means "any": the v4 bind picks it, and the v6 listener joins the same number.
|
|
101
|
+
const candidates = 0 === opts.port ? [0] : Array.from({ length: PORT_SEARCH_SPAN }, (_, i) => opts.port + i);
|
|
102
|
+
let bound;
|
|
103
|
+
for (const port of candidates) {
|
|
104
|
+
try {
|
|
105
|
+
await listen(v4, port, LOOPBACK_V4);
|
|
106
|
+
bound = v4.address().port;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
/* taken: try the next one */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (undefined === bound) {
|
|
114
|
+
throw new Error(`no free port in ${String(opts.port)}–${String(opts.port + PORT_SEARCH_SPAN)}`);
|
|
115
|
+
}
|
|
116
|
+
// `localhost` resolves to ::1 first on macOS, so the page must answer there too. A machine with
|
|
117
|
+
// no IPv6 loopback just keeps the v4 listener.
|
|
118
|
+
const v6 = createServer(handler);
|
|
119
|
+
const hasV6 = await listen(v6, bound, LOOPBACK_V6).then(() => true, () => false);
|
|
120
|
+
return {
|
|
121
|
+
url: `http://${ANNOUNCED_HOST}:${String(bound)}/`,
|
|
122
|
+
close: async () => {
|
|
123
|
+
await closeServer(v4);
|
|
124
|
+
if (hasV6)
|
|
125
|
+
await closeServer(v6);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The command setup runs for a page with no dev server of its own, or undefined when this is not
|
|
131
|
+
* that page. Quoted so an app path with a space survives the shell.
|
|
132
|
+
*/
|
|
133
|
+
export function staticPageDevCommand(opts) {
|
|
134
|
+
if (!opts.isStaticPage || !existsSync(join(opts.appDir, STATIC_PAGE_INDEX)))
|
|
135
|
+
return undefined;
|
|
136
|
+
const script = fileURLToPath(import.meta.url).replace(/\.ts$/, '.js');
|
|
137
|
+
return `"${process.execPath}" "${script}" ${DIR_FLAG} "${opts.appDir}"`;
|
|
138
|
+
}
|
|
139
|
+
function flag(argv, name) {
|
|
140
|
+
const at = argv.indexOf(name);
|
|
141
|
+
return -1 === at ? undefined : argv[at + 1];
|
|
142
|
+
}
|
|
143
|
+
// Run as a script: serve until killed, printing the url the setup wait reads.
|
|
144
|
+
if (undefined !== process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
145
|
+
const dir = flag(process.argv, DIR_FLAG) ?? process.cwd();
|
|
146
|
+
const port = Number(flag(process.argv, PORT_FLAG) ?? STATIC_PAGE_FIRST_PORT);
|
|
147
|
+
startStaticPageServer({ root: dir, port }).then((server) => process.stdout.write(`serving ${dir}\n Local: ${server.url}\n`), (err) => {
|
|
148
|
+
process.stderr.write(`static page server: ${String(err)}\n`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
@@ -53,6 +53,10 @@ interface ShapePolicy {
|
|
|
53
53
|
readonly connectBudgetMs: number;
|
|
54
54
|
/** Said out loud, because a desktop run looks stuck if nobody explains the wait. */
|
|
55
55
|
readonly note: string | undefined;
|
|
56
|
+
/** What the connect wait is waiting on, for the progress lines it prints while it waits. */
|
|
57
|
+
readonly awaiting: string;
|
|
56
58
|
}
|
|
59
|
+
/** "a Tauri app", "an Electron app": printed when the shape is detected. */
|
|
60
|
+
export declare function describeShape(shape: AppShape): string;
|
|
57
61
|
export declare function policyFor(shape: AppShape): ShapePolicy;
|
|
58
62
|
export {};
|
|
@@ -42,7 +42,21 @@ export function isDesktopOrigin(url) {
|
|
|
42
42
|
}
|
|
43
43
|
const WEB_CONNECT_MS = 120_000;
|
|
44
44
|
/** A cold `tauri dev` compiles the Rust side before a window exists. */
|
|
45
|
-
const
|
|
45
|
+
const TAURI_CONNECT_MS = 10 * 60_000;
|
|
46
|
+
/**
|
|
47
|
+
* Electron has no compile step of that size: its window is a Node process loading a dev server that
|
|
48
|
+
* is already up. It was given Tauri's ten minutes, so a run whose window was never going to dial in
|
|
49
|
+
* sat silent for all of them — longer than the daemon it was waiting on stayed alive.
|
|
50
|
+
*/
|
|
51
|
+
const ELECTRON_CONNECT_MS = 3 * 60_000;
|
|
52
|
+
/** "a Tauri app", "an Electron app": printed when the shape is detected. */
|
|
53
|
+
export function describeShape(shape) {
|
|
54
|
+
if (AppShape.ELECTRON === shape)
|
|
55
|
+
return 'an Electron app';
|
|
56
|
+
if (AppShape.TAURI === shape)
|
|
57
|
+
return 'a Tauri app';
|
|
58
|
+
return 'a web app';
|
|
59
|
+
}
|
|
46
60
|
export function policyFor(shape) {
|
|
47
61
|
if (AppShape.WEB === shape) {
|
|
48
62
|
return {
|
|
@@ -50,12 +64,14 @@ export function policyFor(shape) {
|
|
|
50
64
|
requireHttpReady: true,
|
|
51
65
|
connectBudgetMs: WEB_CONNECT_MS,
|
|
52
66
|
note: undefined,
|
|
67
|
+
awaiting: 'the page',
|
|
53
68
|
};
|
|
54
69
|
}
|
|
55
70
|
return {
|
|
56
71
|
openBrowser: false,
|
|
57
72
|
requireHttpReady: false,
|
|
58
|
-
connectBudgetMs:
|
|
73
|
+
connectBudgetMs: AppShape.TAURI === shape ? TAURI_CONNECT_MS : ELECTRON_CONNECT_MS,
|
|
74
|
+
awaiting: AppShape.TAURI === shape ? 'the Tauri window' : 'the Electron window',
|
|
59
75
|
note: AppShape.TAURI === shape
|
|
60
76
|
? 'Tauri app: not opening a browser, because its own window is the client. A cold `tauri dev` builds the Rust side first, so the window can take minutes to appear.'
|
|
61
77
|
: 'Electron app: not opening a browser, because its own window is the client. Waiting for the app to launch and dial in.',
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RETICLE_PORT`, when it names a port. The one reader of it for the CLI, `init` included.
|
|
3
|
+
*
|
|
4
|
+
* `init` took only `--port`, so a shell that exported RETICLE_PORT got a project wired, and a daemon
|
|
5
|
+
* started, on the default port while every other command it ran dialled the exported one. Fed to
|
|
6
|
+
* `portForInit` as the explicit port, so the files phase writes it into `.reticle.json` and the
|
|
7
|
+
* runtime phase binds it: the two halves of `init` cannot disagree.
|
|
8
|
+
*/
|
|
9
|
+
export declare function portFromEnv(env: Readonly<Record<string, string | undefined>>): number | undefined;
|
|
10
|
+
export declare function portForInit(explicit: number | undefined, configured: number | undefined, projectId: string | undefined, deps: {
|
|
11
|
+
/** The projects the daemon on `port` belongs to or is serving right now. */
|
|
12
|
+
daemonProjects: (port: number) => Promise<readonly string[]>;
|
|
13
|
+
daemonPresent: (port: number) => Promise<boolean>;
|
|
14
|
+
pickPort: (preferred: number) => Promise<number>;
|
|
15
|
+
}): Promise<number | undefined>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The daemon port `reticle init` wires a project to.
|
|
3
|
+
*
|
|
4
|
+
* Init used to leave this unset, so every project on a machine was wired to the default port. The
|
|
5
|
+
* second one to run was connected to the first one's daemon, and the bridge refused its app. When a
|
|
6
|
+
* daemon for another project already holds the default, the new project gets its own port, and init
|
|
7
|
+
* records it in `.reticle.json`, where the CLI and the build plugins both read it.
|
|
8
|
+
*
|
|
9
|
+
* Undefined means the default.
|
|
10
|
+
*/
|
|
11
|
+
import { RETICLE_DEFAULT_PORT, ReticleEnv } from '@reticlehq/core';
|
|
12
|
+
const MAX_PORT = 65_535;
|
|
13
|
+
/**
|
|
14
|
+
* `RETICLE_PORT`, when it names a port. The one reader of it for the CLI, `init` included.
|
|
15
|
+
*
|
|
16
|
+
* `init` took only `--port`, so a shell that exported RETICLE_PORT got a project wired, and a daemon
|
|
17
|
+
* started, on the default port while every other command it ran dialled the exported one. Fed to
|
|
18
|
+
* `portForInit` as the explicit port, so the files phase writes it into `.reticle.json` and the
|
|
19
|
+
* runtime phase binds it: the two halves of `init` cannot disagree.
|
|
20
|
+
*/
|
|
21
|
+
export function portFromEnv(env) {
|
|
22
|
+
const raw = env[ReticleEnv.PORT];
|
|
23
|
+
if (undefined === raw || !/^\d+$/.test(raw))
|
|
24
|
+
return undefined;
|
|
25
|
+
const port = Number(raw);
|
|
26
|
+
return 0 < port && port <= MAX_PORT ? port : undefined;
|
|
27
|
+
}
|
|
28
|
+
export async function portForInit(explicit, configured, projectId, deps) {
|
|
29
|
+
if (explicit !== undefined)
|
|
30
|
+
return explicit;
|
|
31
|
+
if (configured !== undefined)
|
|
32
|
+
return configured;
|
|
33
|
+
// Undefined, not the default's number: an explicit default would be written into every generated
|
|
34
|
+
// config as a `port` nobody chose.
|
|
35
|
+
if (!(await deps.daemonPresent(RETICLE_DEFAULT_PORT)))
|
|
36
|
+
return undefined;
|
|
37
|
+
// The bridge's own rule: it refuses a page only when it has served exactly ONE other project. A
|
|
38
|
+
// daemon serving nobody, several projects, or ours accepts this one. A project that has never
|
|
39
|
+
// been through init has no id yet, so a single named holder is somebody else.
|
|
40
|
+
const holders = await deps.daemonProjects(RETICLE_DEFAULT_PORT);
|
|
41
|
+
if (1 !== holders.length || holders[0] === projectId)
|
|
42
|
+
return undefined;
|
|
43
|
+
return deps.pickPort(RETICLE_DEFAULT_PORT + 1);
|
|
44
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* cli.ts is a dispatcher: a command that grew a second half should not make the file that routes
|
|
6
6
|
* every command harder to read.
|
|
7
7
|
*/
|
|
8
|
-
import type
|
|
8
|
+
import { type InitResult } from '@reticlehq/init';
|
|
9
9
|
/** Just enough of the parsed command to decide and run. */
|
|
10
10
|
interface InitRuntimeArgs {
|
|
11
11
|
readonly port: number | undefined;
|