@reticlehq/server 2.12.0 → 2.13.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 +67 -109
- package/dist/bridge/auth-failure-reason.d.ts +6 -2
- package/dist/bridge/auth-failure-reason.js +15 -8
- package/dist/bridge/bridge.d.ts +18 -1
- package/dist/bridge/bridge.js +247 -164
- package/dist/bridge/pairing-token.d.ts +5 -0
- package/dist/bridge/pairing-token.js +28 -0
- package/dist/bridge/protocol-skew.d.ts +37 -0
- package/dist/bridge/protocol-skew.js +45 -0
- package/dist/capsule/capsule-store.d.ts +6 -6
- package/dist/capsule/causal-summary.js +7 -2
- package/dist/cli/cli-doctor.js +31 -1
- package/dist/cli/cli-launch.d.ts +11 -1
- package/dist/cli/cli-launch.js +59 -10
- package/dist/cli/cli-parse.d.ts +15 -1
- package/dist/cli/cli-parse.js +212 -8
- package/dist/cli/cli-verify.d.ts +1 -0
- package/dist/cli/cli-verify.js +3 -2
- package/dist/cli/cloud-cli.js +428 -259
- package/dist/cli/cloud-keystore.d.ts +37 -0
- package/dist/cli/cloud-keystore.js +97 -0
- package/dist/cli/cloud-kit.d.ts +37 -0
- package/dist/cli/cloud-kit.js +121 -0
- package/dist/cli/cloud-login.d.ts +17 -0
- package/dist/cli/cloud-login.js +183 -0
- package/dist/cli/cloud-session.d.ts +48 -0
- package/dist/cli/cloud-session.js +53 -0
- package/dist/cli/config-discovery.js +1 -0
- package/dist/cli/daemon-start-options.d.ts +32 -0
- package/dist/cli/daemon-start-options.js +38 -0
- package/dist/cli/device-flow.d.ts +76 -0
- package/dist/cli/device-flow.js +56 -0
- package/dist/cli/doctor-rows.d.ts +9 -0
- package/dist/cli/doctor-rows.js +9 -0
- package/dist/cli/drive-attach.d.ts +13 -0
- package/dist/cli/drive-attach.js +18 -0
- package/dist/cli/drive-command.js +7 -3
- package/dist/cli/mcp-command.js +51 -20
- package/dist/cli/project-name.d.ts +30 -0
- package/dist/cli/project-name.js +44 -0
- package/dist/cli/status-next-action.d.ts +14 -0
- package/dist/cli/status-next-action.js +4 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +134 -80
- package/dist/cloud/cloud-config.d.ts +22 -1
- package/dist/cloud/cloud-config.js +64 -5
- package/dist/cloud/cloud-sync.d.ts +5 -5
- package/dist/cloud/cloud-sync.js +33 -2
- package/dist/cloud/sync-cycle.d.ts +110 -0
- package/dist/cloud/sync-cycle.js +270 -0
- package/dist/cloud/sync-daemon.d.ts +69 -0
- package/dist/cloud/sync-daemon.js +260 -0
- package/dist/cloud/sync-disk.d.ts +9 -0
- package/dist/cloud/sync-disk.js +191 -0
- package/dist/cloud/sync-hash.d.ts +2 -0
- package/dist/cloud/sync-hash.js +35 -0
- package/dist/crawl/crawl-tools.js +5 -3
- package/dist/crawl/crawl.d.ts +3 -0
- package/dist/crawl/crawl.js +5 -0
- package/dist/daemon/daemon-resolve.d.ts +43 -0
- package/dist/daemon/daemon-resolve.js +69 -0
- package/dist/daemon/dev-servers.d.ts +15 -0
- package/dist/daemon/dev-servers.js +68 -0
- package/dist/daemon/wake-decision.d.ts +32 -0
- package/dist/daemon/wake-decision.js +38 -0
- package/dist/domain/domain-tools.js +2 -2
- package/dist/events/contradictions.d.ts +18 -0
- package/dist/events/contradictions.js +134 -21
- package/dist/events/declared.d.ts +7 -4
- package/dist/events/declared.js +15 -6
- package/dist/events/echo-mismatch.js +64 -14
- package/dist/events/event-filters.d.ts +8 -0
- package/dist/events/event-filters.js +19 -2
- package/dist/events/predicate-eval.js +53 -5
- package/dist/events/predicate-precheck.d.ts +8 -0
- package/dist/events/predicate-precheck.js +41 -0
- package/dist/events/predicate-route.d.ts +14 -0
- package/dist/events/predicate-route.js +36 -8
- package/dist/events/predicate.d.ts +5 -0
- package/dist/events/predicate.js +37 -5
- package/dist/events/split-text-miss.d.ts +30 -0
- package/dist/events/split-text-miss.js +44 -0
- package/dist/flows/flake.d.ts +6 -6
- package/dist/flows/flow-memory-consult.d.ts +55 -0
- package/dist/flows/flow-memory-consult.js +38 -0
- package/dist/flows/flow-replay-run.d.ts +18 -7
- package/dist/flows/flow-replay-run.js +239 -22
- package/dist/flows/flow-replay.js +11 -2
- package/dist/flows/flow-step-runners.d.ts +8 -0
- package/dist/flows/flow-step-runners.js +8 -2
- package/dist/flows/flow-store-for-session.d.ts +31 -0
- package/dist/flows/flow-store-for-session.js +33 -0
- package/dist/flows/flow-tools.js +58 -32
- package/dist/flows/flows.d.ts +8 -0
- package/dist/flows/flows.js +468 -2
- package/dist/flows/heal-run.js +5 -2
- package/dist/flows/predicate-to-expect.d.ts +17 -12
- package/dist/flows/predicate-to-expect.js +30 -12
- package/dist/flows/recordings.d.ts +13 -1
- package/dist/flows/recordings.js +17 -2
- package/dist/flows/replay.d.ts +22 -2
- package/dist/flows/replay.js +28 -2
- package/dist/flows/verify-change-tools.js +5 -2
- package/dist/honesty/blind-spots.d.ts +55 -2
- package/dist/honesty/blind-spots.js +63 -3
- package/dist/honesty/honesty.d.ts +24 -0
- package/dist/honesty/honesty.js +25 -1
- package/dist/honesty/instrumentation-gaps.d.ts +9 -0
- package/dist/honesty/instrumentation-gaps.js +29 -2
- package/dist/honesty/verified.d.ts +2 -1
- package/dist/honesty/verified.js +16 -1
- package/dist/http-server.js +12 -0
- package/dist/impact/impact-recorder.d.ts +31 -8
- package/dist/impact/impact-recorder.js +104 -21
- package/dist/impact/impact-store.d.ts +15 -1
- package/dist/impact/impact-store.js +43 -2
- package/dist/index.js +113 -7
- package/dist/init/agent-rules.d.ts +13 -6
- package/dist/init/agent-rules.js +23 -12
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +28 -0
- package/dist/init/capabilities.d.ts +4 -1
- package/dist/init/capabilities.js +22 -2
- package/dist/init/confirm.d.ts +31 -7
- package/dist/init/confirm.js +48 -8
- package/dist/init/cra.d.ts +20 -4
- package/dist/init/cra.js +57 -17
- package/dist/init/csp-check.js +17 -2
- package/dist/init/csp-doctor.d.ts +8 -1
- package/dist/init/csp-doctor.js +8 -3
- package/dist/init/dev-script.d.ts +3 -1
- package/dist/init/dev-script.js +8 -5
- package/dist/init/format-generated.d.ts +18 -0
- package/dist/init/format-generated.js +39 -0
- package/dist/init/install-hint.d.ts +9 -0
- package/dist/init/install-hint.js +29 -0
- package/dist/init/mcp-clients.d.ts +7 -0
- package/dist/init/mcp-clients.js +7 -0
- package/dist/init/mcp.d.ts +1 -1
- package/dist/init/mcp.js +1 -1
- package/dist/init/node-io.js +11 -1
- package/dist/init/plan-framework.js +81 -45
- package/dist/init/plan.d.ts +20 -7
- package/dist/init/plan.js +20 -45
- package/dist/init/preflight.d.ts +28 -0
- package/dist/init/preflight.js +38 -0
- package/dist/init/run.d.ts +32 -9
- package/dist/init/run.js +141 -119
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/snippets.js +41 -25
- package/dist/init/vite-config.d.ts +1 -1
- package/dist/init/vite-config.js +39 -25
- package/dist/init/workspace-apps.d.ts +17 -10
- package/dist/init/workspace-apps.js +110 -0
- package/dist/input/network-mock-tools.js +3 -6
- package/dist/input/real-input.js +25 -5
- package/dist/input/scroll-tools.js +4 -6
- package/dist/input/viewport-tools.js +5 -4
- package/dist/intent/inline-intent.d.ts +10 -19
- package/dist/intent/inline-intent.js +91 -1
- package/dist/intent/intent-shard-store.d.ts +72 -0
- package/dist/intent/intent-shard-store.js +203 -0
- package/dist/intent/intent-shard.d.ts +411 -0
- package/dist/intent/intent-shard.js +176 -0
- package/dist/intent/intent-store.d.ts +12 -0
- package/dist/intent/intent-store.js +21 -0
- package/dist/intent/intent-subject.d.ts +45 -0
- package/dist/intent/intent-subject.js +177 -0
- package/dist/intent/intent-tools.js +65 -2
- package/dist/intent/open-intents.d.ts +21 -0
- package/dist/intent/open-intents.js +15 -0
- package/dist/license/license-env.d.ts +29 -0
- package/dist/license/license-env.js +148 -0
- package/dist/loopback-agent.d.ts +31 -0
- package/dist/loopback-agent.js +57 -0
- package/dist/mcp/attach-memory.d.ts +18 -2
- package/dist/mcp/attach-memory.js +50 -7
- package/dist/mcp/client-identity.d.ts +31 -0
- package/dist/mcp/client-identity.js +51 -0
- package/dist/mcp/mcp-post-transport.d.ts +26 -0
- package/dist/mcp/mcp-post-transport.js +109 -0
- package/dist/mcp/mcp-proxy.d.ts +2 -1
- package/dist/mcp/mcp-proxy.js +52 -62
- package/dist/mcp/mcp.js +7 -1
- package/dist/mcp/proxy-backoff.d.ts +11 -0
- package/dist/mcp/proxy-backoff.js +13 -0
- package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
- package/dist/mcp/proxy-daemon-probe.js +7 -1
- package/dist/mcp/proxy-handshake.d.ts +23 -0
- package/dist/mcp/proxy-handshake.js +31 -0
- package/dist/mcp/tool-catalog-cache.d.ts +8 -0
- package/dist/mcp/tool-catalog-cache.js +12 -0
- package/dist/memory/memory-tools.d.ts +2 -0
- package/dist/memory/memory-tools.js +82 -0
- package/dist/memory/project-memory.d.ts +34 -0
- package/dist/memory/project-memory.js +96 -0
- package/dist/oracles/propose-consequences.js +12 -3
- package/dist/pool/browser-pool.d.ts +63 -4
- package/dist/pool/browser-pool.js +113 -4
- package/dist/pool/playwright-launcher.js +9 -0
- package/dist/project/project-tools.js +3 -3
- package/dist/project/reticle-dir.d.ts +15 -2
- package/dist/project/reticle-dir.js +21 -4
- package/dist/runs/run-store.d.ts +10 -0
- package/dist/runs/run-store.js +10 -0
- package/dist/runs/verification-sync.d.ts +0 -11
- package/dist/runs/verification-sync.js +22 -1
- package/dist/runs/verify-server.d.ts +8 -1
- package/dist/runs/verify-server.js +14 -2
- package/dist/session/command-timeout.d.ts +5 -2
- package/dist/session/command-timeout.js +5 -4
- package/dist/session/lease-visibility.d.ts +67 -0
- package/dist/session/lease-visibility.js +76 -0
- package/dist/session/no-session-diagnosis.d.ts +23 -0
- package/dist/session/no-session-diagnosis.js +58 -17
- package/dist/session/no-session-next-action.d.ts +7 -0
- package/dist/session/no-session-next-action.js +42 -3
- package/dist/session/no-session-watch.d.ts +10 -6
- package/dist/session/no-session-watch.js +51 -19
- package/dist/session/pending-commands.d.ts +11 -1
- package/dist/session/pending-commands.js +12 -2
- package/dist/session/resolve-within.d.ts +34 -0
- package/dist/session/resolve-within.js +53 -0
- package/dist/session/session-info.d.ts +18 -0
- package/dist/session/session-info.js +11 -0
- package/dist/session/session-manager.d.ts +14 -0
- package/dist/session/session-manager.js +61 -3
- package/dist/session/session-mirror.d.ts +16 -0
- package/dist/session/session-mirror.js +22 -0
- package/dist/session/session-recommendation.d.ts +19 -5
- package/dist/session/session-recommendation.js +25 -9
- package/dist/session/session-successor.js +36 -1
- package/dist/session/session-tools.js +2 -2
- package/dist/session/session.d.ts +22 -2
- package/dist/session/session.js +87 -7
- package/dist/session/wall-clock.d.ts +12 -0
- package/dist/session/wall-clock.js +14 -0
- package/dist/setup/agent-configs.d.ts +96 -0
- package/dist/setup/agent-configs.js +333 -0
- package/dist/setup/agent-io.d.ts +9 -0
- package/dist/setup/agent-io.js +16 -0
- package/dist/setup/agent-writer.d.ts +51 -0
- package/dist/setup/agent-writer.js +115 -0
- package/dist/setup/approval-migration.d.ts +47 -0
- package/dist/setup/approval-migration.js +68 -0
- package/dist/setup/auto-approve.d.ts +85 -0
- package/dist/setup/auto-approve.js +150 -0
- package/dist/setup/bridge-port.d.ts +15 -0
- package/dist/setup/bridge-port.js +24 -0
- package/dist/setup/desktop-shape.d.ts +57 -0
- package/dist/setup/desktop-shape.js +63 -0
- package/dist/setup/dev-server-wait.d.ts +68 -0
- package/dist/setup/dev-server-wait.js +67 -0
- package/dist/setup/drive-agent.d.ts +71 -0
- package/dist/setup/drive-agent.js +171 -0
- package/dist/setup/drive-plan.d.ts +68 -0
- package/dist/setup/drive-plan.js +130 -0
- package/dist/setup/ensure-daemon.d.ts +29 -0
- package/dist/setup/ensure-daemon.js +58 -0
- package/dist/setup/init-runtime.d.ts +47 -0
- package/dist/setup/init-runtime.js +160 -0
- package/dist/setup/interrupt.d.ts +25 -0
- package/dist/setup/interrupt.js +40 -0
- package/dist/setup/license-key.d.ts +39 -0
- package/dist/setup/license-key.js +72 -0
- package/dist/setup/listeners.d.ts +37 -0
- package/dist/setup/listeners.js +85 -0
- package/dist/setup/node-effects.d.ts +60 -0
- package/dist/setup/node-effects.js +210 -0
- package/dist/setup/page-probe.d.ts +39 -0
- package/dist/setup/page-probe.js +49 -0
- package/dist/setup/relaunch.d.ts +36 -0
- package/dist/setup/relaunch.js +57 -0
- package/dist/setup/remaining-steps.d.ts +19 -0
- package/dist/setup/remaining-steps.js +31 -0
- package/dist/setup/run-setup.d.ts +99 -0
- package/dist/setup/run-setup.js +203 -0
- package/dist/setup/session-pick.d.ts +47 -0
- package/dist/setup/session-pick.js +51 -0
- package/dist/setup/setup-command.d.ts +45 -0
- package/dist/setup/setup-command.js +199 -0
- package/dist/setup/setup-options.d.ts +74 -0
- package/dist/setup/setup-options.js +67 -0
- package/dist/setup/transcripts.d.ts +16 -0
- package/dist/setup/transcripts.js +43 -0
- package/dist/status-payload.d.ts +14 -1
- package/dist/status-payload.js +23 -1
- package/dist/telemetry/daemon-telemetry.d.ts +8 -1
- package/dist/telemetry/daemon-telemetry.js +52 -5
- package/dist/telemetry/error-class.js +1 -1
- package/dist/telemetry/feedback-context.d.ts +9 -1
- package/dist/telemetry/feedback-context.js +60 -29
- package/dist/telemetry/feedback-local.d.ts +15 -0
- package/dist/telemetry/feedback-local.js +56 -0
- package/dist/telemetry/feedback.d.ts +11 -0
- package/dist/telemetry/feedback.js +19 -6
- package/dist/telemetry/init-telemetry.d.ts +2 -0
- package/dist/telemetry/init-telemetry.js +2 -0
- package/dist/telemetry/install-source.d.ts +14 -0
- package/dist/telemetry/install-source.js +30 -0
- package/dist/telemetry/license-activation.d.ts +7 -1
- package/dist/telemetry/license-activation.js +9 -5
- package/dist/telemetry/mcp-connection.js +7 -0
- package/dist/telemetry/project-profile.d.ts +12 -1
- package/dist/telemetry/project-profile.js +11 -6
- package/dist/telemetry/proxy-telemetry.d.ts +3 -0
- package/dist/telemetry/proxy-telemetry.js +21 -0
- package/dist/telemetry/session-metrics.d.ts +12 -0
- package/dist/telemetry/session-metrics.js +35 -1
- package/dist/telemetry/telemetry.js +17 -2
- package/dist/telemetry/tool-refused.d.ts +3 -1
- package/dist/telemetry/tool-refused.js +34 -2
- package/dist/tools/act-capsule.d.ts +10 -0
- package/dist/tools/act-capsule.js +8 -2
- package/dist/tools/act-danger.d.ts +2 -1
- package/dist/tools/act-danger.js +11 -3
- package/dist/tools/act-observation.d.ts +41 -0
- package/dist/tools/act-observation.js +36 -0
- package/dist/tools/act-preflight.d.ts +20 -0
- package/dist/tools/act-preflight.js +98 -0
- package/dist/tools/act-sequence-retry.d.ts +45 -0
- package/dist/tools/act-sequence-retry.js +58 -0
- package/dist/tools/act-sequence-tool.d.ts +13 -0
- package/dist/tools/act-sequence-tool.js +135 -0
- package/dist/tools/act-target.d.ts +14 -0
- package/dist/tools/act-target.js +47 -0
- package/dist/tools/act-tools.d.ts +21 -1
- package/dist/tools/act-tools.js +100 -213
- package/dist/tools/assert-verdict.js +22 -16
- package/dist/tools/browser-tools.js +6 -2
- package/dist/tools/dynamic-tools.js +24 -5
- package/dist/tools/error-recovery.d.ts +1 -0
- package/dist/tools/error-recovery.js +8 -0
- package/dist/tools/invoke-tool.js +59 -9
- package/dist/tools/lease-tools.d.ts +30 -4
- package/dist/tools/lease-tools.js +169 -5
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +22 -0
- package/dist/tools/numeric-bounds.d.ts +100 -0
- package/dist/tools/numeric-bounds.js +125 -0
- package/dist/tools/observe-tools.js +47 -40
- package/dist/tools/read-tools.js +28 -7
- package/dist/tools/real-input-attempt.d.ts +10 -0
- package/dist/tools/real-input-attempt.js +59 -1
- package/dist/tools/reconcile-tools.js +4 -4
- package/dist/tools/tool-kit.d.ts +12 -0
- package/dist/tools/tool-kit.js +6 -1
- package/dist/tools/tool-names.d.ts +2 -0
- package/dist/tools/tool-names.js +2 -0
- package/dist/tools/tools-helpers.d.ts +19 -0
- package/dist/tools/tools-helpers.js +46 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +56 -8
- package/dist/update/reticle-deps.d.ts +9 -0
- package/dist/update/reticle-deps.js +27 -0
- package/dist/update/sdk-sync.d.ts +1 -2
- package/dist/update/sdk-sync.js +1 -21
- package/dist/update/update-nudge.d.ts +18 -0
- package/dist/update/update-nudge.js +20 -0
- package/dist/version/peer-announce.js +4 -2
- package/dist/version/sdk-fix.d.ts +47 -0
- package/dist/version/sdk-fix.js +127 -0
- package/dist/version/version-nudge.d.ts +14 -0
- package/dist/version/version-nudge.js +21 -0
- package/dist/version/version-skew.d.ts +38 -3
- package/dist/version/version-skew.js +83 -7
- package/dist/visual/visual-store.d.ts +6 -6
- package/dist/visual/visual-store.js +18 -16
- package/dist/visual/visual-tools.js +57 -12
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/serve.mdx +4 -2
- package/docs/cli.mdx +7 -3
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +1 -1
- package/docs/getting-started.md +19 -3
- package/docs/index.mdx +9 -6
- package/docs/install-agentic.mdx +38 -12
- package/docs/install-manual.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/skill-file.mdx +3 -3
- package/docs/telemetry-contract.md +108 -4
- package/docs/tools-act-sequence.mdx +16 -0
- package/docs/usage.md +1 -1
- package/package.json +4 -3
- package/dist/init/cursor.d.ts +0 -24
- package/dist/init/cursor.js +0 -80
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Making sure something is listening before the app is asked to dial it.
|
|
3
|
+
*
|
|
4
|
+
* The setup phases wait for the page to appear as a session, which cannot happen unless a daemon
|
|
5
|
+
* holds the bridge port. Nothing in `init` started one. On a developer machine that is invisible,
|
|
6
|
+
* because an editor running `reticle mcp` has had a daemon up for hours; on a machine installing
|
|
7
|
+
* Reticle for the first time, which is every machine this command exists for, there is nothing to
|
|
8
|
+
* dial and setup ends with "the SDK is in the page and never dialled the bridge" — the SDK being
|
|
9
|
+
* blamed for the absence of the thing it was dialling.
|
|
10
|
+
*
|
|
11
|
+
* Idempotent by construction: an alive daemon for this project is adopted, and one belonging to
|
|
12
|
+
* somebody else is refused rather than stolen, which is the same rule `reticle mcp` follows.
|
|
13
|
+
*/
|
|
14
|
+
import { log } from '../log.js';
|
|
15
|
+
import { spawnDaemon } from '../daemon/daemon.js';
|
|
16
|
+
import { probePresence, presenceIsUsable } from '../daemon/port-presence.js';
|
|
17
|
+
import { waitForDaemon, probeDaemon } from '../mcp/mcp-proxy.js';
|
|
18
|
+
import { daemonSpawnArgs } from '../cli/daemon-start-options.js';
|
|
19
|
+
import { fetchStatus } from '../cli/cli-launch.js';
|
|
20
|
+
export const EnsureDaemon = {
|
|
21
|
+
/** Something usable was already there. */
|
|
22
|
+
ADOPTED: 'adopted',
|
|
23
|
+
STARTED: 'started',
|
|
24
|
+
/** Could not be started, and the caller has to say so rather than blame the page. */
|
|
25
|
+
UNAVAILABLE: 'unavailable',
|
|
26
|
+
};
|
|
27
|
+
export async function ensureDaemon(port, deps) {
|
|
28
|
+
if (await deps.usable(port))
|
|
29
|
+
return EnsureDaemon.ADOPTED;
|
|
30
|
+
if (undefined === deps.scriptPath)
|
|
31
|
+
return EnsureDaemon.UNAVAILABLE;
|
|
32
|
+
if (!deps.spawn(port))
|
|
33
|
+
return EnsureDaemon.UNAVAILABLE;
|
|
34
|
+
try {
|
|
35
|
+
// Readiness means the port ACCEPTS, not that a child was spawned: a readiness signal that
|
|
36
|
+
// precedes readiness is worse than none, because what waits on it stops waiting too early.
|
|
37
|
+
await deps.waitReady(port);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return EnsureDaemon.UNAVAILABLE;
|
|
41
|
+
}
|
|
42
|
+
return (await deps.usable(port)) ? EnsureDaemon.STARTED : EnsureDaemon.UNAVAILABLE;
|
|
43
|
+
}
|
|
44
|
+
export function nodeEnsureDaemonDeps() {
|
|
45
|
+
return {
|
|
46
|
+
usable: async (port) => presenceIsUsable(await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus })),
|
|
47
|
+
spawn: (port) => {
|
|
48
|
+
const scriptPath = process.argv[1];
|
|
49
|
+
if (undefined === scriptPath)
|
|
50
|
+
return false;
|
|
51
|
+
const started = spawnDaemon(process.execPath, scriptPath, daemonSpawnArgs({ port, headless: true, http: false }), port);
|
|
52
|
+
log('reticle_setup_daemon_started', { port, started });
|
|
53
|
+
return started;
|
|
54
|
+
},
|
|
55
|
+
waitReady: (port) => waitForDaemon(port),
|
|
56
|
+
scriptPath: process.argv[1],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `init` does after the files are written, and how it decides whether to.
|
|
3
|
+
*
|
|
4
|
+
* Lives here rather than in cli.ts because it is a cohesive unit with its own reasons, and because
|
|
5
|
+
* cli.ts is a dispatcher: a command that grew a second half should not make the file that routes
|
|
6
|
+
* every command harder to read.
|
|
7
|
+
*/
|
|
8
|
+
import type { InitResult } from '../init/run.js';
|
|
9
|
+
/** Just enough of the parsed command to decide and run. */
|
|
10
|
+
export interface InitRuntimeArgs {
|
|
11
|
+
readonly port: number | undefined;
|
|
12
|
+
readonly dryRun: boolean;
|
|
13
|
+
readonly filesOnly?: boolean | undefined;
|
|
14
|
+
readonly json?: boolean | undefined;
|
|
15
|
+
readonly drive?: boolean | undefined;
|
|
16
|
+
readonly open?: boolean | undefined;
|
|
17
|
+
readonly agents?: boolean | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* `--no-mcp`. It governs agent registration too, and pre-approval with it.
|
|
20
|
+
*
|
|
21
|
+
* The flag's own help says it skips MORE than the server registration, "because all three only
|
|
22
|
+
* make sense once the tools are reachable" — and a pre-approval rule for a server the machine was
|
|
23
|
+
* told not to register is the clearest case of that. It went unread when the registration moved
|
|
24
|
+
* here, so a run that asked us to leave the machine's config alone wrote to four files in the
|
|
25
|
+
* user's home. The install gate caught it, because the gate runs with --no-mcp.
|
|
26
|
+
*/
|
|
27
|
+
readonly mcp?: boolean | undefined;
|
|
28
|
+
readonly flow?: string | undefined;
|
|
29
|
+
readonly env?: string[] | undefined;
|
|
30
|
+
readonly url?: string | undefined;
|
|
31
|
+
readonly timeoutSeconds?: number | undefined;
|
|
32
|
+
/** Restart the calling client so IT gets the tools — see relaunch.ts. */
|
|
33
|
+
readonly relaunch?: boolean | undefined;
|
|
34
|
+
readonly driveModel?: string | undefined;
|
|
35
|
+
readonly licenseKey?: string | undefined;
|
|
36
|
+
}
|
|
37
|
+
export interface RuntimePrintIo {
|
|
38
|
+
readonly print: (line: string) => void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Carry on from a finished `init`, or stop where it used to.
|
|
42
|
+
*
|
|
43
|
+
* `--files-only` is what init did before it learned to boot the app, and a dry run is a preview:
|
|
44
|
+
* both keep the old ending. Everyone else gets the rest, because writing files was never the same
|
|
45
|
+
* thing as an install working.
|
|
46
|
+
*/
|
|
47
|
+
export declare function continueAfterInit(parsed: InitRuntimeArgs, result: InitResult, io: RuntimePrintIo, cwd: string): Promise<void>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `init` does after the files are written, and how it decides whether to.
|
|
3
|
+
*
|
|
4
|
+
* Lives here rather than in cli.ts because it is a cohesive unit with its own reasons, and because
|
|
5
|
+
* cli.ts is a dispatcher: a command that grew a second half should not make the file that routes
|
|
6
|
+
* every command harder to read.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { RETICLE_DEFAULT_PORT } from '@reticlehq/core';
|
|
10
|
+
import { confirmInstall, nodeConfirmDeps } from '../init/confirm.js';
|
|
11
|
+
import { writeLicenseKey } from './license-key.js';
|
|
12
|
+
import { registerOtherAgents, runSetupCommand } from './setup-command.js';
|
|
13
|
+
import { bridgeOccupied } from './bridge-port.js';
|
|
14
|
+
import { relaunchDecision } from './relaunch.js';
|
|
15
|
+
import { claudeTranscriptExists, codexSessionFor } from './transcripts.js';
|
|
16
|
+
import { probePresence } from '../daemon/port-presence.js';
|
|
17
|
+
import { probeDaemon } from '../mcp/proxy-daemon-probe.js';
|
|
18
|
+
import { fetchStatus } from '../cli/cli-launch.js';
|
|
19
|
+
import { collectEnv, DEFAULT_DRIVE_BUDGET_USD, DEFAULT_PHASE_TIMEOUT_MS } from './setup-options.js';
|
|
20
|
+
/** How often the runtime phases look again: fast enough not to be the wait, slow enough to be free. */
|
|
21
|
+
const POLL_MS = 250;
|
|
22
|
+
/** Registration is global MCP wiring, so both flags that disown it are read here. */
|
|
23
|
+
function wantsAgents(parsed) {
|
|
24
|
+
return false !== parsed.agents && false !== parsed.mcp;
|
|
25
|
+
}
|
|
26
|
+
const licenseIo = {
|
|
27
|
+
exists: (path) => existsSync(path),
|
|
28
|
+
readFile: (path) => readFileSync(path, 'utf8'),
|
|
29
|
+
writeFile: (path, contents) => writeFileSync(path, contents),
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Carry on from a finished `init`, or stop where it used to.
|
|
33
|
+
*
|
|
34
|
+
* `--files-only` is what init did before it learned to boot the app, and a dry run is a preview:
|
|
35
|
+
* both keep the old ending. Everyone else gets the rest, because writing files was never the same
|
|
36
|
+
* thing as an install working.
|
|
37
|
+
*/
|
|
38
|
+
export async function continueAfterInit(parsed, result, io, cwd) {
|
|
39
|
+
const port = parsed.port ?? RETICLE_DEFAULT_PORT;
|
|
40
|
+
// Before anything else: the key belongs in .env whichever way this run ends, and the CLI folds a
|
|
41
|
+
// project-local .env into the environment on every invocation.
|
|
42
|
+
if (undefined !== parsed.licenseKey) {
|
|
43
|
+
const written = writeLicenseKey(cwd, parsed.licenseKey, licenseIo);
|
|
44
|
+
io.print(written.message);
|
|
45
|
+
}
|
|
46
|
+
if (true === parsed.filesOnly || parsed.dryRun) {
|
|
47
|
+
// Registration and pre-approval still run here, and this is the ONLY route an existing user
|
|
48
|
+
// has: nothing reaches back into a machine that installed Reticle a version ago, so the
|
|
49
|
+
// upgrade path is re-running init, and the light form of init has to be enough to carry it.
|
|
50
|
+
// A dry run writes nothing anywhere, including here.
|
|
51
|
+
if (true === parsed.filesOnly && wantsAgents(parsed))
|
|
52
|
+
registerOtherAgents(io.print);
|
|
53
|
+
return confirmInstall(result, io, nodeConfirmDeps(port)).then(() => {
|
|
54
|
+
if (!result.ok)
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const context = result.context;
|
|
59
|
+
if (context === undefined) {
|
|
60
|
+
// Nothing was established, so there is nothing to run against. init has already said why.
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
// A PENDING connect step is not a reason to skip the runtime phase, and treating it as one made
|
|
64
|
+
// `init` stop with "paste this snippet" while never looking at the app.
|
|
65
|
+
//
|
|
66
|
+
// `result.ok` is exactly `!connectPending`, so every project whose instrumentation needs a manual
|
|
67
|
+
// step — a plain-HTML app, anything with no recognised build config — exited here. The user was
|
|
68
|
+
// told to do something by hand and told nothing about whether their server was even up, whether
|
|
69
|
+
// the snippet had landed, or which url was checked. The runtime phase answers all three, and its
|
|
70
|
+
// answers are the actionable ones: "nothing is serving http://…", "the SDK is NOT in the page".
|
|
71
|
+
//
|
|
72
|
+
// The run still ends non-zero: the phase returns `ok: false` when no session appears, and a
|
|
73
|
+
// session appearing means the manual step WAS done and the app really did connect — which is a
|
|
74
|
+
// green worth reporting, not one to suppress.
|
|
75
|
+
// What a restart should do, decided and printed. Never performed: opening a terminal is not
|
|
76
|
+
// something a one-shot command should do behind a flag, and the half worth having is the refusal —
|
|
77
|
+
// `--resume` on an id with no transcript opens an EMPTY conversation that looks exactly like it
|
|
78
|
+
// worked. See relaunch.ts.
|
|
79
|
+
if (true === parsed.relaunch) {
|
|
80
|
+
io.print(relaunchDecision({
|
|
81
|
+
...(undefined === process.env['CLAUDE_CODE_SESSION_ID']
|
|
82
|
+
? {}
|
|
83
|
+
: { claudeSessionId: process.env['CLAUDE_CODE_SESSION_ID'] }),
|
|
84
|
+
...(undefined === codexSessionFor(cwd) ? {} : { codexSessionId: codexSessionFor(cwd) }),
|
|
85
|
+
transcriptExists: claudeTranscriptExists,
|
|
86
|
+
cwd,
|
|
87
|
+
}).message);
|
|
88
|
+
io.print('');
|
|
89
|
+
}
|
|
90
|
+
// Before the connect wait, never after: a bridge held by a stranger makes a session impossible,
|
|
91
|
+
// so going ahead spends the entire budget and then reports what reads as an instrumentation
|
|
92
|
+
// problem — the one place that is fine. See bridge-port.ts.
|
|
93
|
+
const refusal = bridgeOccupied(await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus }), port);
|
|
94
|
+
if (refusal !== undefined) {
|
|
95
|
+
io.print(refusal);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
return runSetupCommand({
|
|
99
|
+
appDir: context.appDir,
|
|
100
|
+
invokedAt: cwd,
|
|
101
|
+
bridgePort: port,
|
|
102
|
+
env: collectEnv(parsed.env ?? []),
|
|
103
|
+
openBrowser: false !== parsed.open,
|
|
104
|
+
drive: false !== parsed.drive,
|
|
105
|
+
registerAgents: wantsAgents(parsed),
|
|
106
|
+
escalateWeakFlow: true,
|
|
107
|
+
driveBudgetUsd: DEFAULT_DRIVE_BUDGET_USD,
|
|
108
|
+
phaseTimeoutMs: undefined === parsed.timeoutSeconds
|
|
109
|
+
? DEFAULT_PHASE_TIMEOUT_MS
|
|
110
|
+
: parsed.timeoutSeconds * 1000,
|
|
111
|
+
// Only when the caller actually said so — see connectBudgetMs. Omitted, the shape's policy
|
|
112
|
+
// keeps deciding, so nobody who passed nothing waits less than they used to.
|
|
113
|
+
...(undefined === parsed.timeoutSeconds
|
|
114
|
+
? {}
|
|
115
|
+
: { connectBudgetMs: parsed.timeoutSeconds * 1000 }),
|
|
116
|
+
pollMs: POLL_MS,
|
|
117
|
+
...(undefined === context.devCommand ? {} : { devCommand: context.devCommand }),
|
|
118
|
+
...(undefined === parsed.flow ? {} : { flow: parsed.flow }),
|
|
119
|
+
...(undefined === parsed.url ? {} : { suppliedUrl: parsed.url }),
|
|
120
|
+
...(undefined === parsed.driveModel ? {} : { driveModel: parsed.driveModel }),
|
|
121
|
+
}, (line) => io.print(line)).then((outcome) => {
|
|
122
|
+
// One object, so an agent reads a result instead of interpreting a report.
|
|
123
|
+
if (true === parsed.json) {
|
|
124
|
+
process.stdout.write(`${JSON.stringify(outcome, null, 2)}\n`);
|
|
125
|
+
if (!outcome.ok)
|
|
126
|
+
process.exit(1);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
io.print('');
|
|
130
|
+
// The drive's own account, whether it ended well or not. Discarding it left a run that reached
|
|
131
|
+
// the drive, produced something, and told the reader nothing about what it found.
|
|
132
|
+
if (undefined !== outcome.verdict && '' !== outcome.verdict) {
|
|
133
|
+
io.print(outcome.verdict);
|
|
134
|
+
io.print('');
|
|
135
|
+
}
|
|
136
|
+
if (outcome.ok && !outcome.flowSaved) {
|
|
137
|
+
// Success, and no flow. Saying "a flow was driven" here would replace a wrong exit code with
|
|
138
|
+
// a wrong sentence, which is the worse of the two: the exit code is read by CI and the
|
|
139
|
+
// sentence is read by a person deciding whether their app is verified. It is not.
|
|
140
|
+
io.print(`✓ ${outcome.url ?? 'the app'} is instrumented and connected — but NOT verified.`);
|
|
141
|
+
for (const [i, step] of outcome.fallback.entries())
|
|
142
|
+
io.print(` ${String(i + 1)}. ${step}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (outcome.ok) {
|
|
146
|
+
io.print(`✓ setup complete — ${outcome.url ?? 'the app'} is instrumented and a flow was driven.`);
|
|
147
|
+
// A passing flow shows the mechanism working. What the run SAW is the part nobody can get for
|
|
148
|
+
// themselves, and it deserves a line of its own rather than a paragraph that gets skimmed.
|
|
149
|
+
io.print(' Read the FINDINGS above before moving on: a flow can pass with a failed request or a ' +
|
|
150
|
+
'console error behind it, and that is the app, not the check.');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// A run that produced no verdict did not succeed, and the exit code is the one place a caller
|
|
154
|
+
// reads that without parsing anything.
|
|
155
|
+
io.print('⚠ setup did not finish. To carry on from here:');
|
|
156
|
+
for (const [i, step] of outcome.fallback.entries())
|
|
157
|
+
io.print(` ${i + 1}. ${step}`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Take the dev server with us when setup is interrupted.
|
|
3
|
+
*
|
|
4
|
+
* `setup` starts a dev server the user did not start — detached, in its own process group — and
|
|
5
|
+
* stops it in a `finally`. A `finally` does not run on SIGINT: the process is terminated. So Ctrl-C
|
|
6
|
+
* during setup left that server running forever, holding a port nobody could account for, and the
|
|
7
|
+
* next run's port probe then found a stranger on it.
|
|
8
|
+
*
|
|
9
|
+
* It hid behind an earlier bug. `init` used to exit before the dev server was ever started whenever
|
|
10
|
+
* instrumentation needed a manual step, so the orphan check found nothing and passed for the wrong
|
|
11
|
+
* reason. Letting the runtime phase run is what exposed this.
|
|
12
|
+
*/
|
|
13
|
+
/** The parts of `process` this needs, so the behaviour is testable without signalling anything. */
|
|
14
|
+
export interface SignalTarget {
|
|
15
|
+
on(signal: 'SIGINT' | 'SIGTERM', handler: () => void): void;
|
|
16
|
+
off(signal: 'SIGINT' | 'SIGTERM', handler: () => void): void;
|
|
17
|
+
exit(code: number): void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Run `stop` if this process is interrupted, then leave with the signal's own code.
|
|
21
|
+
*
|
|
22
|
+
* Returns a disposer, and callers must use it: `init` keeps running after setup, and a handler left
|
|
23
|
+
* behind would tear down a server it no longer owns.
|
|
24
|
+
*/
|
|
25
|
+
export declare function stopOnInterrupt(stop: () => void, target: SignalTarget): () => void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Take the dev server with us when setup is interrupted.
|
|
3
|
+
*
|
|
4
|
+
* `setup` starts a dev server the user did not start — detached, in its own process group — and
|
|
5
|
+
* stops it in a `finally`. A `finally` does not run on SIGINT: the process is terminated. So Ctrl-C
|
|
6
|
+
* during setup left that server running forever, holding a port nobody could account for, and the
|
|
7
|
+
* next run's port probe then found a stranger on it.
|
|
8
|
+
*
|
|
9
|
+
* It hid behind an earlier bug. `init` used to exit before the dev server was ever started whenever
|
|
10
|
+
* instrumentation needed a manual step, so the orphan check found nothing and passed for the wrong
|
|
11
|
+
* reason. Letting the runtime phase run is what exposed this.
|
|
12
|
+
*/
|
|
13
|
+
/** 128 + the signal number, which is what a shell reports — so a caller can tell Ctrl-C from a failure. */
|
|
14
|
+
const EXIT_CODE = { SIGINT: 130, SIGTERM: 143 };
|
|
15
|
+
/**
|
|
16
|
+
* Run `stop` if this process is interrupted, then leave with the signal's own code.
|
|
17
|
+
*
|
|
18
|
+
* Returns a disposer, and callers must use it: `init` keeps running after setup, and a handler left
|
|
19
|
+
* behind would tear down a server it no longer owns.
|
|
20
|
+
*/
|
|
21
|
+
export function stopOnInterrupt(stop, target) {
|
|
22
|
+
// Once only. `stop` is not idempotent — it kills a process GROUP — and on a recycled pid a second
|
|
23
|
+
// kill is somebody else's process.
|
|
24
|
+
let done = false;
|
|
25
|
+
const handlers = ['SIGINT', 'SIGTERM'].map((signal) => {
|
|
26
|
+
const handler = () => {
|
|
27
|
+
if (!done) {
|
|
28
|
+
done = true;
|
|
29
|
+
stop();
|
|
30
|
+
}
|
|
31
|
+
target.exit(EXIT_CODE[signal]);
|
|
32
|
+
};
|
|
33
|
+
target.on(signal, handler);
|
|
34
|
+
return { signal, handler };
|
|
35
|
+
});
|
|
36
|
+
return () => {
|
|
37
|
+
for (const { signal, handler } of handlers)
|
|
38
|
+
target.off(signal, handler);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing a license key where the CLI will find it, without leaking it.
|
|
3
|
+
*
|
|
4
|
+
* The instructions used to ask an agent to do this by hand in three steps: append to `.env`, check
|
|
5
|
+
* `.gitignore`, confirm. Every one of those is deterministic, and the second is the one that costs
|
|
6
|
+
* something when skipped — a license key committed to git is a leaked credential, and it stays
|
|
7
|
+
* leaked after it is deleted.
|
|
8
|
+
*
|
|
9
|
+
* The key is never printed, never returned, and never put in a result object. The only thing said
|
|
10
|
+
* out loud is that a key was written.
|
|
11
|
+
*/
|
|
12
|
+
export interface LicenseIo {
|
|
13
|
+
readonly exists: (path: string) => boolean;
|
|
14
|
+
readonly readFile: (path: string) => string;
|
|
15
|
+
readonly writeFile: (path: string, contents: string) => void;
|
|
16
|
+
}
|
|
17
|
+
export declare const LicenseWrite: {
|
|
18
|
+
readonly WRITTEN: "written";
|
|
19
|
+
readonly ALREADY: "already";
|
|
20
|
+
readonly REPLACED: "replaced";
|
|
21
|
+
};
|
|
22
|
+
export type LicenseWrite = (typeof LicenseWrite)[keyof typeof LicenseWrite];
|
|
23
|
+
export interface LicenseResult {
|
|
24
|
+
readonly action: LicenseWrite;
|
|
25
|
+
/** True when `.gitignore` had to be changed so the key cannot be committed. */
|
|
26
|
+
readonly gitignoreUpdated: boolean;
|
|
27
|
+
/** Safe to print. Never contains the key. */
|
|
28
|
+
readonly message: string;
|
|
29
|
+
}
|
|
30
|
+
/** Whether `.gitignore` already covers `.env`, allowing for the forms people actually write. */
|
|
31
|
+
export declare function gitignoreCoversEnv(contents: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Put the key in `.env`, and make sure `.env` cannot be committed.
|
|
34
|
+
*
|
|
35
|
+
* An existing assignment is REPLACED rather than appended to: two `RETICLE_LICENSE_KEY` lines is a
|
|
36
|
+
* file where the answer depends on which one the parser reads last, and nobody should have to know
|
|
37
|
+
* which that is.
|
|
38
|
+
*/
|
|
39
|
+
export declare function writeLicenseKey(dir: string, key: string, io: LicenseIo): LicenseResult;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing a license key where the CLI will find it, without leaking it.
|
|
3
|
+
*
|
|
4
|
+
* The instructions used to ask an agent to do this by hand in three steps: append to `.env`, check
|
|
5
|
+
* `.gitignore`, confirm. Every one of those is deterministic, and the second is the one that costs
|
|
6
|
+
* something when skipped — a license key committed to git is a leaked credential, and it stays
|
|
7
|
+
* leaked after it is deleted.
|
|
8
|
+
*
|
|
9
|
+
* The key is never printed, never returned, and never put in a result object. The only thing said
|
|
10
|
+
* out loud is that a key was written.
|
|
11
|
+
*/
|
|
12
|
+
const ENV_FILE = '.env';
|
|
13
|
+
const GITIGNORE = '.gitignore';
|
|
14
|
+
const ENV_VAR = 'RETICLE_LICENSE_KEY';
|
|
15
|
+
export const LicenseWrite = {
|
|
16
|
+
WRITTEN: 'written',
|
|
17
|
+
ALREADY: 'already',
|
|
18
|
+
REPLACED: 'replaced',
|
|
19
|
+
};
|
|
20
|
+
const joinPath = (dir, file) => `${dir.replace(/\/$/, '')}/${file}`;
|
|
21
|
+
/** Whether `.gitignore` already covers `.env`, allowing for the forms people actually write. */
|
|
22
|
+
export function gitignoreCoversEnv(contents) {
|
|
23
|
+
return contents
|
|
24
|
+
.split('\n')
|
|
25
|
+
.map((l) => l.trim())
|
|
26
|
+
.some((l) => ENV_FILE === l || '.env*' === l || '*.env' === l || '/.env' === l);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Put the key in `.env`, and make sure `.env` cannot be committed.
|
|
30
|
+
*
|
|
31
|
+
* An existing assignment is REPLACED rather than appended to: two `RETICLE_LICENSE_KEY` lines is a
|
|
32
|
+
* file where the answer depends on which one the parser reads last, and nobody should have to know
|
|
33
|
+
* which that is.
|
|
34
|
+
*/
|
|
35
|
+
export function writeLicenseKey(dir, key, io) {
|
|
36
|
+
const envPath = joinPath(dir, ENV_FILE);
|
|
37
|
+
const existing = io.exists(envPath) ? io.readFile(envPath) : '';
|
|
38
|
+
const lines = '' === existing ? [] : existing.split('\n');
|
|
39
|
+
const at = lines.findIndex((l) => l.trimStart().startsWith(`${ENV_VAR}=`));
|
|
40
|
+
const already = -1 !== at && lines[at]?.trim() === `${ENV_VAR}=${key}`;
|
|
41
|
+
let action = LicenseWrite.WRITTEN;
|
|
42
|
+
if (already)
|
|
43
|
+
action = LicenseWrite.ALREADY;
|
|
44
|
+
else if (-1 !== at) {
|
|
45
|
+
lines[at] = `${ENV_VAR}=${key}`;
|
|
46
|
+
action = LicenseWrite.REPLACED;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
if (0 < lines.length && '' !== lines[lines.length - 1])
|
|
50
|
+
lines.push('');
|
|
51
|
+
lines.push(`${ENV_VAR}=${key}`, '');
|
|
52
|
+
}
|
|
53
|
+
if (LicenseWrite.ALREADY !== action)
|
|
54
|
+
io.writeFile(envPath, lines.join('\n'));
|
|
55
|
+
// The order matters: a key in git is leaked whether or not the file is later removed.
|
|
56
|
+
const ignorePath = joinPath(dir, GITIGNORE);
|
|
57
|
+
const ignore = io.exists(ignorePath) ? io.readFile(ignorePath) : '';
|
|
58
|
+
let gitignoreUpdated = false;
|
|
59
|
+
if (!gitignoreCoversEnv(ignore)) {
|
|
60
|
+
const next = '' === ignore ? `${ENV_FILE}\n` : `${ignore.replace(/\n*$/, '\n')}${ENV_FILE}\n`;
|
|
61
|
+
io.writeFile(ignorePath, next);
|
|
62
|
+
gitignoreUpdated = true;
|
|
63
|
+
}
|
|
64
|
+
const wrote = LicenseWrite.ALREADY === action
|
|
65
|
+
? `license key already in ${ENV_FILE}`
|
|
66
|
+
: `license key written to ${ENV_FILE}`;
|
|
67
|
+
return {
|
|
68
|
+
action,
|
|
69
|
+
gitignoreUpdated,
|
|
70
|
+
message: gitignoreUpdated ? `${wrote}, and ${ENV_FILE} added to ${GITIGNORE}` : wrote,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which ports a process tree is listening on, parsed from the tools that can answer that.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from the commands that produce the text so the Windows behaviour is testable from
|
|
5
|
+
* any machine. That is not a stylistic preference: port discovery on Windows returned nothing at
|
|
6
|
+
* all until this was extracted, so a dev server that prints no parseable URL was undiscoverable
|
|
7
|
+
* there — and `react-scripts` prints no parseable URL outside a tty, which makes it the majority
|
|
8
|
+
* platform's most common React setup.
|
|
9
|
+
*/
|
|
10
|
+
/** One LISTENING socket and the process that owns it. */
|
|
11
|
+
export interface Listener {
|
|
12
|
+
readonly port: number;
|
|
13
|
+
readonly pid: number;
|
|
14
|
+
}
|
|
15
|
+
/** `lsof -a -p <pids> -iTCP -sTCP:LISTEN -P -n`, NAME column only. */
|
|
16
|
+
export declare function parseLsofPorts(out: string): number[];
|
|
17
|
+
/**
|
|
18
|
+
* `netstat -ano` on Windows.
|
|
19
|
+
*
|
|
20
|
+
* The port is whatever follows the LAST colon: `[::]:3000` has three of them, so splitting on ':'
|
|
21
|
+
* and taking index 1 reads every IPv6 row as port 0 and silently drops half the table.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseNetstatListeners(out: string): Listener[];
|
|
24
|
+
/** One (pid, parent) pair, as `wmic process get ParentProcessId,ProcessId` prints them. */
|
|
25
|
+
export interface ProcessPair {
|
|
26
|
+
readonly pid: number;
|
|
27
|
+
readonly ppid: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function parseWmicProcesses(out: string): ProcessPair[];
|
|
30
|
+
/**
|
|
31
|
+
* `root` and everything descended from it.
|
|
32
|
+
*
|
|
33
|
+
* The dev server is almost never the process we spawned: `npm run dev` starts a shell, which starts
|
|
34
|
+
* node, which binds the port. Matching only the pid we hold finds nothing, and nothing is
|
|
35
|
+
* indistinguishable from "no server".
|
|
36
|
+
*/
|
|
37
|
+
export declare function descendants(pairs: readonly ProcessPair[], root: number): number[];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which ports a process tree is listening on, parsed from the tools that can answer that.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from the commands that produce the text so the Windows behaviour is testable from
|
|
5
|
+
* any machine. That is not a stylistic preference: port discovery on Windows returned nothing at
|
|
6
|
+
* all until this was extracted, so a dev server that prints no parseable URL was undiscoverable
|
|
7
|
+
* there — and `react-scripts` prints no parseable URL outside a tty, which makes it the majority
|
|
8
|
+
* platform's most common React setup.
|
|
9
|
+
*/
|
|
10
|
+
/** `lsof -a -p <pids> -iTCP -sTCP:LISTEN -P -n`, NAME column only. */
|
|
11
|
+
export function parseLsofPorts(out) {
|
|
12
|
+
const ports = new Set();
|
|
13
|
+
for (const m of String(out).matchAll(/:(\d+)\s*$/gm))
|
|
14
|
+
ports.add(Number(m[1]));
|
|
15
|
+
return [...ports];
|
|
16
|
+
}
|
|
17
|
+
const NETSTAT_PROTO = 'TCP';
|
|
18
|
+
const NETSTAT_STATE = 'LISTENING';
|
|
19
|
+
const NETSTAT_MIN_COLUMNS = 5;
|
|
20
|
+
/**
|
|
21
|
+
* `netstat -ano` on Windows.
|
|
22
|
+
*
|
|
23
|
+
* The port is whatever follows the LAST colon: `[::]:3000` has three of them, so splitting on ':'
|
|
24
|
+
* and taking index 1 reads every IPv6 row as port 0 and silently drops half the table.
|
|
25
|
+
*/
|
|
26
|
+
export function parseNetstatListeners(out) {
|
|
27
|
+
const rows = [];
|
|
28
|
+
for (const line of String(out).split(/\r?\n/)) {
|
|
29
|
+
const f = line.trim().split(/\s+/);
|
|
30
|
+
if (f.length < NETSTAT_MIN_COLUMNS)
|
|
31
|
+
continue;
|
|
32
|
+
if (f[0]?.toUpperCase() !== NETSTAT_PROTO || f[3]?.toUpperCase() !== NETSTAT_STATE)
|
|
33
|
+
continue;
|
|
34
|
+
const local = f[1] ?? '';
|
|
35
|
+
const port = Number(local.slice(local.lastIndexOf(':') + 1));
|
|
36
|
+
const pid = Number(f[4]);
|
|
37
|
+
if (Number.isInteger(port) && port > 0 && Number.isInteger(pid))
|
|
38
|
+
rows.push({ port, pid });
|
|
39
|
+
}
|
|
40
|
+
return rows;
|
|
41
|
+
}
|
|
42
|
+
export function parseWmicProcesses(out) {
|
|
43
|
+
const pairs = [];
|
|
44
|
+
for (const line of String(out).split(/\r?\n/)) {
|
|
45
|
+
const f = line.trim().split(/\s+/);
|
|
46
|
+
if (f.length !== 2)
|
|
47
|
+
continue;
|
|
48
|
+
const ppid = Number(f[0]);
|
|
49
|
+
const pid = Number(f[1]);
|
|
50
|
+
if (Number.isInteger(pid) && Number.isInteger(ppid))
|
|
51
|
+
pairs.push({ pid, ppid });
|
|
52
|
+
}
|
|
53
|
+
return pairs;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `root` and everything descended from it.
|
|
57
|
+
*
|
|
58
|
+
* The dev server is almost never the process we spawned: `npm run dev` starts a shell, which starts
|
|
59
|
+
* node, which binds the port. Matching only the pid we hold finds nothing, and nothing is
|
|
60
|
+
* indistinguishable from "no server".
|
|
61
|
+
*/
|
|
62
|
+
export function descendants(pairs, root) {
|
|
63
|
+
const byParent = new Map();
|
|
64
|
+
for (const { pid, ppid } of pairs) {
|
|
65
|
+
const kids = byParent.get(ppid);
|
|
66
|
+
if (kids === undefined)
|
|
67
|
+
byParent.set(ppid, [pid]);
|
|
68
|
+
else
|
|
69
|
+
kids.push(pid);
|
|
70
|
+
}
|
|
71
|
+
const found = new Set([root]);
|
|
72
|
+
const queue = [root];
|
|
73
|
+
while (queue.length > 0) {
|
|
74
|
+
const next = queue.shift();
|
|
75
|
+
if (next === undefined)
|
|
76
|
+
continue;
|
|
77
|
+
for (const child of byParent.get(next) ?? []) {
|
|
78
|
+
if (found.has(child))
|
|
79
|
+
continue;
|
|
80
|
+
found.add(child);
|
|
81
|
+
queue.push(child);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return [...found];
|
|
85
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The real world, bound to the injected shape `runSetupPhases` expects.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is a thin binding to something that already exists — `openInBrowser`,
|
|
5
|
+
* `fetchStatus`, the driver table — with one exception: the dev server. Nothing else in the package
|
|
6
|
+
* owns a spawned process, and owning one properly is most of this file.
|
|
7
|
+
*
|
|
8
|
+
* The promise this makes about that process: setup leaves it running on success, because an
|
|
9
|
+
* instrumented app the user can watch IS the deliverable, and stops it on every other ending. A
|
|
10
|
+
* server nobody started, holding a port nobody can account for, surviving the terminal that spawned
|
|
11
|
+
* it, is the failure this file exists to avoid — measured, an interrupted run used to leave one
|
|
12
|
+
* listening indefinitely.
|
|
13
|
+
*/
|
|
14
|
+
import type { CandidateSession } from './session-pick.js';
|
|
15
|
+
import type { PageProbe } from './page-probe.js';
|
|
16
|
+
/** The dev server this process started, and the only one it will ever stop. */
|
|
17
|
+
export declare class OwnedDevServer {
|
|
18
|
+
private child;
|
|
19
|
+
private buffer;
|
|
20
|
+
private lastOutputAt;
|
|
21
|
+
private handedOver;
|
|
22
|
+
start(command: string, cwd: string, env: Readonly<Record<string, string>>): void;
|
|
23
|
+
/** The process group leader, for a caller that has to clean up what it handed over. */
|
|
24
|
+
pid(): number | undefined;
|
|
25
|
+
output(): string;
|
|
26
|
+
exited(): boolean;
|
|
27
|
+
quietForMs(): number;
|
|
28
|
+
/** Ports anything in this server's process tree is listening on. */
|
|
29
|
+
listeningPorts(): number[];
|
|
30
|
+
/** Hand the running server to the user. After this, `stop()` does nothing. */
|
|
31
|
+
/**
|
|
32
|
+
* Leave the server running and stop holding it.
|
|
33
|
+
*
|
|
34
|
+
* `unref` on the child is NOT enough: its stdout and stderr are pipes owned by this process, and
|
|
35
|
+
* an open pipe keeps the event loop alive on its own. So `init` printed "setup complete" and then
|
|
36
|
+
* hung until something killed it, turning a successful run into a non-zero exit — which is
|
|
37
|
+
* exactly what the install gate reported, on a run whose every other assertion passed.
|
|
38
|
+
*
|
|
39
|
+
* The streams are unref'd rather than destroyed: the server is still writing to them, and
|
|
40
|
+
* destroying the read end of a live pipe risks an EPIPE in somebody else's dev server.
|
|
41
|
+
*/
|
|
42
|
+
handOver(): void;
|
|
43
|
+
stop(): void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Fetch the page and say what came back.
|
|
47
|
+
*
|
|
48
|
+
* A refused certificate is reported separately because the server ANSWERED: a self-signed dev cert
|
|
49
|
+
* is an ordinary local setup, and calling it "nothing is listening" sends somebody to start a
|
|
50
|
+
* server that is already running.
|
|
51
|
+
*/
|
|
52
|
+
export declare function probePage(url: string): Promise<PageProbe>;
|
|
53
|
+
/** Sessions the daemon is holding, in the shape the picker reads. */
|
|
54
|
+
export declare function listSessions(port: number): Promise<CandidateSession[]>;
|
|
55
|
+
/** Whether this project has a saved flow, wherever the project keeps one. */
|
|
56
|
+
export declare function flowsSaved(roots: readonly string[]): boolean;
|
|
57
|
+
/** On PATH at all. The separate question of whether it RUNS is asked by chooseDriver. */
|
|
58
|
+
export declare function binaryExists(bin: string): boolean;
|
|
59
|
+
/** Whether a directory looks like a project setup has already wired. */
|
|
60
|
+
export declare function alreadyWired(dir: string): boolean;
|