litmus-cli 1.4.93 → 1.5.0-beta.1
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 +48 -0
- package/dist/commands/connect.d.ts +6 -2
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +222 -35
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/forward.d.ts +7 -1
- package/dist/commands/forward.d.ts.map +1 -1
- package/dist/commands/forward.js +8 -7
- package/dist/commands/forward.js.map +1 -1
- package/dist/lib/ai-tracking.d.ts +8 -0
- package/dist/lib/ai-tracking.d.ts.map +1 -1
- package/dist/lib/ai-tracking.js +79 -3
- package/dist/lib/ai-tracking.js.map +1 -1
- package/dist/lib/connect-skills.d.ts +1 -0
- package/dist/lib/connect-skills.d.ts.map +1 -1
- package/dist/lib/connect-skills.js +1 -1
- package/dist/lib/connect-skills.js.map +1 -1
- package/dist/lib/desktop-readiness.d.ts +9 -0
- package/dist/lib/desktop-readiness.d.ts.map +1 -0
- package/dist/lib/desktop-readiness.js +131 -0
- package/dist/lib/desktop-readiness.js.map +1 -0
- package/dist/lib/editor-foreground.d.ts +6 -0
- package/dist/lib/editor-foreground.d.ts.map +1 -0
- package/dist/lib/editor-foreground.js +38 -0
- package/dist/lib/editor-foreground.js.map +1 -0
- package/dist/lib/environment-checks.d.ts +2 -0
- package/dist/lib/environment-checks.d.ts.map +1 -1
- package/dist/lib/environment-checks.js +4 -2
- package/dist/lib/environment-checks.js.map +1 -1
- package/dist/lib/hosted-capture.d.ts +1 -1
- package/dist/lib/hosted-capture.d.ts.map +1 -1
- package/dist/lib/hosted-capture.js +22 -12
- package/dist/lib/hosted-capture.js.map +1 -1
- package/dist/lib/installed-tools.d.ts +29 -0
- package/dist/lib/installed-tools.d.ts.map +1 -0
- package/dist/lib/installed-tools.js +159 -0
- package/dist/lib/installed-tools.js.map +1 -0
- package/dist/lib/portal-launcher-background.d.ts +13 -0
- package/dist/lib/portal-launcher-background.d.ts.map +1 -0
- package/dist/lib/portal-launcher-background.js +51 -0
- package/dist/lib/portal-launcher-background.js.map +1 -0
- package/dist/lib/portal-launcher-worker.d.ts +2 -0
- package/dist/lib/portal-launcher-worker.d.ts.map +1 -0
- package/dist/lib/portal-launcher-worker.js +43 -0
- package/dist/lib/portal-launcher-worker.js.map +1 -0
- package/dist/lib/portal-launcher.d.ts +43 -0
- package/dist/lib/portal-launcher.d.ts.map +1 -0
- package/dist/lib/portal-launcher.js +408 -0
- package/dist/lib/portal-launcher.js.map +1 -0
- package/dist/lib/prepared-desktop.d.ts +7 -0
- package/dist/lib/prepared-desktop.d.ts.map +1 -0
- package/dist/lib/prepared-desktop.js +35 -0
- package/dist/lib/prepared-desktop.js.map +1 -0
- package/dist/lib/prepared-editor.d.ts +26 -0
- package/dist/lib/prepared-editor.d.ts.map +1 -0
- package/dist/lib/prepared-editor.js +150 -0
- package/dist/lib/prepared-editor.js.map +1 -0
- package/dist/lib/schemas.d.ts +1 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +2 -0
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/ssh-client.d.ts +12 -0
- package/dist/lib/ssh-client.d.ts.map +1 -1
- package/dist/lib/ssh-client.js +1 -1
- package/dist/lib/ssh-client.js.map +1 -1
- package/dist/lib/ssh-config-file.d.ts +3 -0
- package/dist/lib/ssh-config-file.d.ts.map +1 -1
- package/dist/lib/ssh-config-file.js +42 -0
- package/dist/lib/ssh-config-file.js.map +1 -1
- package/dist/lib/vscode-server-preparation.d.ts +7 -0
- package/dist/lib/vscode-server-preparation.d.ts.map +1 -0
- package/dist/lib/vscode-server-preparation.js +78 -0
- package/dist/lib/vscode-server-preparation.js.map +1 -0
- package/dist/lib/workspace-start-wait.d.ts +23 -0
- package/dist/lib/workspace-start-wait.d.ts.map +1 -0
- package/dist/lib/workspace-start-wait.js +50 -0
- package/dist/lib/workspace-start-wait.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,6 +80,52 @@ as `litmus push <token>` with the token from your assessment page. Either way it
|
|
|
80
80
|
captures the workspace exactly as it stands, so there is nothing to commit or push
|
|
81
81
|
first. The Submit button on your assessment page does the same thing.
|
|
82
82
|
|
|
83
|
+
Run `litmus connect <token>` on your laptop. It prepares
|
|
84
|
+
SSH, finds installed editors and desktop agents, then opens the assessment page.
|
|
85
|
+
The page's **Your tools** launcher enables the supported applications it finds;
|
|
86
|
+
you can open several tools against the same remote workspace. No local workspace
|
|
87
|
+
copy is created. Only tool identifiers and dispatch results are reported, not
|
|
88
|
+
application paths or process lists. After setup, the launcher runs in a detached
|
|
89
|
+
background process, so you can close the terminal. It stops when the assessment
|
|
90
|
+
ends, another connection replaces it, or its server authorization is lost (with
|
|
91
|
+
a 24-hour maximum lifetime). Run `litmus connect` again to reconnect. It does not
|
|
92
|
+
install an OS service or restart at login. Credentials are handed to the process
|
|
93
|
+
over IPC and are not written to a launcher settings or log file. Stopping the
|
|
94
|
+
launcher does not stop the workspace or submit your work.
|
|
95
|
+
|
|
96
|
+
The launcher waits for commands using outbound HTTP long polls (up to 25 seconds).
|
|
97
|
+
A committed launch wakes the request through PostgreSQL notifications; saved
|
|
98
|
+
commands recover missed notifications and interrupted connections. No incoming
|
|
99
|
+
laptop port is opened. Older backends retain five-second polling. Installed-app
|
|
100
|
+
discovery refreshes about every 30 seconds, separately from command delivery.
|
|
101
|
+
|
|
102
|
+
When capacity is full, connect reports that it is waiting for an available
|
|
103
|
+
workspace and retries allocation with backoff for up to two minutes. Once an
|
|
104
|
+
allocation starts, it only checks readiness. The portal distinguishes terminal
|
|
105
|
+
prompts, capacity waits, workspace startup and tool checks, and retains the last
|
|
106
|
+
known stage through a temporary polling failure.
|
|
107
|
+
|
|
108
|
+
VS Code and Cursor reuse the existing Remote-SSH setup and attachment checks.
|
|
109
|
+
Desktop buttons stay disabled if the app lacks its URI handler, Claude is older
|
|
110
|
+
than its supported SSH-link version, or the remote runtime/recording capability
|
|
111
|
+
probe fails. The page distinguishes an app update from a workspace setup problem.
|
|
112
|
+
The OS opener's exit status is checked; dispatch is not proof of an attached session.
|
|
113
|
+
Codex Desktop receives the SSH host, `/home/candidate/workspace`, and a request
|
|
114
|
+
to enable the connection and open that remote project automatically. Complete any
|
|
115
|
+
sign-in or trust prompt in Codex. Older versions may still require enabling the
|
|
116
|
+
host and selecting the folder manually. Claude Desktop receives the SSH host and
|
|
117
|
+
the same remote folder, then requires its own confirmation before using them
|
|
118
|
+
(requires SSH-link support, 2.110.0+). The portal reports **Open request sent** for
|
|
119
|
+
Codex and **Confirm in the app** for Claude, never an unverified claim that code
|
|
120
|
+
is saving.
|
|
121
|
+
Terminal / SSH opens a remote shell where you can run `claude` or `codex`.
|
|
122
|
+
The portal also retains manual connection instructions and its browser route.
|
|
123
|
+
Explicit `--ide vscode`, `--ide cursor`, `--ide ssh`, and `--ide browser` preserve
|
|
124
|
+
their existing behavior. On Linux, desktop-agent discovery is not yet supported;
|
|
125
|
+
PATH and known editor installations plus supported terminal launchers are detected.
|
|
126
|
+
Windows desktop discovery covers conventional installer locations; Store and
|
|
127
|
+
portable installations may require the manual route.
|
|
128
|
+
|
|
83
129
|
`litmus connect` also asks permission to record supported local AI-agent prompts
|
|
84
130
|
and responses across folders during the assessment, alongside hosted capture.
|
|
85
131
|
A background capture watcher stays on your computer after connect returns. It
|
|
@@ -328,3 +374,5 @@ The bundled Litmus companion is installed for this handoff. If it is unavailable
|
|
|
328
374
|
continue in the IDE and open Agents Window manually. Submission cleanup remains
|
|
329
375
|
opt-in with `--cursor-session-cleanup`; it disconnects the ended IDE and removes
|
|
330
376
|
its Open Recent entry, but does not delete saved chats or Agents sidebar entries.
|
|
377
|
+
|
|
378
|
+
The default portal launcher reports connection preparation before workspace startup. The portal shows an indeterminate progress bar until the workspace is ready; preparation sessions expire if the CLI stops unexpectedly.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PreparedLauncher } from "../lib/portal-launcher-background.js";
|
|
1
2
|
import { ConnectionAttempt } from "../lib/connection-attempt.js";
|
|
2
3
|
import { type ResolvedEditorCommand } from "../lib/editor-binary.js";
|
|
3
4
|
import type { NativeIde } from "../lib/editor-ide.js";
|
|
@@ -53,7 +54,7 @@ export declare function parseSshCommand(cmd: string): ParsedSsh;
|
|
|
53
54
|
* copy here is what applies when there is nothing global to lose to, and keeps this
|
|
54
55
|
* block correct on its own.
|
|
55
56
|
*/
|
|
56
|
-
export declare function buildSshConfigBlock(alias: string, keyPath: string, p: ParsedSsh, platform?: NodeJS.Platform): string;
|
|
57
|
+
export declare function buildSshConfigBlock(alias: string, keyPath: string, p: ParsedSsh, platform?: NodeJS.Platform, launcherObserve?: boolean): string;
|
|
57
58
|
/**
|
|
58
59
|
* ENG-2681 — an ssh_config argument, quoted so a space cannot split it.
|
|
59
60
|
*
|
|
@@ -499,6 +500,7 @@ export declare function openEditorLaunchLog(logPath: string): number | null;
|
|
|
499
500
|
*/
|
|
500
501
|
export interface EditorLaunchHandle {
|
|
501
502
|
launchFailure: () => string | null;
|
|
503
|
+
waitForDispatch?: () => Promise<void>;
|
|
502
504
|
}
|
|
503
505
|
/**
|
|
504
506
|
* ENG-3217: Windows Cursor 3.24.9 accepts --remote but routes it to the local
|
|
@@ -535,7 +537,7 @@ export declare function editorLaunchPlan(ed: NativeEditor, alias: string, platfo
|
|
|
535
537
|
* Whichever signal arrives first wins and neither overwrites the other, because both
|
|
536
538
|
* describe the same thing — no window was created — and the first is the more specific.
|
|
537
539
|
*/
|
|
538
|
-
export declare function launchEditor(ed: NativeEditor, command: string, alias: string, logPath: string): EditorLaunchHandle;
|
|
540
|
+
export declare function launchEditor(ed: NativeEditor, command: string, alias: string, logPath: string, foreground?: boolean): EditorLaunchHandle;
|
|
539
541
|
/**
|
|
540
542
|
* Point at the editor's own output, under `--verbose` only (ENG-2890: the failure copy
|
|
541
543
|
* says "Details: add --verbose." instead of printing a path by default).
|
|
@@ -787,4 +789,6 @@ export interface TunnelSession {
|
|
|
787
789
|
attempt: ConnectionAttempt;
|
|
788
790
|
}
|
|
789
791
|
export declare function runConnect(tokenArg: string | undefined, ideOpt?: string, opts?: ConnectOptions): Promise<void>;
|
|
792
|
+
/** Used only by the detached launcher after the interactive setup has completed. */
|
|
793
|
+
export declare function runPreparedPortalLauncher(context: PreparedLauncher, onReady: () => void, signal: AbortSignal): Promise<void>;
|
|
790
794
|
//# 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,14 +1,24 @@
|
|
|
1
|
+
import { configureClaudeDesktopConnection, removeClaudeDesktopConnection } from "../lib/ai-tracking.js";
|
|
2
|
+
import { waitForWorkspace } from "../lib/workspace-start-wait.js";
|
|
3
|
+
import { startBackgroundLauncher } from "../lib/portal-launcher-background.js";
|
|
4
|
+
import { desktopRuntimeReady } from "../lib/desktop-readiness.js";
|
|
5
|
+
import { startPortalPreparation, runPortalLauncher, openDesktop, openTerminal, openNativeEditor } from "../lib/portal-launcher.js";
|
|
1
6
|
import { startHostedCapture } from "../lib/hosted-capture.js";
|
|
2
7
|
import { installSshForwardHook } from "../lib/install-ssh-forward-hook.js";
|
|
3
8
|
import { runSsh } from "./ssh.js";
|
|
4
9
|
import { cursorSessionOptions, prepareCursorSession } from "../lib/cursor-session.js";
|
|
5
10
|
import { ConnectionAttempt } from "../lib/connection-attempt.js";
|
|
11
|
+
import { editorAppBundle, waitForEditorDispatch } from "../lib/editor-foreground.js";
|
|
12
|
+
import { createPreparedEditorLauncher, parseWorkspaceGeneration } from "../lib/prepared-editor.js";
|
|
13
|
+
import { createPreparedDesktopReadiness } from "../lib/prepared-desktop.js";
|
|
14
|
+
import { findInstalledExecutable } from "../lib/installed-tools.js";
|
|
15
|
+
import { prepareVscodeServer } from "../lib/vscode-server-preparation.js";
|
|
6
16
|
import { closedState } from "../lib/session-state.js";
|
|
7
|
-
import { updateSshConfig } from "../lib/ssh-config-file.js";
|
|
17
|
+
import { updateSshConfig, prepareSshConfigCleanup } from "../lib/ssh-config-file.js";
|
|
8
18
|
import os from "os";
|
|
9
19
|
import path from "path";
|
|
10
20
|
import readline from "readline";
|
|
11
|
-
import { closeSync, constants, existsSync, mkdirSync, openSync, readFileSync } from "fs";
|
|
21
|
+
import { closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, realpathSync } from "fs";
|
|
12
22
|
import { randomBytes } from "crypto";
|
|
13
23
|
import { execFileSync, spawn, spawnSync } from "child_process";
|
|
14
24
|
import chalk from "chalk";
|
|
@@ -136,7 +146,7 @@ export function parseSshCommand(cmd) {
|
|
|
136
146
|
* copy here is what applies when there is nothing global to lose to, and keeps this
|
|
137
147
|
* block correct on its own.
|
|
138
148
|
*/
|
|
139
|
-
export function buildSshConfigBlock(alias, keyPath, p, platform = process.platform) {
|
|
149
|
+
export function buildSshConfigBlock(alias, keyPath, p, platform = process.platform, launcherObserve = false) {
|
|
140
150
|
const common = [
|
|
141
151
|
` IdentityFile ${quoteConfigArg(keyPath)}`,
|
|
142
152
|
` IdentitiesOnly yes`,
|
|
@@ -146,10 +156,10 @@ export function buildSshConfigBlock(alias, keyPath, p, platform = process.platfo
|
|
|
146
156
|
const out = [CONNECT_BEGIN(alias)];
|
|
147
157
|
if (p.jumpHost) {
|
|
148
158
|
const bastionAlias = `${alias}-bastion`;
|
|
149
|
-
out.push(`Host ${bastionAlias}`, ` HostName ${p.jumpHost}`, ` Port ${p.jumpPort ?? "22"}`, ` User ${p.jumpUser}`, ...common, "", `Host ${alias}`, ` HostName ${p.host}`, ` User ${p.user}`, ` ProxyJump ${bastionAlias}`, ...common);
|
|
159
|
+
out.push(`Host ${bastionAlias}`, ` HostName ${p.jumpHost}`, ` Port ${p.jumpPort ?? "22"}`, ` User ${p.jumpUser}`, ...common, "", `Host ${alias}`, ` HostName ${p.host}`, ` User ${p.user}`, ` ProxyJump ${bastionAlias}`, ...(launcherObserve ? [` IgnoreUnknown SetEnv`, ` SetEnv LITMUS_LAUNCHER_OBSERVE=1`] : []), ...common);
|
|
150
160
|
}
|
|
151
161
|
else {
|
|
152
|
-
out.push(`Host ${alias}`, ` HostName ${p.host}`, ` User ${p.user}`, ...(p.port ? [` Port ${p.port}`] : []), ...common);
|
|
162
|
+
out.push(`Host ${alias}`, ` HostName ${p.host}`, ` User ${p.user}`, ...(p.port ? [` Port ${p.port}`] : []), ...(launcherObserve ? [` IgnoreUnknown SetEnv`, ` SetEnv LITMUS_LAUNCHER_OBSERVE=1`] : []), ...common);
|
|
153
163
|
}
|
|
154
164
|
out.push(CONNECT_END(alias));
|
|
155
165
|
return out.join("\n") + "\n";
|
|
@@ -829,7 +839,7 @@ export function editorLaunchPlan(ed, alias, platform = process.platform) {
|
|
|
829
839
|
* Whichever signal arrives first wins and neither overwrites the other, because both
|
|
830
840
|
* describe the same thing — no window was created — and the first is the more specific.
|
|
831
841
|
*/
|
|
832
|
-
export function launchEditor(ed, command, alias, logPath) {
|
|
842
|
+
export function launchEditor(ed, command, alias, logPath, foreground = false) {
|
|
833
843
|
const spec = editorSpawnSpec(command, editorLaunchPlan(ed, alias).args, process.platform);
|
|
834
844
|
if (!spec) {
|
|
835
845
|
// No argv drives this command here, so nothing was started and nothing will be. This
|
|
@@ -870,8 +880,20 @@ export function launchEditor(ed, command, alias, logPath) {
|
|
|
870
880
|
else if (code !== null && code !== 0)
|
|
871
881
|
record(`${ed.label} exited immediately with code ${code}`);
|
|
872
882
|
});
|
|
883
|
+
// The remote server can already be attached, so verification alone may finish
|
|
884
|
+
// before the local CLI has selected its window. Keep the helper alive through
|
|
885
|
+
// dispatch, then activate that installation (never reuse an unrelated window).
|
|
886
|
+
const bundle = foreground && process.platform === "darwin" ? editorAppBundle(command) : null;
|
|
887
|
+
const dispatched = bundle ? waitForEditorDispatch(child, () => {
|
|
888
|
+
try {
|
|
889
|
+
execFileSync("/usr/bin/open", ["-a", bundle], { timeout: 3000, stdio: "ignore" });
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
// Activation is best effort; a successful editor open remains usable.
|
|
893
|
+
}
|
|
894
|
+
}) : Promise.resolve();
|
|
873
895
|
child.unref();
|
|
874
|
-
return { launchFailure: () => failure };
|
|
896
|
+
return { launchFailure: () => failure, waitForDispatch: () => dispatched };
|
|
875
897
|
}
|
|
876
898
|
/**
|
|
877
899
|
* Point at the editor's own output, under `--verbose` only (ENG-2890: the failure copy
|
|
@@ -991,6 +1013,7 @@ export async function attemptEditorLaunch(ed, io) {
|
|
|
991
1013
|
const logPath = io.launchLogPath();
|
|
992
1014
|
const launched = io.launch(logPath);
|
|
993
1015
|
const verdict = await io.verifyAttached(baseline, launched);
|
|
1016
|
+
await launched.waitForDispatch?.();
|
|
994
1017
|
const launchLog = io.readLaunchLog(logPath);
|
|
995
1018
|
// Read AFTER the await, never before it: `spawn` reports this on a later tick, so a
|
|
996
1019
|
// check taken at the call site would still be null for a launch that had already
|
|
@@ -1121,6 +1144,9 @@ export function shouldResendCursorOpen(ide, startedFresh, waitedSeconds, resend)
|
|
|
1121
1144
|
export function resendableLaunch(launch) {
|
|
1122
1145
|
const sent = [];
|
|
1123
1146
|
const handle = {
|
|
1147
|
+
waitForDispatch: async () => {
|
|
1148
|
+
await Promise.all(sent.map((s) => s.handle.waitForDispatch?.()));
|
|
1149
|
+
},
|
|
1124
1150
|
launchFailure: () => {
|
|
1125
1151
|
const failures = sent.map((s) => s.handle.launchFailure());
|
|
1126
1152
|
return failures.length > 0 && failures.every((f) => f !== null) ? failures[failures.length - 1] : null;
|
|
@@ -1277,7 +1303,7 @@ function promptIdeAnswer() {
|
|
|
1277
1303
|
*/
|
|
1278
1304
|
function openBrowser(url) {
|
|
1279
1305
|
const spec = process.platform === "darwin" ? ["open", [url]]
|
|
1280
|
-
: process.platform === "win32" ? ["
|
|
1306
|
+
: process.platform === "win32" ? ["rundll32.exe", ["url.dll,FileProtocolHandler", url]]
|
|
1281
1307
|
: ["xdg-open", [url]];
|
|
1282
1308
|
return new Promise((resolve) => {
|
|
1283
1309
|
try {
|
|
@@ -1371,6 +1397,12 @@ export async function runConnect(tokenArg, ideOpt, opts = {}) {
|
|
|
1371
1397
|
}
|
|
1372
1398
|
finally {
|
|
1373
1399
|
try {
|
|
1400
|
+
if (process.env.LITMUS_PORTAL_LAUNCHER_CHILD === "1" && process.connected) {
|
|
1401
|
+
process.send?.({ type: "litmus-connect-result", result: attempt.outcome, ...(skillImport.preparedEditor ? { preparedEditor: skillImport.preparedEditor } : {}) }, () => { });
|
|
1402
|
+
}
|
|
1403
|
+
if (attempt.outcome !== "ssh_ready")
|
|
1404
|
+
skillImport.stopPreparation?.fail("connection_failed");
|
|
1405
|
+
await skillImport.stopPreparation?.();
|
|
1374
1406
|
if (attempt.outcome === "not_connected")
|
|
1375
1407
|
await skillImport.rollbackCapture?.();
|
|
1376
1408
|
await attempt.flush();
|
|
@@ -1385,6 +1417,10 @@ export async function runConnect(tokenArg, ideOpt, opts = {}) {
|
|
|
1385
1417
|
releaseExitHold();
|
|
1386
1418
|
}
|
|
1387
1419
|
}
|
|
1420
|
+
if (skillImport.launcher) {
|
|
1421
|
+
await skillImport.launcher();
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1388
1424
|
// The ssh route's session runs last, so nothing we print lands inside the remote shell.
|
|
1389
1425
|
if (!skillImport.sshAlias)
|
|
1390
1426
|
return;
|
|
@@ -1443,14 +1479,21 @@ async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
|
1443
1479
|
// carries only the assessment id, never the token.
|
|
1444
1480
|
const pageUrl = candidatePageUrl(apiBase, meta.assessmentId);
|
|
1445
1481
|
attempt.stage("selection");
|
|
1446
|
-
const
|
|
1482
|
+
const launcherBetaEnabled = meta.launcherBetaEnabled === true;
|
|
1483
|
+
const portalLauncher = launcherBetaEnabled && !ideOpt && !opts.tunnel;
|
|
1484
|
+
const { ide: chosenIde, chosen: ideWasChosen } = portalLauncher
|
|
1485
|
+
? { ide: "ssh", chosen: true }
|
|
1486
|
+
: await resolveIde(ideOpt, console.log);
|
|
1447
1487
|
let ide = chosenIde;
|
|
1448
1488
|
attempt.editor(ide);
|
|
1449
1489
|
// ENG-2814: asked here, next to the editor choice, and uploaded once the workspace is
|
|
1450
1490
|
// ready. Never fails the connect. See `lib/connect-skills.ts`.
|
|
1451
1491
|
attempt.stage("skills");
|
|
1492
|
+
if (portalLauncher)
|
|
1493
|
+
skillImport.stopPreparation = await startPortalPreparation(backendUrl, token);
|
|
1494
|
+
skillImport.stopPreparation?.update("waiting_input");
|
|
1452
1495
|
const pendingSkills = await offerSkillImport({
|
|
1453
|
-
backendUrl, token, flag: opts.skills, verbose,
|
|
1496
|
+
backendUrl, token, flag: opts.skills, verbose, launcherBetaEnabled,
|
|
1454
1497
|
});
|
|
1455
1498
|
attempt.stage("ssh_key");
|
|
1456
1499
|
// 2. Ensure a local SSH key (private key never leaves this machine).
|
|
@@ -1470,9 +1513,11 @@ async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
|
1470
1513
|
skillImport.rollbackCapture = await startHostedCapture(meta, token, apiBase);
|
|
1471
1514
|
// 3. Start the workspace with our public key.
|
|
1472
1515
|
attempt.stage("workspace_start");
|
|
1516
|
+
skillImport.stopPreparation?.update("starting_workspace");
|
|
1473
1517
|
const boot = ora("Starting your workspace...").start();
|
|
1518
|
+
let initialStart;
|
|
1474
1519
|
try {
|
|
1475
|
-
await startWorkspace(backendUrl, token, pubkey);
|
|
1520
|
+
initialStart = await startWorkspace(backendUrl, token, pubkey);
|
|
1476
1521
|
}
|
|
1477
1522
|
catch (err) {
|
|
1478
1523
|
attempt.evidence(workspaceStartNetworkDetail(err));
|
|
@@ -1511,35 +1556,62 @@ async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
|
1511
1556
|
// 4. Poll until healthy (cold provision is ~5s; give it up to ~2 min, longer while
|
|
1512
1557
|
// the backend still reports it `starting` — see `keepWaitingForWorkspace`).
|
|
1513
1558
|
let conn = null;
|
|
1514
|
-
|
|
1515
|
-
|
|
1559
|
+
let capacityTimedOut = false;
|
|
1560
|
+
if (!launcherBetaEnabled) {
|
|
1561
|
+
// Preserve the stable CLI's polling and retry contract outside enrollment.
|
|
1562
|
+
const pollStartedAt = Date.now();
|
|
1563
|
+
while (keepWaitingForWorkspace(Date.now() - pollStartedAt, conn?.workspace_status)) {
|
|
1564
|
+
try {
|
|
1565
|
+
conn = await getConnection(backendUrl, token);
|
|
1566
|
+
}
|
|
1567
|
+
catch { /* transient */ }
|
|
1568
|
+
if (conn?.healthy && (ide === "browser" || conn.ssh_command))
|
|
1569
|
+
break;
|
|
1570
|
+
const waited = Math.round((Date.now() - pollStartedAt) / 1000);
|
|
1571
|
+
boot.text = Date.now() - pollStartedAt < WORKSPACE_READY_WAIT_MS
|
|
1572
|
+
? `Starting your workspace... (${waited}s)`
|
|
1573
|
+
: `Still starting your workspace. This can take a few minutes. (${waited}s)`;
|
|
1574
|
+
await sleep(2000);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
else {
|
|
1516
1578
|
try {
|
|
1517
|
-
|
|
1579
|
+
const waited = await waitForWorkspace({
|
|
1580
|
+
initial: initialStart,
|
|
1581
|
+
start: () => startWorkspace(backendUrl, token, pubkey),
|
|
1582
|
+
connection: () => getConnection(backendUrl, token),
|
|
1583
|
+
ready: value => Boolean(value.healthy && (ide === "browser" || value.ssh_command)),
|
|
1584
|
+
permanentFailure: error => error instanceof ServerError && Boolean(error.statusCode && error.statusCode >= 400 && error.statusCode < 500 && ![408, 429].includes(error.statusCode)),
|
|
1585
|
+
keepWaiting: keepWaitingForWorkspace,
|
|
1586
|
+
progress: (stage, elapsed) => {
|
|
1587
|
+
skillImport.stopPreparation?.update(stage);
|
|
1588
|
+
const seconds = Math.round(elapsed / 1000);
|
|
1589
|
+
boot.text = stage === "waiting_capacity" ? `Waiting for an available workspace... (${seconds}s)`
|
|
1590
|
+
: elapsed < WORKSPACE_READY_WAIT_MS ? `Starting your workspace... (${seconds}s)`
|
|
1591
|
+
: `Still starting your workspace. This can take a few minutes. (${seconds}s)`;
|
|
1592
|
+
},
|
|
1593
|
+
});
|
|
1594
|
+
conn = waited.connection ?? null;
|
|
1595
|
+
capacityTimedOut = waited.capacityTimedOut;
|
|
1518
1596
|
}
|
|
1519
|
-
catch {
|
|
1520
|
-
|
|
1597
|
+
catch (error) {
|
|
1598
|
+
boot.stop();
|
|
1599
|
+
fatal(error instanceof Error ? error.message : "Could not check your workspace connection.", "Refresh your assessment page, then run the connection command again.", { severity: "warning" });
|
|
1600
|
+
return;
|
|
1521
1601
|
}
|
|
1522
|
-
// VS Code / SSH need the ssh_command. The browser URL is config-derived (it
|
|
1523
|
-
// appears together with `running`, not on a timer), so break on `healthy`
|
|
1524
|
-
// alone — the branch below then gives a fast, specific message when no URL is
|
|
1525
|
-
// configured, instead of spinning the full 120s on a v1 / pre-ingress box.
|
|
1526
|
-
const ready = conn?.healthy && (ide === "browser" || !!conn.ssh_command);
|
|
1527
|
-
if (ready)
|
|
1528
|
-
break;
|
|
1529
|
-
// ENG-2895: elapsed time, not the backend's state name (`not_created`, `provisioning`),
|
|
1530
|
-
// and no "fresh host": neither is something the candidate can act on.
|
|
1531
|
-
const waited = Math.round((Date.now() - pollStartedAt) / 1000);
|
|
1532
|
-
boot.text = Date.now() - pollStartedAt < WORKSPACE_READY_WAIT_MS
|
|
1533
|
-
? `Starting your workspace... (${waited}s)`
|
|
1534
|
-
: `Still starting your workspace. This can take a few minutes. (${waited}s)`;
|
|
1535
|
-
await sleep(2000);
|
|
1536
1602
|
}
|
|
1537
1603
|
if (!conn?.healthy || (ide !== "browser" && !conn.ssh_command)) {
|
|
1538
1604
|
boot.stop();
|
|
1539
|
-
|
|
1605
|
+
if (capacityTimedOut)
|
|
1606
|
+
skillImport.stopPreparation?.fail("capacity_timeout");
|
|
1607
|
+
fatal(capacityTimedOut ? "No workspace is available yet." : "Your workspace isn't ready yet.", capacityTimedOut ? "We couldn't allocate a workspace. Try the connection command again shortly or contact the assessment team." : "Run the same `litmus connect` command again in a minute. If it keeps happening, contact the company.", { severity: "warning" });
|
|
1540
1608
|
return;
|
|
1541
1609
|
}
|
|
1542
|
-
|
|
1610
|
+
if (portalLauncher)
|
|
1611
|
+
boot.stop();
|
|
1612
|
+
else
|
|
1613
|
+
boot.succeed("Workspace ready");
|
|
1614
|
+
skillImport.stopPreparation?.update("checking_connection");
|
|
1543
1615
|
attempt.stage("workspace_ready");
|
|
1544
1616
|
attempt.checkpoint();
|
|
1545
1617
|
if (pendingSkills)
|
|
@@ -1577,7 +1649,7 @@ async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
|
1577
1649
|
const parsed = parseSshCommand(conn.ssh_command);
|
|
1578
1650
|
const alias = aliasFor(conn.ssh_hostname, token);
|
|
1579
1651
|
pruneStaleHostKeys(parsed);
|
|
1580
|
-
const cfgPath = await writeSshConfig(alias, buildSshConfigBlock(alias, KEY_PATH, parsed), buildControlPathBlock(alias, parsed));
|
|
1652
|
+
const cfgPath = await writeSshConfig(alias, buildSshConfigBlock(alias, KEY_PATH, parsed, process.platform, launcherBetaEnabled), buildControlPathBlock(alias, parsed));
|
|
1581
1653
|
if (verbose)
|
|
1582
1654
|
info(`SSH config written to ${cfgPath} (Host "${alias}")`);
|
|
1583
1655
|
}
|
|
@@ -1633,7 +1705,7 @@ async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
|
1633
1705
|
attempt.stage("ssh_config");
|
|
1634
1706
|
const alias = aliasFor(conn.ssh_hostname, token);
|
|
1635
1707
|
pruneStaleHostKeys(parsed);
|
|
1636
|
-
const block = buildSshConfigBlock(alias, KEY_PATH, parsed);
|
|
1708
|
+
const block = buildSshConfigBlock(alias, KEY_PATH, parsed, process.platform, launcherBetaEnabled);
|
|
1637
1709
|
const cfgPath = await writeSshConfig(alias, block, buildControlPathBlock(alias, parsed));
|
|
1638
1710
|
if (verbose)
|
|
1639
1711
|
info(`SSH config written to ${cfgPath} (Host "${alias}")`);
|
|
@@ -1669,6 +1741,21 @@ async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
|
1669
1741
|
});
|
|
1670
1742
|
return true;
|
|
1671
1743
|
};
|
|
1744
|
+
if (portalLauncher) {
|
|
1745
|
+
if (await preflightStops("ssh"))
|
|
1746
|
+
return;
|
|
1747
|
+
attempt.finish("ssh_ready");
|
|
1748
|
+
skillImport.launcher = async () => {
|
|
1749
|
+
const ready = await startBackgroundLauncher({ backendUrl, token, alias, pageUrl, browserAvailable: Boolean(conn.browser_ide_url), assessmentName: meta.assessmentName });
|
|
1750
|
+
if (ready) {
|
|
1751
|
+
ora().succeed("Workspace ready · Laptop connected");
|
|
1752
|
+
console.log("\nLaunch your tools from the assessment page.");
|
|
1753
|
+
}
|
|
1754
|
+
else
|
|
1755
|
+
console.log(`Tool launching is unavailable on this deployment. Use your assessment page or ssh ${alias}.`);
|
|
1756
|
+
};
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1672
1759
|
// SSH option: open the shell here (no editor launch). Without a terminal on both ends
|
|
1673
1760
|
// there is no one to hand a shell to, so a piped or scripted run gets the command.
|
|
1674
1761
|
if (ide === "ssh") {
|
|
@@ -1879,6 +1966,11 @@ async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
|
1879
1966
|
// property of the SEQUENCE, so it is held in one testable unit rather than in the
|
|
1880
1967
|
// arrangement of the lines below. Everything this block still owns is output.
|
|
1881
1968
|
const probe = sshProbeRunner(alias, sshExecutable);
|
|
1969
|
+
if (editor.ide === "vscode" && resolved.version.commit) {
|
|
1970
|
+
const preparing = ora("Preparing VS Code for your workspace...").start();
|
|
1971
|
+
await prepareVscodeServer(alias, resolved.version.commit, new AbortController().signal);
|
|
1972
|
+
preparing.stop();
|
|
1973
|
+
}
|
|
1882
1974
|
// Read once for both the seed check and attachment baseline.
|
|
1883
1975
|
const readSpinner = ora(`Checking your workspace...`).start();
|
|
1884
1976
|
const baseline = probeWorkspace(editor, probe, resolved.version.commit);
|
|
@@ -1911,7 +2003,7 @@ async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
|
1911
2003
|
const spinners = { verify: null };
|
|
1912
2004
|
const resend = { done: false };
|
|
1913
2005
|
const launchPlan = editorLaunchPlan(editor, alias);
|
|
1914
|
-
const launches = resendableLaunch((logPath) => launchEditor(editor, resolved.command, alias, logPath));
|
|
2006
|
+
const launches = resendableLaunch((logPath) => launchEditor(editor, resolved.command, alias, logPath, launcherBetaEnabled));
|
|
1915
2007
|
// Local UI extension: it survives a remote-host disconnect. Never installed on
|
|
1916
2008
|
// VS Code, browser or plain SSH paths. Handoff defaults on for Mac/Windows.
|
|
1917
2009
|
if (editor.ide === "cursor") {
|
|
@@ -2005,6 +2097,18 @@ async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
|
2005
2097
|
attempt.evidence(outcome.launchLog);
|
|
2006
2098
|
if (outcome.verdict.kind === "connected") {
|
|
2007
2099
|
attempt.finish("server_detected");
|
|
2100
|
+
let generation = null;
|
|
2101
|
+
try {
|
|
2102
|
+
generation = parseWorkspaceGeneration(JSON.parse(process.env.LITMUS_LAUNCH_GENERATION ?? "null"));
|
|
2103
|
+
}
|
|
2104
|
+
catch { /* old launcher */ }
|
|
2105
|
+
const binary = findInstalledExecutable(resolved.command, process.platform, process.env);
|
|
2106
|
+
if (generation && binary && resolved.version.commit) {
|
|
2107
|
+
try {
|
|
2108
|
+
skillImport.preparedEditor = { tool: editor.ide, command: realpathSync(binary), commit: resolved.version.commit, alias, generation };
|
|
2109
|
+
}
|
|
2110
|
+
catch { /* no reusable binary */ }
|
|
2111
|
+
}
|
|
2008
2112
|
launchSpinner?.succeed(`${editor.label}'s server is running in your workspace.`);
|
|
2009
2113
|
skillImport.footer.push(...editorConnectedLines(editor, alias, verbose));
|
|
2010
2114
|
}
|
|
@@ -2043,4 +2147,87 @@ async function connectFlow(token, ideOpt, opts, skillImport, attempt) {
|
|
|
2043
2147
|
// which also carries why we do not simply close the window.
|
|
2044
2148
|
skillImport.footer.push(...editorEndingForewarning());
|
|
2045
2149
|
}
|
|
2150
|
+
/** Used only by the detached launcher after the interactive setup has completed. */
|
|
2151
|
+
export function runPreparedPortalLauncher(context, onReady, signal) {
|
|
2152
|
+
const checkedDesktopApps = new Set();
|
|
2153
|
+
const attemptedVscode = new Set();
|
|
2154
|
+
const native = createPreparedEditorLauncher(context.alias);
|
|
2155
|
+
const desktopReadiness = createPreparedDesktopReadiness(context.alias);
|
|
2156
|
+
const cleanupSsh = prepareSshConfigCleanup(path.join(os.homedir(), ".ssh", "config"), context.alias);
|
|
2157
|
+
return runPortalLauncher({
|
|
2158
|
+
...context, onReady, signal,
|
|
2159
|
+
onSubmitted: async () => {
|
|
2160
|
+
try {
|
|
2161
|
+
removeClaudeDesktopConnection(context.alias, context.assessmentName ?? "Assessment");
|
|
2162
|
+
}
|
|
2163
|
+
catch { /* Preserve settings if busy or malformed. */ }
|
|
2164
|
+
try {
|
|
2165
|
+
await cleanupSsh();
|
|
2166
|
+
}
|
|
2167
|
+
catch { /* Preserve config if busy or externally edited. */ }
|
|
2168
|
+
},
|
|
2169
|
+
openUrl: url => url === context.pageUrl ? Promise.resolve(true) : openBrowser(url),
|
|
2170
|
+
prepareTools: async (installed, signal) => Promise.all(installed.map(async (app) => {
|
|
2171
|
+
if (app.tool === "vscode" && !app.unavailable) {
|
|
2172
|
+
if (attemptedVscode.has(app.command))
|
|
2173
|
+
return app;
|
|
2174
|
+
attemptedVscode.add(app.command);
|
|
2175
|
+
const resolved = resolveEditorCommand(NATIVE_EDITORS.vscode);
|
|
2176
|
+
const commit = resolved?.version.commit;
|
|
2177
|
+
if (resolved && commit && identifyEditorCli(NATIVE_EDITORS.vscode, resolved.command).kind === "match") {
|
|
2178
|
+
await prepareVscodeServer(context.alias, commit, signal);
|
|
2179
|
+
}
|
|
2180
|
+
return app;
|
|
2181
|
+
}
|
|
2182
|
+
if (app.unavailable || (app.tool !== "codex" && app.tool !== "claude"))
|
|
2183
|
+
return app;
|
|
2184
|
+
const key = `${app.tool}:${app.command}`;
|
|
2185
|
+
if (checkedDesktopApps.has(key))
|
|
2186
|
+
return app;
|
|
2187
|
+
if (await desktopRuntimeReady(context.alias, app.tool, signal)) {
|
|
2188
|
+
if (app.tool === "claude") {
|
|
2189
|
+
try {
|
|
2190
|
+
configureClaudeDesktopConnection(context.alias, context.assessmentName ?? "Assessment");
|
|
2191
|
+
}
|
|
2192
|
+
catch {
|
|
2193
|
+
return { ...app, unavailable: "desktop_setup_failed" };
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
checkedDesktopApps.add(key);
|
|
2197
|
+
return app;
|
|
2198
|
+
}
|
|
2199
|
+
return { ...app, unavailable: "workspace_not_ready" };
|
|
2200
|
+
})),
|
|
2201
|
+
launch: async (tool, installed, signal, launchContext) => {
|
|
2202
|
+
if (tool === "vscode" || tool === "cursor") {
|
|
2203
|
+
return await native.launch(tool, launchContext?.generation, remember => openNativeEditor(tool, context.token, signal, undefined, remember, launchContext?.generation), signal);
|
|
2204
|
+
}
|
|
2205
|
+
if (tool === "browser") {
|
|
2206
|
+
const current = await getConnection(context.backendUrl, context.token);
|
|
2207
|
+
return current.browser_ide_url && await openBrowser(current.browser_ide_url) ? "dispatched" : "failed";
|
|
2208
|
+
}
|
|
2209
|
+
if (tool === "codex" || tool === "claude") {
|
|
2210
|
+
const app = installed.find(i => i.tool === tool);
|
|
2211
|
+
if (!app || app.unavailable || !await desktopReadiness.check(tool, app.command, launchContext?.generation, signal))
|
|
2212
|
+
return "failed";
|
|
2213
|
+
if (tool === "claude") {
|
|
2214
|
+
try {
|
|
2215
|
+
configureClaudeDesktopConnection(context.alias, context.assessmentName ?? "Assessment");
|
|
2216
|
+
}
|
|
2217
|
+
catch {
|
|
2218
|
+
return "failed";
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
// OS handoff is all the opener proves. Do not invent a confirmation
|
|
2222
|
+
// prompt or claim an active assessment connection from its exit code.
|
|
2223
|
+
const opened = await openDesktop(app, context.alias, signal);
|
|
2224
|
+
if (!opened)
|
|
2225
|
+
desktopReadiness.invalidate();
|
|
2226
|
+
return opened ? "dispatched" : "failed";
|
|
2227
|
+
}
|
|
2228
|
+
const terminal = installed.find(i => i.tool === "ssh");
|
|
2229
|
+
return terminal && await openTerminal(context.alias, terminal.command, launchContext?.requestId) ? "dispatched" : "failed";
|
|
2230
|
+
},
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2046
2233
|
//# sourceMappingURL=connect.js.map
|