premanmcp 0.10.4 → 0.10.6

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/README.md CHANGED
@@ -64,21 +64,28 @@ and then retries from your home directory, where the override cannot reach it
64
64
  first, then your agent. Agents you start in the overriding directory keep using its
65
65
  backend, which is the point of the file.
66
66
 
67
- Once linked, `connect` finishes onboarding without handing you homework:
67
+ Once linked, `connect` installs the git pre-push hook — so `git push` checks the endpoints
68
+ you touched — prints what is left, and stops. It asks nothing. Everything else it used to
69
+ run is its own command, because each one can fail on its own and none of them should hold
70
+ up a link that already worked:
68
71
 
69
- | Step | What happens |
70
- |------|--------------|
71
- | Endpoints | Runs your agent on the discovery brief, then reports `N endpoints · M runnable` |
72
- | First test | Generates and runs scenarios against the first runnable request |
73
- | Runner | Pairs this machine and starts `preman runner` in the background, so PreMan can apply fixes here instead of only describing them |
74
- | Desktop app | Offers the macOS app for watching runs and endpoints |
75
- | Integrations | Shows GitHub / AWS / Slack, opens what is missing, and picks the connection up when you finish in the browser or the desktop app |
76
- | Testing on push | Installs the git pre-push hook, so `git push` checks the endpoints you touched |
72
+ ```bash
73
+ preman endpoints discover # map this repo's endpoints
74
+ preman runner start --background # let PreMan apply fixes on this machine
75
+ preman github # or connect it in the dashboard
76
+ preman status # which of those are done
77
+ ```
78
+
79
+ `preman onboard` (or `setup`) is the prompted walk through all of it sign in, coding
80
+ agent, endpoints, runner, GitHub, AWS, Slack — one question per step, `b` to go back, and a
81
+ summary at the end. `connect --guide` runs the old full pass inside connect itself:
82
+ discovery, a first test, the runner, the desktop app and the integration prompts.
77
83
 
78
84
  Useful flags: `--agent cursor|claude-code|codex` skips the picker, `--project` writes
79
- project-local config, `--print` shows the config without writing it, `--yes` takes every
80
- step's default without asking, `--no-guide` skips all of them, and `--no-runner` /
81
- `--no-desktop` / `--no-integrations` / `--no-hook` skip one each.
85
+ project-local config, `--print` shows the config without writing it, `--no-hook` leaves
86
+ push testing alone, and `--no-guide` connects and nothing else. With `--guide`, `--yes`
87
+ takes every step's default without asking and `--no-runner` / `--no-desktop` /
88
+ `--no-integrations` skip one each.
82
89
 
83
90
  `--yes` deliberately does *not* install the desktop app: that step's default is no, because
84
91
  it downloads a hundred-odd megabytes and writes to `/Applications`. Run `install-desktop`
@@ -126,6 +133,7 @@ npm exec -y premanmcp@latest -- connect --project
126
133
  ## What It Does
127
134
 
128
135
  - Converts API endpoints into agent-callable MCP tools.
136
+ - Migrates a Postman collection in one call (`migrate_from_postman`): keeps the assertions your `pm.test` blocks declared, splits the environment into shared variables and encrypted secrets, and schedules a monitored test suite per request.
129
137
  - Creates/connects a PreMan account from the terminal or IDE agent.
130
138
  - Lets agents test real backend endpoints from the IDE.
131
139
  - Syncs endpoint inventory across backend and frontend workflows.
@@ -159,6 +167,10 @@ Convert these endpoints into an MCP.
159
167
  Show me the audit log for this hosted MCP.
160
168
  ```
161
169
 
170
+ ```text
171
+ Move my Postman collection over to PreMan.
172
+ ```
173
+
162
174
  ```text
163
175
  Pull my pending PreMan fix tasks and fix the failing endpoint.
164
176
  ```
@@ -218,17 +230,51 @@ npm exec -y premanmcp@latest -- login # Create/login and generate a PreMan API
218
230
  npm exec -y premanmcp@latest -- install # Cursor-only installer (legacy)
219
231
  ```
220
232
 
233
+ ### Push testing
234
+
235
+ ```bash
236
+ preman hook install # Write the pre-push hook (connect does this too)
237
+ preman hook status # Installed? And does the command in it still answer?
238
+ preman hook repair # Rewrite it if it stopped working (usually automatic)
239
+ preman hook uninstall # Remove it, restoring any hook it replaced
240
+ ```
241
+
242
+ The hook is generated shell that runs `preman verify --pre-push` and can only ever be
243
+ advisory: no backend, no credentials, a crash or a timeout all exit 0 with a notice. Before
244
+ writing it, `install` runs the command it is about to embed and requires an answer — a hook
245
+ holding a `preman` that belongs to another package prints `checks skipped` at every push and
246
+ looks installed forever. The embedded command is pinned to the version that wrote it rather
247
+ than `@latest`, so upgrading us never changes what your pushes run; re-run `hook install`
248
+ to move it. Set `PREMAN_HOOK_INVOCATION` to write a command of your own instead, and
249
+ `PREMAN_SKIP_HOOK=1` to silence the hook for a push.
250
+
251
+ You should not have to run `repair`. A hook of ours that stops answering is rewritten in the
252
+ background by anything that proves PreMan runs here — any command, or your agent starting the
253
+ MCP server — because nothing re-runs `connect` after an upgrade to notice, and the broken
254
+ state is silent. It is narrow on purpose: a hook that still works keeps the version it pins, a
255
+ hook we did not write is never touched, and no hook is installed where you never asked for
256
+ one. The answer is remembered for an hour per directory, and `PREMAN_NO_HOOK_REPAIR=1` turns
257
+ it off.
258
+
221
259
  ### Runner
222
260
 
223
- `connect` sets this up for you; these are for managing it afterwards.
261
+ `preman onboard` and `connect --guide` set this up for you; these are for managing it
262
+ afterwards.
224
263
 
225
264
  ```bash
226
265
  preman runner status # Paired? Running?
227
266
  preman runner start --background # Hold the job stream, log to ~/.preman/runner.log
228
267
  preman runner stop # Stop it and report offline
229
- preman runner register --agent claude-code # Re-pair (e.g. a different project)
268
+ preman runner register --agent claude-code # Pair without starting
230
269
  ```
231
270
 
271
+ `start` pairs this machine itself when it is not paired yet, and re-pairs it when the
272
+ backend has revoked the token it held, so neither is a command you have to be told to run.
273
+ It works out which agent to pair as from the session it is running in, then from the agent
274
+ PreMan is already configured in, then from the only one installed — and asks only when
275
+ those disagree. Pass `--agent cursor|claude-code|codex` to settle it yourself, which is
276
+ also what a machine with no terminal to ask in needs.
277
+
232
278
  The runner holds one outbound connection to PreMan and runs the work PreMan queues for
233
279
  this machine — a failing endpoint becomes an agent run in your own repo instead of a
234
280
  prompt you have to paste. It is bound to the agent and directory it was registered with
package/bin/account.js CHANGED
@@ -11,6 +11,7 @@ import { existsSync, rmSync } from "node:fs";
11
11
  import os from "node:os";
12
12
 
13
13
  import { detectCandidates } from "./detect.js";
14
+ import { hookStatus } from "./hook.js";
14
15
  import {
15
16
  CREDENTIALS_FILE,
16
17
  backendUrl,
@@ -150,6 +151,39 @@ export async function doctorCommand(commandArgs = []) {
150
151
  )
151
152
  );
152
153
 
154
+ // An installed hook whose command no longer answers is the one failure here
155
+ // that reports itself as success everywhere else: `git push` prints a single
156
+ // skipped line and nothing else ever mentions it.
157
+ const hook = (() => {
158
+ try {
159
+ return hookStatus({ probe: true });
160
+ } catch {
161
+ return null; // not a git repository
162
+ }
163
+ })();
164
+ if (hook && hook.state === "installed") {
165
+ results.push(
166
+ line(
167
+ "push hook",
168
+ hook.works,
169
+ hook.works
170
+ ? `${hook.invocation}${hook.current ? "" : ` — out of date, run \`${cliInvocation()} hook install --force\``}`
171
+ : `${hook.invocation || "unreadable"} does not answer — run \`${cliInvocation()} hook install --force\``
172
+ )
173
+ );
174
+ if (!hook.works) failures += 1;
175
+ } else if (hook) {
176
+ results.push(
177
+ line(
178
+ "push hook",
179
+ null,
180
+ hook.state === "foreign"
181
+ ? "another tool owns pre-push here"
182
+ : `not installed — run \`${cliInvocation()} hook install\``
183
+ )
184
+ );
185
+ }
186
+
153
187
  if (status?.ok) {
154
188
  const integrations = status.integrations || {};
155
189
  for (const [name, section] of Object.entries(integrations)) {
package/bin/api_tools.js CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { readFileSync } from "node:fs";
10
10
 
11
- import { callBackendJson, makeArgs, resolveApiKey } from "./shared.js";
11
+ import { callBackendJson, cliInvocation, makeArgs, resolveApiKey } from "./shared.js";
12
12
 
13
13
  export const ENDPOINTS_HELP = `
14
14
  Endpoints:
package/bin/cli.js CHANGED
@@ -21,7 +21,9 @@ import {
21
21
  CONNECT_HELP,
22
22
  DISPATCH_HELP,
23
23
  connectCommand,
24
+ discoverEndpoints,
24
25
  dispatchCommand,
26
+ resolveAgentForPairing,
25
27
  writeCursorConfig,
26
28
  } from "./connect.js";
27
29
  import {
@@ -33,7 +35,7 @@ import {
33
35
  } from "./integrations.js";
34
36
  import { HOSTED_HELP, linkCommand, runCommand, toolsCommand } from "./hosted.js";
35
37
  import { STATUS_HELP, statusCommand } from "./status.js";
36
- import { HOOK_HELP, hookCommand } from "./hook.js";
38
+ import { HOOK_HELP, hookCommand, scheduleHookRepair } from "./hook.js";
37
39
  import { RUNNER_HELP, runnerCommand } from "./runner.js";
38
40
  import { VERIFY_HELP, verifyCommand } from "./verify.js";
39
41
  import { DESKTOP_HELP, installDesktopCommand } from "./desktop.js";
@@ -84,7 +86,7 @@ function printHelp() {
84
86
  ["runner start|status|stop", "Run PreMan's queued agent work on this machine"],
85
87
  ["doctor", "Diagnose credentials, backend, target, integrations"],
86
88
  ["install-desktop", "Download and install the PreMan desktop app"],
87
- ["onboard", "Sign in, then connect agent, GitHub, AWS, Slack"],
89
+ ["onboard", "Sign in, then agent, endpoints, runner, GitHub, AWS, Slack"],
88
90
  ["connect [options]", "Pick a coding agent and connect it"],
89
91
  ["dispatch [options]", "Let PreMan start agent runs for you"],
90
92
  ["aws | github | slack", "Connect one integration on its own"],
@@ -134,6 +136,7 @@ async function loginCommand() {
134
136
  await loginBrowser(cliArgs);
135
137
  return;
136
138
  }
139
+ const cli = cliInvocation();
137
140
  const creds = await authenticateTerminal(cliArgs);
138
141
  process.stdout.write(`PreMan account ready.
139
142
 
@@ -152,6 +155,7 @@ You can now run:
152
155
  * the same write plus agent choice, pairing, and dispatch setup.
153
156
  */
154
157
  async function installCursorMcp() {
158
+ const cli = cliInvocation();
155
159
  const serverName = argValue("--name", "preman");
156
160
  const projectInstall = hasFlag("--project");
157
161
 
@@ -178,7 +182,7 @@ Server name: ${serverName}
178
182
  Backend: ${serverConfig.env.PREMAN_BACKEND}
179
183
 
180
184
  Next steps:
181
- 1. ${hasInlineKey ? "Your PreMan API key was written to the MCP config." : hasStoredKey ? `Your PreMan API key is saved in ${CREDENTIALS_FILE}; the MCP server will load it automatically.` : "Run ${cli} login to create/connect your account and generate an API key."}
185
+ 1. ${hasInlineKey ? "Your PreMan API key was written to the MCP config." : hasStoredKey ? `Your PreMan API key is saved in ${CREDENTIALS_FILE}; the MCP server will load it automatically.` : `Run ${cli} login to create/connect your account and generate an API key.`}
182
186
  2. Restart Cursor or toggle the PreMan MCP server off/on.
183
187
  3. In your API repo, ask your coding agent:
184
188
  "Use PreMan to convert the endpoints I choose into a hosted MCP server, then give me the Cursor/Claude install snippet."
@@ -215,6 +219,11 @@ function startServer() {
215
219
  }
216
220
 
217
221
  async function main() {
222
+ // Running at all is the signal. `hook` and `verify` own the hook themselves,
223
+ // and connect reinstalls it in the foreground, so a background rewrite there
224
+ // would only race with the one the user is watching.
225
+ if (!["hook", "verify", "connect"].includes(command)) scheduleHookRepair();
226
+
218
227
  if (command === "login") {
219
228
  await loginCommand();
220
229
  } else if (command === "status") {
@@ -224,7 +233,10 @@ async function main() {
224
233
  } else if (command === "hook") {
225
234
  await hookCommand(commandArgs);
226
235
  } else if (command === "runner") {
227
- await runnerCommand(commandArgs);
236
+ // Injected rather than imported inside runner.js, which connect.js already
237
+ // depends on: the agent picker lives there, and importing it back would
238
+ // close the cycle.
239
+ await runnerCommand(commandArgs, { resolveAgent: resolveAgentForPairing });
228
240
  } else if (command === "logout") {
229
241
  await logoutCommand();
230
242
  } else if (command === "doctor") {
@@ -240,7 +252,13 @@ async function main() {
240
252
  } else if (command === "onboard" || command === "setup") {
241
253
  // makeArgs/authenticateTerminal/connectCommand are injected rather than
242
254
  // imported there, so integrations.js stays free of a cycle back into the CLI.
243
- await onboardCommand(commandArgs, { makeArgs, authenticateTerminal, connectCommand });
255
+ await onboardCommand(commandArgs, {
256
+ makeArgs,
257
+ authenticateTerminal,
258
+ connectCommand,
259
+ discoverEndpoints,
260
+ runnerCommand,
261
+ });
244
262
  } else if (command === "aws") {
245
263
  await awsCommand(makeArgs(commandArgs));
246
264
  } else if (command === "github") {
package/bin/connect.js CHANGED
@@ -21,6 +21,7 @@ import { installHook, hookStatus } from "./hook.js";
21
21
  import { MARK, awsCommand, githubCommand, slackCommand } from "./integrations.js";
22
22
  import {
23
23
  confirmRunnerOnline,
24
+ pairingIsLive,
24
25
  readRunnerState,
25
26
  registerRunner,
26
27
  runnerIsAlive,
@@ -156,6 +157,52 @@ export function runningInside(env = process.env) {
156
157
  return "";
157
158
  }
158
159
 
160
+ /** Is PreMan's MCP server already written into this agent's config? */
161
+ function agentIsLinked(agentId, serverName = "preman") {
162
+ try {
163
+ const home = os.homedir();
164
+ if (agentId === "cursor") {
165
+ return Boolean(readJsonFile(path.join(home, ".cursor", "mcp.json")).mcpServers?.[serverName]);
166
+ }
167
+ if (agentId === "claude_code") {
168
+ return Boolean(readJsonFile(path.join(home, ".claude.json")).mcpServers?.[serverName]);
169
+ }
170
+ if (agentId === "codex") {
171
+ const file = path.join(process.env.CODEX_HOME || path.join(home, ".codex"), "config.toml");
172
+ return existsSync(file) && readFileSync(file, "utf8").includes(`[mcp_servers.${serverName}]`);
173
+ }
174
+ } catch {
175
+ // An unreadable config is not an answer; fall through to the other signals.
176
+ }
177
+ return false;
178
+ }
179
+
180
+ /**
181
+ * Which agent this machine should pair as, or "" when only a person can say.
182
+ *
183
+ * Pairing needs an agent id, and demanding `--agent` for it turned "start the
184
+ * runner" into a command someone had to be told twice. The signals are ordered
185
+ * by how much they actually know: the session we are inside, then the agent
186
+ * PreMan is already configured in, then the only one installed. Asking is the
187
+ * last resort rather than the first, and a machine with no terminal to ask in
188
+ * gets "" so the caller can fail with a sentence instead of hanging on a prompt.
189
+ */
190
+ export async function resolveAgentForPairing({ interactive = Boolean(process.stdin.isTTY) } = {}) {
191
+ const inside = runningInside();
192
+ if (inside) return inside;
193
+
194
+ const linked = AGENTS.filter((agent) => agentIsLinked(agent.id));
195
+ if (linked.length === 1) return linked[0].id;
196
+
197
+ const detected = detectAgents();
198
+ const present = AGENTS.filter((agent) => detected[agent.id]);
199
+ if (present.length === 1) return present[0].id;
200
+
201
+ if (!interactive) return "";
202
+ const picked = await promptAgentChoice(detected);
203
+ return picked?.id || "";
204
+ }
205
+
159
206
  async function promptAgentChoice(detected, insideId = runningInside()) {
160
207
  process.stdout.write("Which coding agent?\n");
161
208
  AGENTS.forEach((agent, index) => {
@@ -1060,6 +1107,26 @@ function nextStepsBlock(agent) {
1060
1107
  );
1061
1108
  }
1062
1109
 
1110
+ /**
1111
+ * What is left once the link is done, as commands rather than as questions.
1112
+ *
1113
+ * Connect used to run all of these itself, which bought a prompt per step and a
1114
+ * wait as long as the slowest broken one — and a link that had already succeeded
1115
+ * ended on a failed runner and a five-minute GitHub poll. Each is one command, so
1116
+ * this lists them and gets out of the way.
1117
+ */
1118
+ function whatIsLeftBlock(args) {
1119
+ const cli = cliInvocation();
1120
+ return (
1121
+ "\nWhat is left:\n" +
1122
+ ` ${cli} endpoints discover # map this repo's endpoints\n` +
1123
+ ` ${cli} runner start --background # let PreMan apply fixes on this machine\n` +
1124
+ ` ${cli} github # connect GitHub, or do it in the dashboard\n` +
1125
+ `\n${cli} status reports your endpoints, last push and integrations.\n` +
1126
+ `Watch runs at ${frontendUrl(args)}\n`
1127
+ );
1128
+ }
1129
+
1063
1130
  /**
1064
1131
  * Ask, or take the step's own default when nobody can answer.
1065
1132
  *
@@ -1130,7 +1197,7 @@ export function headlessDiscovery(agent, serverName, instructions = []) {
1130
1197
  *
1131
1198
  * Never throws: a failed discovery must not fail the connect.
1132
1199
  */
1133
- async function discoverEndpoints(args, agent, serverName, blockedHere = "") {
1200
+ export async function discoverEndpoints(args, agent, serverName, blockedHere = "") {
1134
1201
  try {
1135
1202
  const before = await endpointCounts(args);
1136
1203
  if (before.registered) {
@@ -1241,6 +1308,24 @@ async function runFirstTest(args, runnable, { assumeYes }) {
1241
1308
  }
1242
1309
  }
1243
1310
 
1311
+ /**
1312
+ * Whether a stored pairing can still be used, rather than merely matching.
1313
+ *
1314
+ * Matching agent and path only prove the state was written for this project. The
1315
+ * token behind it can be revoked, replaced by another device, or left over from an
1316
+ * API key that no longer exists — and reusing one of those starts a daemon that
1317
+ * 401s on its first stream and exits, which reads as "the runner cannot start
1318
+ * here" when the pairing is the only thing that needed replacing.
1319
+ *
1320
+ * A backend we cannot reach answers "usable": re-pairing is for a token the
1321
+ * backend rejected, not for a network that was briefly down.
1322
+ */
1323
+ export async function runnerPairingIsUsable(args, existing, agentId, { cwd = process.cwd() } = {}) {
1324
+ if (!existing || existing.agent !== agentId) return false;
1325
+ if (path.resolve(String(existing.project_path || "")) !== path.resolve(cwd)) return false;
1326
+ return pairingIsLive(args, existing);
1327
+ }
1328
+
1244
1329
  /**
1245
1330
  * Pair this machine as a runner and leave it running.
1246
1331
  *
@@ -1267,7 +1352,7 @@ async function setUpRunner(args, agent, { assumeYes }) {
1267
1352
  }
1268
1353
 
1269
1354
  try {
1270
- if (!existing || existing.agent !== agent.id || existing.project_path !== path.resolve(process.cwd())) {
1355
+ if (!(await runnerPairingIsUsable(args, existing, agent.id))) {
1271
1356
  await registerRunner(args, { agent: agent.id, projectPath: process.cwd() });
1272
1357
  }
1273
1358
  const started = startBackground([]);
@@ -1409,7 +1494,7 @@ async function setUpPushTesting(args) {
1409
1494
  if (args.has("--no-hook")) return { state: "skipped" };
1410
1495
  const current = (() => {
1411
1496
  try {
1412
- return hookStatus();
1497
+ return hookStatus({ probe: true });
1413
1498
  } catch {
1414
1499
  return null; // not a git repository
1415
1500
  }
@@ -1418,9 +1503,10 @@ async function setUpPushTesting(args) {
1418
1503
  process.stdout.write(`${MARK.skip()} Not a git repository — no push testing here.\n`);
1419
1504
  return { state: "unavailable" };
1420
1505
  }
1421
- // A hook whose invocation went stale is reinstalled rather than reported as on:
1422
- // it is the case where PreMan looks connected and silently checks nothing.
1423
- if (current.state === "installed" && current.current) {
1506
+ // A hook whose invocation went stale, or no longer answers, is reinstalled
1507
+ // rather than reported as on: it is the case where PreMan looks connected and
1508
+ // silently checks nothing.
1509
+ if (current.state === "installed" && current.current && current.works) {
1424
1510
  process.stdout.write(`${MARK.ok()} Push testing already on.\n`);
1425
1511
  return { state: "installed" };
1426
1512
  }
@@ -1432,6 +1518,13 @@ async function setUpPushTesting(args) {
1432
1518
  );
1433
1519
  return result;
1434
1520
  }
1521
+ if (result.action === "unproven") {
1522
+ process.stdout.write(
1523
+ `${MARK.skip()} No push testing: ${result.detail}\n` +
1524
+ ` Then: ${cliInvocation()} hook install\n`
1525
+ );
1526
+ return result;
1527
+ }
1435
1528
  process.stdout.write(
1436
1529
  `${MARK.ok()} Push testing on — \`git push\` now checks the endpoints you touched.\n` +
1437
1530
  " It never blocks a push; PREMAN_SKIP_HOOK=1 silences it.\n"
@@ -1440,10 +1533,12 @@ async function setUpPushTesting(args) {
1440
1533
  }
1441
1534
 
1442
1535
  /**
1443
- * Everything after the link, in one pass, with nothing left for the user to run.
1536
+ * Everything after the link, in one pass, for whoever asks for it with `--guide`.
1444
1537
  *
1445
1538
  * Order follows what a new account needs to see: what PreMan found, then where to
1446
- * watch it, then who to tell, then when to run it.
1539
+ * watch it, then who to tell, then when to run it. It is opt-in because running
1540
+ * all of it unasked is what turned a finished connect into a screen of prompts,
1541
+ * a dead runner and a five-minute wait on an integration nobody had asked for.
1447
1542
  */
1448
1543
  async function guidedFirstRun(args, agent, apiKey, serverName, { blockedHere = "" } = {}) {
1449
1544
  const assumeYes = args.has("--yes");
@@ -1544,10 +1639,12 @@ Connect options:
1544
1639
  --no-self-test Do not start the MCP server to finish the link
1545
1640
  --no-auto-checkin Do not run the agent to finish the link
1546
1641
  --no-wait Do not wait for the agent to check in
1547
- --no-guide Skip the guided first run after connecting
1548
- --no-runner Do not pair this machine as a job runner
1549
- --no-desktop Do not offer the desktop app
1550
- --no-integrations Do not check or offer GitHub / AWS / Slack
1642
+ --guide Also run discovery, the runner, the desktop app
1643
+ and the integration prompts after connecting
1644
+ --no-guide Connect only: no push hook, no closing summary
1645
+ --no-runner With --guide, do not pair this machine as a runner
1646
+ --no-desktop With --guide, do not offer the desktop app
1647
+ --no-integrations With --guide, do not offer GitHub / AWS / Slack
1551
1648
  --no-hook Do not install the git pre-push hook
1552
1649
  --yes Take every step's default without prompting
1553
1650
  (the desktop app defaults to no; install-desktop)
@@ -1670,18 +1767,36 @@ export async function connectCommand(commandArgs) {
1670
1767
  serverConfig,
1671
1768
  projectInstall,
1672
1769
  });
1770
+ // The agent goes back to the caller because `onboard` runs steps after this one
1771
+ // that need to know which agent to drive, and asking twice is a question we
1772
+ // already have the answer to.
1773
+ const connected = { agent, serverName, linked: checkIn.linked };
1673
1774
  if (!checkIn.linked) {
1674
1775
  // Still honour an explicitly-passed credential, but do not open a new prompt
1675
1776
  // on top of a connect that just told the user something went wrong.
1676
1777
  await captureDispatchCredential(args, agent, apiKey, { prompt: false });
1677
- return;
1778
+ return connected;
1678
1779
  }
1679
1780
 
1680
1781
  process.stdout.write(`${MARK.ok()} Connected as ${agent.label}.\n`);
1681
- if (!args.has("--no-guide")) {
1782
+
1783
+ // Connecting is one job. Running discovery, pairing a runner, offering the
1784
+ // desktop app and installing three integrations is five more, each with its
1785
+ // own prompt and its own way to fail -- and a connect that ends in a failed
1786
+ // runner and a five-minute GitHub poll reads as a broken product rather than a
1787
+ // finished link. The push hook stays because it is the only one that is not a
1788
+ // question: it is what makes PreMan run at all, and it costs a file write.
1789
+ if (args.has("--guide") && !args.has("--no-guide")) {
1682
1790
  await guidedFirstRun(args, agent, apiKey, serverName, { blockedHere: checkIn.blockedHere });
1791
+ await captureDispatchCredential(args, agent, apiKey);
1792
+ return connected;
1683
1793
  }
1684
- await captureDispatchCredential(args, agent, apiKey);
1794
+ if (!args.has("--no-guide")) {
1795
+ await setUpPushTesting(args);
1796
+ process.stdout.write(whatIsLeftBlock(args));
1797
+ }
1798
+ await captureDispatchCredential(args, agent, apiKey, { prompt: false });
1799
+ return connected;
1685
1800
  }
1686
1801
 
1687
1802
  /**