@reticlehq/server 3.8.1 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -104
- package/NOTICE +5 -0
- package/README.md +1 -1
- package/bin/launch.js +13 -0
- package/bin/package.json +3 -0
- package/bin/reticle.js +66 -8
- package/dist/chromium-launch-options.d.ts +25 -0
- package/dist/chromium-launch-options.js +21 -1
- package/dist/command/cli/adhoc-verdict.js +34 -7
- package/dist/command/cli/answers/gate-exit.d.ts +6 -2
- package/dist/command/cli/answers/gate-exit.js +12 -2
- package/dist/command/cli/ci-hint.d.ts +44 -0
- package/dist/command/cli/ci-hint.js +72 -0
- package/dist/command/cli/cli-doctor.d.ts +6 -1
- package/dist/command/cli/cli-doctor.js +45 -4
- package/dist/command/cli/cli-flow-commands.d.ts +7 -1
- package/dist/command/cli/cli-flow-commands.js +39 -17
- package/dist/command/cli/cli-kill.d.ts +21 -0
- package/dist/command/cli/cli-kill.js +46 -3
- package/dist/command/cli/cli-parse.d.ts +5 -2
- package/dist/command/cli/cli-parse.js +10 -5
- package/dist/command/cli/cli-usage.js +3 -2
- package/dist/command/cli/cli-verify.d.ts +6 -1
- package/dist/command/cli/cli-verify.js +32 -13
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
- package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
- package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
- package/dist/command/cli/doctor/doctor-fix.js +89 -0
- package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
- package/dist/command/cli/doctor/doctor-rows.js +4 -0
- package/dist/command/cli/doctor-repairs.d.ts +64 -0
- package/dist/command/cli/doctor-repairs.js +214 -0
- package/dist/command/cli/drive/drive-command.js +3 -7
- package/dist/command/cli/first-use-checks.d.ts +41 -0
- package/dist/command/cli/first-use-checks.js +127 -0
- package/dist/command/cli/first-verdict-node.d.ts +3 -0
- package/dist/command/cli/first-verdict-node.js +189 -0
- package/dist/command/cli/first-verdict.d.ts +126 -0
- package/dist/command/cli/first-verdict.js +267 -0
- package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
- package/dist/command/cli/launch/sdk-version-match.js +3 -2
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +6 -5
- package/dist/command/cli/mcp-command.js +13 -4
- package/dist/command/cli/ports/port-holder.d.ts +2 -0
- package/dist/command/cli/ports/port-holder.js +11 -3
- package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
- package/dist/command/cli/setup-mcp-cli.js +98 -23
- package/dist/command/cli/status/status-lines.d.ts +1 -0
- package/dist/command/cli/status/status-lines.js +50 -17
- package/dist/command/cli/status-fields.js +17 -3
- package/dist/command/cli/try-command.d.ts +7 -0
- package/dist/command/cli/try-command.js +11 -3
- package/dist/command/cli.js +70 -33
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
- package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
- package/dist/command/daemon/daemon-argv.d.ts +9 -0
- package/dist/command/daemon/daemon-argv.js +11 -0
- package/dist/command/daemon/daemon.d.ts +10 -3
- package/dist/command/daemon/daemon.js +17 -6
- package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
- package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
- package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
- package/dist/command/daemon/lifetime/death-cause.js +130 -0
- package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
- package/dist/command/daemon/lifetime/heartbeat.js +4 -13
- package/dist/command/daemon/port-state.d.ts +14 -0
- package/dist/command/daemon/port-state.js +31 -0
- package/dist/command/setup/agent-configs.d.ts +2 -0
- package/dist/command/setup/agent-configs.js +42 -15
- package/dist/command/setup/agent-next-steps.d.ts +77 -0
- package/dist/command/setup/agent-next-steps.js +284 -0
- package/dist/command/setup/agent-writer.js +11 -6
- package/dist/command/setup/init/init-host.js +2 -0
- package/dist/command/setup/init/init-runtime.d.ts +6 -1
- package/dist/command/setup/init/init-runtime.js +57 -21
- package/dist/command/setup/mcp-launcher.d.ts +3 -0
- package/dist/command/setup/mcp-launcher.js +11 -0
- package/dist/command/setup/setup-command.js +30 -12
- package/dist/command/setup/setup-mcp.d.ts +8 -0
- package/dist/command/setup/setup-mcp.js +47 -20
- package/dist/command/setup/start-dev-server.d.ts +8 -0
- package/dist/command/setup/start-dev-server.js +43 -0
- package/dist/command/version/identity/server-version.d.ts +2 -0
- package/dist/command/version/identity/server-version.js +2 -0
- package/dist/command/version/version-skew.d.ts +7 -0
- package/dist/command/version/version-skew.js +13 -2
- package/dist/features/license/license.d.ts +11 -71
- package/dist/features/license/license.js +43 -136
- package/dist/features/visual/desktop-capture.d.ts +33 -0
- package/dist/features/visual/desktop-capture.js +94 -0
- package/dist/features/visual/step-shots.js +139 -35
- package/dist/features/visual/visual-tools.d.ts +0 -18
- package/dist/features/visual/visual-tools.js +2 -81
- package/dist/import-graph.js +5 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +14 -5
- package/dist/judgement/runs/run-ledger.d.ts +7 -5
- package/dist/judgement/runs/run-ledger.js +98 -69
- package/dist/judgement/runs/verification-sync.d.ts +6 -3
- package/dist/judgement/runs/verification-sync.js +21 -10
- package/dist/language/flows/flow-store-for-session.js +7 -5
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -13
- package/dist/language/flows/recording/tape/recordings.js +41 -44
- package/dist/language/flows/replay.d.ts +17 -7
- package/dist/language/flows/replay.js +95 -44
- package/dist/launch-chromium.d.ts +1 -0
- package/dist/launch-chromium.js +49 -12
- package/dist/memory/cloud/sync-screenshots.js +6 -1
- package/dist/portal/bridge/bridge.d.ts +13 -4
- package/dist/portal/bridge/bridge.js +10 -9
- package/dist/portal/input/drive-error.d.ts +6 -0
- package/dist/portal/input/drive-error.js +9 -0
- package/dist/portal/input/network-mock.js +3 -2
- package/dist/portal/input/real-input-press.d.ts +34 -0
- package/dist/portal/input/real-input-press.js +150 -0
- package/dist/portal/input/real-input.d.ts +21 -23
- package/dist/portal/input/real-input.js +153 -194
- package/dist/portal/input/viewport-tools.js +6 -3
- package/dist/portal/pool/browser-pool.d.ts +5 -1
- package/dist/portal/pool/browser-pool.js +32 -38
- package/dist/portal/pool/context-permissions.d.ts +20 -17
- package/dist/portal/pool/context-permissions.js +63 -1
- package/dist/portal/pool/csp-widen.d.ts +62 -0
- package/dist/portal/pool/csp-widen.js +132 -0
- package/dist/portal/pool/lease-page-error.d.ts +29 -0
- package/dist/portal/pool/lease-page-error.js +18 -0
- package/dist/portal/pool/page-error-journal.d.ts +57 -0
- package/dist/portal/pool/page-error-journal.js +181 -0
- package/dist/portal/pool/playwright-launcher.js +21 -8
- package/dist/portal/pool/pool-contract.d.ts +22 -0
- package/dist/portal/pool/pool-lease.d.ts +35 -2
- package/dist/portal/pool/pool-lease.js +23 -0
- package/dist/portal/pool/zero-install.js +69 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
- package/dist/portal/session/dev-server/dev-command.js +56 -8
- package/dist/portal/session/no-session-diagnosis.js +10 -3
- package/dist/portal/session/no-session-next-action.d.ts +12 -0
- package/dist/portal/session/no-session-next-action.js +44 -4
- package/dist/portal/session/no-session-watch.js +3 -1
- package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
- package/dist/portal/session/page-commands/undeclared-command.js +16 -7
- package/dist/portal/session/recorded-gaps.d.ts +1 -1
- package/dist/portal/session/recorded-gaps.js +5 -2
- package/dist/portal/session/session-manager.js +40 -21
- package/dist/portal/session/session.js +4 -3
- package/dist/portal/session/zero-install-first-call.d.ts +20 -1
- package/dist/portal/session/zero-install-first-call.js +34 -2
- package/dist/start-wiring.d.ts +29 -2
- package/dist/start-wiring.js +73 -9
- package/dist/surface/mcp/mcp-proxy.js +10 -2
- package/dist/surface/mcp/mcp.js +5 -3
- package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
- package/dist/surface/mcp/peer/agents-connected.js +58 -0
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
- package/dist/surface/tools/act/act-preflight.js +37 -17
- package/dist/surface/tools/act/act-target.js +3 -1
- package/dist/surface/tools/act/after-match.d.ts +7 -0
- package/dist/surface/tools/act/after-match.js +33 -0
- package/dist/surface/tools/act/resolve-target.d.ts +2 -0
- package/dist/surface/tools/act/resolve-target.js +35 -8
- package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
- package/dist/surface/tools/act/sequence-grade.js +8 -1
- package/dist/surface/tools/act-sequence-tool.js +3 -0
- package/dist/surface/tools/act-tools.js +12 -12
- package/dist/surface/tools/assert/assert-verdict.js +28 -1
- package/dist/surface/tools/browser-tools.js +75 -7
- package/dist/surface/tools/dynamic-tools.js +2 -1
- package/dist/surface/tools/empty-session-list.d.ts +2 -0
- package/dist/surface/tools/empty-session-list.js +4 -2
- package/dist/surface/tools/error-recovery.js +43 -22
- package/dist/surface/tools/invoke-tool.js +27 -20
- package/dist/surface/tools/lease-hint.d.ts +7 -0
- package/dist/surface/tools/lease-hint.js +10 -3
- package/dist/surface/tools/lease-readiness.d.ts +21 -0
- package/dist/surface/tools/lease-readiness.js +47 -0
- package/dist/surface/tools/lease-seed.d.ts +2 -0
- package/dist/surface/tools/lease-seed.js +2 -1
- package/dist/surface/tools/lease-tools.js +43 -15
- package/dist/surface/tools/real-input-attempt.js +70 -31
- package/dist/surface/tools/repeat-hint.d.ts +40 -0
- package/dist/surface/tools/repeat-hint.js +171 -0
- package/dist/surface/tools/run-report.d.ts +2 -2
- package/dist/surface/tools/run-report.js +27 -15
- package/dist/surface/tools/tool-kit.d.ts +12 -0
- package/dist/surface/tools/tool-kit.js +7 -0
- package/dist/surface/tools/tools.js +26 -13
- package/dist/telemetry/argument-shape.js +16 -7
- package/dist/telemetry/cli-telemetry.d.ts +10 -1
- package/dist/telemetry/cli-telemetry.js +15 -0
- package/dist/telemetry/init-telemetry.d.ts +2 -1
- package/dist/telemetry/init-telemetry.js +47 -2
- package/dist/telemetry/install-trace.js +23 -4
- package/dist/telemetry/onboarding-funnel.d.ts +19 -2
- package/dist/telemetry/onboarding-funnel.js +25 -3
- package/dist/telemetry/proxy-telemetry.d.ts +2 -1
- package/dist/telemetry/proxy-telemetry.js +2 -2
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +67 -27
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +60 -28
- package/dist/telemetry/tool-refused.d.ts +2 -2
- package/dist/telemetry/tool-refused.js +9 -2
- package/docs/architecture.md +3 -6
- package/docs/cli/doctor.mdx +41 -2
- package/docs/cli/help.mdx +4 -2
- package/docs/cli/license.mdx +4 -5
- package/docs/cli/setup.mdx +13 -2
- package/docs/cli/status.mdx +4 -0
- package/docs/cli.mdx +1 -1
- package/docs/deploy-checks.md +25 -0
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +15 -20
- package/docs/environment.mdx +1 -0
- package/docs/first-drive.md +1 -1
- package/docs/install-manual.mdx +16 -1
- package/docs/packages/engine.mdx +1 -1
- package/docs/packages/react.mdx +14 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/test.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +1 -0
- package/docs/packages.mdx +1 -1
- package/docs/platform-integration.md +1 -1
- package/docs/skill.md +1 -1
- package/docs/telemetry.md +4 -2
- package/package.json +14 -10
- package/LICENSE-ENTERPRISE +0 -37
- package/dist/features/ee/audit-log.d.ts +0 -14
- package/dist/features/ee/audit-log.js +0 -11
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import {
|
|
4
|
+
import { mcpLauncher } from '../setup/mcp-launcher.js';
|
|
5
|
+
import { claudePluginInstalled, probeCli, terminalWidth, wrapForTerminal } from '@reticlehq/init';
|
|
5
6
|
import { tutorialShownSteps } from './tutorial.js';
|
|
6
7
|
import { setupMcp, knownClientLabels } from '../setup/setup-mcp.js';
|
|
7
8
|
import { reportInstallSteps } from '../setup/setup-install.js';
|
|
8
9
|
import { registerOtherAgents } from '../setup/setup-command.js';
|
|
10
|
+
import { agentLines, agentRows, awaitedClients, claudeStateDir, connectionLines, installedAgentRows, waitForAgents, } from '../setup/agent-next-steps.js';
|
|
11
|
+
import { connectedAgentClients } from '../../surface/mcp/peer/agents-connected.js';
|
|
12
|
+
import { reticleStateHome } from '../daemon/daemon.js';
|
|
9
13
|
/** How many coding agents on this machine have Reticle's tools after this run. */
|
|
10
|
-
export function handleSetupMcp(reportStep, standalone = true
|
|
14
|
+
export function handleSetupMcp(reportStep, standalone = true,
|
|
15
|
+
/** Wait for the agents to connect. The caller decides: a terminal, and not CI. */
|
|
16
|
+
waitForConnect = false) {
|
|
11
17
|
const io = {
|
|
18
|
+
launcher: mcpLauncher(),
|
|
12
19
|
exists: (p) => existsSync(p),
|
|
13
20
|
readFile: (p) => {
|
|
14
21
|
try {
|
|
@@ -48,25 +55,12 @@ export function handleSetupMcp(reportStep, standalone = true) {
|
|
|
48
55
|
: `No coding agent config found. Looked for: ${knownClientLabels().join(', ')}. Install one, then run \`reticle setup mcp\` again.`);
|
|
49
56
|
return 0;
|
|
50
57
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
io.print(` ✓ ${id} (already set up)`);
|
|
55
|
-
for (const id of result.failed) {
|
|
56
|
-
io.print(` ⚠ ${id}: Reticle registration failed; this agent may not have the tools.`);
|
|
57
|
-
io.print(id === McpClient.CLAUDE_CODE
|
|
58
|
-
? ` Run this command to see the error and retry: ${claudeAddCommand().display}`
|
|
59
|
-
: ' Check your agent config file permissions, then retry: reticle setup mcp');
|
|
58
|
+
const rows = agentRows(result, io.launcher);
|
|
59
|
+
for (const line of agentLines(rows)) {
|
|
60
|
+
io.print(line);
|
|
60
61
|
}
|
|
61
|
-
if (result.failed.length > 0)
|
|
62
|
+
if (result.failed.length > 0) {
|
|
62
63
|
process.exitCode = 1;
|
|
63
|
-
// Said, not skipped. These are the clients whose format we will not rewrite, and they used to be
|
|
64
|
-
// counted as registered: the line below is the difference between a user who adds four lines to
|
|
65
|
-
// one file and a user who believes they are set up and has no tools.
|
|
66
|
-
for (const client of result.manual) {
|
|
67
|
-
io.print(` ⚠ ${client.id}: add the reticle entry to ${client.configPath} by hand`);
|
|
68
|
-
if (client.docs !== undefined)
|
|
69
|
-
io.print(` see ${client.docs}`);
|
|
70
64
|
}
|
|
71
65
|
/*
|
|
72
66
|
* The OTHER agents, which `init` already reaches and this did not.
|
|
@@ -90,10 +84,14 @@ export function handleSetupMcp(reportStep, standalone = true) {
|
|
|
90
84
|
// closing's Next list, where it belongs with the other things to do, not between the stages.
|
|
91
85
|
if (standalone) {
|
|
92
86
|
io.print('');
|
|
93
|
-
io.print('Restart your agent for it to pick up the new server.');
|
|
94
87
|
io.print('Then: cd <your project> && reticle init');
|
|
88
|
+
if (waitForConnect) {
|
|
89
|
+
void reportConnections(rows, (line) => {
|
|
90
|
+
io.print(line);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
95
93
|
}
|
|
96
|
-
return result.registered.length + result.alreadyThere.length;
|
|
94
|
+
return result.registered.length + result.alreadyThere.length + result.pluginActive.length;
|
|
97
95
|
}
|
|
98
96
|
/**
|
|
99
97
|
* The one line the launchers read and strip: what they cannot know and the next steps depend on.
|
|
@@ -107,11 +105,18 @@ export const INSTALL_RESULT_PREFIX = 'RETICLE_INSTALL';
|
|
|
107
105
|
const APP_MARKERS = ['package.json', 'index.html'];
|
|
108
106
|
/** Whether the folder the installer ran in is an app: `init` runs here, or after a `cd`. */
|
|
109
107
|
export const InstallAppHere = { HERE: 'here', NONE: 'none' };
|
|
108
|
+
/**
|
|
109
|
+
* `wait=yes`: this CLI has `reticle setup wait-agents`, and an agent has Reticle to wait for. A
|
|
110
|
+
* launcher runs the wait only when it reads this, so a launcher served from main never runs a
|
|
111
|
+
* subcommand that an older CLI does not have.
|
|
112
|
+
*/
|
|
113
|
+
const INSTALL_WAIT = ' wait=yes';
|
|
110
114
|
export function installResultLine(agents, cwd) {
|
|
111
115
|
const app = APP_MARKERS.some((file) => existsSync(join(cwd, file)))
|
|
112
116
|
? InstallAppHere.HERE
|
|
113
117
|
: InstallAppHere.NONE;
|
|
114
|
-
|
|
118
|
+
const wait = 0 < agents ? INSTALL_WAIT : '';
|
|
119
|
+
return `${INSTALL_RESULT_PREFIX} agents=${String(agents)} app=${app}${wait}`;
|
|
115
120
|
}
|
|
116
121
|
/**
|
|
117
122
|
* `reticle setup install` — the Node half of the one-line installer.
|
|
@@ -132,7 +137,77 @@ export function handleSetupInstall(opts, reportStep) {
|
|
|
132
137
|
// No closing here: the installer prints the next step and the agent prompt itself, right after
|
|
133
138
|
// this returns, so what this prints is progress and nothing else. Those two blocks are what the
|
|
134
139
|
// ONBOARD steps below now stand for: what Reticle is for, shown before anything is asked of anyone.
|
|
135
|
-
for (const step of tutorialShownSteps())
|
|
140
|
+
for (const step of tutorialShownSteps()) {
|
|
136
141
|
reportStep(step);
|
|
142
|
+
}
|
|
137
143
|
process.stdout.write(`${installResultLine(agents, process.cwd())}\n`);
|
|
138
144
|
}
|
|
145
|
+
/** How long to wait for an agent to connect. Short: the person may not restart now. */
|
|
146
|
+
const CONNECT_WAIT_MS = 60_000;
|
|
147
|
+
const CONNECT_POLL_MS = 1000;
|
|
148
|
+
/** Wait only where a person watches: a terminal, and not CI. */
|
|
149
|
+
export function shouldWaitForAgents(isTty, isCi) {
|
|
150
|
+
return true === isTty && !isCi;
|
|
151
|
+
}
|
|
152
|
+
const nodeWait = {
|
|
153
|
+
read: () => connectedAgentClients(reticleStateHome()),
|
|
154
|
+
clock: {
|
|
155
|
+
now: () => Date.now(),
|
|
156
|
+
sleep: (ms) => new Promise((resolve) => {
|
|
157
|
+
setTimeout(resolve, ms);
|
|
158
|
+
}),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
/** Wait (bounded) for the registered agents to connect, then say which did and what is missing. */
|
|
162
|
+
export async function reportConnections(rows, print, wait = nodeWait) {
|
|
163
|
+
const want = awaitedClients(rows);
|
|
164
|
+
if (0 === want.length) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
print('');
|
|
168
|
+
print(`Waiting up to ${String(CONNECT_WAIT_MS / 1000)}s for your agents to connect. Press Ctrl+C to skip.`);
|
|
169
|
+
const seen = await waitForAgents(want, wait.read, wait.clock, CONNECT_WAIT_MS, CONNECT_POLL_MS);
|
|
170
|
+
for (const line of connectionLines(rows, seen)) {
|
|
171
|
+
print(line);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* `reticle init`: the same block and wait as `reticle setup mcp`, after its restart notice. Print the
|
|
176
|
+
* "Your agents:" block now. Returns the wait, for the caller to run last: a no-op when
|
|
177
|
+
* nobody watches.
|
|
178
|
+
*/
|
|
179
|
+
export function agentsBlock(rows, print, interactive, wait = nodeWait) {
|
|
180
|
+
for (const line of agentLines(rows)) {
|
|
181
|
+
print(line);
|
|
182
|
+
}
|
|
183
|
+
return interactive ? () => reportConnections(rows, print, wait) : () => Promise.resolve();
|
|
184
|
+
}
|
|
185
|
+
/** The agents on this machine and whether Reticle is in each config. Read only. */
|
|
186
|
+
export function readAgentRows(cwd) {
|
|
187
|
+
const io = {
|
|
188
|
+
exists: (p) => existsSync(p),
|
|
189
|
+
readFile: (p) => {
|
|
190
|
+
try {
|
|
191
|
+
return readFileSync(p, 'utf8');
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
homeDir: () => homedir(),
|
|
198
|
+
};
|
|
199
|
+
return installedAgentRows(io, cwd, claudePluginInstalled(io), claudeStateDir(), mcpLauncher());
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* `reticle setup wait-agents`: the installer's last step. The installer sends `setup install` output
|
|
203
|
+
* to a file, so that run cannot wait; this one runs with the terminal attached. Waits only where a
|
|
204
|
+
* person watches.
|
|
205
|
+
*/
|
|
206
|
+
export async function handleSetupWaitAgents(interactive) {
|
|
207
|
+
if (!interactive) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
await reportConnections(readAgentRows(process.cwd()), (line) => {
|
|
211
|
+
process.stdout.write(`${line}\n`);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
@@ -19,6 +19,7 @@ export declare const StatusLabel: {
|
|
|
19
19
|
readonly DAEMON: "daemon";
|
|
20
20
|
readonly SESSIONS: "sessions";
|
|
21
21
|
readonly AGENT_LINK: "agent link";
|
|
22
|
+
readonly AGENTS: "agents";
|
|
22
23
|
readonly ACCOUNT: "account";
|
|
23
24
|
readonly NEXT: "next";
|
|
24
25
|
readonly SPLIT_BRAIN: "split brain";
|
|
@@ -19,6 +19,7 @@ export const StatusLabel = {
|
|
|
19
19
|
DAEMON: 'daemon',
|
|
20
20
|
SESSIONS: 'sessions',
|
|
21
21
|
AGENT_LINK: 'agent link',
|
|
22
|
+
AGENTS: 'agents',
|
|
22
23
|
ACCOUNT: 'account',
|
|
23
24
|
NEXT: 'next',
|
|
24
25
|
SPLIT_BRAIN: 'split brain',
|
|
@@ -49,11 +50,13 @@ function num(fields, key) {
|
|
|
49
50
|
}
|
|
50
51
|
function sessionViews(fields) {
|
|
51
52
|
const raw = fields['sessions'];
|
|
52
|
-
if (!Array.isArray(raw))
|
|
53
|
+
if (!Array.isArray(raw)) {
|
|
53
54
|
return [];
|
|
55
|
+
}
|
|
54
56
|
return raw.flatMap((s) => {
|
|
55
|
-
if ('object' !== typeof s || null === s)
|
|
57
|
+
if ('object' !== typeof s || null === s) {
|
|
56
58
|
return [];
|
|
59
|
+
}
|
|
57
60
|
const r = s;
|
|
58
61
|
// Each of these explains something a reader would otherwise call a bug: a throttled or hidden
|
|
59
62
|
// tab answers slowly, a stale one answers about a page that has moved on, and an unresponsive
|
|
@@ -81,13 +84,15 @@ export function statusLines(fields) {
|
|
|
81
84
|
// and "nothing is here" were the same output once, and the true sentence was said by nothing a
|
|
82
85
|
// person sees — so the word belongs in the prose, not only in the event.
|
|
83
86
|
const presence = str(fields, 'presence');
|
|
84
|
-
if (presence !== undefined)
|
|
87
|
+
if (presence !== undefined) {
|
|
85
88
|
lines.push(continuation(`port presence: ${presence}`));
|
|
89
|
+
}
|
|
86
90
|
// The refusal a FOREIGN port produces. It is the whole answer when it is present, so it rides the
|
|
87
91
|
// daemon row rather than waiting for a `next` that is not built in that branch.
|
|
88
92
|
const reason = str(fields, 'reason');
|
|
89
|
-
if (reason !== undefined)
|
|
93
|
+
if (reason !== undefined) {
|
|
90
94
|
lines.push(continuation(reason));
|
|
95
|
+
}
|
|
91
96
|
if (running) {
|
|
92
97
|
const count = num(fields, 'sessionCount') ?? 0;
|
|
93
98
|
lines.push(row(StatusLabel.SESSIONS, count > 0
|
|
@@ -99,8 +104,9 @@ export function statusLines(fields) {
|
|
|
99
104
|
// The daemon's own explanation for an empty session list, which outranks anything this command
|
|
100
105
|
// could infer — it is the one holding the connection.
|
|
101
106
|
const why = str(fields, 'why');
|
|
102
|
-
if (why !== undefined)
|
|
107
|
+
if (why !== undefined) {
|
|
103
108
|
lines.push(continuation(why));
|
|
109
|
+
}
|
|
104
110
|
}
|
|
105
111
|
// Who is signed in, and whether this repo is linked: what `whoami` used to answer. Only when the
|
|
106
112
|
// caller looked, which is what the key being present means.
|
|
@@ -112,29 +118,33 @@ export function statusLines(fields) {
|
|
|
112
118
|
// A key the platform refused at the last sync is the answer, not "not signed in": a member
|
|
113
119
|
// removed from the workspace was told to connect while the key was set, and refused.
|
|
114
120
|
const refused = str(fields, 'keyRefused');
|
|
115
|
-
lines.push(row(StatusLabel.ACCOUNT, refused
|
|
116
|
-
? `${NOT_OK} refused at the last sync: ${refused}`
|
|
117
|
-
: who === undefined
|
|
118
|
-
? `${NOT_OK} not signed in`
|
|
119
|
-
: `${OK} signed in as ${who}`));
|
|
121
|
+
lines.push(row(StatusLabel.ACCOUNT, accountText(refused, who)));
|
|
120
122
|
const apps = appLinks(fields['workspaceApps']);
|
|
121
|
-
if (project === undefined && 0 < apps.length)
|
|
122
|
-
for (const { app, project: linked } of apps)
|
|
123
|
+
if (project === undefined && 0 < apps.length) {
|
|
124
|
+
for (const { app, project: linked } of apps) {
|
|
123
125
|
lines.push(continuation(null === linked
|
|
124
126
|
? `${app} is not linked: run \`reticle connect\` in ${app}`
|
|
125
127
|
: `${app} is linked to ${linked}`));
|
|
126
|
-
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
127
131
|
lines.push(continuation(project === undefined
|
|
128
132
|
? 'this repo is not linked: run `reticle connect`'
|
|
129
133
|
: `this repo is linked to ${project}${workspace === undefined ? '' : ` in workspace "${workspace}"`}`));
|
|
134
|
+
}
|
|
130
135
|
}
|
|
131
136
|
const client = str(fields, 'mcpClient');
|
|
132
137
|
if (client !== undefined) {
|
|
133
138
|
// The state in words (`mcpClientSaid`), never the record's own name for it.
|
|
134
139
|
lines.push(row(StatusLabel.AGENT_LINK, str(fields, 'mcpClientSaid') ?? client));
|
|
135
140
|
const action = str(fields, 'mcpClientAction');
|
|
136
|
-
if (action !== undefined)
|
|
141
|
+
if (action !== undefined) {
|
|
137
142
|
lines.push(continuation(action));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Per agent: connected, or the step still missing. The installer prints the same lines.
|
|
146
|
+
for (const [i, line] of agentLinesOf(fields['agents']).entries()) {
|
|
147
|
+
lines.push(0 === i ? row(StatusLabel.AGENTS, line) : continuation(line));
|
|
138
148
|
}
|
|
139
149
|
for (const [label, key] of [
|
|
140
150
|
[StatusLabel.NEXT, 'nextAction'],
|
|
@@ -142,18 +152,41 @@ export function statusLines(fields) {
|
|
|
142
152
|
[StatusLabel.UPDATE, 'updateAvailable'],
|
|
143
153
|
]) {
|
|
144
154
|
const value = str(fields, key);
|
|
145
|
-
if (value !== undefined)
|
|
155
|
+
if (value !== undefined) {
|
|
146
156
|
lines.push(row(label, value));
|
|
157
|
+
}
|
|
147
158
|
}
|
|
148
159
|
return lines;
|
|
149
160
|
}
|
|
161
|
+
/** The account row: refused at the last sync, not signed in, or signed in. */
|
|
162
|
+
function accountText(refused, who) {
|
|
163
|
+
if (refused !== undefined) {
|
|
164
|
+
return `${NOT_OK} refused at the last sync: ${refused}`;
|
|
165
|
+
}
|
|
166
|
+
return who === undefined ? `${NOT_OK} not signed in` : `${OK} signed in as ${who}`;
|
|
167
|
+
}
|
|
168
|
+
/** The `agents` field as lines, read defensively: it is the status object, not a typed value. */
|
|
169
|
+
function agentLinesOf(value) {
|
|
170
|
+
if (!Array.isArray(value)) {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
return value.flatMap((raw) => {
|
|
174
|
+
if ('object' !== typeof raw || null === raw) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
const { said } = raw;
|
|
178
|
+
return 'string' === typeof said ? [said] : [];
|
|
179
|
+
});
|
|
180
|
+
}
|
|
150
181
|
/** A workspace root's apps and their links, read defensively off the status fields. */
|
|
151
182
|
function appLinks(value) {
|
|
152
|
-
if (!Array.isArray(value))
|
|
183
|
+
if (!Array.isArray(value)) {
|
|
153
184
|
return [];
|
|
185
|
+
}
|
|
154
186
|
return value.flatMap((raw) => {
|
|
155
|
-
if ('object' !== typeof raw || null === raw)
|
|
187
|
+
if ('object' !== typeof raw || null === raw) {
|
|
156
188
|
return [];
|
|
189
|
+
}
|
|
157
190
|
const { app, project } = raw;
|
|
158
191
|
return 'string' === typeof app
|
|
159
192
|
? [{ app, project: 'string' === typeof project ? project : null }]
|
|
@@ -5,10 +5,14 @@ import { hasAnyAppConnectedBefore, hasProjectConnectedBefore, } from '../../memo
|
|
|
5
5
|
import { devServersForProject } from '@reticlehq/core';
|
|
6
6
|
import { readDevServers } from '../daemon/dev-servers.js';
|
|
7
7
|
import { attachStatusFields } from '../../surface/mcp/attach-memory.js';
|
|
8
|
+
import { connectedAgentClients } from '../../surface/mcp/peer/agents-connected.js';
|
|
9
|
+
import { agentStatuses } from '../setup/agent-next-steps.js';
|
|
10
|
+
import { readAgentRows } from './setup-mcp-cli.js';
|
|
8
11
|
import { probeDaemon } from '../../surface/mcp/mcp-proxy.js';
|
|
9
12
|
import { availableUpdate } from '../update/update-nudge.js';
|
|
10
13
|
import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
|
|
11
|
-
import { PortPresence, probePresence, presenceIsUsable,
|
|
14
|
+
import { PortPresence, probePresence, presenceIsUsable, } from '../daemon/binding/port-presence.js';
|
|
15
|
+
import { describePortState } from '../daemon/port-state.js';
|
|
12
16
|
import { summarizeStatus } from './launch/cli-launch.js';
|
|
13
17
|
import { readProjectId } from './ports/resolve/cli-port.js';
|
|
14
18
|
import { accountStatusFields } from './cloud-cli.js';
|
|
@@ -56,6 +60,16 @@ export function reportStatus(fields, json) {
|
|
|
56
60
|
}
|
|
57
61
|
process.stdout.write(`${statusLines(fields).join('\n')}\n`);
|
|
58
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Each agent on this machine: connected, or the step still missing. The same lines the installer
|
|
65
|
+
* prints after its wait. Absent when no agent is on this machine.
|
|
66
|
+
*/
|
|
67
|
+
function agentStatusFields() {
|
|
68
|
+
const rows = readAgentRows(process.cwd());
|
|
69
|
+
return 0 === rows.length
|
|
70
|
+
? {}
|
|
71
|
+
: { agents: agentStatuses(rows, connectedAgentClients(reticleStateHome())) };
|
|
72
|
+
}
|
|
59
73
|
export async function handleStatus(port, json = false) {
|
|
60
74
|
// A broken session file must not cost the reader the rest of the status.
|
|
61
75
|
const account = await accountStatusFields().catch(() => ({}));
|
|
@@ -81,7 +95,7 @@ export async function handleStatus(port, json = false) {
|
|
|
81
95
|
// The OTHER half of the install, and the half this command could not see. A host that registers
|
|
82
96
|
// Reticle and never lists its tools looks exactly like an abandoned install from here — same idle
|
|
83
97
|
// daemon, same zero sessions — and the user sees a tool that does nothing. See attach-memory.ts.
|
|
84
|
-
const client = attachStatusFields(reticleStateHome(), port);
|
|
98
|
+
const client = { ...attachStatusFields(reticleStateHome(), port), ...agentStatusFields() };
|
|
85
99
|
// What the dev servers themselves said. The reason this command could previously only guess at
|
|
86
100
|
// whether the app was running is that it had no way to see one; now the ones carrying Reticle
|
|
87
101
|
// announce, and the advice below stops contradicting the terminal the reader is looking at.
|
|
@@ -103,7 +117,7 @@ export async function handleStatus(port, json = false) {
|
|
|
103
117
|
port,
|
|
104
118
|
running: false,
|
|
105
119
|
presence,
|
|
106
|
-
...(presence === PortPresence.FOREIGN ? { reason:
|
|
120
|
+
...(presence === PortPresence.FOREIGN ? { reason: describePortState(presence, port) } : {}),
|
|
107
121
|
// `init` promises this command says why the app has not connected. Without it the answer was
|
|
108
122
|
// `running: false` and nothing else, which reads as "Reticle is broken" for what is usually
|
|
109
123
|
// just a daemon that has not been asked to do anything yet.
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { CloudConfig } from '../../memory/cloud/cloud-sync.js';
|
|
2
2
|
import { type FreeDrive } from '../../features/harness/platform/platform-drives.js';
|
|
3
3
|
export declare const MSG_SIGN_IN_FIRST = "Link this folder first: reticle connect. It signs you in if you are not, and it is free, no card.";
|
|
4
|
+
/**
|
|
5
|
+
* The local path that needs no account and no MCP tools. An agent whose client had no reticle_*
|
|
6
|
+
* tools ran `try`, was told to sign in, and stopped with no verdict.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MSG_LOCAL_CHECK: string;
|
|
4
9
|
/** The whole drive, model turns and browser included, before the CLI stops waiting for it. */
|
|
5
10
|
export declare const TRY_BUDGET_MS = 120000;
|
|
6
11
|
/** Model turns in the one free drive. Bounds what it costs, not what it may find. */
|
|
@@ -43,6 +48,8 @@ export declare function summarizeTry(journeys: readonly TryJourney[]): string[];
|
|
|
43
48
|
export declare function runTry(args: TryArgs, ports: TryPorts): Promise<number>;
|
|
44
49
|
/** The journeys out of an explore report, or one journey graded from the drive when unplanned. */
|
|
45
50
|
export declare function journeysOf(report: Record<string, unknown>, persona?: string): TryJourney[];
|
|
51
|
+
/** A daemon serving `port`: the one already there, or one started now. Undefined when ready. */
|
|
52
|
+
export declare function ensureTryDaemon(port: number): Promise<string | undefined>;
|
|
46
53
|
/** What `try` borrows from the cloud commands, passed in so neither module imports the other. */
|
|
47
54
|
export interface TryCloud {
|
|
48
55
|
push: () => Promise<number>;
|
|
@@ -19,7 +19,8 @@ import { describeUnsynced, unsyncedRoots } from '../../memory/cloud/unsynced-roo
|
|
|
19
19
|
import { createNodeFileSystem } from '../../memory/project/fs/fs-port.js';
|
|
20
20
|
import { defaultPairingTokenDir, readOrCreatePairingTokenSync, } from '../../portal/bridge/pairing-token.js';
|
|
21
21
|
import { spawnDaemon } from '../daemon/daemon.js';
|
|
22
|
-
import { PortPresence,
|
|
22
|
+
import { PortPresence, presenceIsUsable, probePresence, } from '../daemon/binding/port-presence.js';
|
|
23
|
+
import { describePortState } from '../daemon/port-state.js';
|
|
23
24
|
import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
|
|
24
25
|
import { probeDaemon, waitForDaemonBind } from '../../surface/mcp/mcp-proxy.js';
|
|
25
26
|
import { readProjectPort } from './ports/resolve/cli-port.js';
|
|
@@ -28,6 +29,12 @@ import { seeRunLine } from '../../memory/project/sync-status.js';
|
|
|
28
29
|
import { DAEMON_INNER_COMMAND, PORT_FLAG } from './cli-parse.js';
|
|
29
30
|
import { acquireLease, connectOverSse, endpointFor, exploreToEnd, refusalText, releaseLease, verdictOf, } from './adhoc-verdict.js';
|
|
30
31
|
export const MSG_SIGN_IN_FIRST = 'Link this folder first: reticle connect. It signs you in if you are not, and it is free, no card.';
|
|
32
|
+
/**
|
|
33
|
+
* The local path that needs no account and no MCP tools. An agent whose client had no reticle_*
|
|
34
|
+
* tools ran `try`, was told to sign in, and stopped with no verdict.
|
|
35
|
+
*/
|
|
36
|
+
export const MSG_LOCAL_CHECK = 'To check one thing on this machine with no account and no MCP tools: ' +
|
|
37
|
+
`reticle verify <url> --expect '{"kind":"text","contains":"..."}'. Exit 0 means it held.`;
|
|
31
38
|
const PERSONA_FLAG = '--persona';
|
|
32
39
|
/** The whole drive, model turns and browser included, before the CLI stops waiting for it. */
|
|
33
40
|
export const TRY_BUDGET_MS = 120_000;
|
|
@@ -89,6 +96,7 @@ export async function runTry(args, ports) {
|
|
|
89
96
|
const cloud = await ports.linked();
|
|
90
97
|
if (null === cloud) {
|
|
91
98
|
ports.fail(MSG_SIGN_IN_FIRST);
|
|
99
|
+
ports.fail(MSG_LOCAL_CHECK);
|
|
92
100
|
return EXIT_FAIL;
|
|
93
101
|
}
|
|
94
102
|
const grant = await ports.requestDrive(cloud);
|
|
@@ -165,13 +173,13 @@ function tryPort(cwd) {
|
|
|
165
173
|
return readProjectPort(cwd) ?? RETICLE_DEFAULT_PORT;
|
|
166
174
|
}
|
|
167
175
|
/** A daemon serving `port`: the one already there, or one started now. Undefined when ready. */
|
|
168
|
-
async function ensureTryDaemon(port) {
|
|
176
|
+
export async function ensureTryDaemon(port) {
|
|
169
177
|
const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
|
|
170
178
|
if (presenceIsUsable(presence))
|
|
171
179
|
return undefined;
|
|
172
180
|
// A stranger on the port: a daemon spawned now could not bind, so say who holds it instead.
|
|
173
181
|
if (PortPresence.FREE !== presence)
|
|
174
|
-
return
|
|
182
|
+
return describePortState(presence, port);
|
|
175
183
|
const script = process.argv[1];
|
|
176
184
|
if (script === undefined)
|
|
177
185
|
return 'Cannot locate the reticle daemon script.';
|
package/dist/command/cli.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { PlatformLinkVia } from '@reticlehq/core/telemetry';
|
|
2
|
+
import { OnboardingPhase, PlatformLinkVia } from '@reticlehq/core/telemetry';
|
|
3
3
|
import { hudSignIn } from './cli/cloud-login.js';
|
|
4
|
-
import { handleSetupMcp, handleSetupInstall } from './cli/setup-mcp-cli.js';
|
|
4
|
+
import { agentsBlock, handleSetupMcp, handleSetupInstall, handleSetupWaitAgents, readAgentRows, shouldWaitForAgents, } from './cli/setup-mcp-cli.js';
|
|
5
5
|
import { reportStepFromCli } from './cli-onboarding.js';
|
|
6
6
|
import { pathToFileURL } from 'node:url';
|
|
7
7
|
import { openFailureNote, openLaunchFailureNote } from './cli/answers/open-note.js';
|
|
8
|
+
import { writeCiHint } from './cli/ci-hint.js';
|
|
9
|
+
import { currentAutomationHint, isCiEnv } from '../telemetry/automation-hint.js';
|
|
8
10
|
import { realpathSync } from 'node:fs';
|
|
9
11
|
import { join } from 'node:path';
|
|
10
12
|
import { installCrashGuard } from './setup/setup-command.js';
|
|
@@ -18,6 +20,7 @@ import { handleHook } from './cli/hook/hook-command.js';
|
|
|
18
20
|
import { HOOK_COMMAND } from './cli/cli-parse.js';
|
|
19
21
|
import { daemonProjectAt, readDaemonRegistry, resolveDaemonForProject, } from './daemon/daemon-resolve.js';
|
|
20
22
|
import { pickDaemonPortToBind } from './daemon/binding/free-port.js';
|
|
23
|
+
import { exitAfterClose } from './daemon/lifetime/daemon-exit.js';
|
|
21
24
|
import { isSameOrAbove, portForInit, portFromEnv } from './setup/init/init-port.js';
|
|
22
25
|
import { daemonStartOptions } from './cli/daemon-start-options.js';
|
|
23
26
|
import { handleGate, loadNamedFlows, resolveChangedFiles } from './cli/cli-flow-commands.js';
|
|
@@ -34,7 +37,8 @@ import { cmdLink, connectInstalls, isCloudCommand, runCloudCommand } from './cli
|
|
|
34
37
|
import { SERVER_VERSION } from './version/identity/server-version.js';
|
|
35
38
|
import { log } from '../log.js';
|
|
36
39
|
import { isAlive, removePid, spawnDaemon, discoverDaemonPort, writeDaemonRegistry, } from './daemon/daemon.js';
|
|
37
|
-
import { PortPresence, probePresence, presenceIsUsable
|
|
40
|
+
import { PortPresence, probePresence, presenceIsUsable } from './daemon/binding/port-presence.js';
|
|
41
|
+
import { describePortState } from './daemon/port-state.js';
|
|
38
42
|
import { waitForDaemon, probeDaemon } from '../surface/mcp/mcp-proxy.js';
|
|
39
43
|
import { installDaemonResilience, recordExitReason, DaemonExitReason, } from './daemon/daemon-resilience.js';
|
|
40
44
|
import { IdleShutdown, resolveIdleShutdownMs, resolveIdleCheckMs } from './daemon/idle-shutdown.js';
|
|
@@ -46,11 +50,15 @@ import { NAVIGATE_NO_SESSION_ID, navigateLeftTab, openCandidates } from './cli/o
|
|
|
46
50
|
import { fetchStatus } from './daemon/binding/daemon-status-probe.js';
|
|
47
51
|
import { handleDrive } from './cli/drive/drive-command.js';
|
|
48
52
|
import { handleVerify } from './cli/cli-verify.js';
|
|
53
|
+
import { FIRST_VERDICT_REASON, bareRunsFirstVerdict } from './cli/first-verdict.js';
|
|
54
|
+
import { cmdFirstVerdict } from './cli/first-verdict-node.js';
|
|
55
|
+
import { reportOnboardingStop } from '../telemetry/onboarding-funnel.js';
|
|
49
56
|
import { runKill } from './cli/cli-kill.js';
|
|
50
57
|
import { runInit, buildNodeIo, InitFailure } from '@reticlehq/init';
|
|
51
58
|
import { continueAfterInit } from './setup/init/init-runtime.js';
|
|
52
59
|
import { initFirstFlow } from './cli/try-command.js';
|
|
53
60
|
import { handleDoctor } from './cli/cli-doctor.js';
|
|
61
|
+
import { writeFirstUseWarnings } from './cli/first-use-checks.js';
|
|
54
62
|
import { serverInitHost } from './setup/init/init-host.js';
|
|
55
63
|
import { describeLicense } from '../features/license/license.js';
|
|
56
64
|
import { isLikelyDevServerPort, devServerPortWarning, readProjectPort, readProjectId, workspacePortConflict, projectDirOf, } from './cli/ports/resolve/cli-port.js';
|
|
@@ -60,7 +68,7 @@ import { renderHelp } from './cli/cli-usage.js';
|
|
|
60
68
|
import { RENAMED_COMMANDS, renamedCommandFor, renamedNote } from './cli/cli-parse-grammar.js';
|
|
61
69
|
import { handleFeedback, handleTelemetry } from '../telemetry/feedback-cli.js';
|
|
62
70
|
import { installDaemonTelemetry } from '../telemetry/daemon-telemetry.js';
|
|
63
|
-
import { reportCliRun } from '../telemetry/cli-telemetry.js';
|
|
71
|
+
import { reportCliRun, reportGateCompleted } from '../telemetry/cli-telemetry.js';
|
|
64
72
|
// Re-exported so existing imports (and the CLI tests) keep resolving from '@/cli.js'.
|
|
65
73
|
export { parseCliArgs, CLI_USAGE };
|
|
66
74
|
/** Said when `connect` carries on past an init whose setup did not finish. */
|
|
@@ -94,6 +102,11 @@ async function handleInit(parsed) {
|
|
|
94
102
|
// RETICLE_PORT counts as explicit, as it does for every other command: resolved once here, so the
|
|
95
103
|
// port written into the project and the port the runtime phase binds are the same number.
|
|
96
104
|
const explicit = parsed.port ?? portFromEnv(process.env);
|
|
105
|
+
// The bridge port matters only when init goes on to start the app.
|
|
106
|
+
const bridgePort = true === parsed.filesOnly || parsed.dryRun
|
|
107
|
+
? undefined
|
|
108
|
+
: (explicit ?? readProjectPort(cwd) ?? RETICLE_DEFAULT_PORT);
|
|
109
|
+
await writeFirstUseWarnings(bridgePort);
|
|
97
110
|
const port = await portForInit(explicit, readProjectPort(cwd), readProjectId(cwd), {
|
|
98
111
|
// The registry names a daemon's owner; an older daemon that never registered is known by the
|
|
99
112
|
// projects its connected pages announced.
|
|
@@ -134,7 +147,7 @@ async function handleInit(parsed) {
|
|
|
134
147
|
deferOutcome: true,
|
|
135
148
|
continuesToRuntime: true !== parsed.filesOnly && true !== parsed.dryRun,
|
|
136
149
|
}, io);
|
|
137
|
-
await continueAfterInit({ ...parsed, port }, result, io, cwd, initFirstFlow);
|
|
150
|
+
await continueAfterInit({ ...parsed, port }, result, io, cwd, initFirstFlow, (print, mayWait) => agentsBlock(readAgentRows(cwd), print, mayWait && shouldWaitForAgents(process.stdout.isTTY, isCiEnv(process.env))));
|
|
138
151
|
return result;
|
|
139
152
|
}
|
|
140
153
|
// `serve`, `stop` and `restart` live in `cli/lifecycle/daemon-lifecycle.ts`: one idea, and the
|
|
@@ -194,7 +207,7 @@ function ensureDaemon(port) {
|
|
|
194
207
|
// success for a daemon that never bound" shape. Refuse instead, with the sentence `doctor`
|
|
195
208
|
// says.
|
|
196
209
|
if (PortPresence.FREE !== presence) {
|
|
197
|
-
throw new Error(
|
|
210
|
+
throw new Error(describePortState(presence, port));
|
|
198
211
|
}
|
|
199
212
|
const scriptPath = process.argv[1];
|
|
200
213
|
if (scriptPath === undefined) {
|
|
@@ -212,6 +225,8 @@ function ensureDaemon(port) {
|
|
|
212
225
|
*/
|
|
213
226
|
/** How long `reticle open` waits for the launched page to register before reporting on it. */
|
|
214
227
|
const OPEN_SESSION_WAIT_MS = 8000;
|
|
228
|
+
/** For the heartbeat's resident memory, in MB. */
|
|
229
|
+
const BYTES_PER_MB = 1024 * 1024;
|
|
215
230
|
/**
|
|
216
231
|
* Poll until a NEW session shows up, or the wait runs out. Returns whether one did.
|
|
217
232
|
*
|
|
@@ -375,24 +390,15 @@ function handleDaemonInner(parsed) {
|
|
|
375
390
|
// to reach a socket that is still open.
|
|
376
391
|
server.announceShutdown?.();
|
|
377
392
|
// Awaited before the close/exit chain: `process.exit(0)` kills an in-flight POST, and this is
|
|
378
|
-
// the one event carrying the whole session.
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
.shutdown(reason)
|
|
386
|
-
.then(() => server.close())
|
|
387
|
-
.then(() => {
|
|
393
|
+
// the one event carrying the whole session. The reason rides out on the session summary;
|
|
394
|
+
// the proxy sees only a socket end. See SessionSummary.exit. The summary and the close share
|
|
395
|
+
// one bound, so a close that never settles cannot keep this process on its port (#1937).
|
|
396
|
+
void exitAfterClose({
|
|
397
|
+
summary: () => daemonTelemetry.shutdown(reason),
|
|
398
|
+
close: () => server.close(),
|
|
399
|
+
}).then((code) => {
|
|
388
400
|
removePid(parsed.port);
|
|
389
|
-
process.exit(
|
|
390
|
-
})
|
|
391
|
-
.catch((err) => {
|
|
392
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
393
|
-
log('reticle_daemon_close_error', { error: message });
|
|
394
|
-
removePid(parsed.port);
|
|
395
|
-
process.exit(1);
|
|
401
|
+
process.exit(code);
|
|
396
402
|
});
|
|
397
403
|
};
|
|
398
404
|
// Wrapped rather than passed directly: a signal handler receives the signal name as its first
|
|
@@ -432,6 +438,7 @@ function handleDaemonInner(parsed) {
|
|
|
432
438
|
facts: () => ({
|
|
433
439
|
sessions: server.bridge.sessions.count(),
|
|
434
440
|
served: everServedToolCall(),
|
|
441
|
+
rssMb: Math.round(process.memoryUsage().rss / BYTES_PER_MB),
|
|
435
442
|
}),
|
|
436
443
|
}).start();
|
|
437
444
|
})
|
|
@@ -617,6 +624,17 @@ export function main() {
|
|
|
617
624
|
void handleInit(parsed);
|
|
618
625
|
break;
|
|
619
626
|
case 'serve':
|
|
627
|
+
// Bare `reticle` in a folder Reticle has not wired: the first verdict, with no agent and no
|
|
628
|
+
// `init`. A wired project, and `reticle serve` itself, keep serving.
|
|
629
|
+
if (bareRunsFirstVerdict(argv, myProjectId !== undefined || projectPort !== undefined || portConflict !== undefined)) {
|
|
630
|
+
void cmdFirstVerdict(parsed.port, !parsed.headless, (row) => reportOnboardingStop({ phase: OnboardingPhase.FIRST_RUN, reason: FIRST_VERDICT_REASON, ...row }, { detach: true }))
|
|
631
|
+
.then((code) => process.exit(code))
|
|
632
|
+
.catch((cause) => {
|
|
633
|
+
process.stderr.write(`reticle: ${cause instanceof Error ? cause.message : String(cause)}\n`);
|
|
634
|
+
process.exit(1);
|
|
635
|
+
});
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
620
638
|
handleServe(parsed);
|
|
621
639
|
break;
|
|
622
640
|
case 'stop':
|
|
@@ -655,17 +673,24 @@ export function main() {
|
|
|
655
673
|
process.exit(1);
|
|
656
674
|
break;
|
|
657
675
|
case 'doctor':
|
|
658
|
-
void handleDoctor(parsed.port);
|
|
676
|
+
void handleDoctor(parsed.port, parsed.fix, reportStepFromCli);
|
|
659
677
|
break;
|
|
660
678
|
case 'setup-mcp':
|
|
661
|
-
|
|
679
|
+
void writeFirstUseWarnings().then(() => {
|
|
680
|
+
handleSetupMcp(reportStepFromCli, true, shouldWaitForAgents(process.stdout.isTTY, isCiEnv(process.env)));
|
|
681
|
+
});
|
|
682
|
+
break;
|
|
683
|
+
case 'setup-wait-agents':
|
|
684
|
+
void handleSetupWaitAgents(shouldWaitForAgents(process.stdout.isTTY, isCiEnv(process.env)));
|
|
662
685
|
break;
|
|
663
686
|
case 'setup-install':
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
687
|
+
void writeFirstUseWarnings().then(() => {
|
|
688
|
+
handleSetupInstall({
|
|
689
|
+
runtimeSecs: parsed.runtimeSecs,
|
|
690
|
+
installSecs: parsed.installSecs,
|
|
691
|
+
mcp: parsed.mcp,
|
|
692
|
+
}, reportStepFromCli);
|
|
693
|
+
});
|
|
669
694
|
break;
|
|
670
695
|
case 'open':
|
|
671
696
|
handleOpen(parsed.port, parsed.url, parsed.navigate);
|
|
@@ -674,13 +699,25 @@ export function main() {
|
|
|
674
699
|
handleDrive(parsed, readProjectId(process.cwd()));
|
|
675
700
|
break;
|
|
676
701
|
case 'verify':
|
|
677
|
-
|
|
702
|
+
void writeFirstUseWarnings().then(() => {
|
|
703
|
+
handleVerify(parsed, () => {
|
|
704
|
+
writeCiHint({ automation: currentAutomationHint(process.env), machineOutput: false });
|
|
705
|
+
});
|
|
706
|
+
});
|
|
678
707
|
break;
|
|
679
708
|
case 'affected':
|
|
680
|
-
void handleAffected(parsed.files, parsed.since)
|
|
709
|
+
void handleAffected(parsed.files, parsed.since).then(() => {
|
|
710
|
+
writeCiHint({ automation: currentAutomationHint(process.env), machineOutput: false });
|
|
711
|
+
});
|
|
681
712
|
break;
|
|
682
713
|
case 'gate':
|
|
683
|
-
void handleGate(parsed.files, parsed.since, parsed.hook, parsed.acceptCoverage)
|
|
714
|
+
void handleGate(parsed.files, parsed.since, parsed.hook, parsed.acceptCoverage, reportGateCompleted).then(() => {
|
|
715
|
+
// A stop hook's output is read by the agent, so it is a machine output mode.
|
|
716
|
+
writeCiHint({
|
|
717
|
+
automation: currentAutomationHint(process.env),
|
|
718
|
+
machineOutput: true === parsed.hook,
|
|
719
|
+
});
|
|
720
|
+
});
|
|
684
721
|
break;
|
|
685
722
|
case 'report':
|
|
686
723
|
void handleReport(parsed.session, parsed.hook);
|