litmus-cli 1.4.70 → 1.4.72-capturetest.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -9
- package/dist/commands/connect.d.ts +33 -2
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +119 -45
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/forward.d.ts +7 -16
- package/dist/commands/forward.d.ts.map +1 -1
- package/dist/commands/forward.js +97 -75
- package/dist/commands/forward.js.map +1 -1
- package/dist/commands/ssh.d.ts +3 -0
- package/dist/commands/ssh.d.ts.map +1 -0
- package/dist/commands/ssh.js +50 -0
- package/dist/commands/ssh.js.map +1 -0
- package/dist/index.js +16 -5
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +7 -1
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +38 -8
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/auto-forward.d.ts +31 -0
- package/dist/lib/auto-forward.d.ts.map +1 -0
- package/dist/lib/auto-forward.js +258 -0
- package/dist/lib/auto-forward.js.map +1 -0
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/connection-attempt.d.ts +13 -2
- package/dist/lib/connection-attempt.d.ts.map +1 -1
- package/dist/lib/connection-attempt.js +16 -3
- package/dist/lib/connection-attempt.js.map +1 -1
- package/dist/lib/cursor-session.d.ts +6 -0
- package/dist/lib/cursor-session.d.ts.map +1 -1
- package/dist/lib/cursor-session.js +21 -1
- package/dist/lib/cursor-session.js.map +1 -1
- package/dist/lib/environment-checks.d.ts +1 -2
- package/dist/lib/environment-checks.d.ts.map +1 -1
- package/dist/lib/environment-checks.js +4 -9
- package/dist/lib/environment-checks.js.map +1 -1
- package/dist/lib/hook-logger.cjs +79 -22
- package/dist/lib/hosted-capture.d.ts +21 -0
- package/dist/lib/hosted-capture.d.ts.map +1 -0
- package/dist/lib/hosted-capture.js +229 -0
- package/dist/lib/hosted-capture.js.map +1 -0
- package/dist/lib/hosted-watcher.d.ts +2 -0
- package/dist/lib/hosted-watcher.d.ts.map +1 -0
- package/dist/lib/hosted-watcher.js +176 -0
- package/dist/lib/hosted-watcher.js.map +1 -0
- package/dist/lib/install-ssh-forward-hook.d.ts +6 -0
- package/dist/lib/install-ssh-forward-hook.d.ts.map +1 -0
- package/dist/lib/install-ssh-forward-hook.js +57 -0
- package/dist/lib/install-ssh-forward-hook.js.map +1 -0
- package/dist/lib/port-forward.d.ts +16 -0
- package/dist/lib/port-forward.d.ts.map +1 -0
- package/dist/lib/port-forward.js +46 -0
- package/dist/lib/port-forward.js.map +1 -0
- package/dist/lib/session-state.d.ts +61 -0
- package/dist/lib/session-state.d.ts.map +1 -0
- package/dist/lib/session-state.js +92 -0
- package/dist/lib/session-state.js.map +1 -0
- package/dist/lib/ssh-client.d.ts +11 -2
- package/dist/lib/ssh-client.d.ts.map +1 -1
- package/dist/lib/ssh-client.js +66 -7
- package/dist/lib/ssh-client.js.map +1 -1
- package/dist/lib/ssh-forward-hook.d.ts +13 -0
- package/dist/lib/ssh-forward-hook.d.ts.map +1 -0
- package/dist/lib/ssh-forward-hook.js +93 -0
- package/dist/lib/ssh-forward-hook.js.map +1 -0
- package/dist/lib/tracker.d.ts.map +1 -1
- package/dist/lib/tracker.js +7 -2
- package/dist/lib/tracker.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Official CLI for [Litmus](https://www.litmushiring.com) engineering assessments.
|
|
|
8
8
|
npm install -g litmus-cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Requires Node.js
|
|
11
|
+
Requires Node.js 20.19+.
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
@@ -86,6 +86,16 @@ as `litmus push <token>` with the token from your assessment page. Either way it
|
|
|
86
86
|
captures the workspace exactly as it stands, so there is nothing to commit or push
|
|
87
87
|
first. The Submit button on your assessment page does the same thing.
|
|
88
88
|
|
|
89
|
+
`litmus connect` also asks permission to record supported local AI-agent prompts
|
|
90
|
+
and responses across folders during the assessment, alongside hosted capture.
|
|
91
|
+
A background capture watcher stays on your computer after connect returns. It
|
|
92
|
+
checks for submission independently of prompts and pauses local capture within
|
|
93
|
+
ten seconds if it loses contact with Litmus. Accepted submission (including the
|
|
94
|
+
website Submit button) retires the capture hooks; a work-trial checkpoint does not.
|
|
95
|
+
Restart already running local tools so they load the hooks. This local hook path
|
|
96
|
+
covers Claude Code, Codex, Cursor and Gemini where supported; browser AI and
|
|
97
|
+
project-scoped Copilot sessions are not covered machine-wide.
|
|
98
|
+
|
|
89
99
|
If the assessment allows it, `litmus connect` also offers to bring your own AI
|
|
90
100
|
skills into the workspace. It looks for skill folders (each with a `SKILL.md`) in
|
|
91
101
|
`~/.claude/skills`, `~/.agents/skills`, `~/.codex/skills`, `~/.copilot/skills`,
|
|
@@ -93,17 +103,38 @@ skills into the workspace. It looks for skill folders (each with a `SKILL.md`) i
|
|
|
93
103
|
again only when your skills change. Add `--skills` to import without being asked, or
|
|
94
104
|
`--no-skills` to skip the offer.
|
|
95
105
|
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
Connecting with `litmus connect <token> --ide ssh` opens a normal SSH shell and
|
|
107
|
+
forwards app ports automatically. Start a dev server in that shell, then open its
|
|
108
|
+
`http://localhost:<port>` on your laptop. Ctrl+C interrupts the remote command;
|
|
109
|
+
forwarding stays active until the SSH shell exits. Reconnect to an already configured
|
|
110
|
+
workspace with bare `ssh litmus-<workspace-id>` or `litmus ssh litmus-<workspace-id>`
|
|
111
|
+
for the same behavior. Setup installs a local SSH connection hook and a durable
|
|
112
|
+
forwarding helper under `~/.ssh/litmus-forward/`; no separate command is needed.
|
|
113
|
+
Existing aliases need the updated connection setup once. The helper stops within
|
|
114
|
+
seconds of the SSH connection closing, and skips connections with piped output
|
|
115
|
+
(such as VS Code/Cursor). Use `ssh -o PermitLocalCommand=no <alias>` to disable the hook for a connection.
|
|
116
|
+
The generated workspace alias disables SSH multiplexing so closing its shell
|
|
117
|
+
also closes its forwards; other hosts retain their existing settings.
|
|
118
|
+
|
|
119
|
+
When working in the browser editor, you can instead open just the forwarding
|
|
120
|
+
connection on your computer and leave it open:
|
|
98
121
|
|
|
99
122
|
```bash
|
|
100
|
-
npx -y litmus-cli@latest forward <token>
|
|
123
|
+
npx -y litmus-cli@latest forward <token>
|
|
101
124
|
```
|
|
102
125
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
Start apps in the workspace terminal whenever you need them. Each discovered TCP
|
|
127
|
+
listener gets a `http://localhost:<port>` link without restarting the command, including
|
|
128
|
+
servers bound only to workspace localhost. Platform ports and ports below 1024 are
|
|
129
|
+
skipped. A busy laptop port is reported without interrupting other forwards.
|
|
130
|
+
|
|
131
|
+
To forward only specific ports, append `5173 3001`. `5174:5173` opens the workspace's
|
|
132
|
+
5173 on your 5174. Explicit ports also work with older workspace images that do not
|
|
133
|
+
support discovery. Up to 256 ports are retained per automatic session, including
|
|
134
|
+
through app restarts. Ctrl+C stops all its forwards.
|
|
135
|
+
If a connection drops it reconnects on its own. Once your assessment is submitted,
|
|
136
|
+
over, or locked, forwarding stops and explains why.
|
|
137
|
+
|
|
107
138
|
|
|
108
139
|
## How it works
|
|
109
140
|
|
|
@@ -174,7 +205,7 @@ at any time by deleting the Litmus entries from the files above.
|
|
|
174
205
|
|
|
175
206
|
## Requirements
|
|
176
207
|
|
|
177
|
-
- Node.js
|
|
208
|
+
- Node.js 20.19+
|
|
178
209
|
- Git (recommended — without it, analysis loses commit history, iteration patterns, and process scoring signals; code quality scoring still works)
|
|
179
210
|
|
|
180
211
|
## Releasing (maintainers)
|
|
@@ -236,3 +267,15 @@ Notes:
|
|
|
236
267
|
- The tag version must match `package.json` or the publish job fails its verify step.
|
|
237
268
|
- npm rejects re-publishing an existing version — never re-tag a version that is
|
|
238
269
|
already on the registry; bump to a new version instead.
|
|
270
|
+
|
|
271
|
+
### Cursor Agents Window
|
|
272
|
+
|
|
273
|
+
On macOS and Windows, `litmus connect <token> --ide cursor` opens the remote IDE
|
|
274
|
+
and hands its assessment workspace to Cursor's Agents Window, without sending a
|
|
275
|
+
prompt or restarting Cursor. Use `--no-cursor-agent-window` for the IDE only.
|
|
276
|
+
Linux users can opt in with `--cursor-agent-window`.
|
|
277
|
+
|
|
278
|
+
The bundled Litmus companion is installed for this handoff. If it is unavailable,
|
|
279
|
+
continue in the IDE and open Agents Window manually. Submission cleanup remains
|
|
280
|
+
opt-in with `--cursor-session-cleanup`; it disconnects the ended IDE and removes
|
|
281
|
+
its Open Recent entry, but does not delete saved chats or Agents sidebar entries.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ConnectionAttempt } from "../lib/connection-attempt.js";
|
|
1
2
|
import { type ResolvedEditorCommand } from "../lib/editor-binary.js";
|
|
2
3
|
import type { NativeIde } from "../lib/editor-ide.js";
|
|
3
4
|
import { type EditorVersion, type SeedVerdict, type VerifyVerdict, type WorkspaceProbe } from "../lib/editor-server.js";
|
|
@@ -135,6 +136,7 @@ export declare function controlPathFor(platform: NodeJS.Platform): string;
|
|
|
135
136
|
export declare function upsertSshConfigContent(existing: string, alias: string, block: string, controlBlock?: string, platform?: NodeJS.Platform): string;
|
|
136
137
|
/** Short, stable alias from the workspace hostname (or token as a fallback). */
|
|
137
138
|
export declare function aliasFor(sshHostname: string | null | undefined, token: string): string;
|
|
139
|
+
export declare function upsertSshForwardHook(existing: string, alias: string, block: string, controlBlock: string, command: string): string;
|
|
138
140
|
/**
|
|
139
141
|
* The native editors `litmus connect` can drive, and everything about them that is
|
|
140
142
|
* not shared. Both are VS Code-family, so one set of helpers serves both: the CLI
|
|
@@ -498,6 +500,20 @@ export declare function openEditorLaunchLog(logPath: string): number | null;
|
|
|
498
500
|
export interface EditorLaunchHandle {
|
|
499
501
|
launchFailure: () => string | null;
|
|
500
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* ENG-3217: Windows Cursor 3.24.9 accepts --remote but routes it to the local
|
|
505
|
+
* Agents Window. Its SSH protocol handler opens the remote IDE, which can then
|
|
506
|
+
* hand off to Agents through the companion. No restart or global settings change.
|
|
507
|
+
*/
|
|
508
|
+
export declare function editorLaunchPlan(ed: NativeEditor, alias: string, platform?: NodeJS.Platform): {
|
|
509
|
+
args: string[];
|
|
510
|
+
guidance: string;
|
|
511
|
+
verifySchedule: number[];
|
|
512
|
+
} | {
|
|
513
|
+
args: string[];
|
|
514
|
+
guidance: null;
|
|
515
|
+
verifySchedule: readonly number[];
|
|
516
|
+
};
|
|
501
517
|
/**
|
|
502
518
|
* ENG-2332, Greptile P1 on #2442 — ON WINDOWS THE `error` EVENT IS NOT THE FAILURE.
|
|
503
519
|
*
|
|
@@ -543,7 +559,7 @@ export declare function editorLaunchOutputLines(logPath: string, launchLog: stri
|
|
|
543
559
|
*/
|
|
544
560
|
export type WorkspaceProbeRunner = (remoteCommand: string) => string | null;
|
|
545
561
|
export declare function sshProbeRunner(alias: string, executable?: string): WorkspaceProbeRunner;
|
|
546
|
-
export declare function verifyEditorAttached(ed: NativeEditor, baseline: WorkspaceProbe | null, run: WorkspaceProbeRunner, onWait: (secondsWaited: number) => void, launched: EditorLaunchHandle, clientCommit?: string | null): Promise<VerifyVerdict>;
|
|
562
|
+
export declare function verifyEditorAttached(ed: NativeEditor, baseline: WorkspaceProbe | null, run: WorkspaceProbeRunner, onWait: (secondsWaited: number) => void, launched: EditorLaunchHandle, clientCommit?: string | null, schedule?: readonly number[]): Promise<VerifyVerdict>;
|
|
547
563
|
/** Whether the editor's Remote-SSH extension was already there, put there, or could not be. */
|
|
548
564
|
export type RemoteSshExtensionState = "present" | "installed" | "failed";
|
|
549
565
|
/**
|
|
@@ -645,6 +661,10 @@ export declare function keepWaitingForWorkspace(elapsedMs: number, status: strin
|
|
|
645
661
|
* both spellings are well-formed URLs that render a page.
|
|
646
662
|
*/
|
|
647
663
|
export declare function candidatePageUrl(apiBase: string, assessmentId: string): string;
|
|
664
|
+
export declare function endedInitRefusal(err: unknown): {
|
|
665
|
+
state: "submitted" | "over";
|
|
666
|
+
hint?: string;
|
|
667
|
+
} | null;
|
|
648
668
|
/** Remote hooks initialize in the remote window; no desktop restart is required (ENG-3176). */
|
|
649
669
|
export declare function cursorSelectionNoticeLines(_platform?: NodeJS.Platform): string[];
|
|
650
670
|
/** How long a freshly started Cursor gets to finish restoring its session (ENG-2884). */
|
|
@@ -753,7 +773,18 @@ export interface ConnectOptions {
|
|
|
753
773
|
* place of the interactive shell: it gets the alias and the ports the workspace
|
|
754
774
|
* publishes. Set, the route needs no terminal, because nothing is handed a shell.
|
|
755
775
|
*/
|
|
756
|
-
tunnel?: (alias: string, publishedPorts: number[]) => Promise<void>;
|
|
776
|
+
tunnel?: (alias: string, publishedPorts: number[], session: TunnelSession) => Promise<void>;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* ENG-3265: what a tunnel needs to tell a closed assessment from a broken connection,
|
|
780
|
+
* and to record which it was on the connection attempt that opened it.
|
|
781
|
+
*/
|
|
782
|
+
export interface TunnelSession {
|
|
783
|
+
backendUrl: string;
|
|
784
|
+
token: string;
|
|
785
|
+
/** The assessment page, which carries only the assessment id. */
|
|
786
|
+
pageUrl: string;
|
|
787
|
+
attempt: ConnectionAttempt;
|
|
757
788
|
}
|
|
758
789
|
export declare function runConnect(token: string, ideOpt?: string, opts?: ConnectOptions): Promise<void>;
|
|
759
790
|
//# sourceMappingURL=connect.d.ts.map
|
|
@@ -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,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { startHostedCapture } from "../lib/hosted-capture.js";
|
|
2
|
+
import { installSshForwardHook } from "../lib/install-ssh-forward-hook.js";
|
|
3
|
+
import { runSsh } from "./ssh.js";
|
|
4
|
+
import { cursorSessionOptions, prepareCursorSession } from "../lib/cursor-session.js";
|
|
2
5
|
import { ConnectionAttempt } from "../lib/connection-attempt.js";
|
|
6
|
+
import { closedState } from "../lib/session-state.js";
|
|
3
7
|
import { updateSshConfig } from "../lib/ssh-config-file.js";
|
|
4
8
|
import os from "os";
|
|
5
9
|
import path from "path";
|
|
@@ -17,7 +21,7 @@ import { editorInstallCandidates, editorSpawnSpec } from "../lib/editor-binary.j
|
|
|
17
21
|
import { isWsl, whichOnPath, windowsSideEditor, wslEditorHint } from "../lib/wsl.js";
|
|
18
22
|
import { parseEditorVersion, parseWorkspaceProbe, remoteProbeCommand, seedVerdict, verifyVerdict, VERIFY_SCHEDULE_MS, } from "../lib/editor-server.js";
|
|
19
23
|
import { fatal, holdExitFor, success, info, warn, setErrorContext, reportEvent } from "../lib/errors.js";
|
|
20
|
-
import { oldWindowsSshHint,
|
|
24
|
+
import { oldWindowsSshHint, parseOpenSshVersion, proxyJumpBroken, readEditorSettings, readSshVersionBanner, sshExecutableFor, sshPreflight, sshPreflightHint, } from "../lib/ssh-client.js";
|
|
21
25
|
import { editorEndingForewarning } from "../lib/session-end.js";
|
|
22
26
|
import { attemptRelaunch, platformSupportsGracefulCursorRestart, realCursorProcessIo, } from "../lib/cursor-restart.js";
|
|
23
27
|
/**
|
|
@@ -325,9 +329,28 @@ function ensureSshKey(keyPath) {
|
|
|
325
329
|
}
|
|
326
330
|
async function writeSshConfig(alias, block, controlBlock) {
|
|
327
331
|
const cfgPath = path.join(os.homedir(), ".ssh", "config");
|
|
328
|
-
|
|
332
|
+
let command;
|
|
333
|
+
try {
|
|
334
|
+
command = installSshForwardHook(alias);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
warn("Could not install automatic forwarding for bare SSH. Use npx -y litmus-cli@latest ssh with this alias, or rerun setup to repair it.");
|
|
338
|
+
await updateSshConfig(cfgPath, (existing) => upsertSshConfigContent(existing, alias, block, controlBlock));
|
|
339
|
+
return cfgPath;
|
|
340
|
+
}
|
|
341
|
+
await updateSshConfig(cfgPath, (existing) => upsertSshForwardHook(existing, alias, block, controlBlock, command));
|
|
329
342
|
return cfgPath;
|
|
330
343
|
}
|
|
344
|
+
export function upsertSshForwardHook(existing, alias, block, controlBlock, command) {
|
|
345
|
+
const begin = `# >>> litmus forwarding ${alias}`;
|
|
346
|
+
const end = `# <<< litmus forwarding ${alias}`;
|
|
347
|
+
// LocalCommand does not run for mux clients, and ControlPersist outlives
|
|
348
|
+
// the shell. This alias needs its own connection to own forwarding cleanup.
|
|
349
|
+
// Bastion sharing and all unrelated hosts retain their existing settings.
|
|
350
|
+
const hook = `${begin}\nHost ${alias}\n ControlMaster no\n ControlPersist no\n ControlPath none\n PermitLocalCommand yes\n LocalCommand ${command}\nHost *\n${end}\n`;
|
|
351
|
+
const clean = withoutRegion(existing, begin, end);
|
|
352
|
+
return prepend(upsertSshConfigContent(clean, alias, block, controlBlock), hook);
|
|
353
|
+
}
|
|
331
354
|
/**
|
|
332
355
|
* The two editors, and the one place every measured difference between them lives.
|
|
333
356
|
*
|
|
@@ -761,6 +784,29 @@ export function openEditorLaunchLog(logPath) {
|
|
|
761
784
|
return null;
|
|
762
785
|
}
|
|
763
786
|
}
|
|
787
|
+
/**
|
|
788
|
+
* ENG-3217: Windows Cursor 3.24.9 accepts --remote but routes it to the local
|
|
789
|
+
* Agents Window. Its SSH protocol handler opens the remote IDE, which can then
|
|
790
|
+
* hand off to Agents through the companion. No restart or global settings change.
|
|
791
|
+
*/
|
|
792
|
+
export function editorLaunchPlan(ed, alias, platform = process.platform) {
|
|
793
|
+
if (ed.ide === "cursor" && platform === "win32") {
|
|
794
|
+
// The alias is generated by Litmus. Never let URI delimiters change its target.
|
|
795
|
+
if (!/^litmus-[a-zA-Z0-9-]+$/.test(alias))
|
|
796
|
+
throw new Error("Invalid Litmus SSH alias");
|
|
797
|
+
return {
|
|
798
|
+
args: ["--open-url", `cursor://vscode-remote/ssh-remote+${alias}${REMOTE_WORKSPACE_DIR}`],
|
|
799
|
+
guidance: `Cursor may ask to open ${alias}${REMOTE_WORKSPACE_DIR}. Choose Yes for this workspace.`,
|
|
800
|
+
// Allow a bounded extra minute for the external-folder confirmation plus startup.
|
|
801
|
+
verifySchedule: [...VERIFY_SCHEDULE_MS, 90000, 120000],
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
return {
|
|
805
|
+
args: ["--remote", `ssh-remote+${alias}`, REMOTE_WORKSPACE_DIR],
|
|
806
|
+
guidance: null,
|
|
807
|
+
verifySchedule: VERIFY_SCHEDULE_MS,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
764
810
|
/**
|
|
765
811
|
* ENG-2332, Greptile P1 on #2442 — ON WINDOWS THE `error` EVENT IS NOT THE FAILURE.
|
|
766
812
|
*
|
|
@@ -783,7 +829,7 @@ export function openEditorLaunchLog(logPath) {
|
|
|
783
829
|
* describe the same thing — no window was created — and the first is the more specific.
|
|
784
830
|
*/
|
|
785
831
|
export function launchEditor(ed, command, alias, logPath) {
|
|
786
|
-
const spec = editorSpawnSpec(command,
|
|
832
|
+
const spec = editorSpawnSpec(command, editorLaunchPlan(ed, alias).args, process.platform);
|
|
787
833
|
if (!spec) {
|
|
788
834
|
// No argv drives this command here, so nothing was started and nothing will be. This
|
|
789
835
|
// is a launch failure rather than a throw for the reason `editorSpawnSpec` gives: the
|
|
@@ -900,11 +946,11 @@ function probeWorkspace(ed, run, clientCommit = null) {
|
|
|
900
946
|
* than taken whole.
|
|
901
947
|
*/
|
|
902
948
|
const SPAWN_ERROR_POLL_MS = 200;
|
|
903
|
-
export async function verifyEditorAttached(ed, baseline, run, onWait, launched, clientCommit = null) {
|
|
949
|
+
export async function verifyEditorAttached(ed, baseline, run, onWait, launched, clientCommit = null, schedule = VERIFY_SCHEDULE_MS) {
|
|
904
950
|
const started = Date.now();
|
|
905
|
-
const budget =
|
|
951
|
+
const budget = schedule[schedule.length - 1];
|
|
906
952
|
let sawNotConnected = false;
|
|
907
|
-
for (const at of
|
|
953
|
+
for (const at of schedule) {
|
|
908
954
|
const remaining = at - (Date.now() - started);
|
|
909
955
|
// A probe that spends its own timeout can carry the elapsed time past a later entry;
|
|
910
956
|
// skipping those keeps the WAIT the candidate experiences at the stated budget rather
|
|
@@ -1030,6 +1076,28 @@ export function keepWaitingForWorkspace(elapsedMs, status) {
|
|
|
1030
1076
|
export function candidatePageUrl(apiBase, assessmentId) {
|
|
1031
1077
|
return `${apiBase}/candidate/${assessmentId}`;
|
|
1032
1078
|
}
|
|
1079
|
+
/**
|
|
1080
|
+
* ENG-3265: `/api/cli/init`'s refusals for an assessment that has closed, by the stable
|
|
1081
|
+
* `error` code each sends (frontend/app/api/cli/init/route.ts and
|
|
1082
|
+
* frontend/lib/candidate-terminal.ts). Each message already says what happened, and the
|
|
1083
|
+
* ones that can be undone say to contact the company, so the hint never offers a retry.
|
|
1084
|
+
*/
|
|
1085
|
+
const ENDED_INIT_REFUSALS = {
|
|
1086
|
+
"Already submitted": {
|
|
1087
|
+
state: "submitted",
|
|
1088
|
+
hint: "Your workspace closed when the assessment was submitted. Open the assessment page from your invitation email to see its status.",
|
|
1089
|
+
},
|
|
1090
|
+
"Session over": { state: "over", hint: "Open the assessment page from your invitation email to see its status." },
|
|
1091
|
+
"Assessment expired": { state: "over" },
|
|
1092
|
+
"Time's up": { state: "over" },
|
|
1093
|
+
"Deadline passed": { state: "over" },
|
|
1094
|
+
"Invitation withdrawn": { state: "over" },
|
|
1095
|
+
};
|
|
1096
|
+
export function endedInitRefusal(err) {
|
|
1097
|
+
if (!(err instanceof ServerError) || !err.errorCode)
|
|
1098
|
+
return null;
|
|
1099
|
+
return Object.prototype.hasOwnProperty.call(ENDED_INIT_REFUSALS, err.errorCode) ? ENDED_INIT_REFUSALS[err.errorCode] : null;
|
|
1100
|
+
}
|
|
1033
1101
|
/** Remote hooks initialize in the remote window; no desktop restart is required (ENG-3176). */
|
|
1034
1102
|
export function cursorSelectionNoticeLines(_platform = process.platform) {
|
|
1035
1103
|
return [];
|
|
@@ -1271,7 +1339,7 @@ export async function runConnect(token, ideOpt, opts = {}) {
|
|
|
1271
1339
|
// last read is where they are and what the session ending looks like.
|
|
1272
1340
|
setErrorContext({ token, apiBase: resolveApiBase(process.env.LITMUS_API_URL) });
|
|
1273
1341
|
const attempt = new ConnectionAttempt();
|
|
1274
|
-
const skillImport = { report: null, footer: [], sshAlias: null, publishedPorts: [] };
|
|
1342
|
+
const skillImport = { report: null, footer: [], sshAlias: null, publishedPorts: [], tunnelSession: null };
|
|
1275
1343
|
// Register before entering the flow: a fast fatal report can settle before the
|
|
1276
1344
|
// caller's finally begins, especially while an async SSH config write is unwinding.
|
|
1277
1345
|
let releaseExitHold;
|
|
@@ -1280,7 +1348,10 @@ export async function runConnect(token, ideOpt, opts = {}) {
|
|
|
1280
1348
|
await connectFlow(token, ideOpt, opts, skillImport, attempt);
|
|
1281
1349
|
}
|
|
1282
1350
|
catch (error) {
|
|
1283
|
-
|
|
1351
|
+
// ENG-3265: a closed assessment refuses the connect on purpose; that stays recorded
|
|
1352
|
+
// as the expected outcome it is, not as a failed connection.
|
|
1353
|
+
if (attempt.outcome !== "session_ended")
|
|
1354
|
+
attempt.finish("failed");
|
|
1284
1355
|
attempt.evidence(error instanceof Error ? error.message : null);
|
|
1285
1356
|
throw error;
|
|
1286
1357
|
}
|
|
@@ -1301,32 +1372,11 @@ export async function runConnect(token, ideOpt, opts = {}) {
|
|
|
1301
1372
|
// The ssh route's session runs last, so nothing we print lands inside the remote shell.
|
|
1302
1373
|
if (!skillImport.sshAlias)
|
|
1303
1374
|
return;
|
|
1304
|
-
if (opts.tunnel)
|
|
1305
|
-
await opts.tunnel(skillImport.sshAlias, skillImport.publishedPorts);
|
|
1306
|
-
else
|
|
1307
|
-
await openSshRoute(skillImport.sshAlias);
|
|
1308
|
-
}
|
|
1309
|
-
/**
|
|
1310
|
-
* The ssh route: the candidate ran one command from the assessment page and lands in the
|
|
1311
|
-
* workspace's shell, rather than being handed a second command to run. ssh owns the
|
|
1312
|
-
* terminal until it exits, and its exit status becomes ours.
|
|
1313
|
-
*
|
|
1314
|
-
* Exit 255 is ssh's connection failure, but a remote shell can exit 255 too, so that case
|
|
1315
|
-
* offers a reconnect without claiming the connection failed. When ssh could not be
|
|
1316
|
-
* started at all, the command is printed and the exit status is a failure.
|
|
1317
|
-
*/
|
|
1318
|
-
async function openSshRoute(alias) {
|
|
1319
|
-
const status = await openSshSession(alias, sshExecutableFor("ssh", null));
|
|
1320
|
-
if (status === null) {
|
|
1321
|
-
console.log(` Connect from your terminal: ${chalk.cyan(`ssh ${alias}`)}`);
|
|
1322
|
-
process.exitCode = 1;
|
|
1323
|
-
return;
|
|
1324
|
-
}
|
|
1325
|
-
if (status === SSH_CONNECTION_FAILED) {
|
|
1326
|
-
console.log(` If the connection dropped, reconnect with: ${chalk.cyan(`ssh ${alias}`)}`);
|
|
1375
|
+
if (opts.tunnel && skillImport.tunnelSession) {
|
|
1376
|
+
await opts.tunnel(skillImport.sshAlias, skillImport.publishedPorts, skillImport.tunnelSession);
|
|
1327
1377
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1378
|
+
else
|
|
1379
|
+
await runSsh(skillImport.sshAlias);
|
|
1330
1380
|
}
|
|
1331
1381
|
async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
1332
1382
|
const verbose = Boolean(opts.verbose);
|
|
@@ -1357,6 +1407,15 @@ async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
|
1357
1407
|
fatal("Couldn't reach Litmus.", "Check your internet connection, then run the same litmus connect command again.", { internalDetail: msg, severity });
|
|
1358
1408
|
return;
|
|
1359
1409
|
}
|
|
1410
|
+
// ENG-3265: the server's sentence already says the assessment is closed and what to
|
|
1411
|
+
// do; "try again in a moment" under it would send the candidate round again.
|
|
1412
|
+
const ended = endedInitRefusal(err);
|
|
1413
|
+
if (ended) {
|
|
1414
|
+
attempt.finish("session_ended");
|
|
1415
|
+
attempt.fact("sessionState", ended.state);
|
|
1416
|
+
fatal(msg, ended.hint, { internalDetail: detail, severity: "warning" });
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1360
1419
|
fatal(msg, hintForError(err), { internalDetail: detail, severity });
|
|
1361
1420
|
return;
|
|
1362
1421
|
}
|
|
@@ -1408,6 +1467,14 @@ async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
|
1408
1467
|
return;
|
|
1409
1468
|
}
|
|
1410
1469
|
if (code === "assessment_closed") {
|
|
1470
|
+
// ENG-3265: closed is also what a submitted assessment answers, so ask which.
|
|
1471
|
+
const closed = await closedState(backendUrl, token);
|
|
1472
|
+
attempt.finish("session_ended");
|
|
1473
|
+
attempt.fact("sessionState", closed.kind);
|
|
1474
|
+
if (closed.kind === "submitted") {
|
|
1475
|
+
fatal("This assessment has been submitted, so its workspace is closed.", `Open the assessment page to see its status: ${pageUrl}`, { severity: "warning" });
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1411
1478
|
fatal("This assessment is closed. Its deadline has passed.", "Contact the company if you need an extension.", { severity: "warning" });
|
|
1412
1479
|
return;
|
|
1413
1480
|
}
|
|
@@ -1479,6 +1546,7 @@ async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
|
1479
1546
|
return;
|
|
1480
1547
|
}
|
|
1481
1548
|
}
|
|
1549
|
+
await startHostedCapture(meta, token, apiBase);
|
|
1482
1550
|
if (ide === "browser" && conn.browser_ide_url) {
|
|
1483
1551
|
attempt.stage("browser_open");
|
|
1484
1552
|
attempt.finish("browser_requested");
|
|
@@ -1594,6 +1662,7 @@ async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
|
1594
1662
|
if (opts.tunnel) {
|
|
1595
1663
|
skillImport.sshAlias = alias;
|
|
1596
1664
|
skillImport.publishedPorts = (conn.forwarded_ports ?? []).map((p) => p.port);
|
|
1665
|
+
skillImport.tunnelSession = { backendUrl, token, pageUrl, attempt };
|
|
1597
1666
|
return;
|
|
1598
1667
|
}
|
|
1599
1668
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
@@ -1822,21 +1891,24 @@ async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
|
1822
1891
|
// where the next thing the candidate may see is a refusal that installed nothing.
|
|
1823
1892
|
const spinners = { verify: null };
|
|
1824
1893
|
const resend = { done: false };
|
|
1894
|
+
const launchPlan = editorLaunchPlan(editor, alias);
|
|
1825
1895
|
const launches = resendableLaunch((logPath) => launchEditor(editor, resolved.command, alias, logPath));
|
|
1826
1896
|
// Local UI extension: it survives a remote-host disconnect. Never installed on
|
|
1827
|
-
//
|
|
1828
|
-
if (editor.ide === "cursor"
|
|
1829
|
-
|
|
1897
|
+
// VS Code, browser or plain SSH paths. Handoff defaults on for Mac/Windows.
|
|
1898
|
+
if (editor.ide === "cursor") {
|
|
1899
|
+
const sessionOptions = cursorSessionOptions(opts);
|
|
1900
|
+
const enabled = sessionOptions.handoff || sessionOptions.cleanup;
|
|
1901
|
+
if (enabled)
|
|
1902
|
+
info("Setting up the Litmus Cursor companion for this workspace.");
|
|
1830
1903
|
try {
|
|
1831
|
-
const ready = await prepareCursorSession(resolved.command, alias, backendUrl, token,
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
: "Cursor companion unavailable. Continue in the IDE; use Open Agents Window or Close Remote Connection manually.");
|
|
1904
|
+
const ready = await prepareCursorSession(resolved.command, alias, backendUrl, token, sessionOptions);
|
|
1905
|
+
if (enabled)
|
|
1906
|
+
skillImport.footer.push(ready
|
|
1907
|
+
? "Cursor companion enabled. Handoff targets this IDE workspace; optional cleanup disconnects the ended IDE and removes its Open Recent entry. Saved chats and Agents Window entries are not deleted."
|
|
1908
|
+
: "Cursor companion unavailable. Continue in the IDE; use Open Agents Window or Close Remote Connection manually.");
|
|
1837
1909
|
}
|
|
1838
1910
|
catch {
|
|
1839
|
-
skillImport.footer.push("Cursor companion setup failed. Your normal IDE connection is unaffected.");
|
|
1911
|
+
skillImport.footer.push("Cursor companion setup failed. Your normal IDE connection is unaffected; automatic Agents handoff preferences may not have been updated.");
|
|
1840
1912
|
}
|
|
1841
1913
|
}
|
|
1842
1914
|
const outcome = await attemptEditorLaunch(editor, {
|
|
@@ -1864,6 +1936,8 @@ async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
|
1864
1936
|
launch: (logPath) => {
|
|
1865
1937
|
attempt.stage("editor_launch");
|
|
1866
1938
|
attempt.checkpoint();
|
|
1939
|
+
if (launchPlan.guidance)
|
|
1940
|
+
info(launchPlan.guidance);
|
|
1867
1941
|
const launched = launches.send(logPath);
|
|
1868
1942
|
// The launch is not the result, so nothing claims one here. What used to be printed
|
|
1869
1943
|
// at this point was `✔ Opening <editor> → <alias>`, which is true of a spawn that is
|
|
@@ -1886,7 +1960,7 @@ async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
|
1886
1960
|
resend.done = true;
|
|
1887
1961
|
launches.send(editorLaunchLogPath());
|
|
1888
1962
|
}
|
|
1889
|
-
}, launched, resolved.version.commit);
|
|
1963
|
+
}, launched, resolved.version.commit, launchPlan.verifySchedule);
|
|
1890
1964
|
},
|
|
1891
1965
|
});
|
|
1892
1966
|
if (outcome.kind === "refused") {
|