litmus-cli 1.4.66 → 1.4.68
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/dist/commands/connect.d.ts +41 -38
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +132 -182
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/diagnose-connection.d.ts +8 -0
- package/dist/commands/diagnose-connection.d.ts.map +1 -0
- package/dist/commands/diagnose-connection.js +77 -0
- package/dist/commands/diagnose-connection.js.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +2 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +48 -7
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/connection-attempt.d.ts +26 -0
- package/dist/lib/connection-attempt.d.ts.map +1 -0
- package/dist/lib/connection-attempt.js +83 -0
- package/dist/lib/connection-attempt.js.map +1 -0
- package/dist/lib/cursor-restart.d.ts +11 -10
- package/dist/lib/cursor-restart.d.ts.map +1 -1
- package/dist/lib/cursor-restart.js +29 -14
- package/dist/lib/cursor-restart.js.map +1 -1
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +17 -6
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/ssh-config-file.d.ts +3 -0
- package/dist/lib/ssh-config-file.d.ts.map +1 -0
- package/dist/lib/ssh-config-file.js +55 -0
- package/dist/lib/ssh-config-file.js.map +1 -0
- package/package.json +3 -1
package/dist/commands/connect.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { ConnectionAttempt } from "../lib/connection-attempt.js";
|
|
2
|
+
import { updateSshConfig } from "../lib/ssh-config-file.js";
|
|
1
3
|
import os from "os";
|
|
2
4
|
import path from "path";
|
|
3
5
|
import readline from "readline";
|
|
4
|
-
import { closeSync, constants, existsSync, mkdirSync, openSync, readFileSync
|
|
6
|
+
import { closeSync, constants, existsSync, mkdirSync, openSync, readFileSync } from "fs";
|
|
5
7
|
import { randomBytes } from "crypto";
|
|
6
8
|
import { execFileSync, spawn, spawnSync } from "child_process";
|
|
7
9
|
import chalk from "chalk";
|
|
8
10
|
import ora from "ora";
|
|
9
|
-
import { fetchInitMetadata, startWorkspace, getConnection, fetchEditorChatConfig, ServerError, hintForError, isNetworkError } from "../lib/api.js";
|
|
11
|
+
import { fetchInitMetadata, startWorkspace, getConnection, fetchEditorChatConfig, ServerError, hintForError, isNetworkError, workspaceStartNetworkDetail } from "../lib/api.js";
|
|
10
12
|
import { applyEditorChatConfig } from "../lib/chat-model-config.js";
|
|
11
13
|
import { finishSkillImport, offerSkillImport, startSkillImport } from "../lib/connect-skills.js";
|
|
12
14
|
import { resolveApiBase } from "../lib/api-base.js";
|
|
@@ -16,7 +18,7 @@ import { parseEditorVersion, parseWorkspaceProbe, remoteProbeCommand, seedVerdic
|
|
|
16
18
|
import { fatal, holdExitFor, success, info, warn, setErrorContext, reportEvent } from "../lib/errors.js";
|
|
17
19
|
import { oldWindowsSshHint, openSshSession, parseOpenSshVersion, proxyJumpBroken, readEditorSettings, readSshVersionBanner, SSH_CONNECTION_FAILED, sshExecutableFor, sshPreflight, sshPreflightHint, } from "../lib/ssh-client.js";
|
|
18
20
|
import { editorEndingForewarning } from "../lib/session-end.js";
|
|
19
|
-
import {
|
|
21
|
+
import { attemptRelaunch, platformSupportsGracefulCursorRestart, realCursorProcessIo, } from "../lib/cursor-restart.js";
|
|
20
22
|
/**
|
|
21
23
|
* `litmus connect <token>` — the CLI connector for the v2 codespace, modeled on
|
|
22
24
|
* `gh codespace ssh/code`. It removes the manual "paste your public key + copy
|
|
@@ -320,13 +322,9 @@ function ensureSshKey(keyPath) {
|
|
|
320
322
|
stdio: "ignore",
|
|
321
323
|
});
|
|
322
324
|
}
|
|
323
|
-
function writeSshConfig(alias, block, controlBlock) {
|
|
325
|
+
async function writeSshConfig(alias, block, controlBlock) {
|
|
324
326
|
const cfgPath = path.join(os.homedir(), ".ssh", "config");
|
|
325
|
-
|
|
326
|
-
if (!existsSync(dir))
|
|
327
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
328
|
-
const existing = existsSync(cfgPath) ? readFileSync(cfgPath, "utf8") : "";
|
|
329
|
-
writeFileSync(cfgPath, upsertSshConfigContent(existing, alias, block, controlBlock), { mode: 0o600 });
|
|
327
|
+
await updateSshConfig(cfgPath, (existing) => upsertSshConfigContent(existing, alias, block, controlBlock));
|
|
330
328
|
return cfgPath;
|
|
331
329
|
}
|
|
332
330
|
/**
|
|
@@ -412,7 +410,7 @@ function probeEditorCli(command) {
|
|
|
412
410
|
return null;
|
|
413
411
|
}
|
|
414
412
|
}
|
|
415
|
-
function resolveEditorCommand(ed) {
|
|
413
|
+
export function resolveEditorCommand(ed) {
|
|
416
414
|
const onPath = probeEditorCli(ed.bin);
|
|
417
415
|
if (onPath)
|
|
418
416
|
return { command: ed.bin, source: "path", version: onPath };
|
|
@@ -599,9 +597,6 @@ export function editorSeedMismatchLines(ed, verdict, pageUrl, alias) {
|
|
|
599
597
|
export function editorNotFoundLines(ed, alias, pageUrl) {
|
|
600
598
|
return [
|
|
601
599
|
chalk.yellow.bold(`⚠ Couldn't find ${ed.label}'s \`${ed.bin}\` command, so nothing was opened.`),
|
|
602
|
-
// Greptile on #3071: this route never restarts Cursor, and Cursor only loads the
|
|
603
|
-
// workspace's prompt recording when the app starts (gotcha 090).
|
|
604
|
-
...(ed.ide === "cursor" ? [" Quit Cursor completely and reopen it first, or your AI prompts won't be recorded."] : []),
|
|
605
600
|
` In ${ed.label}: Command Palette → ${chalk.bold("Remote-SSH: Connect to Host…")} → ${chalk.bold(alias)}`,
|
|
606
601
|
` To get the \`${ed.bin}\` command for next time: Command Palette → "${ed.shellCommand}".`,
|
|
607
602
|
workNowLine(alias, pageUrl),
|
|
@@ -690,7 +685,7 @@ export function editorUnverifiedLines(ed, alias, pageUrl) {
|
|
|
690
685
|
* null skips the conflict check rather than stopping connect: the check only refuses on
|
|
691
686
|
* evidence, never on a failed read.
|
|
692
687
|
*/
|
|
693
|
-
function listEditorExtensions(command) {
|
|
688
|
+
export function listEditorExtensions(command) {
|
|
694
689
|
const list = editorSpawnSpec(command, ["--list-extensions"], process.platform);
|
|
695
690
|
if (!list)
|
|
696
691
|
return null;
|
|
@@ -702,22 +697,13 @@ function listEditorExtensions(command) {
|
|
|
702
697
|
return null;
|
|
703
698
|
}
|
|
704
699
|
}
|
|
705
|
-
function ensureRemoteSshExtension(ed, command) {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
// `failed` is already best-effort and never aborts connect.
|
|
709
|
-
if (!list)
|
|
700
|
+
export function ensureRemoteSshExtension(ed, command, listed) {
|
|
701
|
+
// An unreadable inventory is not evidence of absence; don't mutate it blindly.
|
|
702
|
+
if (listed === null)
|
|
710
703
|
return "failed";
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
return "present";
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
catch {
|
|
718
|
-
// Couldn't list (unexpected) — fall through and try to install anyway.
|
|
719
|
-
}
|
|
720
|
-
const install = editorSpawnSpec(command, ["--install-extension", ed.remoteSshExt, "--force"], process.platform);
|
|
704
|
+
if (listed.split(/\r?\n/).some((l) => l.trim().toLowerCase() === ed.remoteSshExt))
|
|
705
|
+
return "present";
|
|
706
|
+
const install = editorSpawnSpec(command, ["--install-extension", ed.remoteSshExt], process.platform);
|
|
721
707
|
if (!install)
|
|
722
708
|
return "failed";
|
|
723
709
|
try {
|
|
@@ -871,11 +857,11 @@ function readEditorLaunchLog(logPath) {
|
|
|
871
857
|
return null;
|
|
872
858
|
}
|
|
873
859
|
}
|
|
874
|
-
function sshProbeRunner(alias) {
|
|
860
|
+
export function sshProbeRunner(alias, executable = "ssh") {
|
|
875
861
|
return (remoteCommand) => {
|
|
876
862
|
try {
|
|
877
|
-
const probed = spawnSync(
|
|
878
|
-
if (probed.error)
|
|
863
|
+
const probed = spawnSync(executable, ["-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "-o", "LogLevel=ERROR", alias, remoteCommand], { encoding: "utf8", timeout: 20000 });
|
|
864
|
+
if (probed.error || probed.status !== 0)
|
|
879
865
|
return null;
|
|
880
866
|
return probed.stdout ?? null;
|
|
881
867
|
}
|
|
@@ -1043,52 +1029,10 @@ export function keepWaitingForWorkspace(elapsedMs, status) {
|
|
|
1043
1029
|
export function candidatePageUrl(apiBase, assessmentId) {
|
|
1044
1030
|
return `${apiBase}/candidate/${assessmentId}`;
|
|
1045
1031
|
}
|
|
1046
|
-
/**
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
* Cursor reads its prompt-recording setup once, at application startup (ENG-1728,
|
|
1050
|
-
* measured), so a Cursor that is already open when the workspace opens records no AI
|
|
1051
|
-
* prompts while everything else works. ENG-2890: on macOS connect quits and reopens
|
|
1052
|
-
* Cursor itself (`lib/cursor-restart.ts`), so nothing is said here and the restart step
|
|
1053
|
-
* speaks when it acts. Elsewhere a graceful quit cannot be confirmed, so the candidate is
|
|
1054
|
-
* asked to do it, in two lines: the 23-line notice this replaced was the wall the
|
|
1055
|
-
* ENG-2890 candidate read past. The second line exists because quitting Cursor also
|
|
1056
|
-
* closes its integrated terminal, and with it this command (ENG-2145).
|
|
1057
|
-
*/
|
|
1058
|
-
export function cursorSelectionNoticeLines(platform = process.platform) {
|
|
1059
|
-
if (platformSupportsGracefulCursorRestart(platform))
|
|
1060
|
-
return [];
|
|
1061
|
-
return [
|
|
1062
|
-
" Before Cursor opens, quit it completely (File → Exit), or your AI prompts won't be recorded.",
|
|
1063
|
-
" If this terminal is inside Cursor, run the same litmus connect command from another terminal.",
|
|
1064
|
-
];
|
|
1065
|
-
}
|
|
1066
|
-
/**
|
|
1067
|
-
* ENG-2890 — is this process running in Cursor's integrated terminal?
|
|
1068
|
-
*
|
|
1069
|
-
* Quitting Cursor kills its terminals, so the macOS auto-restart would kill this CLI
|
|
1070
|
-
* between the quit and the relaunch, leaving no editor and no command. `TERM_PROGRAM`
|
|
1071
|
-
* is "vscode" in VS Code AND in Cursor's terminal, so one of Cursor's own markers is
|
|
1072
|
-
* required as well: its trace id, its ToDesktop bundle id, or its bundled askpass node.
|
|
1073
|
-
* VS Code's terminal (bundle id `com.microsoft.VSCode`, no trace id) is not a match.
|
|
1074
|
-
*/
|
|
1075
|
-
export function runningInsideCursorTerminal(env = process.env) {
|
|
1076
|
-
if (env.TERM_PROGRAM !== "vscode")
|
|
1077
|
-
return false;
|
|
1078
|
-
return (Boolean(env.CURSOR_TRACE_ID) ||
|
|
1079
|
-
(env.__CFBundleIdentifier ?? "").includes("todesktop") ||
|
|
1080
|
-
(env.VSCODE_GIT_ASKPASS_NODE ?? "").includes("Cursor"));
|
|
1081
|
-
}
|
|
1082
|
-
/** ENG-2890 — said instead of quitting Cursor out from under this terminal. */
|
|
1083
|
-
export function insideCursorTerminalLines() {
|
|
1084
|
-
return [
|
|
1085
|
-
chalk.yellow.bold("⚠ This terminal is inside Cursor, and restarting Cursor would close it."),
|
|
1086
|
-
" Run the same litmus connect command from Terminal or iTerm instead.",
|
|
1087
|
-
];
|
|
1032
|
+
/** Remote hooks initialize in the remote window; no desktop restart is required (ENG-3176). */
|
|
1033
|
+
export function cursorSelectionNoticeLines(_platform = process.platform) {
|
|
1034
|
+
return [];
|
|
1088
1035
|
}
|
|
1089
|
-
/** ENG-2890 — the one line connect prints before its macOS auto-restart of Cursor. */
|
|
1090
|
-
/** Cursor success when this command could not confirm a fresh start (gotcha 090). */
|
|
1091
|
-
export const CURSOR_RECORDING_CAVEAT_LINE = " If Cursor was open before this, quit it and run the same litmus connect command again, or your AI prompts won't be recorded.";
|
|
1092
1036
|
/** How long a freshly started Cursor gets to finish restoring its session (ENG-2884). */
|
|
1093
1037
|
export const CURSOR_SETTLE_MS = 6000;
|
|
1094
1038
|
/** When, into the attach wait, the open request is sent to Cursor a second time. */
|
|
@@ -1131,18 +1075,6 @@ export function resendableLaunch(launch) {
|
|
|
1131
1075
|
},
|
|
1132
1076
|
};
|
|
1133
1077
|
}
|
|
1134
|
-
export const CURSOR_RESTARTING_LINE = "Restarting Cursor so your AI prompts are recorded (it reopens on its own)...";
|
|
1135
|
-
/**
|
|
1136
|
-
* ENG-2890 — the quit was asked for and Cursor is still running, so nothing is
|
|
1137
|
-
* launched (see the gate in `connectFlow`). `renderCursorRestartOutcome`'s longer form
|
|
1138
|
-
* stays with init and doctor.
|
|
1139
|
-
*/
|
|
1140
|
-
export function cursorQuitUnconfirmedLines() {
|
|
1141
|
-
return [
|
|
1142
|
-
chalk.red.bold("✖ Cursor didn't quit. It may be waiting on a \"Save changes?\" dialog."),
|
|
1143
|
-
" Quit Cursor yourself, then run the same litmus connect command again from Terminal or iTerm.",
|
|
1144
|
-
];
|
|
1145
|
-
}
|
|
1146
1078
|
/**
|
|
1147
1079
|
* Map a menu answer to an editor. (ENG-1667 item 3; Cursor added by ENG-1950.)
|
|
1148
1080
|
*
|
|
@@ -1309,8 +1241,7 @@ function pruneStaleHostKeys(p) {
|
|
|
1309
1241
|
// known_hosts as [host]:port, so a bare-host -R would miss it.
|
|
1310
1242
|
if (p.port)
|
|
1311
1243
|
targets.push(`[${p.host}]:${p.port}`);
|
|
1312
|
-
|
|
1313
|
-
targets.push(p.jumpPort ? `[${p.jumpHost}]:${p.jumpPort}` : p.jumpHost);
|
|
1244
|
+
// The shared bastion is not reprovisioned with this workspace. Preserve its pinned key.
|
|
1314
1245
|
for (const t of targets) {
|
|
1315
1246
|
try {
|
|
1316
1247
|
execFileSync("ssh-keygen", ["-R", t], { stdio: "ignore" });
|
|
@@ -1337,18 +1268,34 @@ export async function runConnect(token, ideOpt, opts = {}) {
|
|
|
1337
1268
|
// printed last, on every way out of the flow, so it neither delays the editor nor
|
|
1338
1269
|
// is lost when a later step fails. The workspace lines follow it, so the candidate's
|
|
1339
1270
|
// last read is where they are and what the session ending looks like.
|
|
1271
|
+
setErrorContext({ token, apiBase: resolveApiBase(process.env.LITMUS_API_URL) });
|
|
1272
|
+
const attempt = new ConnectionAttempt();
|
|
1340
1273
|
const skillImport = { report: null, footer: [], sshAlias: null, publishedPorts: [] };
|
|
1274
|
+
// Register before entering the flow: a fast fatal report can settle before the
|
|
1275
|
+
// caller's finally begins, especially while an async SSH config write is unwinding.
|
|
1276
|
+
let releaseExitHold;
|
|
1277
|
+
holdExitFor(new Promise((resolve) => { releaseExitHold = resolve; }));
|
|
1341
1278
|
try {
|
|
1342
|
-
await connectFlow(token, ideOpt, opts, skillImport);
|
|
1279
|
+
await connectFlow(token, ideOpt, opts, skillImport, attempt);
|
|
1280
|
+
}
|
|
1281
|
+
catch (error) {
|
|
1282
|
+
attempt.finish("failed");
|
|
1283
|
+
attempt.evidence(error instanceof Error ? error.message : null);
|
|
1284
|
+
throw error;
|
|
1343
1285
|
}
|
|
1344
1286
|
finally {
|
|
1345
|
-
|
|
1346
|
-
|
|
1287
|
+
try {
|
|
1288
|
+
await attempt.flush();
|
|
1289
|
+
if (opts.verbose || attempt.needsSupport)
|
|
1290
|
+
console.log(chalk.dim(` Connection ID: ${attempt.id}`));
|
|
1347
1291
|
if (skillImport.report)
|
|
1348
1292
|
await finishSkillImport(skillImport.report);
|
|
1349
1293
|
for (const line of skillImport.footer)
|
|
1350
1294
|
console.log(line);
|
|
1351
|
-
}
|
|
1295
|
+
}
|
|
1296
|
+
finally {
|
|
1297
|
+
releaseExitHold();
|
|
1298
|
+
}
|
|
1352
1299
|
}
|
|
1353
1300
|
// The ssh route's session runs last, so nothing we print lands inside the remote shell.
|
|
1354
1301
|
if (!skillImport.sshAlias)
|
|
@@ -1380,7 +1327,7 @@ async function openSshRoute(alias) {
|
|
|
1380
1327
|
if (status)
|
|
1381
1328
|
process.exitCode = status;
|
|
1382
1329
|
}
|
|
1383
|
-
async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
1330
|
+
async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
1384
1331
|
const verbose = Boolean(opts.verbose);
|
|
1385
1332
|
const apiBase = resolveApiBase(process.env.LITMUS_API_URL);
|
|
1386
1333
|
setErrorContext({ token, apiBase });
|
|
@@ -1395,6 +1342,8 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1395
1342
|
spinner.succeed(meta.assessmentName);
|
|
1396
1343
|
}
|
|
1397
1344
|
catch (err) {
|
|
1345
|
+
attempt.evidence(workspaceStartNetworkDetail(err));
|
|
1346
|
+
attempt.evidence(err instanceof Error ? err.message : null);
|
|
1398
1347
|
// ENG-2895: the Error line below says what failed; a second "Connection failed" line
|
|
1399
1348
|
// above it was the same fact twice, and wrong when the cause was a rejected token.
|
|
1400
1349
|
spinner.stop();
|
|
@@ -1417,28 +1366,37 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1417
1366
|
// ENG-2890 — the assessment page is the one re-entry route connect prints: its URL
|
|
1418
1367
|
// carries only the assessment id, never the token.
|
|
1419
1368
|
const pageUrl = candidatePageUrl(apiBase, meta.assessmentId);
|
|
1369
|
+
attempt.stage("selection");
|
|
1420
1370
|
const { ide: chosenIde, chosen: ideWasChosen } = await resolveIde(ideOpt, console.log);
|
|
1421
1371
|
let ide = chosenIde;
|
|
1372
|
+
attempt.editor(ide);
|
|
1422
1373
|
// ENG-2814: asked here, next to the editor choice, and uploaded once the workspace is
|
|
1423
1374
|
// ready. Never fails the connect. See `lib/connect-skills.ts`.
|
|
1375
|
+
attempt.stage("skills");
|
|
1424
1376
|
const pendingSkills = await offerSkillImport({
|
|
1425
1377
|
backendUrl, token, flag: opts.skills, verbose,
|
|
1426
1378
|
});
|
|
1379
|
+
attempt.stage("ssh_key");
|
|
1427
1380
|
// 2. Ensure a local SSH key (private key never leaves this machine).
|
|
1428
1381
|
try {
|
|
1429
1382
|
ensureSshKey(KEY_PATH);
|
|
1430
1383
|
}
|
|
1431
1384
|
catch (err) {
|
|
1385
|
+
attempt.evidence(workspaceStartNetworkDetail(err));
|
|
1386
|
+
attempt.evidence(err instanceof Error ? err.message : null);
|
|
1432
1387
|
fatal("Couldn't create an SSH key.", "Install OpenSSH (it includes ssh-keygen), then run the same litmus connect command again.", { internalDetail: err instanceof Error ? err.message : String(err), severity: "warning" });
|
|
1433
1388
|
return;
|
|
1434
1389
|
}
|
|
1435
1390
|
const pubkey = readFileSync(`${KEY_PATH}.pub`, "utf8").trim();
|
|
1436
1391
|
// 3. Start the workspace with our public key.
|
|
1392
|
+
attempt.stage("workspace_start");
|
|
1437
1393
|
const boot = ora("Starting your workspace...").start();
|
|
1438
1394
|
try {
|
|
1439
1395
|
await startWorkspace(backendUrl, token, pubkey);
|
|
1440
1396
|
}
|
|
1441
1397
|
catch (err) {
|
|
1398
|
+
attempt.evidence(workspaceStartNetworkDetail(err));
|
|
1399
|
+
attempt.evidence(err instanceof Error ? err.message : null);
|
|
1442
1400
|
// The Error line below names what went wrong; see the sign-in spinner above.
|
|
1443
1401
|
boot.stop();
|
|
1444
1402
|
const code = err instanceof ServerError ? err.errorCode : undefined;
|
|
@@ -1452,6 +1410,10 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1452
1410
|
fatal("This assessment is closed. Its deadline has passed.", "Contact the company if you need an extension.", { severity: "warning" });
|
|
1453
1411
|
return;
|
|
1454
1412
|
}
|
|
1413
|
+
if (isNetworkError(err)) {
|
|
1414
|
+
fatal("Couldn't reach Litmus to start your workspace.", "Check your internet connection, then run the same litmus connect command again. If it keeps happening, contact the company.", { internalDetail: workspaceStartNetworkDetail(err), severity: "error" });
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1455
1417
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1456
1418
|
fatal(msg, "Run the same litmus connect command again in a moment. If it keeps happening, contact the company.", {
|
|
1457
1419
|
severity: err instanceof ServerError && err.statusCode && err.statusCode < 500 ? "warning" : "error",
|
|
@@ -1490,6 +1452,8 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1490
1452
|
return;
|
|
1491
1453
|
}
|
|
1492
1454
|
boot.succeed("Workspace ready");
|
|
1455
|
+
attempt.stage("workspace_ready");
|
|
1456
|
+
attempt.checkpoint();
|
|
1493
1457
|
if (pendingSkills)
|
|
1494
1458
|
skillImport.report = holdExitFor(startSkillImport(backendUrl, token, pendingSkills, undefined, () => ide));
|
|
1495
1459
|
// Browser IDE: no local SSH / VS Code needed — just open the signed URL.
|
|
@@ -1507,6 +1471,7 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1507
1471
|
if (fallback === "vscode") {
|
|
1508
1472
|
warn("The browser IDE isn't available here. Opening VS Code instead.");
|
|
1509
1473
|
ide = "vscode";
|
|
1474
|
+
attempt.editor(ide);
|
|
1510
1475
|
}
|
|
1511
1476
|
else if (fallback === "unavailable") {
|
|
1512
1477
|
fatal("The browser IDE isn't available for this workspace yet.", "Open it in VS Code instead: run the same litmus connect command with --ide vscode.", { severity: "warning" });
|
|
@@ -1514,6 +1479,8 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1514
1479
|
}
|
|
1515
1480
|
}
|
|
1516
1481
|
if (ide === "browser" && conn.browser_ide_url) {
|
|
1482
|
+
attempt.stage("browser_open");
|
|
1483
|
+
attempt.finish("browser_requested");
|
|
1517
1484
|
// ENG-3137 — the workspace now trusts this machine's key, so the assessment page's
|
|
1518
1485
|
// Plain SSH offers `ssh <alias>`. Write the entry that alias names, as every other
|
|
1519
1486
|
// route does. Best-effort: a config we could not write must not cost the browser route.
|
|
@@ -1522,11 +1489,13 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1522
1489
|
const parsed = parseSshCommand(conn.ssh_command);
|
|
1523
1490
|
const alias = aliasFor(conn.ssh_hostname, token);
|
|
1524
1491
|
pruneStaleHostKeys(parsed);
|
|
1525
|
-
const cfgPath = writeSshConfig(alias, buildSshConfigBlock(alias, KEY_PATH, parsed), buildControlPathBlock(alias, parsed));
|
|
1492
|
+
const cfgPath = await writeSshConfig(alias, buildSshConfigBlock(alias, KEY_PATH, parsed), buildControlPathBlock(alias, parsed));
|
|
1526
1493
|
if (verbose)
|
|
1527
1494
|
info(`SSH config written to ${cfgPath} (Host "${alias}")`);
|
|
1528
1495
|
}
|
|
1529
1496
|
catch (err) {
|
|
1497
|
+
attempt.evidence(workspaceStartNetworkDetail(err));
|
|
1498
|
+
attempt.evidence(err instanceof Error ? err.message : null);
|
|
1530
1499
|
if (verbose)
|
|
1531
1500
|
info(`SSH config not written: ${err instanceof Error ? err.message : String(err)}`);
|
|
1532
1501
|
}
|
|
@@ -1552,6 +1521,13 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1552
1521
|
// Prune stale host keys first so a re-provisioned container's new key is
|
|
1553
1522
|
// accepted, not refused (ENG-1221).
|
|
1554
1523
|
const parsed = parseSshCommand(conn.ssh_command);
|
|
1524
|
+
// Reuse this observation across compatibility checks, preflight and diagnostics.
|
|
1525
|
+
const sshBanners = new Map();
|
|
1526
|
+
const sshBanner = (executable) => {
|
|
1527
|
+
if (!sshBanners.has(executable))
|
|
1528
|
+
sshBanners.set(executable, readSshVersionBanner(executable));
|
|
1529
|
+
return sshBanners.get(executable) ?? null;
|
|
1530
|
+
};
|
|
1555
1531
|
// ENG-2682 — Windows' own OpenSSH below 8.1 cannot run the ProxyJump every bastion
|
|
1556
1532
|
// route depends on (see `lib/ssh-client.ts`). Refuse before writing anything, with the
|
|
1557
1533
|
// version named and a way out, rather than hand an editor a connection it will lose
|
|
@@ -1559,17 +1535,18 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1559
1535
|
// `remote.SSH.path` when it names one, PATH's ssh otherwise.
|
|
1560
1536
|
if (process.platform === "win32" && parsed.jumpHost && ide !== "browser") {
|
|
1561
1537
|
const executable = sshExecutableFor(ide, ide === "ssh" ? null : readEditorSettings(ide));
|
|
1562
|
-
const client = parseOpenSshVersion(
|
|
1538
|
+
const client = parseOpenSshVersion(sshBanner(executable) ?? "");
|
|
1563
1539
|
if (client && proxyJumpBroken(client)) {
|
|
1564
1540
|
const where = executable === "ssh" ? "" : ` at ${executable}, set in remote.SSH.path`;
|
|
1565
1541
|
fatal(`Your Windows SSH (${client.label}${where}) is too old to reach the workspace.`, oldWindowsSshHint(Boolean(conn.browser_ide_url)), { severity: "warning" });
|
|
1566
1542
|
return;
|
|
1567
1543
|
}
|
|
1568
1544
|
}
|
|
1545
|
+
attempt.stage("ssh_config");
|
|
1569
1546
|
const alias = aliasFor(conn.ssh_hostname, token);
|
|
1570
1547
|
pruneStaleHostKeys(parsed);
|
|
1571
1548
|
const block = buildSshConfigBlock(alias, KEY_PATH, parsed);
|
|
1572
|
-
const cfgPath = writeSshConfig(alias, block, buildControlPathBlock(alias, parsed));
|
|
1549
|
+
const cfgPath = await writeSshConfig(alias, block, buildControlPathBlock(alias, parsed));
|
|
1573
1550
|
if (verbose)
|
|
1574
1551
|
info(`SSH config written to ${cfgPath} (Host "${alias}")`);
|
|
1575
1552
|
// ENG-2693 — prove the connection before handing it off (see `sshPreflight`). The
|
|
@@ -1582,19 +1559,23 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1582
1559
|
// Called per route, not here once: the native route runs it only AFTER the WSL check
|
|
1583
1560
|
// (Greptile on #2844), so a candidate whose Windows editor cannot see WSL's config is
|
|
1584
1561
|
// told that, rather than a generic ssh failure from the wrong side of the pairing.
|
|
1562
|
+
const sshExecutable = sshExecutableFor(ide, ide === "ssh" ? null : readEditorSettings(ide));
|
|
1585
1563
|
const preflightStops = async (route) => {
|
|
1586
|
-
const sshExecutable = sshExecutableFor(route, route === "ssh" ? null : readEditorSettings(route));
|
|
1587
1564
|
// ENG-2895: up to two 45s attempts, so it gets a spinner; it says nothing when it works.
|
|
1588
1565
|
const checking = ora("Checking the connection to your workspace...").start();
|
|
1566
|
+
attempt.stage("ssh_preflight");
|
|
1567
|
+
attempt.fact("sshSelection", sshExecutable === "ssh" ? "path" : "editor-setting");
|
|
1568
|
+
attempt.fact("sshVersion", parseOpenSshVersion(sshBanner(sshExecutable) ?? "")?.label);
|
|
1589
1569
|
const preflight = await sshPreflight(alias, sshExecutable);
|
|
1590
1570
|
checking.stop();
|
|
1591
1571
|
if (preflight.ok)
|
|
1592
1572
|
return false;
|
|
1573
|
+
attempt.evidence(preflight.detail.join("\n"));
|
|
1593
1574
|
fatal("Your computer couldn't open an SSH connection to the workspace.", sshPreflightHint(preflight.detail, Boolean(conn.browser_ide_url), verbose), {
|
|
1594
1575
|
severity: "error",
|
|
1595
1576
|
internalDetail: [
|
|
1596
1577
|
`route=${route} platform=${process.platform} ssh=${sshExecutable}`,
|
|
1597
|
-
`client=${
|
|
1578
|
+
`client=${sshBanner(sshExecutable) ?? "unreadable"}`,
|
|
1598
1579
|
...preflight.detail,
|
|
1599
1580
|
].join("\n"),
|
|
1600
1581
|
});
|
|
@@ -1605,6 +1586,8 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1605
1586
|
if (ide === "ssh") {
|
|
1606
1587
|
if (await preflightStops("ssh"))
|
|
1607
1588
|
return;
|
|
1589
|
+
attempt.stage("ssh_handoff");
|
|
1590
|
+
attempt.finish("ssh_ready");
|
|
1608
1591
|
if (verbose)
|
|
1609
1592
|
console.log(chalk.dim(` (equivalently: ${conn.ssh_command})`));
|
|
1610
1593
|
if (opts.tunnel) {
|
|
@@ -1630,7 +1613,12 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1630
1613
|
// places its installer puts it, because "not on PATH" was being reported as "not
|
|
1631
1614
|
// installed" on a machine where the binary was sitting in the application bundle the
|
|
1632
1615
|
// whole time. Null here means we genuinely could not find a way to drive it.
|
|
1616
|
+
attempt.stage("editor_resolution");
|
|
1633
1617
|
const resolved = resolveEditorCommand(editor);
|
|
1618
|
+
attempt.editor(ide, resolved?.version.version);
|
|
1619
|
+
attempt.fact("editorFound", Boolean(resolved));
|
|
1620
|
+
attempt.fact("wsl", isWsl());
|
|
1621
|
+
attempt.fact("editorCommit", resolved?.version.commit);
|
|
1634
1622
|
// ENG-2683 — inside WSL, the editor on PATH is usually the WINDOWS app, and its
|
|
1635
1623
|
// Remote-SSH reads Windows' ssh config, not the WSL one written above (see
|
|
1636
1624
|
// `lib/wsl.ts`). Launching it would fail in the editor with nothing on our side, so say
|
|
@@ -1652,7 +1640,9 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1652
1640
|
// With nothing resolved there is nothing to probe, and `unverified` is what that means:
|
|
1653
1641
|
// it is the absence branch's job to say so, below the mismatch refusal, so a candidate
|
|
1654
1642
|
// whose `code` is Cursor is told THAT rather than that their editor is missing.
|
|
1643
|
+
attempt.stage("editor_resolution");
|
|
1655
1644
|
const identity = resolved ? identifyEditorCli(editor, resolved.command) : { kind: "unverified" };
|
|
1645
|
+
attempt.fact("editorIdentity", identity.kind);
|
|
1656
1646
|
// 5b. Point this machine's Copilot Chat at the Litmus gateway (ENG-1475).
|
|
1657
1647
|
//
|
|
1658
1648
|
// ONLY ON THE NATIVE VS CODE PATH, and that is the whole reason it is here rather
|
|
@@ -1696,8 +1686,7 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1696
1686
|
// no-op that touches nothing.
|
|
1697
1687
|
//
|
|
1698
1688
|
// Cursor is still given no laptop-side equivalent of the WRITE: its AI is captured
|
|
1699
|
-
// in-container
|
|
1700
|
-
// selection time) protects.
|
|
1689
|
+
// in-container through the remote user hooks.
|
|
1701
1690
|
//
|
|
1702
1691
|
// ENG-2152 — a MISMATCH refuses below and opens nothing, so it may not WRITE. See
|
|
1703
1692
|
// `chatConfigActionForIdentity`, which is where that belongs: keying the write on
|
|
@@ -1756,8 +1745,6 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1756
1745
|
// ENG-2332 — the CLI is not on PATH and not where the installer puts it. Everything
|
|
1757
1746
|
// offered here is reachable WITHOUT it; the line this replaced offered
|
|
1758
1747
|
// `<bin> --remote …`, i.e. the binary whose absence is the message.
|
|
1759
|
-
// For Cursor these lines already say to quit and reopen it first, so the selection
|
|
1760
|
-
// notice is not restated after them (ENG-2895: it was the third time in one run).
|
|
1761
1748
|
for (const line of editorNotFoundLines(editor, alias, pageUrl))
|
|
1762
1749
|
console.log(line);
|
|
1763
1750
|
return;
|
|
@@ -1779,12 +1766,18 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1779
1766
|
info(`Using ${editor.label} at ${resolved.command}`);
|
|
1780
1767
|
console.log(chalk.dim(` (\`${editor.bin}\` isn't on your PATH. ${editor.label}'s "${editor.shellCommand}" adds it.)`));
|
|
1781
1768
|
}
|
|
1782
|
-
// ENG-3158 — before the workspace read
|
|
1769
|
+
// ENG-3158 — before the workspace read and the launch, so a candidate
|
|
1783
1770
|
// whose editor would take the connection with the wrong Remote-SSH loses no open Cursor
|
|
1784
1771
|
// windows for a launch that cannot hold. Read-only: nothing is installed or removed.
|
|
1772
|
+
attempt.stage("extensions");
|
|
1773
|
+
const listed = listEditorExtensions(resolved.command);
|
|
1774
|
+
attempt.fact("extensionInventory", listed === null ? "unavailable" : "read");
|
|
1775
|
+
attempt.fact("requiredExtension", listed === null ? "unknown" : listed.split(/\r?\n/).includes(editor.remoteSshExt) ? "present" : "missing");
|
|
1776
|
+
if (listed !== null)
|
|
1777
|
+
attempt.fact("conflictingExtension", conflictingRemoteSshInstalled(editor, listed));
|
|
1785
1778
|
if (editor.conflictingRemoteSshExt) {
|
|
1786
|
-
const listed = listEditorExtensions(resolved.command);
|
|
1787
1779
|
if (listed !== null && conflictingRemoteSshInstalled(editor, listed)) {
|
|
1780
|
+
attempt.finish("extension_conflict");
|
|
1788
1781
|
const editorCmd = pasteableEditorCommand(editor, resolved, process.platform);
|
|
1789
1782
|
for (const line of conflictingRemoteSshLines(editor, editorCmd, alias, pageUrl))
|
|
1790
1783
|
console.log(line);
|
|
@@ -1796,70 +1789,19 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1796
1789
|
// it. Both, and their order, live in `attemptEditorLaunch`: the guarantee here is a
|
|
1797
1790
|
// property of the SEQUENCE, so it is held in one testable unit rather than in the
|
|
1798
1791
|
// arrangement of the lines below. Everything this block still owns is output.
|
|
1799
|
-
const probe = sshProbeRunner(alias);
|
|
1800
|
-
//
|
|
1801
|
-
// any decision to quit Cursor, and the resulting `seedVerdict` is checked
|
|
1802
|
-
// before quitting too. `attemptEditorLaunch` below REFUSES the launch on a
|
|
1803
|
-
// mismatched build for any editor with `requiresSeedMatch` set (none today,
|
|
1804
|
-
// ENG-2884), and a refusal opens nothing — so a quit sequenced
|
|
1805
|
-
// ahead of that check could close every Cursor window on a path that was
|
|
1806
|
-
// always going to end in "nothing was opened", with no way given back in
|
|
1807
|
-
// locally. Checking the identical rule first means a quit only ever
|
|
1808
|
-
// happens on a path already known to go on to a real launch attempt.
|
|
1809
|
-
//
|
|
1810
|
-
// The reading is taken ONCE and handed to `attemptEditorLaunch` as its own
|
|
1811
|
-
// baseline too (`probe: () => baseline` below) — one SSH round trip, not
|
|
1812
|
-
// two, for the same reason this file already takes it once for the seed
|
|
1813
|
-
// check and the verification baseline together.
|
|
1792
|
+
const probe = sshProbeRunner(alias, sshExecutable);
|
|
1793
|
+
// Read once for both the seed check and attachment baseline.
|
|
1814
1794
|
const readSpinner = ora(`Checking your workspace...`).start();
|
|
1815
1795
|
const baseline = probeWorkspace(editor, probe, resolved.version.commit);
|
|
1816
1796
|
readSpinner.stop();
|
|
1817
1797
|
const seed = seedVerdict(resolved.version, baseline);
|
|
1818
1798
|
const seedWillRefuseLaunch = seed.kind === "mismatch" && editor.requiresSeedMatch;
|
|
1819
|
-
// ENG-
|
|
1820
|
-
//
|
|
1821
|
-
// ALREADY-RUNNING local Cursor never reloads even for a brand new
|
|
1822
|
-
// Remote-SSH connection. This quits it with the same graceful, never-a-kill
|
|
1823
|
-
// quit `init` uses (see lib/cursor-restart.ts). The launch a few lines
|
|
1824
|
-
// below IS the relaunch — connect already opens the editor, so there is
|
|
1825
|
-
// nothing else to relaunch it into.
|
|
1826
|
-
//
|
|
1827
|
-
// ENG-2890 — NOT from Cursor's own terminal: the quit would kill this
|
|
1828
|
-
// process before the relaunch, leaving no editor and no command. That case
|
|
1829
|
-
// stops before anything is touched, the same way an unconfirmed quit does.
|
|
1830
|
-
//
|
|
1831
|
-
// NEVER proceeds to launch on an unconfirmed quit. Doing so would hand the
|
|
1832
|
-
// new `--remote` argv to the SAME still-running process — Cursor's
|
|
1833
|
-
// single-instance lock just forwards it as a new window into a hooks
|
|
1834
|
-
// service that already initialised — which is exactly today's silent
|
|
1835
|
-
// failure, not a fix for it. So an unconfirmed quit stops here, the same
|
|
1836
|
-
// way every other refusal in this file does: nothing opened, workspace
|
|
1837
|
-
// still up and waiting, and the exact way back in.
|
|
1838
|
-
// Whether the Cursor about to open is a FRESH start, so its prompt recording loads:
|
|
1839
|
-
// it was not running, or this command confirmed it quit. Only then may success say the
|
|
1840
|
-
// work is recorded (Greptile on #3071); elsewhere (Linux, Windows, an unreadable process
|
|
1841
|
-
// list) the candidate was only asked to quit it, and nothing checked that they did.
|
|
1799
|
+
// ENG-3176: never quit an existing desktop for remote hooks. Only a confirmed
|
|
1800
|
+
// cold start needs the historical macOS launch/settle workaround.
|
|
1842
1801
|
let cursorStartsFresh = false;
|
|
1843
1802
|
if (!seedWillRefuseLaunch && ide === "cursor" && platformSupportsGracefulCursorRestart(process.platform)) {
|
|
1844
1803
|
const cursorIo = realCursorProcessIo();
|
|
1845
|
-
|
|
1846
|
-
if (presence === "not-running")
|
|
1847
|
-
cursorStartsFresh = true;
|
|
1848
|
-
if (presence === "running") {
|
|
1849
|
-
if (runningInsideCursorTerminal()) {
|
|
1850
|
-
for (const line of insideCursorTerminalLines())
|
|
1851
|
-
console.log(line);
|
|
1852
|
-
return;
|
|
1853
|
-
}
|
|
1854
|
-
info(CURSOR_RESTARTING_LINE);
|
|
1855
|
-
const quit = await attemptQuit(cursorIo);
|
|
1856
|
-
if (!quit) {
|
|
1857
|
-
for (const line of cursorQuitUnconfirmedLines())
|
|
1858
|
-
console.log(line);
|
|
1859
|
-
return;
|
|
1860
|
-
}
|
|
1861
|
-
cursorStartsFresh = true;
|
|
1862
|
-
}
|
|
1804
|
+
cursorStartsFresh = cursorIo.isRunning() === "not-running";
|
|
1863
1805
|
// ENG-2884. A COLD start handed `--remote` loses it: measured twice on a real
|
|
1864
1806
|
// profile (2026-10-01 21:11 and 2026-10-02 00:26 PT), Cursor 3 came up restoring its
|
|
1865
1807
|
// previous session (the Agents window plus old folders) and never created the
|
|
@@ -1887,7 +1829,8 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1887
1829
|
clientVersion: resolved.version,
|
|
1888
1830
|
ensureExtension: () => {
|
|
1889
1831
|
const extSpinner = ora(`Checking ${editor.label} Remote-SSH extension...`).start();
|
|
1890
|
-
const state = ensureRemoteSshExtension(editor, resolved.command);
|
|
1832
|
+
const state = ensureRemoteSshExtension(editor, resolved.command, listed);
|
|
1833
|
+
attempt.fact("extensionSetup", state);
|
|
1891
1834
|
// ENG-2890: an internal step, so it says nothing when it worked.
|
|
1892
1835
|
if (state === "failed") {
|
|
1893
1836
|
extSpinner.warn(`Couldn't install ${editor.label}'s Remote-SSH extension. If ${editor.label} asks for it, install it from the Extensions view.`);
|
|
@@ -1902,6 +1845,8 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1902
1845
|
},
|
|
1903
1846
|
launchLogPath: editorLaunchLogPath,
|
|
1904
1847
|
launch: (logPath) => {
|
|
1848
|
+
attempt.stage("editor_launch");
|
|
1849
|
+
attempt.checkpoint();
|
|
1905
1850
|
const launched = launches.send(logPath);
|
|
1906
1851
|
// The launch is not the result, so nothing claims one here. What used to be printed
|
|
1907
1852
|
// at this point was `✔ Opening <editor> → <alias>`, which is true of a spawn that is
|
|
@@ -1911,18 +1856,21 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1911
1856
|
return launched;
|
|
1912
1857
|
},
|
|
1913
1858
|
readLaunchLog: () => launches.readLog(readEditorLaunchLog),
|
|
1914
|
-
verifyAttached: (baseline, launched) =>
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1859
|
+
verifyAttached: (baseline, launched) => {
|
|
1860
|
+
attempt.stage("editor_verify");
|
|
1861
|
+
return verifyEditorAttached(editor, baseline, probe, (waited) => {
|
|
1862
|
+
if (spinners.verify) {
|
|
1863
|
+
spinners.verify.text = `Opening ${editor.label} in your workspace... (${waited}s)`;
|
|
1864
|
+
}
|
|
1865
|
+
// ENG-2884: if the first request still produced no attached window, ask the
|
|
1866
|
+
// (now running) Cursor once more. A window already opening for this folder is
|
|
1867
|
+
// focused rather than duplicated, so a slow first attach costs nothing.
|
|
1868
|
+
if (shouldResendCursorOpen(editor.ide, cursorStartsFresh, waited, resend)) {
|
|
1869
|
+
resend.done = true;
|
|
1870
|
+
launches.send(editorLaunchLogPath());
|
|
1871
|
+
}
|
|
1872
|
+
}, launched, resolved.version.commit);
|
|
1873
|
+
},
|
|
1926
1874
|
});
|
|
1927
1875
|
if (outcome.kind === "refused") {
|
|
1928
1876
|
for (const line of editorSeedMismatchLines(editor, outcome.seed, pageUrl, alias)) {
|
|
@@ -1932,6 +1880,9 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1932
1880
|
}
|
|
1933
1881
|
const launchSpinner = spinners.verify;
|
|
1934
1882
|
if (outcome.kind === "launch-failed") {
|
|
1883
|
+
attempt.finish("launch_failed");
|
|
1884
|
+
attempt.evidence(outcome.error);
|
|
1885
|
+
attempt.evidence(outcome.launchLog);
|
|
1935
1886
|
// No window was created, so none of the not-connected copy is true here. See
|
|
1936
1887
|
// `editorLaunchFailedLines`.
|
|
1937
1888
|
launchSpinner?.fail(`Couldn't start ${editor.label}.`);
|
|
@@ -1941,16 +1892,14 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1941
1892
|
console.log(line);
|
|
1942
1893
|
return;
|
|
1943
1894
|
}
|
|
1895
|
+
attempt.evidence(outcome.launchLog);
|
|
1944
1896
|
if (outcome.verdict.kind === "connected") {
|
|
1945
|
-
|
|
1946
|
-
launchSpinner?.succeed(
|
|
1947
|
-
? `${editor.label} is open in your workspace. Your work is recorded.`
|
|
1948
|
-
: `${editor.label} is open in your workspace.`);
|
|
1949
|
-
if (!recordingKnown)
|
|
1950
|
-
skillImport.footer.push(CURSOR_RECORDING_CAVEAT_LINE);
|
|
1897
|
+
attempt.finish("server_detected");
|
|
1898
|
+
launchSpinner?.succeed(`${editor.label}'s server is running in your workspace.`);
|
|
1951
1899
|
skillImport.footer.push(...editorConnectedLines(editor, alias, verbose));
|
|
1952
1900
|
}
|
|
1953
1901
|
else if (outcome.verdict.kind === "unverified") {
|
|
1902
|
+
attempt.finish("unverified");
|
|
1954
1903
|
// Three-valued on purpose: we could not reach the workspace to ask, which is not the
|
|
1955
1904
|
// same as knowing nothing came up, and saying either of the other two here would be a
|
|
1956
1905
|
// claim we cannot make. See `verifyVerdict`.
|
|
@@ -1959,6 +1908,7 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1959
1908
|
console.log(line);
|
|
1960
1909
|
}
|
|
1961
1910
|
else {
|
|
1911
|
+
attempt.finish("not_connected");
|
|
1962
1912
|
launchSpinner?.fail(`${editor.label} hasn't connected to your workspace yet.`);
|
|
1963
1913
|
for (const line of editorNotConnectedLines(editor, alias, pageUrl, verbose))
|
|
1964
1914
|
console.log(line);
|