litmus-cli 1.4.67 → 1.4.69
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 +40 -0
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +121 -54
- 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/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +6 -2
- package/dist/commands/push.js.map +1 -1
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +4 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +50 -6
- 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-agent-window.d.ts +13 -0
- package/dist/lib/cursor-agent-window.d.ts.map +1 -0
- package/dist/lib/cursor-agent-window.js +63 -0
- package/dist/lib/cursor-agent-window.js.map +1 -0
- 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
|
@@ -270,6 +270,29 @@ export interface NativeEditor {
|
|
|
270
270
|
* has to fail.
|
|
271
271
|
*/
|
|
272
272
|
export declare const NATIVE_EDITORS: Record<NativeIde, NativeEditor>;
|
|
273
|
+
/**
|
|
274
|
+
* ENG-2332 — PATH first, then the places the installer puts it.
|
|
275
|
+
*
|
|
276
|
+
* The old code asked only whether `ed.bin` was on PATH, and answered a "no" with a
|
|
277
|
+
* manual command that begins with `ed.bin` — i.e. told the candidate to run the binary
|
|
278
|
+
* whose absence was the error. On the machine that reported this, the binary was sitting
|
|
279
|
+
* at `/Applications/Cursor.app/Contents/Resources/app/bin/cursor` throughout.
|
|
280
|
+
*
|
|
281
|
+
* PATH still wins when it answers, because that is the command the candidate's own
|
|
282
|
+
* tooling and any support answer will use. A bundle path is a fallback, and it is
|
|
283
|
+
* probed with `probeEditorCli` rather than trusted on `existsSync` alone: a file at the
|
|
284
|
+
* expected path that cannot execute is not a route, and offering it as one would replace
|
|
285
|
+
* one silent failure with another.
|
|
286
|
+
*
|
|
287
|
+
* The build it reported rides back on the result, so the seed check below reads the
|
|
288
|
+
* version THIS command printed. A second `--version` spawn could answer for a different
|
|
289
|
+
* binary — a PATH that changed under us, a shim that dispatches — and would then be
|
|
290
|
+
* compared against a workspace on behalf of an editor we are not about to launch.
|
|
291
|
+
*/
|
|
292
|
+
interface ResolvedEditor extends ResolvedEditorCommand {
|
|
293
|
+
version: EditorVersion;
|
|
294
|
+
}
|
|
295
|
+
export declare function resolveEditorCommand(ed: NativeEditor): ResolvedEditor | null;
|
|
273
296
|
/**
|
|
274
297
|
* ENG-2152 — what the editor CLI we are about to drive says it IS.
|
|
275
298
|
*
|
|
@@ -403,6 +426,20 @@ export declare function editorLaunchFailedLines(ed: NativeEditor, alias: string,
|
|
|
403
426
|
* has no workspace files to list.
|
|
404
427
|
*/
|
|
405
428
|
export declare function editorUnverifiedLines(ed: NativeEditor, alias: string, pageUrl: string | null): string[];
|
|
429
|
+
/**
|
|
430
|
+
* Ensure the Remote-SSH extension is installed in the user's editor before we
|
|
431
|
+
* open the remote window (the `gh codespace code` model). Best-effort: a
|
|
432
|
+
* marketplace/network failure returns "failed" rather than aborting connect —
|
|
433
|
+
* the caller tells the user how to install it by hand. Assumes the editor's CLI
|
|
434
|
+
* has been resolved (caller gates on `resolveEditorCommand`).
|
|
435
|
+
*/
|
|
436
|
+
/**
|
|
437
|
+
* ENG-3158 — `<command> --list-extensions` output, or null when it cannot be read. A
|
|
438
|
+
* null skips the conflict check rather than stopping connect: the check only refuses on
|
|
439
|
+
* evidence, never on a failed read.
|
|
440
|
+
*/
|
|
441
|
+
export declare function listEditorExtensions(command: string): string | null;
|
|
442
|
+
export declare function ensureRemoteSshExtension(ed: NativeEditor, command: string, listed: string | null): "present" | "installed" | "failed";
|
|
406
443
|
/**
|
|
407
444
|
* ENG-2332 — where the editor's own output goes.
|
|
408
445
|
*
|
|
@@ -505,6 +542,7 @@ export declare function editorLaunchOutputLines(logPath: string, launchLog: stri
|
|
|
505
542
|
* never as "there is no server" — see `verifyVerdict`.
|
|
506
543
|
*/
|
|
507
544
|
export type WorkspaceProbeRunner = (remoteCommand: string) => string | null;
|
|
545
|
+
export declare function sshProbeRunner(alias: string, executable?: string): WorkspaceProbeRunner;
|
|
508
546
|
export declare function verifyEditorAttached(ed: NativeEditor, baseline: WorkspaceProbe | null, run: WorkspaceProbeRunner, onWait: (secondsWaited: number) => void, launched: EditorLaunchHandle, clientCommit?: string | null): Promise<VerifyVerdict>;
|
|
509
547
|
/** Whether the editor's Remote-SSH extension was already there, put there, or could not be. */
|
|
510
548
|
export type RemoteSshExtensionState = "present" | "installed" | "failed";
|
|
@@ -698,6 +736,8 @@ export declare function chatConfigActionForIdentity(ide: IdeChoice, state: "conf
|
|
|
698
736
|
*/
|
|
699
737
|
export declare function browserFallback(hasUrl: boolean, ideWasChosen: boolean): "browser" | "vscode" | "unavailable";
|
|
700
738
|
export interface ConnectOptions {
|
|
739
|
+
/** ENG-3217: opt-in desktop experiment; never proof of agent attachment. */
|
|
740
|
+
cursorAgentWindow?: boolean;
|
|
701
741
|
/**
|
|
702
742
|
* ENG-2332 item 4 — print what the editor wrote while opening, and where that log is.
|
|
703
743
|
* The output is CAPTURED either way (a failed verification prints its tail whether or
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../src/commands/connect.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../src/commands/connect.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/connect.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { requestCursorAgentWindow, cursorAgentWindowLines } from "../lib/cursor-agent-window.js";
|
|
2
|
+
import { ConnectionAttempt } from "../lib/connection-attempt.js";
|
|
3
|
+
import { updateSshConfig } from "../lib/ssh-config-file.js";
|
|
1
4
|
import os from "os";
|
|
2
5
|
import path from "path";
|
|
3
6
|
import readline from "readline";
|
|
4
|
-
import { closeSync, constants, existsSync, mkdirSync, openSync, readFileSync
|
|
7
|
+
import { closeSync, constants, existsSync, mkdirSync, openSync, readFileSync } from "fs";
|
|
5
8
|
import { randomBytes } from "crypto";
|
|
6
9
|
import { execFileSync, spawn, spawnSync } from "child_process";
|
|
7
10
|
import chalk from "chalk";
|
|
@@ -320,13 +323,9 @@ function ensureSshKey(keyPath) {
|
|
|
320
323
|
stdio: "ignore",
|
|
321
324
|
});
|
|
322
325
|
}
|
|
323
|
-
function writeSshConfig(alias, block, controlBlock) {
|
|
326
|
+
async function writeSshConfig(alias, block, controlBlock) {
|
|
324
327
|
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 });
|
|
328
|
+
await updateSshConfig(cfgPath, (existing) => upsertSshConfigContent(existing, alias, block, controlBlock));
|
|
330
329
|
return cfgPath;
|
|
331
330
|
}
|
|
332
331
|
/**
|
|
@@ -412,7 +411,7 @@ function probeEditorCli(command) {
|
|
|
412
411
|
return null;
|
|
413
412
|
}
|
|
414
413
|
}
|
|
415
|
-
function resolveEditorCommand(ed) {
|
|
414
|
+
export function resolveEditorCommand(ed) {
|
|
416
415
|
const onPath = probeEditorCli(ed.bin);
|
|
417
416
|
if (onPath)
|
|
418
417
|
return { command: ed.bin, source: "path", version: onPath };
|
|
@@ -687,7 +686,7 @@ export function editorUnverifiedLines(ed, alias, pageUrl) {
|
|
|
687
686
|
* null skips the conflict check rather than stopping connect: the check only refuses on
|
|
688
687
|
* evidence, never on a failed read.
|
|
689
688
|
*/
|
|
690
|
-
function listEditorExtensions(command) {
|
|
689
|
+
export function listEditorExtensions(command) {
|
|
691
690
|
const list = editorSpawnSpec(command, ["--list-extensions"], process.platform);
|
|
692
691
|
if (!list)
|
|
693
692
|
return null;
|
|
@@ -699,22 +698,13 @@ function listEditorExtensions(command) {
|
|
|
699
698
|
return null;
|
|
700
699
|
}
|
|
701
700
|
}
|
|
702
|
-
function ensureRemoteSshExtension(ed, command) {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
// `failed` is already best-effort and never aborts connect.
|
|
706
|
-
if (!list)
|
|
701
|
+
export function ensureRemoteSshExtension(ed, command, listed) {
|
|
702
|
+
// An unreadable inventory is not evidence of absence; don't mutate it blindly.
|
|
703
|
+
if (listed === null)
|
|
707
704
|
return "failed";
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
return "present";
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
catch {
|
|
715
|
-
// Couldn't list (unexpected) — fall through and try to install anyway.
|
|
716
|
-
}
|
|
717
|
-
const install = editorSpawnSpec(command, ["--install-extension", ed.remoteSshExt, "--force"], process.platform);
|
|
705
|
+
if (listed.split(/\r?\n/).some((l) => l.trim().toLowerCase() === ed.remoteSshExt))
|
|
706
|
+
return "present";
|
|
707
|
+
const install = editorSpawnSpec(command, ["--install-extension", ed.remoteSshExt], process.platform);
|
|
718
708
|
if (!install)
|
|
719
709
|
return "failed";
|
|
720
710
|
try {
|
|
@@ -868,11 +858,11 @@ function readEditorLaunchLog(logPath) {
|
|
|
868
858
|
return null;
|
|
869
859
|
}
|
|
870
860
|
}
|
|
871
|
-
function sshProbeRunner(alias) {
|
|
861
|
+
export function sshProbeRunner(alias, executable = "ssh") {
|
|
872
862
|
return (remoteCommand) => {
|
|
873
863
|
try {
|
|
874
|
-
const probed = spawnSync(
|
|
875
|
-
if (probed.error)
|
|
864
|
+
const probed = spawnSync(executable, ["-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "-o", "LogLevel=ERROR", alias, remoteCommand], { encoding: "utf8", timeout: 20000 });
|
|
865
|
+
if (probed.error || probed.status !== 0)
|
|
876
866
|
return null;
|
|
877
867
|
return probed.stdout ?? null;
|
|
878
868
|
}
|
|
@@ -1252,8 +1242,7 @@ function pruneStaleHostKeys(p) {
|
|
|
1252
1242
|
// known_hosts as [host]:port, so a bare-host -R would miss it.
|
|
1253
1243
|
if (p.port)
|
|
1254
1244
|
targets.push(`[${p.host}]:${p.port}`);
|
|
1255
|
-
|
|
1256
|
-
targets.push(p.jumpPort ? `[${p.jumpHost}]:${p.jumpPort}` : p.jumpHost);
|
|
1245
|
+
// The shared bastion is not reprovisioned with this workspace. Preserve its pinned key.
|
|
1257
1246
|
for (const t of targets) {
|
|
1258
1247
|
try {
|
|
1259
1248
|
execFileSync("ssh-keygen", ["-R", t], { stdio: "ignore" });
|
|
@@ -1280,18 +1269,34 @@ export async function runConnect(token, ideOpt, opts = {}) {
|
|
|
1280
1269
|
// printed last, on every way out of the flow, so it neither delays the editor nor
|
|
1281
1270
|
// is lost when a later step fails. The workspace lines follow it, so the candidate's
|
|
1282
1271
|
// last read is where they are and what the session ending looks like.
|
|
1272
|
+
setErrorContext({ token, apiBase: resolveApiBase(process.env.LITMUS_API_URL) });
|
|
1273
|
+
const attempt = new ConnectionAttempt();
|
|
1283
1274
|
const skillImport = { report: null, footer: [], sshAlias: null, publishedPorts: [] };
|
|
1275
|
+
// Register before entering the flow: a fast fatal report can settle before the
|
|
1276
|
+
// caller's finally begins, especially while an async SSH config write is unwinding.
|
|
1277
|
+
let releaseExitHold;
|
|
1278
|
+
holdExitFor(new Promise((resolve) => { releaseExitHold = resolve; }));
|
|
1284
1279
|
try {
|
|
1285
|
-
await connectFlow(token, ideOpt, opts, skillImport);
|
|
1280
|
+
await connectFlow(token, ideOpt, opts, skillImport, attempt);
|
|
1281
|
+
}
|
|
1282
|
+
catch (error) {
|
|
1283
|
+
attempt.finish("failed");
|
|
1284
|
+
attempt.evidence(error instanceof Error ? error.message : null);
|
|
1285
|
+
throw error;
|
|
1286
1286
|
}
|
|
1287
1287
|
finally {
|
|
1288
|
-
|
|
1289
|
-
|
|
1288
|
+
try {
|
|
1289
|
+
await attempt.flush();
|
|
1290
|
+
if (opts.verbose || attempt.needsSupport)
|
|
1291
|
+
console.log(chalk.dim(` Connection ID: ${attempt.id}`));
|
|
1290
1292
|
if (skillImport.report)
|
|
1291
1293
|
await finishSkillImport(skillImport.report);
|
|
1292
1294
|
for (const line of skillImport.footer)
|
|
1293
1295
|
console.log(line);
|
|
1294
|
-
}
|
|
1296
|
+
}
|
|
1297
|
+
finally {
|
|
1298
|
+
releaseExitHold();
|
|
1299
|
+
}
|
|
1295
1300
|
}
|
|
1296
1301
|
// The ssh route's session runs last, so nothing we print lands inside the remote shell.
|
|
1297
1302
|
if (!skillImport.sshAlias)
|
|
@@ -1323,7 +1328,7 @@ async function openSshRoute(alias) {
|
|
|
1323
1328
|
if (status)
|
|
1324
1329
|
process.exitCode = status;
|
|
1325
1330
|
}
|
|
1326
|
-
async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
1331
|
+
async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
1327
1332
|
const verbose = Boolean(opts.verbose);
|
|
1328
1333
|
const apiBase = resolveApiBase(process.env.LITMUS_API_URL);
|
|
1329
1334
|
setErrorContext({ token, apiBase });
|
|
@@ -1338,6 +1343,8 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1338
1343
|
spinner.succeed(meta.assessmentName);
|
|
1339
1344
|
}
|
|
1340
1345
|
catch (err) {
|
|
1346
|
+
attempt.evidence(workspaceStartNetworkDetail(err));
|
|
1347
|
+
attempt.evidence(err instanceof Error ? err.message : null);
|
|
1341
1348
|
// ENG-2895: the Error line below says what failed; a second "Connection failed" line
|
|
1342
1349
|
// above it was the same fact twice, and wrong when the cause was a rejected token.
|
|
1343
1350
|
spinner.stop();
|
|
@@ -1360,28 +1367,37 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1360
1367
|
// ENG-2890 — the assessment page is the one re-entry route connect prints: its URL
|
|
1361
1368
|
// carries only the assessment id, never the token.
|
|
1362
1369
|
const pageUrl = candidatePageUrl(apiBase, meta.assessmentId);
|
|
1370
|
+
attempt.stage("selection");
|
|
1363
1371
|
const { ide: chosenIde, chosen: ideWasChosen } = await resolveIde(ideOpt, console.log);
|
|
1364
1372
|
let ide = chosenIde;
|
|
1373
|
+
attempt.editor(ide);
|
|
1365
1374
|
// ENG-2814: asked here, next to the editor choice, and uploaded once the workspace is
|
|
1366
1375
|
// ready. Never fails the connect. See `lib/connect-skills.ts`.
|
|
1376
|
+
attempt.stage("skills");
|
|
1367
1377
|
const pendingSkills = await offerSkillImport({
|
|
1368
1378
|
backendUrl, token, flag: opts.skills, verbose,
|
|
1369
1379
|
});
|
|
1380
|
+
attempt.stage("ssh_key");
|
|
1370
1381
|
// 2. Ensure a local SSH key (private key never leaves this machine).
|
|
1371
1382
|
try {
|
|
1372
1383
|
ensureSshKey(KEY_PATH);
|
|
1373
1384
|
}
|
|
1374
1385
|
catch (err) {
|
|
1386
|
+
attempt.evidence(workspaceStartNetworkDetail(err));
|
|
1387
|
+
attempt.evidence(err instanceof Error ? err.message : null);
|
|
1375
1388
|
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" });
|
|
1376
1389
|
return;
|
|
1377
1390
|
}
|
|
1378
1391
|
const pubkey = readFileSync(`${KEY_PATH}.pub`, "utf8").trim();
|
|
1379
1392
|
// 3. Start the workspace with our public key.
|
|
1393
|
+
attempt.stage("workspace_start");
|
|
1380
1394
|
const boot = ora("Starting your workspace...").start();
|
|
1381
1395
|
try {
|
|
1382
1396
|
await startWorkspace(backendUrl, token, pubkey);
|
|
1383
1397
|
}
|
|
1384
1398
|
catch (err) {
|
|
1399
|
+
attempt.evidence(workspaceStartNetworkDetail(err));
|
|
1400
|
+
attempt.evidence(err instanceof Error ? err.message : null);
|
|
1385
1401
|
// The Error line below names what went wrong; see the sign-in spinner above.
|
|
1386
1402
|
boot.stop();
|
|
1387
1403
|
const code = err instanceof ServerError ? err.errorCode : undefined;
|
|
@@ -1437,6 +1453,8 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1437
1453
|
return;
|
|
1438
1454
|
}
|
|
1439
1455
|
boot.succeed("Workspace ready");
|
|
1456
|
+
attempt.stage("workspace_ready");
|
|
1457
|
+
attempt.checkpoint();
|
|
1440
1458
|
if (pendingSkills)
|
|
1441
1459
|
skillImport.report = holdExitFor(startSkillImport(backendUrl, token, pendingSkills, undefined, () => ide));
|
|
1442
1460
|
// Browser IDE: no local SSH / VS Code needed — just open the signed URL.
|
|
@@ -1454,6 +1472,7 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1454
1472
|
if (fallback === "vscode") {
|
|
1455
1473
|
warn("The browser IDE isn't available here. Opening VS Code instead.");
|
|
1456
1474
|
ide = "vscode";
|
|
1475
|
+
attempt.editor(ide);
|
|
1457
1476
|
}
|
|
1458
1477
|
else if (fallback === "unavailable") {
|
|
1459
1478
|
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" });
|
|
@@ -1461,6 +1480,8 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1461
1480
|
}
|
|
1462
1481
|
}
|
|
1463
1482
|
if (ide === "browser" && conn.browser_ide_url) {
|
|
1483
|
+
attempt.stage("browser_open");
|
|
1484
|
+
attempt.finish("browser_requested");
|
|
1464
1485
|
// ENG-3137 — the workspace now trusts this machine's key, so the assessment page's
|
|
1465
1486
|
// Plain SSH offers `ssh <alias>`. Write the entry that alias names, as every other
|
|
1466
1487
|
// route does. Best-effort: a config we could not write must not cost the browser route.
|
|
@@ -1469,11 +1490,13 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1469
1490
|
const parsed = parseSshCommand(conn.ssh_command);
|
|
1470
1491
|
const alias = aliasFor(conn.ssh_hostname, token);
|
|
1471
1492
|
pruneStaleHostKeys(parsed);
|
|
1472
|
-
const cfgPath = writeSshConfig(alias, buildSshConfigBlock(alias, KEY_PATH, parsed), buildControlPathBlock(alias, parsed));
|
|
1493
|
+
const cfgPath = await writeSshConfig(alias, buildSshConfigBlock(alias, KEY_PATH, parsed), buildControlPathBlock(alias, parsed));
|
|
1473
1494
|
if (verbose)
|
|
1474
1495
|
info(`SSH config written to ${cfgPath} (Host "${alias}")`);
|
|
1475
1496
|
}
|
|
1476
1497
|
catch (err) {
|
|
1498
|
+
attempt.evidence(workspaceStartNetworkDetail(err));
|
|
1499
|
+
attempt.evidence(err instanceof Error ? err.message : null);
|
|
1477
1500
|
if (verbose)
|
|
1478
1501
|
info(`SSH config not written: ${err instanceof Error ? err.message : String(err)}`);
|
|
1479
1502
|
}
|
|
@@ -1499,6 +1522,13 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1499
1522
|
// Prune stale host keys first so a re-provisioned container's new key is
|
|
1500
1523
|
// accepted, not refused (ENG-1221).
|
|
1501
1524
|
const parsed = parseSshCommand(conn.ssh_command);
|
|
1525
|
+
// Reuse this observation across compatibility checks, preflight and diagnostics.
|
|
1526
|
+
const sshBanners = new Map();
|
|
1527
|
+
const sshBanner = (executable) => {
|
|
1528
|
+
if (!sshBanners.has(executable))
|
|
1529
|
+
sshBanners.set(executable, readSshVersionBanner(executable));
|
|
1530
|
+
return sshBanners.get(executable) ?? null;
|
|
1531
|
+
};
|
|
1502
1532
|
// ENG-2682 — Windows' own OpenSSH below 8.1 cannot run the ProxyJump every bastion
|
|
1503
1533
|
// route depends on (see `lib/ssh-client.ts`). Refuse before writing anything, with the
|
|
1504
1534
|
// version named and a way out, rather than hand an editor a connection it will lose
|
|
@@ -1506,17 +1536,18 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1506
1536
|
// `remote.SSH.path` when it names one, PATH's ssh otherwise.
|
|
1507
1537
|
if (process.platform === "win32" && parsed.jumpHost && ide !== "browser") {
|
|
1508
1538
|
const executable = sshExecutableFor(ide, ide === "ssh" ? null : readEditorSettings(ide));
|
|
1509
|
-
const client = parseOpenSshVersion(
|
|
1539
|
+
const client = parseOpenSshVersion(sshBanner(executable) ?? "");
|
|
1510
1540
|
if (client && proxyJumpBroken(client)) {
|
|
1511
1541
|
const where = executable === "ssh" ? "" : ` at ${executable}, set in remote.SSH.path`;
|
|
1512
1542
|
fatal(`Your Windows SSH (${client.label}${where}) is too old to reach the workspace.`, oldWindowsSshHint(Boolean(conn.browser_ide_url)), { severity: "warning" });
|
|
1513
1543
|
return;
|
|
1514
1544
|
}
|
|
1515
1545
|
}
|
|
1546
|
+
attempt.stage("ssh_config");
|
|
1516
1547
|
const alias = aliasFor(conn.ssh_hostname, token);
|
|
1517
1548
|
pruneStaleHostKeys(parsed);
|
|
1518
1549
|
const block = buildSshConfigBlock(alias, KEY_PATH, parsed);
|
|
1519
|
-
const cfgPath = writeSshConfig(alias, block, buildControlPathBlock(alias, parsed));
|
|
1550
|
+
const cfgPath = await writeSshConfig(alias, block, buildControlPathBlock(alias, parsed));
|
|
1520
1551
|
if (verbose)
|
|
1521
1552
|
info(`SSH config written to ${cfgPath} (Host "${alias}")`);
|
|
1522
1553
|
// ENG-2693 — prove the connection before handing it off (see `sshPreflight`). The
|
|
@@ -1529,19 +1560,23 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1529
1560
|
// Called per route, not here once: the native route runs it only AFTER the WSL check
|
|
1530
1561
|
// (Greptile on #2844), so a candidate whose Windows editor cannot see WSL's config is
|
|
1531
1562
|
// told that, rather than a generic ssh failure from the wrong side of the pairing.
|
|
1563
|
+
const sshExecutable = sshExecutableFor(ide, ide === "ssh" ? null : readEditorSettings(ide));
|
|
1532
1564
|
const preflightStops = async (route) => {
|
|
1533
|
-
const sshExecutable = sshExecutableFor(route, route === "ssh" ? null : readEditorSettings(route));
|
|
1534
1565
|
// ENG-2895: up to two 45s attempts, so it gets a spinner; it says nothing when it works.
|
|
1535
1566
|
const checking = ora("Checking the connection to your workspace...").start();
|
|
1567
|
+
attempt.stage("ssh_preflight");
|
|
1568
|
+
attempt.fact("sshSelection", sshExecutable === "ssh" ? "path" : "editor-setting");
|
|
1569
|
+
attempt.fact("sshVersion", parseOpenSshVersion(sshBanner(sshExecutable) ?? "")?.label);
|
|
1536
1570
|
const preflight = await sshPreflight(alias, sshExecutable);
|
|
1537
1571
|
checking.stop();
|
|
1538
1572
|
if (preflight.ok)
|
|
1539
1573
|
return false;
|
|
1574
|
+
attempt.evidence(preflight.detail.join("\n"));
|
|
1540
1575
|
fatal("Your computer couldn't open an SSH connection to the workspace.", sshPreflightHint(preflight.detail, Boolean(conn.browser_ide_url), verbose), {
|
|
1541
1576
|
severity: "error",
|
|
1542
1577
|
internalDetail: [
|
|
1543
1578
|
`route=${route} platform=${process.platform} ssh=${sshExecutable}`,
|
|
1544
|
-
`client=${
|
|
1579
|
+
`client=${sshBanner(sshExecutable) ?? "unreadable"}`,
|
|
1545
1580
|
...preflight.detail,
|
|
1546
1581
|
].join("\n"),
|
|
1547
1582
|
});
|
|
@@ -1552,6 +1587,8 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1552
1587
|
if (ide === "ssh") {
|
|
1553
1588
|
if (await preflightStops("ssh"))
|
|
1554
1589
|
return;
|
|
1590
|
+
attempt.stage("ssh_handoff");
|
|
1591
|
+
attempt.finish("ssh_ready");
|
|
1555
1592
|
if (verbose)
|
|
1556
1593
|
console.log(chalk.dim(` (equivalently: ${conn.ssh_command})`));
|
|
1557
1594
|
if (opts.tunnel) {
|
|
@@ -1577,7 +1614,12 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1577
1614
|
// places its installer puts it, because "not on PATH" was being reported as "not
|
|
1578
1615
|
// installed" on a machine where the binary was sitting in the application bundle the
|
|
1579
1616
|
// whole time. Null here means we genuinely could not find a way to drive it.
|
|
1617
|
+
attempt.stage("editor_resolution");
|
|
1580
1618
|
const resolved = resolveEditorCommand(editor);
|
|
1619
|
+
attempt.editor(ide, resolved?.version.version);
|
|
1620
|
+
attempt.fact("editorFound", Boolean(resolved));
|
|
1621
|
+
attempt.fact("wsl", isWsl());
|
|
1622
|
+
attempt.fact("editorCommit", resolved?.version.commit);
|
|
1581
1623
|
// ENG-2683 — inside WSL, the editor on PATH is usually the WINDOWS app, and its
|
|
1582
1624
|
// Remote-SSH reads Windows' ssh config, not the WSL one written above (see
|
|
1583
1625
|
// `lib/wsl.ts`). Launching it would fail in the editor with nothing on our side, so say
|
|
@@ -1599,7 +1641,9 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1599
1641
|
// With nothing resolved there is nothing to probe, and `unverified` is what that means:
|
|
1600
1642
|
// it is the absence branch's job to say so, below the mismatch refusal, so a candidate
|
|
1601
1643
|
// whose `code` is Cursor is told THAT rather than that their editor is missing.
|
|
1644
|
+
attempt.stage("editor_resolution");
|
|
1602
1645
|
const identity = resolved ? identifyEditorCli(editor, resolved.command) : { kind: "unverified" };
|
|
1646
|
+
attempt.fact("editorIdentity", identity.kind);
|
|
1603
1647
|
// 5b. Point this machine's Copilot Chat at the Litmus gateway (ENG-1475).
|
|
1604
1648
|
//
|
|
1605
1649
|
// ONLY ON THE NATIVE VS CODE PATH, and that is the whole reason it is here rather
|
|
@@ -1726,9 +1770,15 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1726
1770
|
// ENG-3158 — before the workspace read and the launch, so a candidate
|
|
1727
1771
|
// whose editor would take the connection with the wrong Remote-SSH loses no open Cursor
|
|
1728
1772
|
// windows for a launch that cannot hold. Read-only: nothing is installed or removed.
|
|
1773
|
+
attempt.stage("extensions");
|
|
1774
|
+
const listed = listEditorExtensions(resolved.command);
|
|
1775
|
+
attempt.fact("extensionInventory", listed === null ? "unavailable" : "read");
|
|
1776
|
+
attempt.fact("requiredExtension", listed === null ? "unknown" : listed.split(/\r?\n/).includes(editor.remoteSshExt) ? "present" : "missing");
|
|
1777
|
+
if (listed !== null)
|
|
1778
|
+
attempt.fact("conflictingExtension", conflictingRemoteSshInstalled(editor, listed));
|
|
1729
1779
|
if (editor.conflictingRemoteSshExt) {
|
|
1730
|
-
const listed = listEditorExtensions(resolved.command);
|
|
1731
1780
|
if (listed !== null && conflictingRemoteSshInstalled(editor, listed)) {
|
|
1781
|
+
attempt.finish("extension_conflict");
|
|
1732
1782
|
const editorCmd = pasteableEditorCommand(editor, resolved, process.platform);
|
|
1733
1783
|
for (const line of conflictingRemoteSshLines(editor, editorCmd, alias, pageUrl))
|
|
1734
1784
|
console.log(line);
|
|
@@ -1740,7 +1790,7 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1740
1790
|
// it. Both, and their order, live in `attemptEditorLaunch`: the guarantee here is a
|
|
1741
1791
|
// property of the SEQUENCE, so it is held in one testable unit rather than in the
|
|
1742
1792
|
// arrangement of the lines below. Everything this block still owns is output.
|
|
1743
|
-
const probe = sshProbeRunner(alias);
|
|
1793
|
+
const probe = sshProbeRunner(alias, sshExecutable);
|
|
1744
1794
|
// Read once for both the seed check and attachment baseline.
|
|
1745
1795
|
const readSpinner = ora(`Checking your workspace...`).start();
|
|
1746
1796
|
const baseline = probeWorkspace(editor, probe, resolved.version.commit);
|
|
@@ -1780,7 +1830,8 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1780
1830
|
clientVersion: resolved.version,
|
|
1781
1831
|
ensureExtension: () => {
|
|
1782
1832
|
const extSpinner = ora(`Checking ${editor.label} Remote-SSH extension...`).start();
|
|
1783
|
-
const state = ensureRemoteSshExtension(editor, resolved.command);
|
|
1833
|
+
const state = ensureRemoteSshExtension(editor, resolved.command, listed);
|
|
1834
|
+
attempt.fact("extensionSetup", state);
|
|
1784
1835
|
// ENG-2890: an internal step, so it says nothing when it worked.
|
|
1785
1836
|
if (state === "failed") {
|
|
1786
1837
|
extSpinner.warn(`Couldn't install ${editor.label}'s Remote-SSH extension. If ${editor.label} asks for it, install it from the Extensions view.`);
|
|
@@ -1795,6 +1846,8 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1795
1846
|
},
|
|
1796
1847
|
launchLogPath: editorLaunchLogPath,
|
|
1797
1848
|
launch: (logPath) => {
|
|
1849
|
+
attempt.stage("editor_launch");
|
|
1850
|
+
attempt.checkpoint();
|
|
1798
1851
|
const launched = launches.send(logPath);
|
|
1799
1852
|
// The launch is not the result, so nothing claims one here. What used to be printed
|
|
1800
1853
|
// at this point was `✔ Opening <editor> → <alias>`, which is true of a spawn that is
|
|
@@ -1804,18 +1857,21 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1804
1857
|
return launched;
|
|
1805
1858
|
},
|
|
1806
1859
|
readLaunchLog: () => launches.readLog(readEditorLaunchLog),
|
|
1807
|
-
verifyAttached: (baseline, launched) =>
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1860
|
+
verifyAttached: (baseline, launched) => {
|
|
1861
|
+
attempt.stage("editor_verify");
|
|
1862
|
+
return verifyEditorAttached(editor, baseline, probe, (waited) => {
|
|
1863
|
+
if (spinners.verify) {
|
|
1864
|
+
spinners.verify.text = `Opening ${editor.label} in your workspace... (${waited}s)`;
|
|
1865
|
+
}
|
|
1866
|
+
// ENG-2884: if the first request still produced no attached window, ask the
|
|
1867
|
+
// (now running) Cursor once more. A window already opening for this folder is
|
|
1868
|
+
// focused rather than duplicated, so a slow first attach costs nothing.
|
|
1869
|
+
if (shouldResendCursorOpen(editor.ide, cursorStartsFresh, waited, resend)) {
|
|
1870
|
+
resend.done = true;
|
|
1871
|
+
launches.send(editorLaunchLogPath());
|
|
1872
|
+
}
|
|
1873
|
+
}, launched, resolved.version.commit);
|
|
1874
|
+
},
|
|
1819
1875
|
});
|
|
1820
1876
|
if (outcome.kind === "refused") {
|
|
1821
1877
|
for (const line of editorSeedMismatchLines(editor, outcome.seed, pageUrl, alias)) {
|
|
@@ -1825,6 +1881,9 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1825
1881
|
}
|
|
1826
1882
|
const launchSpinner = spinners.verify;
|
|
1827
1883
|
if (outcome.kind === "launch-failed") {
|
|
1884
|
+
attempt.finish("launch_failed");
|
|
1885
|
+
attempt.evidence(outcome.error);
|
|
1886
|
+
attempt.evidence(outcome.launchLog);
|
|
1828
1887
|
// No window was created, so none of the not-connected copy is true here. See
|
|
1829
1888
|
// `editorLaunchFailedLines`.
|
|
1830
1889
|
launchSpinner?.fail(`Couldn't start ${editor.label}.`);
|
|
@@ -1834,11 +1893,18 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1834
1893
|
console.log(line);
|
|
1835
1894
|
return;
|
|
1836
1895
|
}
|
|
1896
|
+
attempt.evidence(outcome.launchLog);
|
|
1837
1897
|
if (outcome.verdict.kind === "connected") {
|
|
1898
|
+
attempt.finish("server_detected");
|
|
1838
1899
|
launchSpinner?.succeed(`${editor.label}'s server is running in your workspace.`);
|
|
1839
1900
|
skillImport.footer.push(...editorConnectedLines(editor, alias, verbose));
|
|
1901
|
+
if (editor.ide === "cursor" && opts.cursorAgentWindow) {
|
|
1902
|
+
const handoff = requestCursorAgentWindow(resolved.command, alias, REMOTE_WORKSPACE_DIR);
|
|
1903
|
+
skillImport.footer.push(...cursorAgentWindowLines(handoff, verbose));
|
|
1904
|
+
}
|
|
1840
1905
|
}
|
|
1841
1906
|
else if (outcome.verdict.kind === "unverified") {
|
|
1907
|
+
attempt.finish("unverified");
|
|
1842
1908
|
// Three-valued on purpose: we could not reach the workspace to ask, which is not the
|
|
1843
1909
|
// same as knowing nothing came up, and saying either of the other two here would be a
|
|
1844
1910
|
// claim we cannot make. See `verifyVerdict`.
|
|
@@ -1847,6 +1913,7 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1847
1913
|
console.log(line);
|
|
1848
1914
|
}
|
|
1849
1915
|
else {
|
|
1916
|
+
attempt.finish("not_connected");
|
|
1850
1917
|
launchSpinner?.fail(`${editor.label} hasn't connected to your workspace yet.`);
|
|
1851
1918
|
for (const line of editorNotConnectedLines(editor, alias, pageUrl, verbose))
|
|
1852
1919
|
console.log(line);
|