@reticlehq/server 2.8.0 → 2.10.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/SKILL.md +130 -813
- package/dist/bridge/bridge.d.ts +23 -0
- package/dist/bridge/bridge.js +80 -2
- package/dist/bridge/bridge.test-harness.d.ts +5 -0
- package/dist/bridge/bridge.test-harness.js +6 -4
- package/dist/capsule/capsule-store.d.ts +2 -2
- package/dist/capsule/causal-summary.d.ts +16 -1
- package/dist/capsule/causal-summary.js +2 -1
- package/dist/cli/cli-doctor.js +41 -13
- package/dist/cli/cli-flow-commands.js +8 -8
- package/dist/cli/cli-launch.d.ts +0 -17
- package/dist/cli/cli-launch.js +0 -28
- package/dist/cli/cli-parse.d.ts +1 -1
- package/dist/cli/cli-parse.js +1 -1
- package/dist/cli/cli-port.d.ts +24 -0
- package/dist/cli/cli-port.js +66 -35
- package/dist/cli/cli-verify.d.ts +21 -0
- package/dist/cli/cli-verify.js +45 -1
- package/dist/cli/config-discovery.d.ts +35 -0
- package/dist/cli/config-discovery.js +163 -0
- package/dist/cli/doctor-daemon-line.js +4 -1
- package/dist/cli/doctor-project-line.d.ts +37 -0
- package/dist/cli/doctor-project-line.js +35 -0
- package/dist/cli/doctor-rows.d.ts +51 -0
- package/dist/cli/doctor-rows.js +54 -0
- package/dist/cli/doctor-sessions-line.js +6 -3
- package/dist/cli/drive-attach.d.ts +99 -0
- package/dist/cli/drive-attach.js +203 -0
- package/dist/cli/drive-command.d.ts +13 -0
- package/dist/cli/drive-command.js +103 -0
- package/dist/cli/open-note.d.ts +20 -0
- package/dist/cli/open-note.js +37 -0
- package/dist/cli/status-next-action.d.ts +37 -0
- package/dist/cli/status-next-action.js +61 -0
- package/dist/cli.js +65 -70
- package/dist/crawl/crawl.d.ts +7 -2
- package/dist/crawl/crawl.js +52 -9
- package/dist/ee/audit-log.js +2 -2
- package/dist/events/body-failures.d.ts +2 -2
- package/dist/events/contradiction-folds.d.ts +45 -0
- package/dist/events/contradiction-folds.js +42 -0
- package/dist/events/contradictions.d.ts +51 -1
- package/dist/events/contradictions.js +26 -7
- package/dist/events/declared.d.ts +45 -0
- package/dist/events/declared.js +73 -0
- package/dist/events/echo-mismatch.d.ts +9 -3
- package/dist/events/echo-mismatch.js +33 -5
- package/dist/events/event-filters.d.ts +1 -1
- package/dist/events/event-filters.js +7 -4
- package/dist/events/predicate-eval.d.ts +17 -91
- package/dist/events/predicate-eval.js +85 -326
- package/dist/events/predicate-parse.js +31 -3
- package/dist/events/predicate-route.d.ts +32 -0
- package/dist/events/predicate-route.js +102 -0
- package/dist/events/predicate-schema.d.ts +99 -0
- package/dist/events/predicate-schema.js +309 -0
- package/dist/events/predicate.d.ts +8 -0
- package/dist/events/predicate.js +127 -5
- package/dist/events/stale-response.d.ts +2 -3
- package/dist/events/stale-response.js +49 -2
- package/dist/events/unit-mismatch.d.ts +2 -2
- package/dist/flows/annotate-tools.js +3 -0
- package/dist/flows/annotate.js +18 -4
- package/dist/flows/coverage.d.ts +38 -5
- package/dist/flows/coverage.js +30 -4
- package/dist/flows/decision.d.ts +15 -0
- package/dist/flows/decision.js +1 -1
- package/dist/flows/flow-replay-run.js +8 -1
- package/dist/flows/flow-tools.js +9 -1
- package/dist/flows/replay.js +3 -0
- package/dist/flows/verify-change-tools.js +1 -1
- package/dist/honesty/accepted-write.d.ts +3 -0
- package/dist/honesty/accepted-write.js +8 -3
- package/dist/honesty/already-true.js +15 -0
- package/dist/honesty/blind-spots.js +4 -0
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +1 -0
- package/dist/honesty/uncaptured-bodies.d.ts +4 -2
- package/dist/honesty/uncaptured-bodies.js +9 -2
- package/dist/honesty/unread-outcome.d.ts +8 -0
- package/dist/honesty/unread-outcome.js +36 -9
- package/dist/honesty/unsettled.d.ts +9 -0
- package/dist/honesty/unsettled.js +7 -3
- package/dist/honesty/verified.d.ts +29 -3
- package/dist/honesty/verified.js +67 -8
- package/dist/http-server.d.ts +19 -0
- package/dist/http-server.js +202 -4
- package/dist/impact/impact-recorder.d.ts +23 -0
- package/dist/impact/impact-recorder.js +76 -0
- package/dist/impact/impact-store.d.ts +47 -0
- package/dist/impact/impact-store.js +171 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +60 -10
- package/dist/init/agent-rules.d.ts +20 -0
- package/dist/init/agent-rules.js +36 -0
- package/dist/init/astro-patch.js +27 -1
- package/dist/init/confirm.d.ts +21 -3
- package/dist/init/confirm.js +32 -5
- package/dist/init/connect-steps.d.ts +2 -0
- package/dist/init/connect-steps.js +50 -0
- package/dist/init/csp-check.d.ts +38 -0
- package/dist/init/csp-check.js +92 -0
- package/dist/init/csp-doctor.d.ts +25 -0
- package/dist/init/csp-doctor.js +49 -0
- package/dist/init/cursor.js +6 -9
- package/dist/init/detect.d.ts +8 -0
- package/dist/init/detect.js +33 -3
- package/dist/init/mcp-clients.d.ts +1 -0
- package/dist/init/mcp-clients.js +51 -7
- package/dist/init/mcp.d.ts +16 -2
- package/dist/init/mcp.js +23 -1
- package/dist/init/plan-framework.d.ts +10 -0
- package/dist/init/plan-framework.js +25 -1
- package/dist/init/plan.d.ts +7 -2
- package/dist/init/plan.js +66 -30
- package/dist/init/run.js +11 -5
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +12 -4
- package/dist/init/snippets.d.ts +1 -1
- package/dist/init/snippets.js +35 -7
- package/dist/init/vite-config.js +24 -2
- package/dist/input/network-mock-tools.js +8 -1
- package/dist/input/scroll-find.d.ts +6 -0
- package/dist/input/scroll-find.js +17 -2
- package/dist/input/scroll-tools.js +4 -0
- package/dist/journal/attach-journal.d.ts +9 -1
- package/dist/journal/attach-journal.js +13 -2
- package/dist/journal/journal-query.d.ts +13 -0
- package/dist/journal/journal-query.js +28 -3
- package/dist/journal/workspace-gitignore.d.ts +10 -0
- package/dist/journal/workspace-gitignore.js +59 -0
- package/dist/license/license.d.ts +69 -4
- package/dist/license/license.js +84 -15
- package/dist/mcp/mcp-proxy.d.ts +7 -93
- package/dist/mcp/mcp-proxy.js +183 -250
- package/dist/mcp/mcp.d.ts +27 -2
- package/dist/mcp/mcp.js +77 -36
- package/dist/mcp/proxy-daemon-probe.d.ts +8 -0
- package/dist/mcp/proxy-daemon-probe.js +48 -0
- package/dist/mcp/proxy-handshake.d.ts +82 -1
- package/dist/mcp/proxy-handshake.js +110 -3
- package/dist/mcp/proxy-lifecycle.d.ts +10 -1
- package/dist/mcp/proxy-lifecycle.js +11 -2
- package/dist/mcp/proxy-log.d.ts +63 -0
- package/dist/mcp/proxy-log.js +132 -0
- package/dist/mcp/server-instructions.d.ts +17 -0
- package/dist/mcp/server-instructions.js +56 -0
- package/dist/mcp/shared-params.d.ts +27 -0
- package/dist/mcp/shared-params.js +36 -0
- package/dist/mcp/sse-frame-parser.d.ts +19 -0
- package/dist/mcp/sse-frame-parser.js +41 -0
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/session/connection-memory.d.ts +59 -0
- package/dist/session/connection-memory.js +108 -0
- package/dist/session/dev-command.d.ts +26 -0
- package/dist/session/dev-command.js +89 -0
- package/dist/session/dev-server-probe.d.ts +68 -0
- package/dist/session/dev-server-probe.js +102 -7
- package/dist/session/last-act.d.ts +26 -0
- package/dist/session/last-act.js +35 -1
- package/dist/session/live-control-tools.js +42 -5
- package/dist/session/no-session-diagnosis.d.ts +55 -2
- package/dist/session/no-session-diagnosis.js +199 -22
- package/dist/session/no-session-next-action.d.ts +46 -0
- package/dist/session/no-session-next-action.js +85 -0
- package/dist/session/no-session-watch.d.ts +40 -1
- package/dist/session/no-session-watch.js +172 -11
- package/dist/session/pending-commands.d.ts +12 -0
- package/dist/session/pending-commands.js +13 -1
- package/dist/session/session-manager.d.ts +4 -0
- package/dist/session/session-manager.js +35 -5
- package/dist/session/session-readiness.js +1 -1
- package/dist/session/session.d.ts +10 -1
- package/dist/session/session.js +146 -5
- package/dist/telemetry/app-instrumented.d.ts +8 -0
- package/dist/telemetry/app-instrumented.js +10 -0
- package/dist/telemetry/bug-found.d.ts +18 -0
- package/dist/telemetry/bug-found.js +14 -0
- package/dist/telemetry/daemon-telemetry.js +29 -0
- package/dist/telemetry/install-source.d.ts +20 -0
- package/dist/telemetry/install-source.js +34 -0
- package/dist/telemetry/instrumentation-stall.d.ts +57 -0
- package/dist/telemetry/instrumentation-stall.js +103 -0
- package/dist/telemetry/license-activation.d.ts +48 -0
- package/dist/telemetry/license-activation.js +57 -0
- package/dist/telemetry/session-metrics.d.ts +8 -0
- package/dist/telemetry/session-metrics.js +10 -0
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +23 -3
- package/dist/tools/act-tools.js +166 -29
- package/dist/tools/assert-verdict.js +32 -7
- package/dist/tools/browser-tools.js +10 -0
- package/dist/tools/dynamic-tools.js +17 -8
- package/dist/tools/error-recovery.js +10 -6
- package/dist/tools/feedback-tools.js +31 -6
- package/dist/tools/invoke-tool.d.ts +1 -1
- package/dist/tools/invoke-tool.js +30 -8
- package/dist/tools/lease-hint.d.ts +30 -8
- package/dist/tools/lease-hint.js +65 -14
- package/dist/tools/lease-tools.d.ts +11 -0
- package/dist/tools/lease-tools.js +141 -96
- package/dist/tools/observe-tools.js +9 -3
- package/dist/tools/react-grace.d.ts +4 -0
- package/dist/tools/react-grace.js +6 -3
- package/dist/tools/read-tools.js +47 -1
- package/dist/tools/resolve-target.d.ts +33 -0
- package/dist/tools/resolve-target.js +53 -0
- package/dist/tools/settle-in-flight.d.ts +15 -0
- package/dist/tools/settle-in-flight.js +42 -2
- package/dist/tools/snapshot-delta.d.ts +3 -0
- package/dist/tools/snapshot-delta.js +67 -6
- package/dist/tools/tool-kit.d.ts +38 -3
- package/dist/tools/tool-names.d.ts +2 -0
- package/dist/tools/tool-names.js +2 -0
- package/dist/tools/tool-surface.d.ts +60 -0
- package/dist/tools/tool-surface.js +84 -19
- package/dist/tools/tools.js +61 -5
- package/dist/tools/unadvertised-help.js +9 -6
- package/docs/README.md +1 -2
- package/docs/actions.mdx +41 -8
- package/docs/agent-cheatsheet.md +6 -4
- package/docs/architecture.md +6 -0
- package/docs/benchmarks.md +60 -12
- package/docs/best-practices.mdx +11 -6
- package/docs/capabilities.mdx +204 -57
- package/docs/cli/affected.mdx +6 -4
- package/docs/cli/capsules.mdx +9 -8
- package/docs/cli/cloud.mdx +30 -4
- package/docs/cli/doctor.mdx +41 -8
- package/docs/cli/drive.mdx +34 -15
- package/docs/cli/gate.mdx +6 -6
- package/docs/cli/help.mdx +8 -6
- package/docs/cli/hunt.mdx +20 -4
- package/docs/cli/identify.mdx +16 -2
- package/docs/cli/init.mdx +35 -8
- package/docs/cli/kill.mdx +5 -5
- package/docs/cli/license.mdx +1 -1
- package/docs/cli/open.mdx +6 -0
- package/docs/cli/restart.mdx +6 -6
- package/docs/cli/serve.mdx +10 -3
- package/docs/cli/status.mdx +29 -5
- package/docs/cli/stop.mdx +8 -0
- package/docs/cli/verify.mdx +20 -1
- package/docs/cli/version.mdx +1 -1
- package/docs/cli/watch.mdx +9 -2
- package/docs/cli.mdx +44 -22
- package/docs/debugging.md +3 -1
- package/docs/deploy-checks.md +31 -2
- package/docs/desktop.mdx +276 -80
- package/docs/docs.json +13 -12
- package/docs/enterprise.md +9 -3
- package/docs/faq.mdx +49 -35
- package/docs/fixtures.md +2 -0
- package/docs/flows.md +7 -5
- package/docs/for-agents.md +3 -3
- package/docs/frameworks.mdx +46 -7
- package/docs/gate-plan.md +4 -2
- package/docs/gates.md +8 -6
- package/docs/getting-started.md +53 -12
- package/docs/human-control.md +2 -0
- package/docs/index.mdx +43 -1
- package/docs/install-agentic.mdx +13 -4
- package/docs/install-manual.mdx +6 -4
- package/docs/instrumentation.mdx +7 -1
- package/docs/integration-patterns.md +3 -1
- package/docs/local-registry.md +4 -2
- package/docs/multi-agent-testing.md +3 -1
- package/docs/packages/babel-plugin.mdx +15 -6
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/core.mdx +26 -16
- package/docs/packages/electron.mdx +5 -7
- package/docs/packages/eslint-plugin.mdx +5 -3
- package/docs/packages/next.mdx +3 -5
- package/docs/packages/react.mdx +3 -3
- package/docs/packages/server.mdx +2 -2
- package/docs/packages/tauri.mdx +6 -6
- package/docs/packages/test.mdx +18 -7
- package/docs/packages/vite-plugin.mdx +4 -6
- package/docs/packages.mdx +14 -13
- package/docs/platform-integration.md +2 -0
- package/docs/predicates.mdx +76 -7
- package/docs/quickstart.mdx +51 -5
- package/docs/recipes.mdx +63 -23
- package/docs/skill-file.mdx +3 -1
- package/docs/state-management.mdx +2 -0
- package/docs/system-map.md +3 -1
- package/docs/telemetry-contract.md +33 -5
- package/docs/telemetry-events.mdx +63 -18
- package/docs/telemetry.md +8 -5
- package/docs/testing.md +35 -1
- package/docs/token-efficiency.md +8 -2
- package/docs/tools-act-and-wait.mdx +64 -14
- package/docs/tools-act-sequence.mdx +34 -17
- package/docs/tools-act.mdx +24 -11
- package/docs/tools-assert.mdx +69 -10
- package/docs/tools-console.mdx +11 -7
- package/docs/tools-inspect.mdx +11 -8
- package/docs/tools-navigate.mdx +41 -5
- package/docs/tools-network.mdx +42 -9
- package/docs/tools-observe.mdx +66 -10
- package/docs/tools-overview.mdx +26 -23
- package/docs/tools-query.mdx +52 -8
- package/docs/tools-session-and-feedback.mdx +35 -9
- package/docs/tools-sessions.mdx +39 -10
- package/docs/tools-snapshot.mdx +57 -3
- package/docs/tools-state.mdx +24 -1
- package/docs/tools-tools-and-run.mdx +55 -24
- package/docs/tools-wait-for.mdx +34 -15
- package/docs/troubleshooting.mdx +80 -10
- package/docs/usage.md +46 -24
- package/docs/vs-chrome-devtools-mcp.mdx +41 -0
- package/docs/vs-playwright-mcp.mdx +42 -2
- package/docs/vs-screenshots.mdx +42 -1
- package/docs/what-is-reticle.mdx +1 -1
- package/docs/why-reticle.mdx +3 -1
- package/package.json +5 -5
- package/docs/desktop-apps.md +0 -265
package/dist/cli/cli-port.js
CHANGED
|
@@ -8,6 +8,50 @@
|
|
|
8
8
|
* 4. RETICLE_DEFAULT_PORT (4400)
|
|
9
9
|
*/
|
|
10
10
|
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { dirname, resolve } from 'node:path';
|
|
12
|
+
/** The project config `reticle init` writes. */
|
|
13
|
+
export const RETICLE_CONFIG_BASENAME = '.reticle.json';
|
|
14
|
+
/**
|
|
15
|
+
* How far up to look for the project config before giving up.
|
|
16
|
+
*
|
|
17
|
+
* A cap rather than "walk to the filesystem root" so a process started somewhere unrelated cannot
|
|
18
|
+
* silently adopt a config from a distant ancestor and dial a port that belongs to another project.
|
|
19
|
+
* Deep enough for the layouts that actually broke: an app in `frontend/` or `apps/web/`, a git
|
|
20
|
+
* worktree checked out beside its main repo, and an agent whose cwd is a package inside a monorepo.
|
|
21
|
+
*/
|
|
22
|
+
const MAX_CONFIG_SEARCH_DEPTH = 6;
|
|
23
|
+
/**
|
|
24
|
+
* Find the nearest `.reticle.json` at or above `cwd`, and return its parsed contents.
|
|
25
|
+
*
|
|
26
|
+
* Every reader below used to join the basename onto `cwd` and stop there, so a process whose
|
|
27
|
+
* working directory was one level away from the config behaved exactly like a project that had
|
|
28
|
+
* never been through `init`: no port, no projectId, no journal setting, and a diagnostic that told
|
|
29
|
+
* the user to run an install they had already run. That is the shape of several field reports at
|
|
30
|
+
* once — an app wired in a subdirectory, an agent running from a repo root, a worktree beside its
|
|
31
|
+
* main checkout — and they are all the same missing walk.
|
|
32
|
+
*
|
|
33
|
+
* Returns undefined when nothing is found, which keeps every caller's existing default intact.
|
|
34
|
+
*/
|
|
35
|
+
export function findProjectConfig(cwd) {
|
|
36
|
+
let dir = resolve(cwd);
|
|
37
|
+
for (let depth = 0; depth <= MAX_CONFIG_SEARCH_DEPTH; depth += 1) {
|
|
38
|
+
try {
|
|
39
|
+
const raw = readFileSync(`${dir}/${RETICLE_CONFIG_BASENAME}`, 'utf8');
|
|
40
|
+
const config = JSON.parse(raw);
|
|
41
|
+
if ('object' === typeof config && config !== null && !Array.isArray(config)) {
|
|
42
|
+
return config;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Absent, unreadable, or not JSON at this level — keep walking.
|
|
47
|
+
}
|
|
48
|
+
const parent = dirname(dir);
|
|
49
|
+
if (parent === dir)
|
|
50
|
+
break;
|
|
51
|
+
dir = parent;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
11
55
|
/**
|
|
12
56
|
* Ports that belong to a DEV SERVER, not to Reticle's bridge. Next defaults to 3000, Vite to 5173,
|
|
13
57
|
* Astro to 4321, and so on.
|
|
@@ -53,18 +97,9 @@ export function devServerPortWarning(port) {
|
|
|
53
97
|
* Returns undefined if the file is absent, unreadable, or has no valid numeric port.
|
|
54
98
|
*/
|
|
55
99
|
export function readProjectPort(cwd) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if ('object' === typeof config && config !== null) {
|
|
60
|
-
const p = config['port'];
|
|
61
|
-
if ('number' === typeof p && Number.isInteger(p) && p > 0 && p < 65536)
|
|
62
|
-
return p;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
//.reticle.json absent or unreadable — fall through to default
|
|
67
|
-
}
|
|
100
|
+
const p = findProjectConfig(cwd)?.['port'];
|
|
101
|
+
if ('number' === typeof p && Number.isInteger(p) && p > 0 && p < 65536)
|
|
102
|
+
return p;
|
|
68
103
|
return undefined;
|
|
69
104
|
}
|
|
70
105
|
/**
|
|
@@ -73,18 +108,23 @@ export function readProjectPort(cwd) {
|
|
|
73
108
|
* undefined if the file is absent/unreadable or has no non-empty string projectId.
|
|
74
109
|
*/
|
|
75
110
|
export function readProjectId(cwd) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
111
|
+
const id = findProjectConfig(cwd)?.['projectId'];
|
|
112
|
+
if ('string' === typeof id && id.length > 0)
|
|
113
|
+
return id;
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The framework `reticle init` stamped into `.reticle.json`, when there is one.
|
|
118
|
+
*
|
|
119
|
+
* Read so the no-session diagnosis can RANK its causes instead of printing one static differential.
|
|
120
|
+
* Nuxt is the case that pays for this: it does not register a newly added plugin on HMR, so a dev
|
|
121
|
+
* server older than the wiring is its single most likely cause — `init` warns about it at install
|
|
122
|
+
* time and the hint the agent reads hours later never mentioned it.
|
|
123
|
+
*/
|
|
124
|
+
export function readProjectFramework(cwd) {
|
|
125
|
+
const framework = findProjectConfig(cwd)?.['framework'];
|
|
126
|
+
if ('string' === typeof framework && framework.length > 0)
|
|
127
|
+
return framework;
|
|
88
128
|
return undefined;
|
|
89
129
|
}
|
|
90
130
|
/**
|
|
@@ -100,17 +140,8 @@ export function readJournalEnabled(cwd, env) {
|
|
|
100
140
|
if ('1' === v || 'true' === v || 'on' === v)
|
|
101
141
|
return true;
|
|
102
142
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const config = JSON.parse(raw);
|
|
106
|
-
if ('object' === typeof config && config !== null) {
|
|
107
|
-
if (false === config['journal'])
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
//.reticle.json absent or unreadable — journaling stays on by default
|
|
113
|
-
}
|
|
143
|
+
if (false === findProjectConfig(cwd)?.['journal'])
|
|
144
|
+
return false;
|
|
114
145
|
return true;
|
|
115
146
|
}
|
|
116
147
|
/**
|
package/dist/cli/cli-verify.d.ts
CHANGED
|
@@ -36,6 +36,27 @@ export declare function urlParts(url: string): {
|
|
|
36
36
|
loopback: boolean;
|
|
37
37
|
};
|
|
38
38
|
/** CLI entry — wires the live ports and runs the one-shot verification. Exits the process itself. */
|
|
39
|
+
/**
|
|
40
|
+
* What a user reads when the bridge port is already taken by a daemon.
|
|
41
|
+
*
|
|
42
|
+
* `verify` boots its OWN daemon, and it has to do so on the port the APP dials — a loopback page
|
|
43
|
+
* connects natively to the port baked into its config, so verify cannot simply pick a free one and
|
|
44
|
+
* still be found. On any machine with Reticle set up, that port already belongs to a running daemon.
|
|
45
|
+
*
|
|
46
|
+
* It used to die there. The listen failure surfaces asynchronously on the server object, after
|
|
47
|
+
* `start` has resolved, so nothing caught it and the process printed a raw `node:net` stack — the
|
|
48
|
+
* worst answer available, and reached most often by the people with the fewest other options, since
|
|
49
|
+
* the skill offers this command as the way to a verdict with no MCP at all.
|
|
50
|
+
*
|
|
51
|
+
* Three ways out, in the order they are worth trying, and the message says all three rather than
|
|
52
|
+
* picking one: an agent that already has the tools should ask the daemon that is running instead of
|
|
53
|
+
* starting a second one; somebody who wants this command specifically can stop the daemon; and a
|
|
54
|
+
* second port works when the app is configured for it.
|
|
55
|
+
*
|
|
56
|
+
* Attaching to the running daemon the way `reticle drive` now does is the better answer and is not
|
|
57
|
+
* this change — see cli/drive-attach.ts for the shape it should take.
|
|
58
|
+
*/
|
|
59
|
+
export declare function portBusyMessage(port: number): string;
|
|
39
60
|
export declare function handleVerify(parsed: {
|
|
40
61
|
url: string;
|
|
41
62
|
headless: boolean;
|
package/dist/cli/cli-verify.js
CHANGED
|
@@ -13,6 +13,9 @@ import { join, basename } from 'node:path';
|
|
|
13
13
|
import { randomUUID } from 'node:crypto';
|
|
14
14
|
import { RETICLE_DEFAULT_PORT, ReticleEnv, bridgeWsUrl, isLoopbackHostname, ReticleDir, RunAgentKind, RunFramework, RunProfile, RunTrigger, VerdictStatus, } from '@reticlehq/core';
|
|
15
15
|
import { start } from '../index.js';
|
|
16
|
+
import { probePresence, PortPresence } from '../daemon/port-presence.js';
|
|
17
|
+
import { probeDaemon } from '../mcp/mcp-proxy.js';
|
|
18
|
+
import { fetchStatus } from './cli-launch.js';
|
|
16
19
|
import { resolveCloudConfig, syncRunToCloud, SyncOutcome } from '../cloud/cloud-sync.js';
|
|
17
20
|
import { ReticleRunner } from '../runs/reticle-runner.js';
|
|
18
21
|
import { createRunnerPort } from '../runs/runner-port.js';
|
|
@@ -199,6 +202,35 @@ async function openLiveConnection(opts) {
|
|
|
199
202
|
};
|
|
200
203
|
}
|
|
201
204
|
/** CLI entry — wires the live ports and runs the one-shot verification. Exits the process itself. */
|
|
205
|
+
/**
|
|
206
|
+
* What a user reads when the bridge port is already taken by a daemon.
|
|
207
|
+
*
|
|
208
|
+
* `verify` boots its OWN daemon, and it has to do so on the port the APP dials — a loopback page
|
|
209
|
+
* connects natively to the port baked into its config, so verify cannot simply pick a free one and
|
|
210
|
+
* still be found. On any machine with Reticle set up, that port already belongs to a running daemon.
|
|
211
|
+
*
|
|
212
|
+
* It used to die there. The listen failure surfaces asynchronously on the server object, after
|
|
213
|
+
* `start` has resolved, so nothing caught it and the process printed a raw `node:net` stack — the
|
|
214
|
+
* worst answer available, and reached most often by the people with the fewest other options, since
|
|
215
|
+
* the skill offers this command as the way to a verdict with no MCP at all.
|
|
216
|
+
*
|
|
217
|
+
* Three ways out, in the order they are worth trying, and the message says all three rather than
|
|
218
|
+
* picking one: an agent that already has the tools should ask the daemon that is running instead of
|
|
219
|
+
* starting a second one; somebody who wants this command specifically can stop the daemon; and a
|
|
220
|
+
* second port works when the app is configured for it.
|
|
221
|
+
*
|
|
222
|
+
* Attaching to the running daemon the way `reticle drive` now does is the better answer and is not
|
|
223
|
+
* this change — see cli/drive-attach.ts for the shape it should take.
|
|
224
|
+
*/
|
|
225
|
+
export function portBusyMessage(port) {
|
|
226
|
+
return (`reticle verify needs port ${String(port)} — the port your app dials — and a Reticle daemon is ` +
|
|
227
|
+
'already listening on it. It did not start a second one.\n\n' +
|
|
228
|
+
' • If you have the Reticle tools, ask the daemon that is already running instead: ' +
|
|
229
|
+
'reticle_run { tool: "reticle_verify", args: { action: "change", files: ["..."] } }\n' +
|
|
230
|
+
` • Or stop it and re-run: npx @reticlehq/server stop --port ${String(port)}\n` +
|
|
231
|
+
` • Or run both on another port, if your app is configured for it: RETICLE_PORT=<port> ` +
|
|
232
|
+
'npx @reticlehq/server verify <url>');
|
|
233
|
+
}
|
|
202
234
|
export function handleVerify(parsed) {
|
|
203
235
|
const now = () => Date.now();
|
|
204
236
|
const reticleRoot = join(process.cwd(), ReticleDir.ROOT);
|
|
@@ -217,5 +249,17 @@ export function handleVerify(parsed) {
|
|
|
217
249
|
fail: (line) => process.stderr.write(`${line}\n`),
|
|
218
250
|
exit: (code) => process.exit(code),
|
|
219
251
|
};
|
|
220
|
-
|
|
252
|
+
// Asked BEFORE anything binds. The listen failure arrives asynchronously on the server object,
|
|
253
|
+
// long after `start` has resolved, so no `.catch` on that promise can ever see it — which is why
|
|
254
|
+
// this used to reach the user as a raw node stack rather than as an answer.
|
|
255
|
+
void (async () => {
|
|
256
|
+
const port = parsed.port ?? RETICLE_DEFAULT_PORT;
|
|
257
|
+
if ((await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus })) ===
|
|
258
|
+
PortPresence.DAEMON) {
|
|
259
|
+
ports.fail(portBusyMessage(port));
|
|
260
|
+
ports.exit(1);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
await runVerify({ url: parsed.url, timeoutMs: parsed.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS }, ports);
|
|
264
|
+
})();
|
|
221
265
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the project config actually is, and — when we cannot find it — where we looked.
|
|
3
|
+
*
|
|
4
|
+
* `findProjectConfig` walks UP from the daemon's working directory, which is right for a developer
|
|
5
|
+
* who ran `reticle` in their project and wrong for the way most daemons are actually started.
|
|
6
|
+
* Editors launch MCP servers from wherever they like, and in at least one popular one that is the
|
|
7
|
+
* user's home directory: the config then sits at `<repo>/apps/web/.reticle.json`, the up-walk from
|
|
8
|
+
* home never touches it, and Reticle confidently reports the project unwired. Reported twice, plus a
|
|
9
|
+
* git-worktree variant of the same shape.
|
|
10
|
+
*
|
|
11
|
+
* So: walk up, then — from the repo root, if there is one — look in the workspace directories the
|
|
12
|
+
* project's OWN manifest declares, falling back to the two conventions everybody uses.
|
|
13
|
+
*
|
|
14
|
+
* The second half is the `searched` list. "There is no `.reticle.json`" is unfalsifiable without it,
|
|
15
|
+
* and this file exists because that claim was made, confidently, about configs that were right
|
|
16
|
+
* there. Every result carries the places that were checked.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately NOT a "pick one" function. Several configs means several projects, and choosing one
|
|
19
|
+
* silently is how an agent ends up with a confident verdict about an app it never touched. Callers
|
|
20
|
+
* are handed the list and must name it.
|
|
21
|
+
*/
|
|
22
|
+
export interface FoundConfig {
|
|
23
|
+
/** Absolute path to the `.reticle.json`. */
|
|
24
|
+
path: string;
|
|
25
|
+
/** The directory it configures — what a reader needs in order to act. */
|
|
26
|
+
directory: string;
|
|
27
|
+
/** Its projectId, when it declares a usable one. */
|
|
28
|
+
projectId?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ConfigDiscovery {
|
|
31
|
+
found: FoundConfig[];
|
|
32
|
+
/** Every directory checked, in the order checked. The answer to "are you sure?". */
|
|
33
|
+
searched: string[];
|
|
34
|
+
}
|
|
35
|
+
export declare function discoverProjectConfigs(cwd: string): ConfigDiscovery;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the project config actually is, and — when we cannot find it — where we looked.
|
|
3
|
+
*
|
|
4
|
+
* `findProjectConfig` walks UP from the daemon's working directory, which is right for a developer
|
|
5
|
+
* who ran `reticle` in their project and wrong for the way most daemons are actually started.
|
|
6
|
+
* Editors launch MCP servers from wherever they like, and in at least one popular one that is the
|
|
7
|
+
* user's home directory: the config then sits at `<repo>/apps/web/.reticle.json`, the up-walk from
|
|
8
|
+
* home never touches it, and Reticle confidently reports the project unwired. Reported twice, plus a
|
|
9
|
+
* git-worktree variant of the same shape.
|
|
10
|
+
*
|
|
11
|
+
* So: walk up, then — from the repo root, if there is one — look in the workspace directories the
|
|
12
|
+
* project's OWN manifest declares, falling back to the two conventions everybody uses.
|
|
13
|
+
*
|
|
14
|
+
* The second half is the `searched` list. "There is no `.reticle.json`" is unfalsifiable without it,
|
|
15
|
+
* and this file exists because that claim was made, confidently, about configs that were right
|
|
16
|
+
* there. Every result carries the places that were checked.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately NOT a "pick one" function. Several configs means several projects, and choosing one
|
|
19
|
+
* silently is how an agent ends up with a confident verdict about an app it never touched. Callers
|
|
20
|
+
* are handed the list and must name it.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
23
|
+
import { dirname, join, resolve } from 'node:path';
|
|
24
|
+
import { RETICLE_CONFIG_BASENAME } from './cli-port.js';
|
|
25
|
+
/** How far up to walk. Matches the existing config search so the two cannot disagree. */
|
|
26
|
+
const MAX_UP_LEVELS = 6;
|
|
27
|
+
/**
|
|
28
|
+
* Workspace directories checked when the manifest declares none.
|
|
29
|
+
*
|
|
30
|
+
* Not a guess at where an app might be — these are the two layouts the JS ecosystem converged on,
|
|
31
|
+
* and both reported cases used one of them.
|
|
32
|
+
*/
|
|
33
|
+
const CONVENTIONAL_WORKSPACES = ['apps', 'packages'];
|
|
34
|
+
/** How many entries of one workspace directory to stat. A backstop, not a policy. */
|
|
35
|
+
const MAX_WORKSPACE_ENTRIES = 64;
|
|
36
|
+
function readConfig(directory) {
|
|
37
|
+
const path = join(directory, RETICLE_CONFIG_BASENAME);
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
40
|
+
if ('object' !== typeof parsed || null === parsed || Array.isArray(parsed))
|
|
41
|
+
return undefined;
|
|
42
|
+
const projectId = parsed['projectId'];
|
|
43
|
+
return {
|
|
44
|
+
path,
|
|
45
|
+
directory,
|
|
46
|
+
...('string' === typeof projectId && projectId.length > 0 ? { projectId } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Absent, unreadable, or not JSON. A malformed config is reported elsewhere; here it is simply
|
|
51
|
+
// not a config we can name a project from.
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** The nearest ancestor holding a `.git`, or undefined. The repo root is where workspaces hang off. */
|
|
56
|
+
function repoRootOf(start) {
|
|
57
|
+
let dir = start;
|
|
58
|
+
for (let level = 0; level <= MAX_UP_LEVELS; level += 1) {
|
|
59
|
+
if (existsSync(join(dir, '.git')))
|
|
60
|
+
return dir;
|
|
61
|
+
const parent = dirname(dir);
|
|
62
|
+
if (parent === dir)
|
|
63
|
+
return undefined;
|
|
64
|
+
dir = parent;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The workspace directories this repo declares, from its own manifest where it has one.
|
|
70
|
+
*
|
|
71
|
+
* Reading `pnpm-workspace.yaml` with a substring scan rather than a YAML parser is deliberate: the
|
|
72
|
+
* only thing wanted from it is the leading path segment of each glob, the file is machine-written in
|
|
73
|
+
* practice, and a parser dependency to read one list of strings does not pay for itself.
|
|
74
|
+
*/
|
|
75
|
+
function declaredWorkspaceDirs(repoRoot) {
|
|
76
|
+
const dirs = new Set();
|
|
77
|
+
try {
|
|
78
|
+
const pkg = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8'));
|
|
79
|
+
if ('object' === typeof pkg && null !== pkg) {
|
|
80
|
+
const workspaces = pkg['workspaces'];
|
|
81
|
+
const globs = Array.isArray(workspaces)
|
|
82
|
+
? workspaces
|
|
83
|
+
: 'object' === typeof workspaces && null !== workspaces
|
|
84
|
+
? (workspaces['packages'] ?? [])
|
|
85
|
+
: [];
|
|
86
|
+
if (Array.isArray(globs)) {
|
|
87
|
+
for (const glob of globs)
|
|
88
|
+
if ('string' === typeof glob)
|
|
89
|
+
dirs.add(firstSegment(glob));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// No manifest, or one we cannot read. The conventions below still apply.
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const yaml = readFileSync(join(repoRoot, 'pnpm-workspace.yaml'), 'utf8');
|
|
98
|
+
for (const line of yaml.split('\n')) {
|
|
99
|
+
const match = /^\s*-\s*['"]?([^'"\s]+)/.exec(line);
|
|
100
|
+
const glob = match?.[1];
|
|
101
|
+
if (glob !== undefined)
|
|
102
|
+
dirs.add(firstSegment(glob));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Not a pnpm workspace.
|
|
107
|
+
}
|
|
108
|
+
for (const conventional of CONVENTIONAL_WORKSPACES)
|
|
109
|
+
dirs.add(conventional);
|
|
110
|
+
return [...dirs].filter((dir) => dir.length > 0 && !dir.startsWith('.'));
|
|
111
|
+
}
|
|
112
|
+
/** `apps/*` -> `apps`. The glob machinery is not needed; the leading directory is all we scan. */
|
|
113
|
+
function firstSegment(glob) {
|
|
114
|
+
const [first] = glob.split('/');
|
|
115
|
+
return first ?? '';
|
|
116
|
+
}
|
|
117
|
+
export function discoverProjectConfigs(cwd) {
|
|
118
|
+
const searched = [];
|
|
119
|
+
const found = [];
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
const check = (directory) => {
|
|
122
|
+
if (seen.has(directory))
|
|
123
|
+
return;
|
|
124
|
+
seen.add(directory);
|
|
125
|
+
searched.push(directory);
|
|
126
|
+
const config = readConfig(directory);
|
|
127
|
+
if (config !== undefined)
|
|
128
|
+
found.push(config);
|
|
129
|
+
};
|
|
130
|
+
let dir = resolve(cwd);
|
|
131
|
+
for (let level = 0; level <= MAX_UP_LEVELS; level += 1) {
|
|
132
|
+
check(dir);
|
|
133
|
+
const parent = dirname(dir);
|
|
134
|
+
if (parent === dir)
|
|
135
|
+
break;
|
|
136
|
+
dir = parent;
|
|
137
|
+
}
|
|
138
|
+
const repoRoot = repoRootOf(resolve(cwd));
|
|
139
|
+
if (repoRoot === undefined)
|
|
140
|
+
return { found, searched };
|
|
141
|
+
for (const workspace of declaredWorkspaceDirs(repoRoot)) {
|
|
142
|
+
const base = join(repoRoot, workspace);
|
|
143
|
+
let entries = [];
|
|
144
|
+
try {
|
|
145
|
+
entries = readdirSync(base).slice(0, MAX_WORKSPACE_ENTRIES);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
continue; // A declared workspace directory that does not exist is not an error here.
|
|
149
|
+
}
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
const candidate = join(base, entry);
|
|
152
|
+
try {
|
|
153
|
+
if (!statSync(candidate).isDirectory())
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
check(candidate);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { found, searched };
|
|
163
|
+
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* at the call site.
|
|
15
15
|
*/
|
|
16
16
|
import { describeSkew } from '../version/version-skew.js';
|
|
17
|
+
import { DoctorRow, doctorRow } from './doctor-rows.js';
|
|
17
18
|
/**
|
|
18
19
|
* `reticle stop`, not `reticle kill`.
|
|
19
20
|
*
|
|
@@ -39,6 +40,8 @@ export function daemonLine(port, pid, peer, self) {
|
|
|
39
40
|
parts.push(`v${peer.version}`);
|
|
40
41
|
const detail = 0 === parts.length ? '' : ` (${parts.join(', ')})`;
|
|
41
42
|
const skew = describeSkew({ what: 'the daemon on this port', version: peer.version, contract: peer.contract, fix: FIX }, self);
|
|
42
|
-
const line = {
|
|
43
|
+
const line = {
|
|
44
|
+
text: doctorRow(DoctorRow.DAEMON, `✓ running on :${String(port)}${detail}`),
|
|
45
|
+
};
|
|
43
46
|
return skew === undefined ? line : { ...line, skew };
|
|
44
47
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether `init` has wired THIS directory — the check `doctor` was missing.
|
|
3
|
+
*
|
|
4
|
+
* `doctor` already read `.reticle.json` for the port, so it knew whether the file existed and never
|
|
5
|
+
* said so. In a project where `init` has not run the checklist came out clean: node fine, chromium
|
|
6
|
+
* fine, a daemon that starts on demand, a bridge port. Every line true, and the app carrying no SDK
|
|
7
|
+
* went unremarked — which is exactly the state that produces "the tools are here and nothing ever
|
|
8
|
+
* verifies", and the state `doctor` is run to explain.
|
|
9
|
+
*
|
|
10
|
+
* Registering the MCP server and instrumenting the app are two different halves of the install, and
|
|
11
|
+
* more than one route does the first without the second.
|
|
12
|
+
*/
|
|
13
|
+
export interface ProjectWiringFacts {
|
|
14
|
+
/** The id from `.reticle.json`, or undefined when there is no config here. */
|
|
15
|
+
projectId: string | undefined;
|
|
16
|
+
/** Has an app for this project ever connected on this port? Durable across daemon restarts. */
|
|
17
|
+
previouslyConnected: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Is there a `.reticle.json` on disk here at all?
|
|
20
|
+
*
|
|
21
|
+
* Separate from `projectId` because the two failures are different facts with the same symptom: an
|
|
22
|
+
* absent config and a corrupt one both read back as "no id". Saying "no .reticle.json here" about a
|
|
23
|
+
* file that is sitting right there sends somebody hunting a missing file when what they have is a
|
|
24
|
+
* broken one — and a corrupt config is itself worth naming, since it is how a project ends up
|
|
25
|
+
* pointed at the wrong daemon.
|
|
26
|
+
*/
|
|
27
|
+
configPresent?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* One line, in the same shape as the checks beside it.
|
|
31
|
+
*
|
|
32
|
+
* `previouslyConnected` outranks the missing file, and that ordering is the important part: the Vite
|
|
33
|
+
* and Babel plugins instrument an app without writing a config, so a project that has connected here
|
|
34
|
+
* is wired whatever the file says. Sending it to `init` would be the same wrong answer pointing the
|
|
35
|
+
* other way — and `init` is the one action that can overwrite a setup that already works.
|
|
36
|
+
*/
|
|
37
|
+
export declare function projectWiringLine(facts: ProjectWiringFacts): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether `init` has wired THIS directory — the check `doctor` was missing.
|
|
3
|
+
*
|
|
4
|
+
* `doctor` already read `.reticle.json` for the port, so it knew whether the file existed and never
|
|
5
|
+
* said so. In a project where `init` has not run the checklist came out clean: node fine, chromium
|
|
6
|
+
* fine, a daemon that starts on demand, a bridge port. Every line true, and the app carrying no SDK
|
|
7
|
+
* went unremarked — which is exactly the state that produces "the tools are here and nothing ever
|
|
8
|
+
* verifies", and the state `doctor` is run to explain.
|
|
9
|
+
*
|
|
10
|
+
* Registering the MCP server and instrumenting the app are two different halves of the install, and
|
|
11
|
+
* more than one route does the first without the second.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* One line, in the same shape as the checks beside it.
|
|
15
|
+
*
|
|
16
|
+
* `previouslyConnected` outranks the missing file, and that ordering is the important part: the Vite
|
|
17
|
+
* and Babel plugins instrument an app without writing a config, so a project that has connected here
|
|
18
|
+
* is wired whatever the file says. Sending it to `init` would be the same wrong answer pointing the
|
|
19
|
+
* other way — and `init` is the one action that can overwrite a setup that already works.
|
|
20
|
+
*/
|
|
21
|
+
export function projectWiringLine(facts) {
|
|
22
|
+
if (facts.projectId !== undefined) {
|
|
23
|
+
return ` project ✓ wired here (${facts.projectId})`;
|
|
24
|
+
}
|
|
25
|
+
if (facts.previouslyConnected) {
|
|
26
|
+
return ' project ✓ an app has connected here before, so it is wired (no .reticle.json — a build plugin can wire an app without one)';
|
|
27
|
+
}
|
|
28
|
+
if (true === facts.configPresent) {
|
|
29
|
+
return (' project ✗ .reticle.json is here but carries no readable projectId — a corrupt or ' +
|
|
30
|
+
'hand-edited config reads the same as no config, and points this project at the default ' +
|
|
31
|
+
'daemon. Run `npx @reticlehq/server init` in the app directory to rewrite it');
|
|
32
|
+
}
|
|
33
|
+
return (' project ✗ no .reticle.json here and nothing has ever connected — the tools may be ' +
|
|
34
|
+
'registered, but this app is not instrumented. Run `npx @reticlehq/server init` in the app directory');
|
|
35
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The row labels `reticle doctor` can print, and the one place that pads them into a column.
|
|
3
|
+
*
|
|
4
|
+
* These were free strings before, hand-padded at every call site across three modules
|
|
5
|
+
* (`cli-doctor.ts`, `doctor-daemon-line.ts`, `doctor-sessions-line.ts`). Two problems with that,
|
|
6
|
+
* and the second is the one that motivated #340:
|
|
7
|
+
*
|
|
8
|
+
* 1. The padding was maintained by eye. Every label happened to be padded to the same column, but
|
|
9
|
+
* nothing enforced it, and a label longer than the column would have quietly broken the
|
|
10
|
+
* alignment of one row in a checklist whose whole readability is the column.
|
|
11
|
+
* 2. Nothing connected the labels to the docs pages that reproduce doctor's output. A row could be
|
|
12
|
+
* added, renamed or removed and the three pages showing sample output would go on describing the
|
|
13
|
+
* old vocabulary, with no gate noticing. `doctor` is the command a stuck user runs, and the docs
|
|
14
|
+
* are what they read when its output does not match what they were told to expect.
|
|
15
|
+
*
|
|
16
|
+
* The label set is exported so the docs guard can derive its expectation from the command rather
|
|
17
|
+
* than from a hand-kept list, which is the direction that catches the case that actually bites: a
|
|
18
|
+
* row added and never documented.
|
|
19
|
+
*
|
|
20
|
+
* NOT every row prints on every run. `version` prints only on skew, `port check` only on a
|
|
21
|
+
* mismatch, `desktop` only on a desktop project, and `sessions` only when a daemon answered. The
|
|
22
|
+
* vocabulary is therefore "what this command can print", not "what one run printed" — a guard built
|
|
23
|
+
* by scraping a single invocation would silently stop covering the conditional rows, which are
|
|
24
|
+
* exactly the ones a reader is least likely to have seen before.
|
|
25
|
+
*/
|
|
26
|
+
/** Every row label `doctor` can emit. Values are the literal text at the head of the row. */
|
|
27
|
+
export declare const DoctorRow: {
|
|
28
|
+
readonly NODE: "node";
|
|
29
|
+
readonly CHROMIUM: "chromium";
|
|
30
|
+
readonly DAEMON: "daemon";
|
|
31
|
+
readonly VERSION: "version";
|
|
32
|
+
readonly SESSIONS: "sessions";
|
|
33
|
+
readonly BRIDGE_PORT: "bridge port";
|
|
34
|
+
readonly PORT_CHECK: "port check";
|
|
35
|
+
readonly DAEMON_LOG: "daemon log";
|
|
36
|
+
readonly TRACING: "tracing";
|
|
37
|
+
readonly DESKTOP: "desktop";
|
|
38
|
+
};
|
|
39
|
+
export type DoctorRowLabel = (typeof DoctorRow)[keyof typeof DoctorRow];
|
|
40
|
+
export declare const DOCTOR_ROW_LABELS: readonly DoctorRowLabel[];
|
|
41
|
+
/**
|
|
42
|
+
* Width the label column pads to.
|
|
43
|
+
*
|
|
44
|
+
* Derived from the longest label rather than hardcoded, so adding a longer row realigns the whole
|
|
45
|
+
* checklist instead of breaking one line of it. With the current set the longest is `bridge port`
|
|
46
|
+
* (11), so this is 13 — byte-identical to the hand-padding it replaces. A test pins that, so a
|
|
47
|
+
* change to the column is a deliberate, visible change rather than an invisible reflow.
|
|
48
|
+
*/
|
|
49
|
+
export declare const LABEL_COLUMN: number;
|
|
50
|
+
/** Build one `doctor` row: the standard indent, the label padded to the column, then `rest`. */
|
|
51
|
+
export declare function doctorRow(label: DoctorRowLabel, rest: string): string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The row labels `reticle doctor` can print, and the one place that pads them into a column.
|
|
3
|
+
*
|
|
4
|
+
* These were free strings before, hand-padded at every call site across three modules
|
|
5
|
+
* (`cli-doctor.ts`, `doctor-daemon-line.ts`, `doctor-sessions-line.ts`). Two problems with that,
|
|
6
|
+
* and the second is the one that motivated #340:
|
|
7
|
+
*
|
|
8
|
+
* 1. The padding was maintained by eye. Every label happened to be padded to the same column, but
|
|
9
|
+
* nothing enforced it, and a label longer than the column would have quietly broken the
|
|
10
|
+
* alignment of one row in a checklist whose whole readability is the column.
|
|
11
|
+
* 2. Nothing connected the labels to the docs pages that reproduce doctor's output. A row could be
|
|
12
|
+
* added, renamed or removed and the three pages showing sample output would go on describing the
|
|
13
|
+
* old vocabulary, with no gate noticing. `doctor` is the command a stuck user runs, and the docs
|
|
14
|
+
* are what they read when its output does not match what they were told to expect.
|
|
15
|
+
*
|
|
16
|
+
* The label set is exported so the docs guard can derive its expectation from the command rather
|
|
17
|
+
* than from a hand-kept list, which is the direction that catches the case that actually bites: a
|
|
18
|
+
* row added and never documented.
|
|
19
|
+
*
|
|
20
|
+
* NOT every row prints on every run. `version` prints only on skew, `port check` only on a
|
|
21
|
+
* mismatch, `desktop` only on a desktop project, and `sessions` only when a daemon answered. The
|
|
22
|
+
* vocabulary is therefore "what this command can print", not "what one run printed" — a guard built
|
|
23
|
+
* by scraping a single invocation would silently stop covering the conditional rows, which are
|
|
24
|
+
* exactly the ones a reader is least likely to have seen before.
|
|
25
|
+
*/
|
|
26
|
+
/** Every row label `doctor` can emit. Values are the literal text at the head of the row. */
|
|
27
|
+
export const DoctorRow = {
|
|
28
|
+
NODE: 'node',
|
|
29
|
+
CHROMIUM: 'chromium',
|
|
30
|
+
DAEMON: 'daemon',
|
|
31
|
+
VERSION: 'version',
|
|
32
|
+
SESSIONS: 'sessions',
|
|
33
|
+
BRIDGE_PORT: 'bridge port',
|
|
34
|
+
PORT_CHECK: 'port check',
|
|
35
|
+
DAEMON_LOG: 'daemon log',
|
|
36
|
+
TRACING: 'tracing',
|
|
37
|
+
DESKTOP: 'desktop',
|
|
38
|
+
};
|
|
39
|
+
export const DOCTOR_ROW_LABELS = Object.values(DoctorRow);
|
|
40
|
+
/** Indent every row carries, before the label. */
|
|
41
|
+
const INDENT = ' ';
|
|
42
|
+
/**
|
|
43
|
+
* Width the label column pads to.
|
|
44
|
+
*
|
|
45
|
+
* Derived from the longest label rather than hardcoded, so adding a longer row realigns the whole
|
|
46
|
+
* checklist instead of breaking one line of it. With the current set the longest is `bridge port`
|
|
47
|
+
* (11), so this is 13 — byte-identical to the hand-padding it replaces. A test pins that, so a
|
|
48
|
+
* change to the column is a deliberate, visible change rather than an invisible reflow.
|
|
49
|
+
*/
|
|
50
|
+
export const LABEL_COLUMN = DOCTOR_ROW_LABELS.reduce((widest, label) => Math.max(widest, label.length), 0) + 2;
|
|
51
|
+
/** Build one `doctor` row: the standard indent, the label padded to the column, then `rest`. */
|
|
52
|
+
export function doctorRow(label, rest) {
|
|
53
|
+
return `${INDENT}${label.padEnd(LABEL_COLUMN)}${rest}`;
|
|
54
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SELF_RECOVERING_MARKER } from '../session/no-session-diagnosis.js';
|
|
2
|
+
import { DoctorRow, doctorRow } from './doctor-rows.js';
|
|
2
3
|
/**
|
|
3
4
|
* The line `doctor` was missing: is anything actually CONNECTED?
|
|
4
5
|
*
|
|
@@ -49,18 +50,20 @@ export function sessionsLine(facts) {
|
|
|
49
50
|
// An older daemon that does not report it. Saying "0 connected" here would be a claim about the
|
|
50
51
|
// app made from a payload that never mentioned the app — the exact kind of confident wrong
|
|
51
52
|
// sentence the no-session diagnosis exists to avoid.
|
|
52
|
-
return {
|
|
53
|
+
return {
|
|
54
|
+
text: doctorRow(DoctorRow.SESSIONS, '? this daemon did not report connected pages (older build)'),
|
|
55
|
+
};
|
|
53
56
|
}
|
|
54
57
|
if (count > 0) {
|
|
55
58
|
const plural = 1 === count ? 'page' : 'pages';
|
|
56
|
-
return { text:
|
|
59
|
+
return { text: doctorRow(DoctorRow.SESSIONS, `✓ ${String(count)} ${plural} connected`) };
|
|
57
60
|
}
|
|
58
61
|
const why = 'string' === typeof facts.why && facts.why.length > 0 ? forAHuman(facts.why) : undefined;
|
|
59
62
|
return {
|
|
60
63
|
// Deliberately ✗ rather than a neutral note. Zero connected pages is why somebody is running
|
|
61
64
|
// doctor, and a ✓-coloured checklist that hides the one failing thing is how the old output
|
|
62
65
|
// convinced people their setup was fine.
|
|
63
|
-
text: '
|
|
66
|
+
text: doctorRow(DoctorRow.SESSIONS, '✗ no page has connected to this daemon'),
|
|
64
67
|
...(why === undefined ? {} : { why }),
|
|
65
68
|
};
|
|
66
69
|
}
|