premanmcp 0.7.1 → 0.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/README.md +23 -13
- package/bin/account.js +212 -0
- package/bin/changed.js +175 -0
- package/bin/cli.js +42 -3
- package/bin/connect.js +279 -59
- package/bin/desktop.js +214 -0
- package/bin/detect.js +378 -0
- package/bin/hook.js +165 -0
- package/bin/integrations.js +6 -26
- package/bin/progress.js +110 -0
- package/bin/shared.js +122 -30
- package/bin/status.js +210 -0
- package/bin/verify.js +701 -0
- package/package.json +4 -2
package/bin/connect.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* never fails the connect.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { execFileSync, spawnSync } from "node:child_process";
|
|
13
|
+
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
14
14
|
import { chmodSync, existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
15
15
|
import os from "node:os";
|
|
16
16
|
import path from "node:path";
|
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
cliInvocation,
|
|
26
26
|
frontendUrl,
|
|
27
27
|
hasKeyAvailable,
|
|
28
|
+
LAUNCHER_ARGS,
|
|
29
|
+
LAUNCHER_COMMAND,
|
|
28
30
|
makeArgs,
|
|
29
31
|
promptSecret,
|
|
30
32
|
promptText,
|
|
@@ -51,7 +53,11 @@ const AGENTS = [
|
|
|
51
53
|
id: "cursor",
|
|
52
54
|
label: "Cursor",
|
|
53
55
|
aliases: ["cursor"],
|
|
54
|
-
dispatch: {
|
|
56
|
+
dispatch: {
|
|
57
|
+
credential: "Cursor API key",
|
|
58
|
+
needsRoutine: false,
|
|
59
|
+
source: "cursor.com/dashboard → Integrations → API Keys",
|
|
60
|
+
},
|
|
55
61
|
snippetHint: "merge into ~/.cursor/mcp.json",
|
|
56
62
|
restartHint: 'Fully quit and reopen Cursor, then Settings → MCP → toggle "preman" off and on.',
|
|
57
63
|
},
|
|
@@ -59,7 +65,13 @@ const AGENTS = [
|
|
|
59
65
|
id: "claude_code",
|
|
60
66
|
label: "Claude Code",
|
|
61
67
|
aliases: ["claude", "claude-code", "claude_code", "claudecode"],
|
|
62
|
-
dispatch: {
|
|
68
|
+
dispatch: {
|
|
69
|
+
credential: "Claude Code routine token",
|
|
70
|
+
needsRoutine: true,
|
|
71
|
+
// Named explicitly because "Claude Code routine token" reads like an
|
|
72
|
+
// Anthropic API key, which is a different credential from a different page.
|
|
73
|
+
source: "claude.ai/code/routines → your routine → Add API trigger",
|
|
74
|
+
},
|
|
63
75
|
snippetHint: "run:",
|
|
64
76
|
restartHint: 'Start a new Claude Code session and run `claude mcp list` — "preman" should be listed.',
|
|
65
77
|
},
|
|
@@ -95,17 +107,36 @@ function detectAgents() {
|
|
|
95
107
|
};
|
|
96
108
|
}
|
|
97
109
|
|
|
98
|
-
|
|
110
|
+
/**
|
|
111
|
+
* The agent whose session this command is running inside, if any.
|
|
112
|
+
*
|
|
113
|
+
* A better default than "first one installed": someone who types this into an
|
|
114
|
+
* agent's terminal almost always means that agent, and on a machine with all
|
|
115
|
+
* three installed the detected-order default is usually wrong.
|
|
116
|
+
*/
|
|
117
|
+
export function runningInside(env = process.env) {
|
|
118
|
+
if (env.CLAUDECODE || env.CLAUDE_CODE) return "claude_code";
|
|
119
|
+
if (env.CURSOR_TRACE_ID) return "cursor";
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function promptAgentChoice(detected, insideId = runningInside()) {
|
|
99
124
|
process.stdout.write("Which coding agent?\n");
|
|
100
125
|
AGENTS.forEach((agent, index) => {
|
|
101
|
-
|
|
126
|
+
let mark = "";
|
|
127
|
+
if (agent.id === insideId) mark = " (this session)";
|
|
128
|
+
else if (detected[agent.id]) mark = " (detected)";
|
|
102
129
|
process.stdout.write(` ${index + 1}. ${agent.label}${mark}\n`);
|
|
103
130
|
});
|
|
104
131
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
132
|
+
const inside = AGENTS.findIndex((a) => a.id === insideId);
|
|
133
|
+
const defaultIndex =
|
|
134
|
+
inside >= 0
|
|
135
|
+
? inside
|
|
136
|
+
: Math.max(
|
|
137
|
+
0,
|
|
138
|
+
AGENTS.findIndex((a) => detected[a.id])
|
|
139
|
+
);
|
|
109
140
|
const answer = await promptText(`Pick [${defaultIndex + 1}]: `);
|
|
110
141
|
if (!answer) return AGENTS[defaultIndex];
|
|
111
142
|
|
|
@@ -272,8 +303,13 @@ export function renderAllAgentSnippets(args, serverName, { projectInstall = fals
|
|
|
272
303
|
PREMAN_BACKEND: backendUrl(args),
|
|
273
304
|
PREMAN_FRONTEND: frontendUrl(args),
|
|
274
305
|
};
|
|
275
|
-
const serverConfig = {
|
|
276
|
-
|
|
306
|
+
const serverConfig = {
|
|
307
|
+
command: LAUNCHER_COMMAND,
|
|
308
|
+
args: [...LAUNCHER_ARGS],
|
|
309
|
+
env,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
|
|
277
313
|
return AGENTS.map((agent) => {
|
|
278
314
|
// Adjust hint based on projectInstall, matching verifyWrittenConfig logic
|
|
279
315
|
let hint = agent.snippetHint;
|
|
@@ -319,8 +355,11 @@ export function verifyWrittenConfig(agent, { serverName, written }) {
|
|
|
319
355
|
if (!block) {
|
|
320
356
|
return { status: "mismatch", detail: `no [mcp_servers.${serverName}] block in ${written.path}` };
|
|
321
357
|
}
|
|
322
|
-
if (!block.some((line) => line.trim() ===
|
|
323
|
-
return {
|
|
358
|
+
if (!block.some((line) => line.trim() === `command = "${LAUNCHER_COMMAND}"`)) {
|
|
359
|
+
return {
|
|
360
|
+
status: "mismatch",
|
|
361
|
+
detail: `${written.path} does not launch ${LAUNCHER_COMMAND}`,
|
|
362
|
+
};
|
|
324
363
|
}
|
|
325
364
|
return { status: "verified" };
|
|
326
365
|
}
|
|
@@ -336,8 +375,11 @@ export function verifyWrittenConfig(agent, { serverName, written }) {
|
|
|
336
375
|
|
|
337
376
|
const entry = readJsonFile(written.path).mcpServers?.[serverName];
|
|
338
377
|
if (!entry) return { status: "mismatch", detail: `${serverName} is missing from ${written.path}` };
|
|
339
|
-
if (entry.command !==
|
|
340
|
-
return {
|
|
378
|
+
if (entry.command !== LAUNCHER_COMMAND) {
|
|
379
|
+
return {
|
|
380
|
+
status: "mismatch",
|
|
381
|
+
detail: `${written.path} does not launch ${LAUNCHER_COMMAND}`,
|
|
382
|
+
};
|
|
341
383
|
}
|
|
342
384
|
return { status: "verified" };
|
|
343
385
|
} catch (error) {
|
|
@@ -368,6 +410,7 @@ export async function waitForConnection(
|
|
|
368
410
|
{
|
|
369
411
|
intervalMs = Number(process.env.PREMAN_CONNECT_POLL_MS) || 3000,
|
|
370
412
|
timeoutMs = Number(process.env.PREMAN_CONNECT_WAIT_MS) || 300000,
|
|
413
|
+
stopWhen = null,
|
|
371
414
|
} = {}
|
|
372
415
|
) {
|
|
373
416
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -383,6 +426,7 @@ export async function waitForConnection(
|
|
|
383
426
|
token: apiKey,
|
|
384
427
|
});
|
|
385
428
|
if (status.ok && status.connected) return true;
|
|
429
|
+
if (stopWhen && stopWhen()) return false;
|
|
386
430
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
387
431
|
}
|
|
388
432
|
} finally {
|
|
@@ -391,9 +435,94 @@ export async function waitForConnection(
|
|
|
391
435
|
return false;
|
|
392
436
|
}
|
|
393
437
|
|
|
438
|
+
// ── Auto check-in ───────────────────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* How to ask each agent to make one PreMan call without opening its UI.
|
|
442
|
+
*
|
|
443
|
+
* The link is established by the agent's first call, so anything that reaches
|
|
444
|
+
* `preman_status` finishes the connect. Claude Code needs the server on its
|
|
445
|
+
* allow-list because print mode refuses un-allowed MCP tools rather than
|
|
446
|
+
* prompting.
|
|
447
|
+
*/
|
|
448
|
+
export function headlessCheckIn(agent, serverName) {
|
|
449
|
+
const prompt = `Call the ${serverName} MCP tool preman_status and report the result.`;
|
|
450
|
+
if (agent.id === "cursor") return { bin: "cursor-agent", args: ["-p", prompt] };
|
|
451
|
+
if (agent.id === "claude_code") {
|
|
452
|
+
return { bin: "claude", args: ["-p", prompt, "--allowedTools", `mcp__${serverName}`] };
|
|
453
|
+
}
|
|
454
|
+
if (agent.id === "codex") return { bin: "codex", args: ["exec", prompt] };
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Finish the link ourselves instead of asking the user to go restart their agent.
|
|
460
|
+
*
|
|
461
|
+
* The config on disk is already correct at this point; all that is missing is
|
|
462
|
+
* one call from the agent, and telling someone to make it from another terminal
|
|
463
|
+
* is a dead end in the one terminal they are sitting in. So run the agent
|
|
464
|
+
* headlessly and poll for the check-in it produces.
|
|
465
|
+
*
|
|
466
|
+
* Returns `ran: false` when the agent's binary is absent or will not start, and
|
|
467
|
+
* the caller falls back to the printed instructions.
|
|
468
|
+
*/
|
|
469
|
+
export async function autoCheckIn(
|
|
470
|
+
args,
|
|
471
|
+
agent,
|
|
472
|
+
apiKey,
|
|
473
|
+
{
|
|
474
|
+
// Never outlast the connect's own wait budget: this phase is part of it, not
|
|
475
|
+
// an extra one bolted on the front.
|
|
476
|
+
timeoutMs = Math.min(
|
|
477
|
+
Number(process.env.PREMAN_AUTO_CHECKIN_MS) || 120000,
|
|
478
|
+
Number(process.env.PREMAN_CONNECT_WAIT_MS) || 300000
|
|
479
|
+
),
|
|
480
|
+
serverName = "preman",
|
|
481
|
+
intervalMs = Number(process.env.PREMAN_CONNECT_POLL_MS) || 3000,
|
|
482
|
+
} = {}
|
|
483
|
+
) {
|
|
484
|
+
const spec = headlessCheckIn(agent, serverName);
|
|
485
|
+
if (!spec) return { ran: false, connected: false, reason: "no headless mode" };
|
|
486
|
+
if (!onPath(spec.bin)) return { ran: false, connected: false, reason: `${spec.bin} is not on PATH` };
|
|
487
|
+
|
|
488
|
+
let child;
|
|
489
|
+
try {
|
|
490
|
+
child = spawn(spec.bin, spec.args, { stdio: "ignore" });
|
|
491
|
+
} catch (error) {
|
|
492
|
+
return { ran: false, connected: false, reason: error.message };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
let spawnError = null;
|
|
496
|
+
let exitedAt = 0;
|
|
497
|
+
child.on("error", (error) => {
|
|
498
|
+
spawnError = error;
|
|
499
|
+
exitedAt = exitedAt || Date.now();
|
|
500
|
+
});
|
|
501
|
+
child.on("exit", () => {
|
|
502
|
+
exitedAt = exitedAt || Date.now();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// The check-in can land moments after the agent's own process ends, so keep
|
|
506
|
+
// polling briefly past its exit rather than declaring failure at the edge.
|
|
507
|
+
const grace = intervalMs * 2;
|
|
508
|
+
try {
|
|
509
|
+
const connected = await waitForConnection(args, apiKey, {
|
|
510
|
+
intervalMs,
|
|
511
|
+
timeoutMs,
|
|
512
|
+
stopWhen: () => Boolean(exitedAt) && Date.now() - exitedAt > grace,
|
|
513
|
+
});
|
|
514
|
+
if (spawnError && !connected) {
|
|
515
|
+
return { ran: false, connected: false, reason: spawnError.message };
|
|
516
|
+
}
|
|
517
|
+
return { ran: true, connected, command: spec.bin };
|
|
518
|
+
} finally {
|
|
519
|
+
if (child.exitCode === null && child.signalCode === null) child.kill();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
394
523
|
// ── Dispatch credential (SCRUM-124) ─────────────────────────────────────
|
|
395
524
|
|
|
396
|
-
async function captureDispatchCredential(args, agent, apiKey) {
|
|
525
|
+
async function captureDispatchCredential(args, agent, apiKey, { prompt = true } = {}) {
|
|
397
526
|
if (!agent.dispatch) return;
|
|
398
527
|
if (args.has("--skip-dispatch-credential")) return;
|
|
399
528
|
|
|
@@ -401,19 +530,20 @@ async function captureDispatchCredential(args, agent, apiKey) {
|
|
|
401
530
|
let routineId = args.value("--routine-id", "");
|
|
402
531
|
|
|
403
532
|
if (!secret) {
|
|
404
|
-
if (!process.stdin.isTTY) return;
|
|
533
|
+
if (!prompt || !process.stdin.isTTY) return;
|
|
405
534
|
// Framed as the expected step rather than an optional aside. Without it
|
|
406
535
|
// PreMan can only suggest fixes; with it, it can run them. Presenting it as
|
|
407
536
|
// "optional, press Enter to skip" meant almost everyone skipped the thing
|
|
408
537
|
// that makes the product act rather than advise.
|
|
409
538
|
process.stdout.write(
|
|
410
|
-
`\
|
|
411
|
-
`run checks on a schedule instead of only telling you what to do.\n`
|
|
539
|
+
`\nOptional — let PreMan start ${agent.label} runs for you, so it can apply\n` +
|
|
540
|
+
`fixes and run checks on a schedule instead of only telling you what to do.\n` +
|
|
541
|
+
`Get the token from ${agent.dispatch.source}.\n`
|
|
412
542
|
);
|
|
413
|
-
secret = await promptSecret(`Paste your ${agent.dispatch.credential} (Enter to
|
|
543
|
+
secret = await promptSecret(`Paste your ${agent.dispatch.credential} (Enter to skip): `);
|
|
414
544
|
if (!secret) {
|
|
415
545
|
process.stdout.write(
|
|
416
|
-
`Skipped. Run '${cliInvocation()}
|
|
546
|
+
`Skipped. Run '${cliInvocation()} dispatch --agent ${agent.id.replace("_", "-")}' when you have the token.\n`
|
|
417
547
|
);
|
|
418
548
|
return;
|
|
419
549
|
}
|
|
@@ -445,6 +575,64 @@ async function captureDispatchCredential(args, agent, apiKey) {
|
|
|
445
575
|
}
|
|
446
576
|
}
|
|
447
577
|
|
|
578
|
+
export const DISPATCH_HELP = `
|
|
579
|
+
Dispatch options:
|
|
580
|
+
--agent <name> cursor | claude-code (defaults to the connected agent)
|
|
581
|
+
--dispatch-credential <t> Cloud-dispatch token (non-interactive)
|
|
582
|
+
--routine-id <id> Claude Code routine id, with --dispatch-credential
|
|
583
|
+
--api-key <key> PreMan API key. If omitted, stored credentials are used
|
|
584
|
+
--backend <url> PreMan backend URL
|
|
585
|
+
`;
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* `preman dispatch` — store the cloud-dispatch credential on its own.
|
|
589
|
+
*
|
|
590
|
+
* Exists so connect never has to ask for a token mid-onboarding. Someone who
|
|
591
|
+
* skipped it (or did not have it yet) comes back here instead of re-running the
|
|
592
|
+
* whole connect.
|
|
593
|
+
*/
|
|
594
|
+
export async function dispatchCommand(commandArgs) {
|
|
595
|
+
const args = makeArgs(commandArgs);
|
|
596
|
+
const apiKey = resolveApiKey(args);
|
|
597
|
+
if (!apiKey) {
|
|
598
|
+
throw new ConnectError(
|
|
599
|
+
`No PreMan credentials. Run '${cliInvocation()} connect' first, or pass --api-key pm_live_….`,
|
|
600
|
+
EXIT_USAGE
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
let agent = findAgent(args.value("--agent", ""));
|
|
605
|
+
if (!agent && args.value("--agent", "")) {
|
|
606
|
+
throw new ConnectError(
|
|
607
|
+
`Unknown agent: ${args.value("--agent", "")}. Use cursor or claude-code.`,
|
|
608
|
+
EXIT_USAGE
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (!agent) {
|
|
613
|
+
const current = await callBackendJson(args, "GET", "/workbench/coding-agent", { token: apiKey });
|
|
614
|
+
agent = findAgent(current.ok ? current.agent : "");
|
|
615
|
+
if (!agent) {
|
|
616
|
+
throw new ConnectError(
|
|
617
|
+
"Could not tell which agent to set up. Pass --agent cursor or --agent claude-code.",
|
|
618
|
+
EXIT_USAGE
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (!agent.dispatch) {
|
|
624
|
+
throw new ConnectError(`${agent.label} has no cloud-dispatch API yet.`, EXIT_USAGE);
|
|
625
|
+
}
|
|
626
|
+
if (!args.value("--dispatch-credential", "") && !process.stdin.isTTY) {
|
|
627
|
+
throw new ConnectError(
|
|
628
|
+
"preman dispatch needs a terminal, or --dispatch-credential <token>.",
|
|
629
|
+
EXIT_USAGE
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
await captureDispatchCredential(args, agent, apiKey);
|
|
634
|
+
}
|
|
635
|
+
|
|
448
636
|
/** Accept either a bare trig_… id or the routine URL it appears in. */
|
|
449
637
|
export function extractRoutineId(value) {
|
|
450
638
|
const raw = String(value || "").trim();
|
|
@@ -589,6 +777,7 @@ Connect options:
|
|
|
589
777
|
--project Write project-local config instead of the user config
|
|
590
778
|
--api-key <key> PreMan API key. If omitted, stored credentials are used
|
|
591
779
|
--email <email> Pre-fill the email prompt when logging in
|
|
780
|
+
--password [value] Also set a dashboard password (signup asks for none)
|
|
592
781
|
--backend <url> PreMan backend URL
|
|
593
782
|
--frontend <url> PreMan frontend URL
|
|
594
783
|
--name <name> MCP server name. Defaults to preman
|
|
@@ -597,6 +786,7 @@ Connect options:
|
|
|
597
786
|
--skip-dispatch-credential Do not ask for a cloud-dispatch credential
|
|
598
787
|
--skip-login Write config without interactive terminal auth
|
|
599
788
|
--no-pair Do not mint a pair code
|
|
789
|
+
--no-auto-checkin Do not run the agent to finish the link
|
|
600
790
|
--no-wait Do not wait for the agent to check in
|
|
601
791
|
--no-guide Skip the guided first run after connecting
|
|
602
792
|
--print Print the config instead of writing it
|
|
@@ -617,31 +807,23 @@ export async function connectCommand(commandArgs) {
|
|
|
617
807
|
);
|
|
618
808
|
}
|
|
619
809
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
"
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
agent = await promptAgentChoice(detectAgents());
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
if (!printOnly) {
|
|
639
|
-
// Verify the machine can run what we are about to write — before any
|
|
640
|
-
// config edits or logins, so failures leave nothing half-done.
|
|
641
|
-
await preflight(args);
|
|
810
|
+
// Nothing to prompt on, so leave behind everything a CI log needs to finish
|
|
811
|
+
// the setup by hand rather than just the reason it stopped.
|
|
812
|
+
if (!agent && !interactive) {
|
|
813
|
+
process.stdout.write(
|
|
814
|
+
`preman connect needs a terminal to pick an agent. Copy-paste setup instead:\n\n${renderAllAgentSnippets(args, serverName, { projectInstall })}\n` +
|
|
815
|
+
'Then restart your agent and ask it: "run preman_status".\n' +
|
|
816
|
+
"Or rerun: preman connect --agent <cursor|claude-code|codex> --api-key pm_live_…\n"
|
|
817
|
+
);
|
|
818
|
+
throw new ConnectError(
|
|
819
|
+
"preman connect needs a terminal. In CI pass --agent <cursor|claude-code|codex> " +
|
|
820
|
+
"and --api-key pm_live_… (or --print), or use one of the snippets above.",
|
|
821
|
+
EXIT_USAGE
|
|
822
|
+
);
|
|
642
823
|
}
|
|
643
824
|
|
|
644
825
|
if (printOnly) {
|
|
826
|
+
if (!agent) agent = await promptAgentChoice(detectAgents());
|
|
645
827
|
const serverConfig = buildServerConfig(args);
|
|
646
828
|
if (agent.id === "codex") {
|
|
647
829
|
process.stdout.write(renderCodexToml(serverName, serverConfig));
|
|
@@ -651,6 +833,12 @@ export async function connectCommand(commandArgs) {
|
|
|
651
833
|
return;
|
|
652
834
|
}
|
|
653
835
|
|
|
836
|
+
// Verify the machine can run what we are about to write — before any config
|
|
837
|
+
// edits or logins, so failures leave nothing half-done.
|
|
838
|
+
await preflight(args);
|
|
839
|
+
|
|
840
|
+
// Account first, so "First, let's connect your PreMan account" is not the
|
|
841
|
+
// second thing that happens.
|
|
654
842
|
if (!args.has("--skip-login") && !hasKeyAvailable(args)) {
|
|
655
843
|
if (!interactive) {
|
|
656
844
|
throw new ConnectError(
|
|
@@ -663,6 +851,8 @@ export async function connectCommand(commandArgs) {
|
|
|
663
851
|
process.stdout.write("\n");
|
|
664
852
|
}
|
|
665
853
|
|
|
854
|
+
if (!agent) agent = await promptAgentChoice(detectAgents());
|
|
855
|
+
|
|
666
856
|
const apiKey = resolveApiKey(args);
|
|
667
857
|
|
|
668
858
|
let pairCode = "";
|
|
@@ -695,27 +885,19 @@ export async function connectCommand(commandArgs) {
|
|
|
695
885
|
);
|
|
696
886
|
}
|
|
697
887
|
|
|
698
|
-
//
|
|
699
|
-
// prompt inside only
|
|
700
|
-
|
|
701
|
-
|
|
888
|
+
// A non-interactive run can still be handed the credential up front, so this
|
|
889
|
+
// stays reachable; the prompt inside only fires when there is a TTY, and by
|
|
890
|
+
// then onboarding is done.
|
|
702
891
|
if (!pairCode || args.has("--no-wait") || !interactive) {
|
|
892
|
+
await captureDispatchCredential(args, agent, apiKey);
|
|
703
893
|
process.stdout.write(nextStepsBlock(agent));
|
|
704
894
|
return;
|
|
705
895
|
}
|
|
706
896
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
if (!(await waitForConnection(args, apiKey))) {
|
|
713
|
-
process.stdout.write(
|
|
714
|
-
"No check-in yet. Troubleshooting:\n" +
|
|
715
|
-
` - ${agent.restartHint}\n` +
|
|
716
|
-
` - Config written to: ${written.path}\n` +
|
|
717
|
-
` - Then ask ${agent.label} to "run preman_status" — it links on its first PreMan call.\n`
|
|
718
|
-
);
|
|
897
|
+
if (!(await establishCheckIn(args, agent, apiKey, { serverName, written }))) {
|
|
898
|
+
// Still honour an explicitly-passed credential, but do not open a new prompt
|
|
899
|
+
// on top of a connect that just told the user something went wrong.
|
|
900
|
+
await captureDispatchCredential(args, agent, apiKey, { prompt: false });
|
|
719
901
|
return;
|
|
720
902
|
}
|
|
721
903
|
|
|
@@ -723,4 +905,42 @@ export async function connectCommand(commandArgs) {
|
|
|
723
905
|
if (!args.has("--no-guide")) {
|
|
724
906
|
await guidedFirstRun(args, agent);
|
|
725
907
|
}
|
|
908
|
+
await captureDispatchCredential(args, agent, apiKey);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Get the agent to make its first PreMan call, by whatever means work here.
|
|
913
|
+
*
|
|
914
|
+
* Prefers running it for the user; falls back to asking them to restart it and
|
|
915
|
+
* waiting, which is all this ever did. Returns whether the check-in landed, and
|
|
916
|
+
* prints the troubleshooting block itself when it did not.
|
|
917
|
+
*/
|
|
918
|
+
async function establishCheckIn(args, agent, apiKey, { serverName, written }) {
|
|
919
|
+
if (!args.has("--no-auto-checkin")) {
|
|
920
|
+
const spec = headlessCheckIn(agent, serverName);
|
|
921
|
+
if (spec && onPath(spec.bin)) {
|
|
922
|
+
process.stdout.write(`\nStarting ${agent.label} to finish the link…\n`);
|
|
923
|
+
}
|
|
924
|
+
const auto = await autoCheckIn(args, agent, apiKey, { serverName });
|
|
925
|
+
if (auto.connected) return true;
|
|
926
|
+
if (auto.ran) {
|
|
927
|
+
process.stdout.write(`${agent.label} ran but did not check in.\n`);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
process.stdout.write(
|
|
932
|
+
`\nRestart ${agent.label} and ask it: "run preman_status"\n` +
|
|
933
|
+
"Waiting for your agent to check in… (Ctrl+C to stop waiting)\n"
|
|
934
|
+
);
|
|
935
|
+
|
|
936
|
+
if (await waitForConnection(args, apiKey)) return true;
|
|
937
|
+
|
|
938
|
+
process.stdout.write(
|
|
939
|
+
"No check-in yet. Troubleshooting:\n" +
|
|
940
|
+
` - ${agent.restartHint}\n` +
|
|
941
|
+
` - Config written to: ${written.path}\n` +
|
|
942
|
+
` - Then ask ${agent.label} to "run preman_status" — it links on its first PreMan call.\n` +
|
|
943
|
+
` - Then: ${cliInvocation()} connect --agent ${agent.id.replace("_", "-")}\n`
|
|
944
|
+
);
|
|
945
|
+
return false;
|
|
726
946
|
}
|