litmus-cli 1.4.46 → 1.4.48
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 +134 -232
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +352 -519
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +2 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/lib/connect-skills.d.ts +5 -1
- package/dist/lib/connect-skills.d.ts.map +1 -1
- package/dist/lib/connect-skills.js +6 -2
- package/dist/lib/connect-skills.js.map +1 -1
- package/dist/lib/cursor-restart.d.ts +1 -1
- package/dist/lib/cursor-restart.js +1 -1
- package/dist/lib/editor-server.d.ts +12 -4
- package/dist/lib/editor-server.d.ts.map +1 -1
- package/dist/lib/editor-server.js +30 -4
- package/dist/lib/editor-server.js.map +1 -1
- package/dist/lib/session-end.d.ts +5 -8
- package/dist/lib/session-end.d.ts.map +1 -1
- package/dist/lib/session-end.js +6 -12
- package/dist/lib/session-end.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/connect.js
CHANGED
|
@@ -16,7 +16,7 @@ import { parseEditorVersion, parseWorkspaceProbe, remoteProbeCommand, seedVerdic
|
|
|
16
16
|
import { fatal, success, info, warn, setErrorContext } from "../lib/errors.js";
|
|
17
17
|
import { oldWindowsSshHint, parseOpenSshVersion, proxyJumpBroken, readEditorSettings, readSshVersionBanner, sshExecutableFor, sshPreflight, sshPreflightHint, } from "../lib/ssh-client.js";
|
|
18
18
|
import { editorEndingForewarning } from "../lib/session-end.js";
|
|
19
|
-
import {
|
|
19
|
+
import { attemptQuit, attemptRelaunch, platformSupportsGracefulCursorRestart, realCursorProcessIo, } from "../lib/cursor-restart.js";
|
|
20
20
|
/**
|
|
21
21
|
* `litmus connect <token>` — the CLI connector for the v2 codespace, modeled on
|
|
22
22
|
* `gh codespace ssh/code`. It removes the manual "paste your public key + copy
|
|
@@ -371,7 +371,7 @@ export const NATIVE_EDITORS = {
|
|
|
371
371
|
downloadUrl: "https://cursor.com/download",
|
|
372
372
|
seedCommitPath: "/opt/cursor-server-cache/COMMIT",
|
|
373
373
|
serverHomeDir: ".cursor-server",
|
|
374
|
-
requiresSeedMatch:
|
|
374
|
+
requiresSeedMatch: false,
|
|
375
375
|
},
|
|
376
376
|
};
|
|
377
377
|
/**
|
|
@@ -492,344 +492,147 @@ export function identifyEditorCli(ed, command, run = runEditorHelp) {
|
|
|
492
492
|
return { kind: "unverified" };
|
|
493
493
|
return actual === ed.ide ? { kind: "match" } : { kind: "mismatch", actual };
|
|
494
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* ENG-2890 — the copy rules every connect outcome below follows.
|
|
497
|
+
*
|
|
498
|
+
* A hiring manager watched a candidate fail to open Cursor behind a ~41-line notice,
|
|
499
|
+
* quit and retry several times, and get nowhere. So each outcome is a few lines, the
|
|
500
|
+
* action first, with no mechanism words, and NO CREDENTIAL: the connect token and the
|
|
501
|
+
* browser-IDE URL (which carries `t=`) are never printed. The way back is "run the same
|
|
502
|
+
* litmus connect command again", which is in the candidate's shell history, and the
|
|
503
|
+
* assessment page, whose URL carries only the assessment id (`candidatePageUrl`).
|
|
504
|
+
*/
|
|
505
|
+
const SAME_COMMAND = "run the same litmus connect command";
|
|
506
|
+
/**
|
|
507
|
+
* The route that needs nothing else to work: the browser IDE, reached from the page,
|
|
508
|
+
* or the alias we have just written into their ~/.ssh/config.
|
|
509
|
+
*/
|
|
510
|
+
export function workNowLine(alias, pageUrl) {
|
|
511
|
+
const page = pageUrl
|
|
512
|
+
? `the browser IDE on your assessment page (${chalk.cyan(pageUrl)})`
|
|
513
|
+
: "the browser IDE on your assessment page";
|
|
514
|
+
return ` Or work now: ${page}, or ${chalk.cyan(`ssh ${alias}`)}.`;
|
|
515
|
+
}
|
|
516
|
+
/** Where the long form went. Not printed when it is already being shown. */
|
|
517
|
+
function detailsLine(verbose) {
|
|
518
|
+
return verbose ? [] : [chalk.dim(" Details: add --verbose.")];
|
|
519
|
+
}
|
|
495
520
|
/**
|
|
496
521
|
* ENG-2152 — the refusal, when the command the candidate's PATH answers with is the
|
|
497
|
-
* other editor.
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
* That ordering is the same one `announceCursorFullQuit` documents — what a candidate can
|
|
503
|
-
* act on immediately is what they read first.
|
|
504
|
-
*
|
|
505
|
-
* The offered command carries the token already inlined, for the reason `CursorReentry`
|
|
506
|
-
* gives: a `litmus connect <token>` a candidate has to repair themselves is worse than no
|
|
507
|
-
* command at all.
|
|
508
|
-
*
|
|
509
|
-
* When the editor that answered is Cursor, the quit requirement rides along. It is the
|
|
510
|
-
* headline only, not the whole ENG-1728 notice, because the command being offered prints
|
|
511
|
-
* that notice in full at selection time before anything opens — this is a FORWARD
|
|
512
|
-
* reference to output the candidate is about to see, not ENG-2145's back-reference to
|
|
513
|
-
* output that has scrolled away. Restating the whole block here would also have to
|
|
514
|
-
* restate its "this command is still running and will open Cursor for you", which on
|
|
515
|
-
* this path is false.
|
|
522
|
+
* other editor. Nothing has been opened and no extension installed when this prints.
|
|
523
|
+
*
|
|
524
|
+
* The editor that answered comes first: on a machine where `code` is Cursor, Cursor is
|
|
525
|
+
* almost certainly the daily driver and VS Code may not be installed at all. The PATH
|
|
526
|
+
* fix follows in one line, then the routes that need neither.
|
|
516
527
|
*/
|
|
517
|
-
export function editorMismatchLines(chosen, actual,
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
"",
|
|
522
|
-
|
|
523
|
-
` installed. Working in ${actual.label} while you believe you are in ${chosen.label} would`,
|
|
524
|
-
" leave your AI work unrecorded, so this stops here instead.",
|
|
525
|
-
"",
|
|
526
|
-
" Your workspace is up and waiting — nothing has been lost.",
|
|
527
|
-
"",
|
|
528
|
-
` To carry on in ${actual.label} — which is what \`${chosen.bin}\` actually is — run:`,
|
|
529
|
-
"",
|
|
530
|
-
` ${chalk.cyan(`${reentry.command} --ide ${actual.ide}`)}`,
|
|
528
|
+
export function editorMismatchLines(chosen, actual, alias, pageUrl) {
|
|
529
|
+
return [
|
|
530
|
+
chalk.yellow.bold(`⚠ The \`${chosen.bin}\` command on your PATH is ${actual.label}, not ${chosen.label}. Nothing was opened.`),
|
|
531
|
+
` To use ${actual.label}: ${SAME_COMMAND} with ${chalk.cyan(`--ide ${actual.ide}`)}.`,
|
|
532
|
+
` To use ${chosen.label}: in ${chosen.label}, run "${chosen.shellCommand}" from the Command Palette, open a new terminal, then ${SAME_COMMAND} again.`,
|
|
533
|
+
workNowLine(alias, pageUrl),
|
|
531
534
|
];
|
|
532
|
-
if (actual.ide === "cursor") {
|
|
533
|
-
lines.push("");
|
|
534
|
-
lines.push(chalk.yellow(" ⚠ Quit Cursor completely before you run that, or your work is not recorded."));
|
|
535
|
-
lines.push(" That command explains why, in full, before anything opens.");
|
|
536
|
-
}
|
|
537
|
-
lines.push("");
|
|
538
|
-
lines.push(` To carry on in ${chosen.label} instead, put its own \`${chosen.bin}\` command first on`);
|
|
539
|
-
lines.push(` your PATH: in ${chosen.label}, run "${chosen.shellCommand}" from the`);
|
|
540
|
-
lines.push(" Command Palette, then open a NEW terminal and check that");
|
|
541
|
-
lines.push(` \`${chosen.bin} --help\` names ${chosen.label}. Then run the same \`litmus connect\` command again.`);
|
|
542
|
-
lines.push("");
|
|
543
|
-
return lines;
|
|
544
535
|
}
|
|
545
536
|
/**
|
|
546
|
-
* ENG-2332 — the refusal when the candidate's editor is a different build from the one
|
|
547
|
-
* this workspace has
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
* ENG-2867 — but it must say the one thing that DOES change the verdict, and say it first.
|
|
554
|
-
* The image bakes the latest Stable build at image build time and Cursor ships about
|
|
555
|
-
* weekly, so the usual mismatch is a candidate one release behind, and updating is what
|
|
556
|
-
* clears it. The ENG-2332 cut never said so. A candidate who had just been told to quit
|
|
557
|
-
* Cursor read that as the remedy and re-ran the same refused command four times. So the
|
|
558
|
-
* order is: what to do, that retrying without it will not help, the exact command to run
|
|
559
|
-
* after, and only then why. The "why" is one sentence; it used to be the whole notice.
|
|
560
|
-
*
|
|
561
|
-
* Update is not guaranteed to land on the baked build: an image built before the newest
|
|
562
|
-
* release is refused again after updating, since `seedVerdict` wants an exact commit. So
|
|
563
|
-
* the copy says "usually", names that outcome as step 4, and the routes that need no
|
|
564
|
-
* update follow it (Greptile on #3033). Step 4 diagnoses nothing: the check compares
|
|
565
|
-
* commits, and a second refusal has more causes (an older image, a second install found
|
|
566
|
-
* first) than a notice can tell apart. The routes below always work, so step 4 sends the
|
|
567
|
-
* candidate there and states only the one fact the version line proves.
|
|
537
|
+
* ENG-2332 — the refusal when the candidate's editor is a different build from the one
|
|
538
|
+
* this workspace has.
|
|
539
|
+
*
|
|
540
|
+
* ENG-2884: no editor sets `requiresSeedMatch` any more, so this is reachable only if one
|
|
541
|
+
* is measured failing again and the flag goes back on. Kept short for that day: what
|
|
542
|
+
* happened, that updating usually clears it, and the routes that need no update. There is
|
|
543
|
+
* deliberately no "open it anyway".
|
|
568
544
|
*/
|
|
569
|
-
export function editorSeedMismatchLines(ed, verdict,
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
chalk.yellow.bold(` from the one this workspace was built for, so it cannot open it.`),
|
|
576
|
-
"",
|
|
577
|
-
` ${`Your ${ed.label}:`.padEnd(width)}${verdict.clientVersion ?? "unknown version"} (${short(verdict.clientCommit)})`,
|
|
578
|
-
` ${"This workspace:".padEnd(width)}${short(verdict.seedCommit)}`,
|
|
579
|
-
"",
|
|
580
|
-
chalk.bold(` Running this again, or quitting ${ed.label}, will not fix it. Updating usually does:`),
|
|
581
|
-
"",
|
|
582
|
-
` 1. Update ${ed.label}: use "Check for Updates" in ${ed.label}'s menu, or download it from`,
|
|
583
|
-
` ${chalk.cyan(ed.downloadUrl)}`,
|
|
584
|
-
` 2. Quit ${ed.label} completely (Cmd+Q on macOS; File → Exit on Windows/Linux).`,
|
|
585
|
-
` 3. From a terminal that is NOT inside ${ed.label}, run:`,
|
|
586
|
-
` ${chalk.cyan(`${reentry.command} --ide ${ed.ide}`)}`,
|
|
587
|
-
" 4. If it is refused again, use one of the routes below. (If \"Your " + ed.label + "\" still",
|
|
588
|
-
` shows the old version, this command is reading a different ${ed.label} install than`,
|
|
589
|
-
" the one you updated.)",
|
|
590
|
-
"",
|
|
591
|
-
chalk.dim(` (Why: ${ed.label} needs a matching server inside the workspace, and the workspace cannot`),
|
|
592
|
-
chalk.dim(" download one. A mismatched window looks normal, but your work in it is not recorded, so"),
|
|
593
|
-
chalk.dim(" nothing was opened. Your workspace is up and waiting, and nothing is lost.)"),
|
|
594
|
-
"",
|
|
595
|
-
` Refused after updating, or short on time? Work in the same workspace, fully recorded:`,
|
|
596
|
-
"",
|
|
545
|
+
export function editorSeedMismatchLines(ed, verdict, pageUrl, alias) {
|
|
546
|
+
const which = verdict.clientVersion ? `${ed.label} ${verdict.clientVersion}` : `This ${ed.label}`;
|
|
547
|
+
return [
|
|
548
|
+
chalk.yellow.bold(`⚠ ${which} can't open this workspace.`),
|
|
549
|
+
` Try updating ${ed.label} (${chalk.cyan(ed.downloadUrl)}), then ${SAME_COMMAND} again.`,
|
|
550
|
+
workNowLine(alias, pageUrl),
|
|
597
551
|
];
|
|
598
|
-
if (browserIdeUrl) {
|
|
599
|
-
lines.push(` Browser IDE: ${chalk.cyan(browserIdeUrl)}`);
|
|
600
|
-
}
|
|
601
|
-
else {
|
|
602
|
-
lines.push(" Browser IDE: open your assessment page and use the browser IDE there");
|
|
603
|
-
}
|
|
604
|
-
if (ed.ide !== "vscode") {
|
|
605
|
-
lines.push(` VS Code: ${chalk.cyan(`${reentry.command} --ide vscode`)}`);
|
|
606
|
-
}
|
|
607
|
-
lines.push(` Terminal: ${chalk.cyan(`ssh ${alias}`)}`);
|
|
608
|
-
lines.push("");
|
|
609
|
-
return lines;
|
|
610
552
|
}
|
|
611
553
|
/**
|
|
612
554
|
* ENG-2332 — what to say when the editor CLI cannot be found anywhere.
|
|
613
555
|
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
618
|
-
* IDE, which needs nothing installed at all.
|
|
556
|
+
* Never `<bin> --remote …`: that is the binary whose absence is the message. Everything
|
|
557
|
+
* here works without it: the editor's own Remote-SSH picker (the alias is already in
|
|
558
|
+
* their `~/.ssh/config`), the palette command that puts the CLI on PATH, and the routes
|
|
559
|
+
* that need no editor.
|
|
619
560
|
*/
|
|
620
|
-
export function editorNotFoundLines(ed, alias,
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
""
|
|
628
|
-
|
|
629
|
-
" with no command line at all:",
|
|
630
|
-
"",
|
|
631
|
-
` Command Palette → ${chalk.bold("Remote-SSH: Connect to Host…")} → ${chalk.bold(alias)}`,
|
|
632
|
-
"",
|
|
633
|
-
` (Needs the "${ed.remoteSshExt}" extension, which ${ed.label} will offer to install.)`,
|
|
634
|
-
"",
|
|
635
|
-
` To get the \`${ed.bin}\` command for next time: in ${ed.label}, Command Palette →`,
|
|
636
|
-
` "${ed.shellCommand}", then open a NEW terminal.`,
|
|
561
|
+
export function editorNotFoundLines(ed, alias, pageUrl) {
|
|
562
|
+
return [
|
|
563
|
+
chalk.yellow.bold(`⚠ Couldn't find ${ed.label}'s \`${ed.bin}\` command, so nothing was opened.`),
|
|
564
|
+
// Greptile on #3071: this route never restarts Cursor, and Cursor only loads the
|
|
565
|
+
// workspace's prompt recording when the app starts (gotcha 090).
|
|
566
|
+
...(ed.ide === "cursor" ? [" Quit Cursor completely and reopen it first, or your AI prompts won't be recorded."] : []),
|
|
567
|
+
` In ${ed.label}: Command Palette → ${chalk.bold("Remote-SSH: Connect to Host…")} → ${chalk.bold(alias)}`,
|
|
568
|
+
` To get the \`${ed.bin}\` command for next time: Command Palette → "${ed.shellCommand}".`,
|
|
569
|
+
workNowLine(alias, pageUrl),
|
|
637
570
|
];
|
|
638
|
-
if (browserIdeUrl) {
|
|
639
|
-
lines.push("");
|
|
640
|
-
lines.push(" Or skip the editor entirely — the browser IDE needs nothing installed:");
|
|
641
|
-
lines.push(` ${chalk.cyan(browserIdeUrl)}`);
|
|
642
|
-
}
|
|
643
|
-
lines.push("");
|
|
644
|
-
return lines;
|
|
645
571
|
}
|
|
646
572
|
/**
|
|
647
|
-
* ENG-2332 — the
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
* connected from an already-running Cursor has a live, correct, never-loaded hook, and
|
|
656
|
-
* their prompts are not recorded although everything else is. The verification above
|
|
657
|
-
* cannot see that — a server is up either way — so a flat "your work is being recorded"
|
|
658
|
-
* would be the same false reassurance this ticket is about, moved one lane along. The
|
|
659
|
-
* full notice is printed at selection time; this restates only the dependency.
|
|
573
|
+
* ENG-2332 — the footer under the verified success line.
|
|
574
|
+
*
|
|
575
|
+
* ENG-2890: for Cursor, one line naming the window that IS connected, since a restart
|
|
576
|
+
* can leave the candidate's other Cursor windows reopening beside it. The window title
|
|
577
|
+
* is safe to name here and only here: this prints after `verifyEditorAttached` saw a
|
|
578
|
+
* server, unlike the failure branches, where the never-attached window carries the same
|
|
579
|
+
* `[SSH: <alias>]` (ENG-2154). The workspace path is for whoever is debugging, so it is
|
|
580
|
+
* `--verbose` only. VS Code needs no footer.
|
|
660
581
|
*/
|
|
661
|
-
export function editorConnectedLines(ed, alias) {
|
|
662
|
-
const lines = [
|
|
582
|
+
export function editorConnectedLines(ed, alias, verbose = false, platform = process.platform) {
|
|
583
|
+
const lines = [];
|
|
584
|
+
if (verbose)
|
|
585
|
+
lines.push(chalk.dim(` Workspace: ${alias}:${REMOTE_WORKSPACE_DIR}`));
|
|
663
586
|
if (ed.ide === "cursor") {
|
|
664
|
-
|
|
665
|
-
lines.push(
|
|
587
|
+
const mod = platform === "darwin" ? "Cmd" : "Ctrl";
|
|
588
|
+
lines.push(` Use the Cursor window titled "workspace [SSH: ${alias}]". ${mod}+Shift+E shows your files.`);
|
|
666
589
|
}
|
|
667
590
|
return lines;
|
|
668
591
|
}
|
|
669
592
|
/**
|
|
670
|
-
* ENG-2332 —
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
* `
|
|
679
|
-
*
|
|
680
|
-
* first thing worth knowing, and it is already in hand.
|
|
681
|
-
*
|
|
682
|
-
* WHAT IT MAY NOT SAY IS THAT NOTHING WILL CHANGE. That is what it said first, and it is
|
|
683
|
-
* a claim about the future made on a reading that stops at the verification budget. A VS
|
|
684
|
-
* Code client whose build the image did not bake is NOT refused (`requiresSeedMatch`), so
|
|
685
|
-
* its launch legitimately goes on to fetch a server through the mirror — a ~28.6 MB CLI
|
|
686
|
-
* and, on a prewarm miss, the whole bundle — and a slow link can still be fetching when
|
|
687
|
-
* the budget runs out. Telling that candidate their window is dead and pointing them
|
|
688
|
-
* elsewhere is the harm `unverified` exists to avoid, arriving on the branch below it.
|
|
689
|
-
* So it states the bound it actually waited (derived from `VERIFY_SCHEDULE_MS`, never a
|
|
690
|
-
* second hard-coded copy), says the window may yet attach, and offers the other two
|
|
691
|
-
* routes rather than instructing an exit. What it still may not do is tell them to re-run
|
|
692
|
-
* this command, which is the original bug.
|
|
693
|
-
*
|
|
694
|
-
* AND IT MAY NOT HAND THEM A CHECK THAT PASSES ON THE DEAD WINDOW. It pointed at the
|
|
695
|
-
* remote indicator — "the bottom-left corner reads SSH: <alias>" — and this repo has
|
|
696
|
-
* measured that indicator PRESENT in exactly the failure case: `lib/editor-server.ts`'s
|
|
697
|
-
* header records the never-attached window carrying `"remoteAuthority":
|
|
698
|
-
* "ssh-remote+<alias>"` and being indistinguishable from a working one, and
|
|
699
|
-
* `lib/session-end.ts` records (ENG-2154) that a window whose transport is gone still
|
|
700
|
-
* reads `[SSH: <host>]`. A candidate told to look at the corner sees what they were told
|
|
701
|
-
* to look for and goes on working unrecorded, which is this ticket's own bug re-entering
|
|
702
|
-
* through its remediation. The honest sentence is that the two windows look the same —
|
|
703
|
-
* that is WHY the CLI checks — so the copy says so and names no visual test at all.
|
|
593
|
+
* ENG-2332 — a window opened and no server came up in the workspace, so that window is
|
|
594
|
+
* running against the candidate's own machine and records nothing.
|
|
595
|
+
*
|
|
596
|
+
* ENG-2890: four lines. Close the window, retry with the cause that usually explains it
|
|
597
|
+
* (an editor that stopped to ask for a sign-in or an update), or work elsewhere now.
|
|
598
|
+
*
|
|
599
|
+
* IT MAY NOT HAND THEM A CHECK THAT PASSES ON THE DEAD WINDOW. The remote indicator
|
|
600
|
+
* ("SSH: <alias>" in the corner) is PRESENT on the never-attached window
|
|
601
|
+
* (`lib/editor-server.ts`'s header; ENG-2154 in `lib/session-end.ts`), so no visual test
|
|
602
|
+
* is named here at all.
|
|
704
603
|
*/
|
|
705
|
-
export function editorNotConnectedLines(ed, alias,
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
chalk.yellow(" laptop, and it is not recorded."),
|
|
714
|
-
"",
|
|
715
|
-
` We watched your workspace for ${waited} seconds after opening it and nothing`,
|
|
716
|
-
` started there. A first-ever ${ed.label} connection can still be downloading its`,
|
|
717
|
-
" server at that point and attach a little later. There is nothing you can check",
|
|
718
|
-
" by eye: an attached window and an unattached one look exactly the same, which is",
|
|
719
|
-
" why this command checks your workspace instead of asking you to.",
|
|
720
|
-
"",
|
|
604
|
+
export function editorNotConnectedLines(ed, alias, pageUrl, verbose = false) {
|
|
605
|
+
return [
|
|
606
|
+
// Greptile on #3071: a first download of this build's server can outlast the check, so
|
|
607
|
+
// the window may still attach; it is not called dead.
|
|
608
|
+
` It may still be setting up. If the ${ed.label} window doesn't show your workspace files in a couple of minutes, close it: work in it isn't recorded.`,
|
|
609
|
+
` Then ${SAME_COMMAND} again. If ${ed.label} asked you to sign in or update, that's usually why.`,
|
|
610
|
+
workNowLine(alias, pageUrl),
|
|
611
|
+
...detailsLine(verbose),
|
|
721
612
|
];
|
|
722
|
-
if (seed.kind === "mismatch") {
|
|
723
|
-
lines.push(` The likely reason: your ${ed.label} is build ${seed.clientCommit.slice(0, 12)} and this`);
|
|
724
|
-
lines.push(` workspace holds ${seed.seedCommit.slice(0, 12)}. It could not fetch a matching one.`);
|
|
725
|
-
lines.push("");
|
|
726
|
-
}
|
|
727
|
-
if (browserIdeUrl) {
|
|
728
|
-
lines.push(" You can work in the browser IDE instead — same workspace, same files, fully");
|
|
729
|
-
lines.push(" recorded, and nothing to install:");
|
|
730
|
-
lines.push(` ${chalk.cyan(browserIdeUrl)}`);
|
|
731
|
-
}
|
|
732
|
-
else {
|
|
733
|
-
lines.push(" You can work in the browser IDE instead, from your assessment page — same");
|
|
734
|
-
lines.push(" workspace, same files, fully recorded.");
|
|
735
|
-
}
|
|
736
|
-
lines.push("");
|
|
737
|
-
lines.push(" You can also work from a terminal, which does not need the editor at all:");
|
|
738
|
-
lines.push(` ${chalk.cyan(`ssh ${alias}`)}`);
|
|
739
|
-
if (launchLog) {
|
|
740
|
-
lines.push("");
|
|
741
|
-
lines.push(chalk.dim(` ${ed.label} said, while trying to open:`));
|
|
742
|
-
for (const line of launchLog.split(/\r?\n/).slice(-8))
|
|
743
|
-
lines.push(chalk.dim(` ${line}`));
|
|
744
|
-
}
|
|
745
|
-
lines.push("");
|
|
746
|
-
return lines;
|
|
747
613
|
}
|
|
748
614
|
/**
|
|
749
|
-
* ENG-2332 —
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
* `
|
|
753
|
-
* running against the candidate's own machine; when `spawn` itself fails that sentence is
|
|
754
|
-
* false, and the reading the verification produces is identical either way (no server,
|
|
755
|
-
* empty launch log). So the spawn error is recorded (`EditorLaunchHandle`) and routed
|
|
756
|
-
* here instead, where the only claim made is the one we can support.
|
|
757
|
-
*
|
|
758
|
-
* It offers the same two routes and, like every other failure branch, offers rather than
|
|
759
|
-
* instructs: there is no window to close, and re-running this command is the remediation
|
|
760
|
-
* the ticket was opened about. The error text is carried verbatim because it is the whole
|
|
761
|
-
* of what we know — `ENOENT` on a resolved path means the app moved or was removed since
|
|
762
|
-
* the `--version` probe a moment earlier, which is a thing the candidate can act on and
|
|
763
|
-
* we cannot diagnose for them.
|
|
615
|
+
* ENG-2332 — the editor never started at all, so no window exists.
|
|
616
|
+
*
|
|
617
|
+
* Its own branch because `editorNotConnectedLines` tells them to close a window, which
|
|
618
|
+
* is false when `spawn` failed. The error is carried verbatim: it is all we know.
|
|
764
619
|
*/
|
|
765
|
-
export function editorLaunchFailedLines(ed, alias,
|
|
766
|
-
|
|
767
|
-
"",
|
|
768
|
-
chalk.yellow.bold(` ⚠ ${ed.label} could not be started, so no window was opened.`),
|
|
769
|
-
"",
|
|
770
|
-
` We found the ${ed.label} command and it answered a moment ago, but running it`,
|
|
771
|
-
" failed:",
|
|
772
|
-
` ${chalk.dim(error)}`,
|
|
773
|
-
"",
|
|
774
|
-
` That usually means ${ed.label} was moved, removed or updated between those two`,
|
|
775
|
-
" moments. Nothing has been opened and nothing is being recorded yet.",
|
|
776
|
-
"",
|
|
777
|
-
];
|
|
778
|
-
if (browserIdeUrl) {
|
|
779
|
-
lines.push(" You can work in the browser IDE instead — same workspace, same files, fully");
|
|
780
|
-
lines.push(" recorded, and nothing to install:");
|
|
781
|
-
lines.push(` ${chalk.cyan(browserIdeUrl)}`);
|
|
782
|
-
}
|
|
783
|
-
else {
|
|
784
|
-
lines.push(" You can work in the browser IDE instead, from your assessment page — same");
|
|
785
|
-
lines.push(" workspace, same files, fully recorded.");
|
|
786
|
-
}
|
|
787
|
-
lines.push("");
|
|
788
|
-
lines.push(" You can also work from a terminal, which does not need the editor at all:");
|
|
789
|
-
lines.push(` ${chalk.cyan(`ssh ${alias}`)}`);
|
|
790
|
-
if (launchLog) {
|
|
791
|
-
lines.push("");
|
|
792
|
-
lines.push(chalk.dim(` ${ed.label} said, while trying to open:`));
|
|
793
|
-
for (const line of launchLog.split(/\r?\n/).slice(-8))
|
|
794
|
-
lines.push(chalk.dim(` ${line}`));
|
|
795
|
-
}
|
|
796
|
-
lines.push("");
|
|
797
|
-
return lines;
|
|
620
|
+
export function editorLaunchFailedLines(ed, alias, pageUrl, error, verbose = false) {
|
|
621
|
+
return [` ${chalk.dim(error)}`, workNowLine(alias, pageUrl), ...detailsLine(verbose)];
|
|
798
622
|
}
|
|
799
623
|
/**
|
|
800
|
-
* ENG-2332 —
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
*
|
|
805
|
-
* — it is bound by the same rule as `editorNotConnectedLines` and broke it the same way.
|
|
806
|
-
*
|
|
807
|
-
* It may NOT tell the candidate to look at the remote indicator. That indicator is
|
|
808
|
-
* rendered from the remote authority, which the never-attached window carries (see
|
|
809
|
-
* `lib/editor-server.ts`'s header, and ENG-2154 in `lib/session-end.ts`), so the check
|
|
810
|
-
* passes in exactly the case it was offered to detect. It states the ambiguity instead
|
|
811
|
-
* and offers the two routes that are not in doubt.
|
|
624
|
+
* ENG-2332 — we could not reach the workspace to ask, which is not "nothing came up".
|
|
625
|
+
*
|
|
626
|
+
* It may NOT offer the remote indicator as a check (ENG-2154: the never-attached window
|
|
627
|
+
* carries it). The file explorer is a fair one: a window not attached to the workspace
|
|
628
|
+
* has no workspace files to list.
|
|
812
629
|
*/
|
|
813
|
-
export function editorUnverifiedLines(ed, alias,
|
|
814
|
-
|
|
815
|
-
`
|
|
816
|
-
"
|
|
817
|
-
|
|
818
|
-
"",
|
|
630
|
+
export function editorUnverifiedLines(ed, alias, pageUrl) {
|
|
631
|
+
return [
|
|
632
|
+
` If the ${ed.label} window shows your workspace files in its file explorer, you're all set.`,
|
|
633
|
+
" If it doesn't, close that window.",
|
|
634
|
+
workNowLine(alias, pageUrl),
|
|
819
635
|
];
|
|
820
|
-
if (browserIdeUrl) {
|
|
821
|
-
lines.push(" You can work in the browser IDE instead — same workspace, same files, fully");
|
|
822
|
-
lines.push(" recorded, and nothing to install:");
|
|
823
|
-
lines.push(` ${chalk.cyan(browserIdeUrl)}`);
|
|
824
|
-
}
|
|
825
|
-
else {
|
|
826
|
-
lines.push(" You can work in the browser IDE instead, from your assessment page — same");
|
|
827
|
-
lines.push(" workspace, same files, fully recorded.");
|
|
828
|
-
}
|
|
829
|
-
lines.push("");
|
|
830
|
-
lines.push(" You can also work from a terminal, which does not need the editor at all:");
|
|
831
|
-
lines.push(` ${chalk.cyan(`ssh ${alias}`)}`);
|
|
832
|
-
return lines;
|
|
833
636
|
}
|
|
834
637
|
/**
|
|
835
638
|
* Ensure the Remote-SSH extension is installed in the user's editor before we
|
|
@@ -976,10 +779,8 @@ export function launchEditor(ed, command, alias, logPath) {
|
|
|
976
779
|
return { launchFailure: () => failure };
|
|
977
780
|
}
|
|
978
781
|
/**
|
|
979
|
-
* Point at the editor's own output
|
|
980
|
-
*
|
|
981
|
-
* `--verbose` was asked for, since `editorLaunchLogPath` gives it a per-run suffix that
|
|
982
|
-
* nobody can guess unaided.
|
|
782
|
+
* Point at the editor's own output, under `--verbose` only (ENG-2890: the failure copy
|
|
783
|
+
* says "Details: add --verbose." instead of printing a path by default).
|
|
983
784
|
*
|
|
984
785
|
* A PATH IS NAMED ONLY WHEN THERE IS SOMETHING AT IT, and the reason is that the file
|
|
985
786
|
* routinely does not exist. `launchEditor` falls back to `stdio: "ignore"` whenever
|
|
@@ -989,15 +790,14 @@ export function launchEditor(ed, command, alias, logPath) {
|
|
|
989
790
|
* Handing a support person a path with nothing behind it reads as the CLI having lost
|
|
990
791
|
* the log, which is a worse answer than saying plainly that the editor wrote nothing.
|
|
991
792
|
*/
|
|
992
|
-
export function editorLaunchOutputLines(logPath, launchLog, verbose
|
|
993
|
-
if (!
|
|
793
|
+
export function editorLaunchOutputLines(logPath, launchLog, verbose) {
|
|
794
|
+
if (!verbose)
|
|
994
795
|
return [];
|
|
995
796
|
if (!launchLog)
|
|
996
797
|
return [chalk.dim(" The editor wrote no output while opening.")];
|
|
997
798
|
const lines = [chalk.dim(` Editor output: ${logPath}`)];
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
lines.push(chalk.dim(` ${line}`));
|
|
799
|
+
for (const line of launchLog.split(/\r?\n/))
|
|
800
|
+
lines.push(chalk.dim(` ${line}`));
|
|
1001
801
|
return lines;
|
|
1002
802
|
}
|
|
1003
803
|
/** What the editor wrote while trying to open, or null when it said nothing. */
|
|
@@ -1023,8 +823,8 @@ function sshProbeRunner(alias) {
|
|
|
1023
823
|
}
|
|
1024
824
|
};
|
|
1025
825
|
}
|
|
1026
|
-
function probeWorkspace(ed, run) {
|
|
1027
|
-
return parseWorkspaceProbe(run(remoteProbeCommand(ed.serverHomeDir, ed.seedCommitPath)));
|
|
826
|
+
function probeWorkspace(ed, run, clientCommit = null) {
|
|
827
|
+
return parseWorkspaceProbe(run(remoteProbeCommand(ed.serverHomeDir, ed.seedCommitPath, clientCommit)));
|
|
1028
828
|
}
|
|
1029
829
|
/**
|
|
1030
830
|
* ENG-2332 — poll the workspace until a server appears, or the budget runs out.
|
|
@@ -1052,7 +852,7 @@ function probeWorkspace(ed, run) {
|
|
|
1052
852
|
* than taken whole.
|
|
1053
853
|
*/
|
|
1054
854
|
const SPAWN_ERROR_POLL_MS = 200;
|
|
1055
|
-
export async function verifyEditorAttached(ed, baseline, run, onWait, launched) {
|
|
855
|
+
export async function verifyEditorAttached(ed, baseline, run, onWait, launched, clientCommit = null) {
|
|
1056
856
|
const started = Date.now();
|
|
1057
857
|
const budget = VERIFY_SCHEDULE_MS[VERIFY_SCHEDULE_MS.length - 1];
|
|
1058
858
|
let sawNotConnected = false;
|
|
@@ -1074,7 +874,7 @@ export async function verifyEditorAttached(ed, baseline, run, onWait, launched)
|
|
|
1074
874
|
if (launched.launchFailure() !== null)
|
|
1075
875
|
return { kind: "unverified" };
|
|
1076
876
|
onWait(Math.round((Date.now() - started) / 1000));
|
|
1077
|
-
const current = probeWorkspace(ed, run);
|
|
877
|
+
const current = probeWorkspace(ed, run, clientCommit);
|
|
1078
878
|
const verdict = verifyVerdict(baseline, current);
|
|
1079
879
|
if (verdict.kind === "connected")
|
|
1080
880
|
return verdict;
|
|
@@ -1112,20 +912,15 @@ export async function attemptEditorLaunch(ed, io) {
|
|
|
1112
912
|
* (piped / CI), so scripted runs never block on the prompt.
|
|
1113
913
|
*
|
|
1114
914
|
* `log` is injectable ONLY so that the Cursor notice below is testable through the real
|
|
1115
|
-
* selection path rather than by reading a string constant
|
|
1116
|
-
*
|
|
1117
|
-
* it was printed is not a test of it.
|
|
1118
|
-
*
|
|
1119
|
-
* `reentry` is the way back in, and it is threaded from `runConnect` rather than
|
|
1120
|
-
* rebuilt here because only the caller holds the token and the resolved base. When it
|
|
1121
|
-
* is absent (a test, a scripted call) the notice degrades to the placeholder form
|
|
1122
|
-
* rather than dropping the route — see `cursorReentryLines`.
|
|
915
|
+
* selection path rather than by reading a string constant, and `platform` so that both
|
|
916
|
+
* of its forms are (see `cursorSelectionNoticeLines`).
|
|
1123
917
|
*/
|
|
1124
|
-
export async function resolveIde(flag, log = console.log,
|
|
918
|
+
export async function resolveIde(flag, log = console.log, platform = process.platform) {
|
|
1125
919
|
const norm = (flag ?? "").trim().toLowerCase();
|
|
1126
920
|
if (norm === "vscode" || norm === "cursor" || norm === "browser" || norm === "ssh") {
|
|
1127
921
|
if (norm === "cursor")
|
|
1128
|
-
|
|
922
|
+
for (const line of cursorSelectionNoticeLines(platform))
|
|
923
|
+
log(line);
|
|
1129
924
|
return { ide: norm, chosen: true };
|
|
1130
925
|
}
|
|
1131
926
|
if (norm)
|
|
@@ -1134,10 +929,10 @@ export async function resolveIde(flag, log = console.log, reentry) {
|
|
|
1134
929
|
return { ide: "vscode", chosen: false };
|
|
1135
930
|
const answered = await promptIdeAnswer();
|
|
1136
931
|
const ide = ideForChoice(answered);
|
|
1137
|
-
// At SELECTION time — not after the ~1-2 minute boot wait,
|
|
1138
|
-
// "opening Cursor" line where it scrolls past. See `announceCursorFullQuit`.
|
|
932
|
+
// At SELECTION time — not after the ~1-2 minute boot wait, where it scrolls past.
|
|
1139
933
|
if (ide === "cursor")
|
|
1140
|
-
|
|
934
|
+
for (const line of cursorSelectionNoticeLines(platform))
|
|
935
|
+
log(line);
|
|
1141
936
|
return { ide, chosen: answered.trim() !== "" };
|
|
1142
937
|
}
|
|
1143
938
|
// How long `litmus connect` waits for a started workspace to become reachable.
|
|
@@ -1185,99 +980,105 @@ export function keepWaitingForWorkspace(elapsedMs, status) {
|
|
|
1185
980
|
export function candidatePageUrl(apiBase, assessmentId) {
|
|
1186
981
|
return `${apiBase}/candidate/${assessmentId}`;
|
|
1187
982
|
}
|
|
1188
|
-
/** The placeholder form, for a call that does not know the token (tests, scripted runs). */
|
|
1189
|
-
const CURSOR_REENTRY_FALLBACK = { command: "litmus connect <token>", pageUrl: null };
|
|
1190
983
|
/**
|
|
1191
|
-
* ENG-
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1194
|
-
*
|
|
1195
|
-
*
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1199
|
-
*
|
|
1200
|
-
*
|
|
1201
|
-
* on screen is a quit editor, no running command, and nothing anywhere naming the way
|
|
1202
|
-
* back. On the clock, that reads as the workspace being gone.
|
|
1203
|
-
*
|
|
1204
|
-
* So the route is stated as an instruction rather than as a promise about a process that
|
|
1205
|
-
* may no longer exist, and the terminal it must be run from is named: a candidate who
|
|
1206
|
-
* has just been told to quit an application cannot be told to use that application's
|
|
1207
|
-
* terminal. The assessment page comes second and is the one that survives closing the
|
|
1208
|
-
* tab AND losing the terminal, which is the state Elena was actually in.
|
|
1209
|
-
*
|
|
1210
|
-
* Both routes reopen the SAME workspace — `litmus connect` re-registers this machine's
|
|
1211
|
-
* key and wakes a parked container through the ENG-2005 machinery, and the portal's own
|
|
1212
|
-
* card does the same — so the reassurance is a fact about the platform, not a hope.
|
|
984
|
+
* ENG-1950 — the one thing a Cursor candidate has to be told when they pick Cursor.
|
|
985
|
+
*
|
|
986
|
+
* Cursor reads its prompt-recording setup once, at application startup (ENG-1728,
|
|
987
|
+
* measured), so a Cursor that is already open when the workspace opens records no AI
|
|
988
|
+
* prompts while everything else works. ENG-2890: on macOS connect quits and reopens
|
|
989
|
+
* Cursor itself (`lib/cursor-restart.ts`), so nothing is said here and the restart step
|
|
990
|
+
* speaks when it acts. Elsewhere a graceful quit cannot be confirmed, so the candidate is
|
|
991
|
+
* asked to do it, in two lines: the 23-line notice this replaced was the wall the
|
|
992
|
+
* ENG-2890 candidate read past. The second line exists because quitting Cursor also
|
|
993
|
+
* closes its integrated terminal, and with it this command (ENG-2145).
|
|
1213
994
|
*/
|
|
1214
|
-
export function
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
"",
|
|
1219
|
-
|
|
1220
|
-
"",
|
|
1221
|
-
" That reopens this same workspace, with your work exactly as you left it.",
|
|
995
|
+
export function cursorSelectionNoticeLines(platform = process.platform) {
|
|
996
|
+
if (platformSupportsGracefulCursorRestart(platform))
|
|
997
|
+
return [];
|
|
998
|
+
return [
|
|
999
|
+
" Before Cursor opens, quit it completely (File → Exit), or your AI prompts won't be recorded.",
|
|
1000
|
+
" If this terminal is inside Cursor, run the same litmus connect command from another terminal.",
|
|
1222
1001
|
];
|
|
1223
|
-
// Deliberately NOT "use the button there". The portal's one-click reopen is the
|
|
1224
|
-
// browser-IDE link, and `browser_ide_url` is null wherever `WORKSPACE_IDE_DOMAIN`
|
|
1225
|
-
// is unset — those candidates get the same command in a copy field instead. Naming
|
|
1226
|
-
// a control a candidate then cannot find costs them the trust they need in the rest
|
|
1227
|
-
// of this notice, which is the same line `workspace-guidance.ts` draws over the tool
|
|
1228
|
-
// list. What is true on every deployment is that the page states the route.
|
|
1229
|
-
if (reentry.pageUrl) {
|
|
1230
|
-
lines.push(" Or open your assessment page, which shows the way back in:");
|
|
1231
|
-
lines.push(` ${chalk.cyan(reentry.pageUrl)}`);
|
|
1232
|
-
}
|
|
1233
|
-
else {
|
|
1234
|
-
lines.push(" Your assessment page shows the way back in too.");
|
|
1235
|
-
}
|
|
1236
|
-
return lines;
|
|
1237
1002
|
}
|
|
1238
1003
|
/**
|
|
1239
|
-
* ENG-
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1246
|
-
* present, correct, and never loaded.
|
|
1247
|
-
*
|
|
1248
|
-
* That failure is entirely silent. The editor connects, the workspace works, files save,
|
|
1249
|
-
* the AI answers — and nothing is recorded, so their Process dimension is graded on an
|
|
1250
|
-
* empty record. It cost about an hour of investigation on 2026-08-28 to diagnose from
|
|
1251
|
-
* the inside, with the container in hand; a candidate has no chance of noticing at all.
|
|
1252
|
-
*
|
|
1253
|
-
* So the copy is written for a candidate, not for us: what to do, and what it costs if
|
|
1254
|
-
* they don't. No "hooks", no "capture lane". The mechanism is our problem.
|
|
1255
|
-
*
|
|
1256
|
-
* ENG-2145 adds the second half of that sentence: what to do is now "quit, THEN run
|
|
1257
|
-
* this", because an instruction to quit with no stated way back is what sent two people
|
|
1258
|
-
* who know the product looking for their workspace. The order is deliberate — the cost
|
|
1259
|
-
* of skipping the quit still comes last, so the thing a candidate acts on immediately is
|
|
1260
|
-
* the thing they read first, and `cursorReentryLines` sits between them rather than
|
|
1261
|
-
* after the whole notice where it would scroll.
|
|
1004
|
+
* ENG-2890 — is this process running in Cursor's integrated terminal?
|
|
1005
|
+
*
|
|
1006
|
+
* Quitting Cursor kills its terminals, so the macOS auto-restart would kill this CLI
|
|
1007
|
+
* between the quit and the relaunch, leaving no editor and no command. `TERM_PROGRAM`
|
|
1008
|
+
* is "vscode" in VS Code AND in Cursor's terminal, so one of Cursor's own markers is
|
|
1009
|
+
* required as well: its trace id, its ToDesktop bundle id, or its bundled askpass node.
|
|
1010
|
+
* VS Code's terminal (bundle id `com.microsoft.VSCode`, no trace id) is not a match.
|
|
1262
1011
|
*/
|
|
1263
|
-
export function
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1012
|
+
export function runningInsideCursorTerminal(env = process.env) {
|
|
1013
|
+
if (env.TERM_PROGRAM !== "vscode")
|
|
1014
|
+
return false;
|
|
1015
|
+
return (Boolean(env.CURSOR_TRACE_ID) ||
|
|
1016
|
+
(env.__CFBundleIdentifier ?? "").includes("todesktop") ||
|
|
1017
|
+
(env.VSCODE_GIT_ASKPASS_NODE ?? "").includes("Cursor"));
|
|
1018
|
+
}
|
|
1019
|
+
/** ENG-2890 — said instead of quitting Cursor out from under this terminal. */
|
|
1020
|
+
export function insideCursorTerminalLines() {
|
|
1021
|
+
return [
|
|
1022
|
+
chalk.yellow.bold("⚠ This terminal is inside Cursor, and restarting Cursor would close it."),
|
|
1023
|
+
" Run the same litmus connect command from Terminal or iTerm instead.",
|
|
1024
|
+
];
|
|
1025
|
+
}
|
|
1026
|
+
/** ENG-2890 — the one line connect prints before its macOS auto-restart of Cursor. */
|
|
1027
|
+
/** Cursor success when this command could not confirm a fresh start (gotcha 090). */
|
|
1028
|
+
export const CURSOR_RECORDING_CAVEAT_LINE = " Your AI prompts are recorded only if Cursor was fully quit before it opened. If it wasn't, quit it and run the same litmus connect command again.";
|
|
1029
|
+
/** How long a freshly started Cursor gets to finish restoring its session (ENG-2884). */
|
|
1030
|
+
export const CURSOR_SETTLE_MS = 6000;
|
|
1031
|
+
/** When, into the attach wait, the open request is sent to Cursor a second time. */
|
|
1032
|
+
export const CURSOR_RESEND_AFTER_S = 20;
|
|
1033
|
+
/** Whether to send the workspace-open request again (once, Cursor only, after a fresh start). */
|
|
1034
|
+
export function shouldResendCursorOpen(ide, startedFresh, waitedSeconds, resend) {
|
|
1035
|
+
return ide === "cursor" && startedFresh && !resend.done && waitedSeconds >= CURSOR_RESEND_AFTER_S;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* ENG-2884 — the launch when the open request may be sent a second time. Every `send`
|
|
1039
|
+
* answers ONE handle for all the requests so far. It reports a failure only once EVERY
|
|
1040
|
+
* request has failed: a failed retry must not end the wait while the first request may
|
|
1041
|
+
* still be attaching (Greptile on #3078). A retry's failure is kept in `readLog` instead,
|
|
1042
|
+
* beside every request's output, so `--verbose` still shows why it failed.
|
|
1043
|
+
*/
|
|
1044
|
+
export function resendableLaunch(launch) {
|
|
1045
|
+
const sent = [];
|
|
1046
|
+
const handle = {
|
|
1047
|
+
launchFailure: () => {
|
|
1048
|
+
const failures = sent.map((s) => s.handle.launchFailure());
|
|
1049
|
+
return failures.length > 0 && failures.every((f) => f !== null) ? failures[failures.length - 1] : null;
|
|
1050
|
+
},
|
|
1051
|
+
};
|
|
1052
|
+
return {
|
|
1053
|
+
send: (logPath) => {
|
|
1054
|
+
sent.push({ logPath, handle: launch(logPath) });
|
|
1055
|
+
return handle;
|
|
1056
|
+
},
|
|
1057
|
+
readLog: (read) => {
|
|
1058
|
+
const logs = [];
|
|
1059
|
+
sent.forEach((s, i) => {
|
|
1060
|
+
const log = read(s.logPath);
|
|
1061
|
+
if (log !== null)
|
|
1062
|
+
logs.push(log);
|
|
1063
|
+
const failure = i > 0 ? s.handle.launchFailure() : null;
|
|
1064
|
+
if (failure !== null)
|
|
1065
|
+
logs.push(`Second open request failed: ${failure}`);
|
|
1066
|
+
});
|
|
1067
|
+
return logs.length > 0 ? logs.join("\n") : null;
|
|
1068
|
+
},
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
export const CURSOR_RESTARTING_LINE = "Restarting Cursor so your AI prompts are recorded (it reopens on its own)...";
|
|
1072
|
+
/**
|
|
1073
|
+
* ENG-2890 — the quit was asked for and Cursor is still running, so nothing is
|
|
1074
|
+
* launched (see the gate in `connectFlow`). `renderCursorRestartOutcome`'s longer form
|
|
1075
|
+
* stays with init and doctor.
|
|
1076
|
+
*/
|
|
1077
|
+
export function cursorQuitUnconfirmedLines() {
|
|
1078
|
+
return [
|
|
1079
|
+
chalk.red.bold("✖ Cursor didn't quit. It may be waiting on a \"Save changes?\" dialog."),
|
|
1080
|
+
" Quit Cursor yourself, then run the same litmus connect command again from Terminal or iTerm.",
|
|
1081
|
+
];
|
|
1281
1082
|
}
|
|
1282
1083
|
/**
|
|
1283
1084
|
* Map a menu answer to an editor. (ENG-1667 item 3; Cursor added by ENG-1950.)
|
|
@@ -1316,7 +1117,7 @@ export function ideMenuLines() {
|
|
|
1316
1117
|
` ${chalk.bold("2")}) VS Code — native, via Remote-SSH`,
|
|
1317
1118
|
// ENG-2389. Cursor sits beside VS Code, where it belongs by kinship, and plain SSH is
|
|
1318
1119
|
// last — see `ideForChoice` for what that renumbering costs and why it is bounded.
|
|
1319
|
-
` ${chalk.bold("3")}) Cursor — native, via Remote-SSH
|
|
1120
|
+
` ${chalk.bold("3")}) Cursor — native, via Remote-SSH`,
|
|
1320
1121
|
` ${chalk.bold("4")}) SSH — just print the command for your terminal`,
|
|
1321
1122
|
];
|
|
1322
1123
|
}
|
|
@@ -1403,24 +1204,33 @@ function promptIdeAnswer() {
|
|
|
1403
1204
|
});
|
|
1404
1205
|
});
|
|
1405
1206
|
}
|
|
1406
|
-
/**
|
|
1207
|
+
/**
|
|
1208
|
+
* Open a URL in the user's default browser. Best-effort, cross-platform. Resolves
|
|
1209
|
+
* whether the opener started, so the caller prints its fallback in order rather than
|
|
1210
|
+
* after the success line, and never prints the URL itself: the browser-IDE URL carries
|
|
1211
|
+
* the `t=` credential (ENG-2890).
|
|
1212
|
+
*/
|
|
1407
1213
|
function openBrowser(url) {
|
|
1408
1214
|
const spec = process.platform === "darwin" ? ["open", [url]]
|
|
1409
1215
|
: process.platform === "win32" ? ["cmd", ["/c", "start", "", url]]
|
|
1410
1216
|
: ["xdg-open", [url]];
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1217
|
+
return new Promise((resolve) => {
|
|
1218
|
+
try {
|
|
1219
|
+
const child = spawn(spec[0], spec[1], { detached: true, stdio: "ignore" });
|
|
1220
|
+
// spawn reports a missing opener ASYNCHRONOUSLY via an 'error' event, not a
|
|
1221
|
+
// sync throw — without this listener an ENOENT (e.g. no xdg-open on a minimal
|
|
1222
|
+
// Linux / WSL / headless box) becomes an uncaughtException that crashes the CLI.
|
|
1223
|
+
child.on("error", () => resolve(false));
|
|
1224
|
+
child.on("spawn", () => resolve(true));
|
|
1225
|
+
child.unref();
|
|
1226
|
+
// Never let the browser opener hold connect up: if neither event arrives, the
|
|
1227
|
+
// launch is assumed to have gone ahead, which is how connect behaved before ENG-2890.
|
|
1228
|
+
setTimeout(() => resolve(true), 3000).unref();
|
|
1229
|
+
}
|
|
1230
|
+
catch {
|
|
1231
|
+
resolve(false);
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1424
1234
|
}
|
|
1425
1235
|
/**
|
|
1426
1236
|
* Drop any stale known_hosts entries for this workspace's target + bastion.
|
|
@@ -1501,14 +1311,10 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1501
1311
|
// Pick the editor up front (flag or interactive prompt) so the choice is made
|
|
1502
1312
|
// before the boot wait, not after.
|
|
1503
1313
|
//
|
|
1504
|
-
// ENG-
|
|
1505
|
-
//
|
|
1506
|
-
|
|
1507
|
-
const
|
|
1508
|
-
command: `litmus connect ${token}`,
|
|
1509
|
-
pageUrl: candidatePageUrl(apiBase, meta.assessmentId),
|
|
1510
|
-
};
|
|
1511
|
-
const { ide: chosenIde, chosen: ideWasChosen } = await resolveIde(ideOpt, console.log, reentry);
|
|
1314
|
+
// ENG-2890 — the assessment page is the one re-entry route connect prints: its URL
|
|
1315
|
+
// carries only the assessment id, never the token.
|
|
1316
|
+
const pageUrl = candidatePageUrl(apiBase, meta.assessmentId);
|
|
1317
|
+
const { ide: chosenIde, chosen: ideWasChosen } = await resolveIde(ideOpt, console.log);
|
|
1512
1318
|
let ide = chosenIde;
|
|
1513
1319
|
// ENG-2814: asked here, next to the editor choice, and uploaded once the workspace is
|
|
1514
1320
|
// ready. Never fails the connect. See `lib/connect-skills.ts`.
|
|
@@ -1578,7 +1384,7 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1578
1384
|
}
|
|
1579
1385
|
boot.succeed("Workspace ready");
|
|
1580
1386
|
if (pendingSkills)
|
|
1581
|
-
skillImport.report = startSkillImport(backendUrl, token, pendingSkills);
|
|
1387
|
+
skillImport.report = startSkillImport(backendUrl, token, pendingSkills, undefined, () => ide);
|
|
1582
1388
|
// Browser IDE: no local SSH / VS Code needed — just open the signed URL.
|
|
1583
1389
|
if (ide === "browser") {
|
|
1584
1390
|
// Greptile on #1713. The browser IDE is now the DEFAULT, so this branch is reached by
|
|
@@ -1596,14 +1402,20 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1596
1402
|
ide = "vscode";
|
|
1597
1403
|
}
|
|
1598
1404
|
else if (fallback === "unavailable") {
|
|
1599
|
-
fatal("The browser IDE isn't available for this workspace yet.", "Open it in native VS Code instead: `litmus connect
|
|
1405
|
+
fatal("The browser IDE isn't available for this workspace yet.", "Open it in native VS Code instead: run the same `litmus connect` command with `--ide vscode`.", { severity: "warning" });
|
|
1600
1406
|
return;
|
|
1601
1407
|
}
|
|
1602
1408
|
}
|
|
1603
1409
|
if (ide === "browser" && conn.browser_ide_url) {
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1410
|
+
// ENG-2890 — the URL carries the `t=` credential, so it is opened, never printed,
|
|
1411
|
+
// except under --verbose for whoever is debugging the opener.
|
|
1412
|
+
// `openBrowser` resolving true means the opener started, not that a browser showed the
|
|
1413
|
+
// page (xdg-open can exit with no browser at all), so the fallback is always given.
|
|
1414
|
+
await openBrowser(conn.browser_ide_url);
|
|
1415
|
+
success("✔ Opening your workspace in the browser");
|
|
1416
|
+
console.log(` If nothing opens, use the browser IDE link on your assessment page: ${chalk.cyan(pageUrl)}`);
|
|
1417
|
+
if (verbose)
|
|
1418
|
+
console.log(chalk.dim(` ${conn.browser_ide_url}`));
|
|
1607
1419
|
return;
|
|
1608
1420
|
}
|
|
1609
1421
|
if (!conn.ssh_command) {
|
|
@@ -1634,7 +1446,8 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1634
1446
|
pruneStaleHostKeys(parsed);
|
|
1635
1447
|
const block = buildSshConfigBlock(alias, KEY_PATH, parsed);
|
|
1636
1448
|
const cfgPath = writeSshConfig(alias, block, buildControlPathBlock(alias, parsed));
|
|
1637
|
-
|
|
1449
|
+
if (verbose)
|
|
1450
|
+
info(`SSH config written to ${cfgPath} (Host "${alias}")`);
|
|
1638
1451
|
// ENG-2693 — prove the connection before handing it off (see `sshPreflight`). The
|
|
1639
1452
|
// client is the one the route will run, as in ENG-2682. A failure stops the route with
|
|
1640
1453
|
// ssh's own words, rather than in an editor that fails the same way silently — and it
|
|
@@ -1745,8 +1558,8 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1745
1558
|
// no-op that touches nothing.
|
|
1746
1559
|
//
|
|
1747
1560
|
// Cursor is still given no laptop-side equivalent of the WRITE: its AI is captured
|
|
1748
|
-
// in-container, which is
|
|
1749
|
-
// protects.
|
|
1561
|
+
// in-container, which is what the Cursor restart (or, off macOS, the quit note at
|
|
1562
|
+
// selection time) protects.
|
|
1750
1563
|
//
|
|
1751
1564
|
// ENG-2152 — a MISMATCH refuses below and opens nothing, so it may not WRITE. See
|
|
1752
1565
|
// `chatConfigActionForIdentity`, which is where that belongs: keying the write on
|
|
@@ -1791,7 +1604,7 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1791
1604
|
// manual-instructions branch. Checking the mismatch first means a Windows candidate
|
|
1792
1605
|
// whose `code` is Cursor is told so, rather than told their editor is not installed.
|
|
1793
1606
|
if (identity.kind === "mismatch") {
|
|
1794
|
-
for (const line of editorMismatchLines(editor, NATIVE_EDITORS[identity.actual],
|
|
1607
|
+
for (const line of editorMismatchLines(editor, NATIVE_EDITORS[identity.actual], alias, pageUrl)) {
|
|
1795
1608
|
console.log(line);
|
|
1796
1609
|
}
|
|
1797
1610
|
return;
|
|
@@ -1800,22 +1613,13 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1800
1613
|
// ENG-2332 — the CLI is not on PATH and not where the installer puts it. Everything
|
|
1801
1614
|
// offered here is reachable WITHOUT it; the line this replaced offered
|
|
1802
1615
|
// `<bin> --remote …`, i.e. the binary whose absence is the message.
|
|
1803
|
-
for (const line of editorNotFoundLines(editor, alias,
|
|
1616
|
+
for (const line of editorNotFoundLines(editor, alias, pageUrl))
|
|
1804
1617
|
console.log(line);
|
|
1805
|
-
//
|
|
1806
|
-
// here
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
// ENG-2145: and so does the way back. This used to point at "the note above", which
|
|
1810
|
-
// is a back-reference to a notice printed BEFORE a boot wait that can run to two
|
|
1811
|
-
// minutes and fills the screen — and, worse, one whose own route back was a promise
|
|
1812
|
-
// about this very command, which by then has returned. So the route is restated
|
|
1813
|
-
// here in full rather than referred to.
|
|
1814
|
-
if (ide === "cursor") {
|
|
1815
|
-
console.log(chalk.yellow(" Quit Cursor completely before you connect, or your work is not recorded."));
|
|
1816
|
-
for (const line of cursorReentryLines(reentry))
|
|
1618
|
+
// A candidate connecting by hand does it from a Cursor they still have to quit, and
|
|
1619
|
+
// nothing here restarts it, so the selection note is restated (empty on macOS).
|
|
1620
|
+
if (ide === "cursor")
|
|
1621
|
+
for (const line of cursorSelectionNoticeLines())
|
|
1817
1622
|
console.log(line);
|
|
1818
|
-
}
|
|
1819
1623
|
return;
|
|
1820
1624
|
}
|
|
1821
1625
|
if (identity.kind === "unverified") {
|
|
@@ -1828,9 +1632,10 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1828
1632
|
// printing this first produced "opening it as you asked" immediately followed by
|
|
1829
1633
|
// "not found on PATH" — two lines contradicting each other about whether anything was
|
|
1830
1634
|
// about to open, which is on the ticket in its own right.
|
|
1831
|
-
|
|
1635
|
+
if (verbose)
|
|
1636
|
+
console.log(chalk.dim(` (Couldn't confirm that \`${editor.bin}\` is ${editor.label}; opening it as you asked.)`));
|
|
1832
1637
|
}
|
|
1833
|
-
if (resolved.source === "install-location") {
|
|
1638
|
+
if (verbose && resolved.source === "install-location") {
|
|
1834
1639
|
info(`Using ${editor.label} at ${resolved.command}`);
|
|
1835
1640
|
console.log(chalk.dim(` (\`${editor.bin}\` isn't on your PATH. ${editor.label}'s "${editor.shellCommand}" adds it.)`));
|
|
1836
1641
|
}
|
|
@@ -1841,9 +1646,9 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1841
1646
|
const probe = sshProbeRunner(alias);
|
|
1842
1647
|
// ENG-2491, Greptile P1 on #2613 — the baseline probe is taken HERE, before
|
|
1843
1648
|
// any decision to quit Cursor, and the resulting `seedVerdict` is checked
|
|
1844
|
-
// before quitting too. `attemptEditorLaunch` below
|
|
1845
|
-
//
|
|
1846
|
-
//
|
|
1649
|
+
// before quitting too. `attemptEditorLaunch` below REFUSES the launch on a
|
|
1650
|
+
// mismatched build for any editor with `requiresSeedMatch` set (none today,
|
|
1651
|
+
// ENG-2884), and a refusal opens nothing — so a quit sequenced
|
|
1847
1652
|
// ahead of that check could close every Cursor window on a path that was
|
|
1848
1653
|
// always going to end in "nothing was opened", with no way given back in
|
|
1849
1654
|
// locally. Checking the identical rule first means a quit only ever
|
|
@@ -1854,20 +1659,22 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1854
1659
|
// two, for the same reason this file already takes it once for the seed
|
|
1855
1660
|
// check and the verification baseline together.
|
|
1856
1661
|
const readSpinner = ora(`Checking your workspace...`).start();
|
|
1857
|
-
const baseline = probeWorkspace(editor, probe);
|
|
1662
|
+
const baseline = probeWorkspace(editor, probe, resolved.version.commit);
|
|
1858
1663
|
readSpinner.stop();
|
|
1859
1664
|
const seed = seedVerdict(resolved.version, baseline);
|
|
1860
1665
|
const seedWillRefuseLaunch = seed.kind === "mismatch" && editor.requiresSeedMatch;
|
|
1861
1666
|
// ENG-2491 — Cursor reads its hooks configuration once, at its OWN launch,
|
|
1862
1667
|
// no matter which remote window it then opens (ENG-1728, measured) — so an
|
|
1863
1668
|
// ALREADY-RUNNING local Cursor never reloads even for a brand new
|
|
1864
|
-
// Remote-SSH connection.
|
|
1865
|
-
// selection time, above) has told the candidate this for months; this acts
|
|
1866
|
-
// on it instead of only saying it, with the same graceful, never-a-kill
|
|
1669
|
+
// Remote-SSH connection. This quits it with the same graceful, never-a-kill
|
|
1867
1670
|
// quit `init` uses (see lib/cursor-restart.ts). The launch a few lines
|
|
1868
1671
|
// below IS the relaunch — connect already opens the editor, so there is
|
|
1869
1672
|
// nothing else to relaunch it into.
|
|
1870
1673
|
//
|
|
1674
|
+
// ENG-2890 — NOT from Cursor's own terminal: the quit would kill this
|
|
1675
|
+
// process before the relaunch, leaving no editor and no command. That case
|
|
1676
|
+
// stops before anything is touched, the same way an unconfirmed quit does.
|
|
1677
|
+
//
|
|
1871
1678
|
// NEVER proceeds to launch on an unconfirmed quit. Doing so would hand the
|
|
1872
1679
|
// new `--remote` argv to the SAME still-running process — Cursor's
|
|
1873
1680
|
// single-instance lock just forwards it as a new window into a hooks
|
|
@@ -1875,23 +1682,40 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1875
1682
|
// failure, not a fix for it. So an unconfirmed quit stops here, the same
|
|
1876
1683
|
// way every other refusal in this file does: nothing opened, workspace
|
|
1877
1684
|
// still up and waiting, and the exact way back in.
|
|
1685
|
+
// Whether the Cursor about to open is a FRESH start, so its prompt recording loads:
|
|
1686
|
+
// it was not running, or this command confirmed it quit. Only then may success say the
|
|
1687
|
+
// work is recorded (Greptile on #3071); elsewhere (Linux, Windows, an unreadable process
|
|
1688
|
+
// list) the candidate was only asked to quit it, and nothing checked that they did.
|
|
1689
|
+
let cursorStartsFresh = false;
|
|
1878
1690
|
if (!seedWillRefuseLaunch && ide === "cursor" && platformSupportsGracefulCursorRestart(process.platform)) {
|
|
1879
1691
|
const cursorIo = realCursorProcessIo();
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1692
|
+
const presence = cursorIo.isRunning();
|
|
1693
|
+
if (presence === "not-running")
|
|
1694
|
+
cursorStartsFresh = true;
|
|
1695
|
+
if (presence === "running") {
|
|
1696
|
+
if (runningInsideCursorTerminal()) {
|
|
1697
|
+
for (const line of insideCursorTerminalLines())
|
|
1698
|
+
console.log(line);
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
info(CURSOR_RESTARTING_LINE);
|
|
1883
1702
|
const quit = await attemptQuit(cursorIo);
|
|
1884
1703
|
if (!quit) {
|
|
1885
|
-
for (const line of
|
|
1704
|
+
for (const line of cursorQuitUnconfirmedLines())
|
|
1886
1705
|
console.log(line);
|
|
1887
|
-
console.log();
|
|
1888
|
-
console.log(chalk.yellow.bold(" Quit Cursor completely yourself, then run this again:"));
|
|
1889
|
-
for (const line of cursorReentryLines(reentry))
|
|
1890
|
-
console.log(line);
|
|
1891
|
-
console.log();
|
|
1892
1706
|
return;
|
|
1893
1707
|
}
|
|
1894
|
-
|
|
1708
|
+
cursorStartsFresh = true;
|
|
1709
|
+
}
|
|
1710
|
+
// ENG-2884. A COLD start handed `--remote` loses it: measured twice on a real
|
|
1711
|
+
// profile (2026-10-01 21:11 and 2026-10-02 00:26 PT), Cursor 3 came up restoring its
|
|
1712
|
+
// previous session (the Agents window plus old folders) and never created the
|
|
1713
|
+
// workspace window. Sent to a Cursor that has finished starting, the same request
|
|
1714
|
+
// opens it (./cursorrelaunch variant C). So start Cursor first, let it settle, and
|
|
1715
|
+
// only then launch; if it cannot be started this way, the launch below is the cold
|
|
1716
|
+
// start it always was.
|
|
1717
|
+
if (cursorStartsFresh && (await attemptRelaunch(cursorIo))) {
|
|
1718
|
+
await cursorIo.sleep(CURSOR_SETTLE_MS);
|
|
1895
1719
|
}
|
|
1896
1720
|
}
|
|
1897
1721
|
// A holder rather than a `let`, because the spinner is created inside a callback and
|
|
@@ -1901,6 +1725,8 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1901
1725
|
// "Checking the Remote-SSH extension" through it names the wrong step, on the one path
|
|
1902
1726
|
// where the next thing the candidate may see is a refusal that installed nothing.
|
|
1903
1727
|
const spinners = { verify: null };
|
|
1728
|
+
const resend = { done: false };
|
|
1729
|
+
const launches = resendableLaunch((logPath) => launchEditor(editor, resolved.command, alias, logPath));
|
|
1904
1730
|
const outcome = await attemptEditorLaunch(editor, {
|
|
1905
1731
|
// Reused from the pre-check above — see the comment there for why this
|
|
1906
1732
|
// is a cache, not a second probe.
|
|
@@ -1909,17 +1735,21 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1909
1735
|
ensureExtension: () => {
|
|
1910
1736
|
const extSpinner = ora(`Checking ${editor.label} Remote-SSH extension...`).start();
|
|
1911
1737
|
const state = ensureRemoteSshExtension(editor, resolved.command);
|
|
1912
|
-
|
|
1738
|
+
// ENG-2890: an internal step, so it says nothing when it worked.
|
|
1739
|
+
if (state === "failed") {
|
|
1740
|
+
extSpinner.warn(`Couldn't install the Remote-SSH extension (${editor.remoteSshExt}). Install it from ${editor.label}'s Extensions view if ${editor.label} asks.`);
|
|
1741
|
+
}
|
|
1742
|
+
else if (!verbose)
|
|
1743
|
+
extSpinner.stop();
|
|
1744
|
+
else if (state === "present")
|
|
1913
1745
|
extSpinner.succeed("Remote-SSH extension ready");
|
|
1914
|
-
else if (state === "installed")
|
|
1915
|
-
extSpinner.succeed(`Installed the ${editor.label} Remote-SSH extension`);
|
|
1916
1746
|
else
|
|
1917
|
-
extSpinner.
|
|
1747
|
+
extSpinner.succeed(`Installed the ${editor.label} Remote-SSH extension`);
|
|
1918
1748
|
return state;
|
|
1919
1749
|
},
|
|
1920
1750
|
launchLogPath: editorLaunchLogPath,
|
|
1921
1751
|
launch: (logPath) => {
|
|
1922
|
-
const launched =
|
|
1752
|
+
const launched = launches.send(logPath);
|
|
1923
1753
|
// The launch is not the result, so nothing claims one here. What used to be printed
|
|
1924
1754
|
// at this point was `✔ Opening <editor> → <alias>`, which is true of a spawn that is
|
|
1925
1755
|
// about to fail, and was the last thing the CLI said in a session where nothing was
|
|
@@ -1927,15 +1757,22 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1927
1757
|
spinners.verify = ora(`Opening ${editor.label} — waiting for it to start in your workspace...`).start();
|
|
1928
1758
|
return launched;
|
|
1929
1759
|
},
|
|
1930
|
-
readLaunchLog: readEditorLaunchLog,
|
|
1760
|
+
readLaunchLog: () => launches.readLog(readEditorLaunchLog),
|
|
1931
1761
|
verifyAttached: (baseline, launched) => verifyEditorAttached(editor, baseline, probe, (waited) => {
|
|
1932
1762
|
if (spinners.verify) {
|
|
1933
1763
|
spinners.verify.text = `Opening ${editor.label} — waiting for it to start in your workspace... (${waited}s)`;
|
|
1934
1764
|
}
|
|
1935
|
-
|
|
1765
|
+
// ENG-2884: if the first request still produced no attached window, ask the
|
|
1766
|
+
// (now running) Cursor once more. A window already opening for this folder is
|
|
1767
|
+
// focused rather than duplicated, so a slow first attach costs nothing.
|
|
1768
|
+
if (shouldResendCursorOpen(editor.ide, cursorStartsFresh, waited, resend)) {
|
|
1769
|
+
resend.done = true;
|
|
1770
|
+
launches.send(editorLaunchLogPath());
|
|
1771
|
+
}
|
|
1772
|
+
}, launched, resolved.version.commit),
|
|
1936
1773
|
});
|
|
1937
1774
|
if (outcome.kind === "refused") {
|
|
1938
|
-
for (const line of editorSeedMismatchLines(editor, outcome.seed,
|
|
1775
|
+
for (const line of editorSeedMismatchLines(editor, outcome.seed, pageUrl, alias)) {
|
|
1939
1776
|
console.log(line);
|
|
1940
1777
|
}
|
|
1941
1778
|
return;
|
|
@@ -1945,49 +1782,45 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1945
1782
|
// No window was created, so none of the not-connected copy is true here. See
|
|
1946
1783
|
// `editorLaunchFailedLines`.
|
|
1947
1784
|
launchSpinner?.fail(`Couldn't start ${editor.label}.`);
|
|
1948
|
-
for (const line of editorLaunchFailedLines(editor, alias,
|
|
1785
|
+
for (const line of editorLaunchFailedLines(editor, alias, pageUrl, outcome.error, verbose))
|
|
1949
1786
|
console.log(line);
|
|
1950
|
-
|
|
1951
|
-
for (const line of editorLaunchOutputLines(outcome.logPath, outcome.launchLog, verbose, true))
|
|
1787
|
+
for (const line of editorLaunchOutputLines(outcome.logPath, outcome.launchLog, verbose))
|
|
1952
1788
|
console.log(line);
|
|
1953
1789
|
return;
|
|
1954
1790
|
}
|
|
1955
1791
|
if (outcome.verdict.kind === "connected") {
|
|
1956
|
-
|
|
1957
|
-
|
|
1792
|
+
const recordingKnown = editor.ide !== "cursor" || cursorStartsFresh;
|
|
1793
|
+
launchSpinner?.succeed(recordingKnown
|
|
1794
|
+
? `${editor.label} is open in your workspace. Your work is recorded.`
|
|
1795
|
+
: `${editor.label} is open in your workspace.`);
|
|
1796
|
+
if (!recordingKnown)
|
|
1797
|
+
skillImport.footer.push(CURSOR_RECORDING_CAVEAT_LINE);
|
|
1798
|
+
skillImport.footer.push(...editorConnectedLines(editor, alias, verbose));
|
|
1958
1799
|
}
|
|
1959
1800
|
else if (outcome.verdict.kind === "unverified") {
|
|
1960
1801
|
// Three-valued on purpose: we could not reach the workspace to ask, which is not the
|
|
1961
1802
|
// same as knowing nothing came up, and saying either of the other two here would be a
|
|
1962
1803
|
// claim we cannot make. See `verifyVerdict`.
|
|
1963
|
-
launchSpinner?.warn(`Couldn't
|
|
1964
|
-
for (const line of editorUnverifiedLines(editor, alias,
|
|
1804
|
+
launchSpinner?.warn(`Couldn't confirm ${editor.label} connected to your workspace.`);
|
|
1805
|
+
for (const line of editorUnverifiedLines(editor, alias, pageUrl))
|
|
1965
1806
|
console.log(line);
|
|
1966
1807
|
}
|
|
1967
1808
|
else {
|
|
1968
|
-
launchSpinner?.fail(`${editor.label}
|
|
1969
|
-
|
|
1970
|
-
// withheld rather than repeated; without it that tail is the only view of the output.
|
|
1971
|
-
for (const line of editorNotConnectedLines(editor, alias, conn.browser_ide_url ?? null, outcome.seed, verbose ? null : outcome.launchLog)) {
|
|
1809
|
+
launchSpinner?.fail(`${editor.label} hasn't connected to your workspace yet.`);
|
|
1810
|
+
for (const line of editorNotConnectedLines(editor, alias, pageUrl, verbose))
|
|
1972
1811
|
console.log(line);
|
|
1973
|
-
|
|
1974
|
-
|
|
1812
|
+
// Under --verbose: the log path and the editor's whole output. The extension failure
|
|
1813
|
+
// was already warned about when it happened.
|
|
1814
|
+
if (verbose && outcome.extension === "failed") {
|
|
1975
1815
|
console.log(chalk.dim(` The Remote-SSH extension ("${editor.remoteSshExt}") also failed to install, which would explain this.`));
|
|
1976
1816
|
}
|
|
1977
|
-
|
|
1978
|
-
// suffix — so its PATH is named here whether or not --verbose was asked for.
|
|
1979
|
-
for (const line of editorLaunchOutputLines(outcome.logPath, outcome.launchLog, verbose, true))
|
|
1817
|
+
for (const line of editorLaunchOutputLines(outcome.logPath, outcome.launchLog, verbose))
|
|
1980
1818
|
console.log(line);
|
|
1981
1819
|
return;
|
|
1982
1820
|
}
|
|
1983
|
-
//
|
|
1984
|
-
|
|
1985
|
-
// the editor's own resolver message is the only evidence anybody has — and the name
|
|
1986
|
-
// carries a per-run suffix nobody can guess unaided. `editorLaunchOutputLines` still
|
|
1987
|
-
// withholds a path with nothing behind it.
|
|
1988
|
-
for (const line of editorLaunchOutputLines(outcome.logPath, outcome.launchLog, verbose, outcome.verdict.kind !== "connected")) {
|
|
1821
|
+
// Under --verbose, the editor's own output on both remaining branches (ENG-2890).
|
|
1822
|
+
for (const line of editorLaunchOutputLines(outcome.logPath, outcome.launchLog, verbose))
|
|
1989
1823
|
console.log(line);
|
|
1990
|
-
}
|
|
1991
1824
|
// ENG-2154. The window we have just opened outlives the session, and what it
|
|
1992
1825
|
// does when the session ends looks like a failure. This is the only moment
|
|
1993
1826
|
// anything of ours is on the candidate's own machine — `connect` returns here
|
|
@@ -1995,6 +1828,6 @@ async function connectFlow(token, ideOpt, opts, skillImport) {
|
|
|
1995
1828
|
// assessment page — so it is also the only warning that reaches a candidate
|
|
1996
1829
|
// who ends their session somewhere we do not run. See lib/session-end.ts,
|
|
1997
1830
|
// which also carries why we do not simply close the window.
|
|
1998
|
-
skillImport.footer.push(...editorEndingForewarning(
|
|
1831
|
+
skillImport.footer.push(...editorEndingForewarning());
|
|
1999
1832
|
}
|
|
2000
1833
|
//# sourceMappingURL=connect.js.map
|