litmus-cli 1.4.35 → 1.4.37
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 +28 -2
- package/dist/commands/connect.d.ts +59 -4
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +137 -12
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +3 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/lib/ai-tracking.d.ts +5 -0
- package/dist/lib/ai-tracking.d.ts.map +1 -1
- package/dist/lib/ai-tracking.js +24 -9
- package/dist/lib/ai-tracking.js.map +1 -1
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +6 -5
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/backfill-tools.d.ts +37 -1
- package/dist/lib/backfill-tools.d.ts.map +1 -1
- package/dist/lib/backfill-tools.js +63 -12
- package/dist/lib/backfill-tools.js.map +1 -1
- package/dist/lib/capture-selftest.d.ts +7 -2
- package/dist/lib/capture-selftest.d.ts.map +1 -1
- package/dist/lib/capture-selftest.js +4 -4
- package/dist/lib/capture-selftest.js.map +1 -1
- package/dist/lib/cursor-restart.js +1 -1
- package/dist/lib/cursor-restart.js.map +1 -1
- package/dist/lib/detection.d.ts +75 -11
- package/dist/lib/detection.d.ts.map +1 -1
- package/dist/lib/detection.js +259 -33
- package/dist/lib/detection.js.map +1 -1
- package/dist/lib/hook-logger.cjs +57 -60
- package/dist/lib/platform.d.ts +19 -6
- package/dist/lib/platform.d.ts.map +1 -1
- package/dist/lib/platform.js +40 -9
- package/dist/lib/platform.js.map +1 -1
- package/dist/lib/ssh-client.d.ts +64 -0
- package/dist/lib/ssh-client.d.ts.map +1 -0
- package/dist/lib/ssh-client.js +128 -0
- package/dist/lib/ssh-client.js.map +1 -0
- package/dist/lib/watcher.js +4 -41
- package/dist/lib/watcher.js.map +1 -1
- package/dist/lib/wsl.d.ts +49 -0
- package/dist/lib/wsl.d.ts.map +1 -0
- package/dist/lib/wsl.js +99 -0
- package/dist/lib/wsl.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,7 +102,8 @@ While you're working, the background tracker sends activity data to Litmus
|
|
|
102
102
|
in addition to the submission ZIP you upload at the end:
|
|
103
103
|
|
|
104
104
|
- File-change events, git commits, and AI-tool usage detected on your machine
|
|
105
|
-
(
|
|
105
|
+
(the names of editors and AI tools among your own user's running programs,
|
|
106
|
+
network endpoints) — every few seconds while you work.
|
|
106
107
|
- Captured AI prompts and responses from Claude Code, Codex CLI, Cursor, the
|
|
107
108
|
Gemini CLI, and Copilot CLI via those tools' hooks. `litmus init` installs
|
|
108
109
|
the hooks and marks them trusted for the assessment folder (Codex's per-hook
|
|
@@ -110,7 +111,11 @@ in addition to the submission ZIP you upload at the end:
|
|
|
110
111
|
trusted in Claude Code (`~/.claude.json`) and Codex (`[projects]` in
|
|
111
112
|
`~/.codex/config.toml`), so no tool asks you to approve the hooks or the
|
|
112
113
|
folder; `litmus submit` removes the hooks and every trust entry again. Copilot
|
|
113
|
-
Chat
|
|
114
|
+
Chat in VS Code is captured through the same `.github/hooks/litmus.json` hook
|
|
115
|
+
as the Copilot CLI, when the assessment folder is trusted. This was checked on
|
|
116
|
+
VS Code 1.131 with Copilot Chat 0.59.0 in agent mode. Coverage of every mode
|
|
117
|
+
has not been fully measured: other Copilot Chat modes, and the `copilot` CLI
|
|
118
|
+
beyond one interactive prompt (including `copilot -p`), have not been checked.
|
|
114
119
|
- A periodic codebase snapshot (every five minutes) so the optional
|
|
115
120
|
live-interview agent has up-to-date context. Snapshots are capped at 2 MB
|
|
116
121
|
total and skip binaries, dotfiles (including `.env`), lockfiles,
|
|
@@ -127,6 +132,27 @@ in addition to the submission ZIP you upload at the end:
|
|
|
127
132
|
You can confirm what's installed and what's being captured at any point with
|
|
128
133
|
`litmus status`.
|
|
129
134
|
|
|
135
|
+
## Where the AI hooks live and what they record
|
|
136
|
+
|
|
137
|
+
`litmus init` installs the hooks for Claude Code, Codex, Cursor and the Gemini
|
|
138
|
+
CLI in your user-level config for each tool: `~/.claude/settings.json`,
|
|
139
|
+
`~/.codex/hooks.json`, `~/.cursor/hooks.json` and `~/.gemini/settings.json`.
|
|
140
|
+
The Copilot hook is the exception: it lives inside the assessment folder, in
|
|
141
|
+
`.github/hooks/litmus.json`.
|
|
142
|
+
|
|
143
|
+
Because they are user-level, these hooks run for every prompt you send to those
|
|
144
|
+
tools on this machine, not only prompts made inside the assessment. Each run
|
|
145
|
+
checks where the prompt came from. A prompt made inside the assessment folder is
|
|
146
|
+
recorded. A prompt made anywhere else, including a folder above the assessment
|
|
147
|
+
such as your home folder, is not written down or sent to Litmus. So open the
|
|
148
|
+
assessment folder itself in your editor, or `cd` into it before starting an AI
|
|
149
|
+
tool, when you want that work captured.
|
|
150
|
+
|
|
151
|
+
The hooks are removed when you run `litmus submit`. If you leave an assessment
|
|
152
|
+
unfinished, they stay installed until you remove them, and prompts made inside
|
|
153
|
+
that assessment's folder are still recorded there. You can remove them yourself
|
|
154
|
+
at any time by deleting the Litmus entries from the files above.
|
|
155
|
+
|
|
130
156
|
## Requirements
|
|
131
157
|
|
|
132
158
|
- Node.js 18+
|
|
@@ -40,7 +40,8 @@ export declare function parseSshCommand(cmd: string): ParsedSsh;
|
|
|
40
40
|
* This does NOT turn multiplexing on; with no ControlMaster the directive is inert.
|
|
41
41
|
* It only makes sure that a candidate who HAS enabled it multiplexes over a path that
|
|
42
42
|
* fits. Every stanza gets it, because a ProxyJump opens its own connection to the
|
|
43
|
-
* bastion and that one is the long path.
|
|
43
|
+
* bastion and that one is the long path. "Inert" holds everywhere EXCEPT Windows, where
|
|
44
|
+
* any ControlPath at all breaks the connection — see `controlPathFor` (ENG-2676).
|
|
44
45
|
*
|
|
45
46
|
* WRITING THE DIRECTIVE IS ONLY HALF OF IT. ssh takes the FIRST value it sees for a
|
|
46
47
|
* keyword, so a global `Host *` sitting earlier in the file beats anything we write
|
|
@@ -50,7 +51,23 @@ export declare function parseSshCommand(cmd: string): ParsedSsh;
|
|
|
50
51
|
* copy here is what applies when there is nothing global to lose to, and keeps this
|
|
51
52
|
* block correct on its own.
|
|
52
53
|
*/
|
|
53
|
-
export declare function buildSshConfigBlock(alias: string, keyPath: string, p: ParsedSsh): string;
|
|
54
|
+
export declare function buildSshConfigBlock(alias: string, keyPath: string, p: ParsedSsh, platform?: NodeJS.Platform): string;
|
|
55
|
+
/**
|
|
56
|
+
* ENG-2681 — an ssh_config argument, quoted so a space cannot split it.
|
|
57
|
+
*
|
|
58
|
+
* `keyPath` is built from the candidate's home directory, and a home with a space in it
|
|
59
|
+
* (`C:\Users\<two-word account name>`, common for Windows local accounts) split the unquoted line into
|
|
60
|
+
* two arguments. ssh does not skip a bad line; it rejects the whole file:
|
|
61
|
+
*
|
|
62
|
+
* line 6: keyword identityfile extra arguments at end of line
|
|
63
|
+
* terminating, 1 bad configuration options
|
|
64
|
+
*
|
|
65
|
+
* so every ssh the candidate ran, `git push` to GitHub included, failed until our block
|
|
66
|
+
* was deleted. Measured with `ssh -G`: the quoted form resolves to the path exactly as
|
|
67
|
+
* written, spaces and Windows backslashes (`\U`, `\.`) intact. A double quote cannot
|
|
68
|
+
* appear in a Windows path and has no portable escape in ssh_config, so it is not handled.
|
|
69
|
+
*/
|
|
70
|
+
export declare function quoteConfigArg(value: string): string;
|
|
54
71
|
/**
|
|
55
72
|
* A stanza carrying ONE directive, written ahead of the candidate's own config.
|
|
56
73
|
*
|
|
@@ -66,8 +83,33 @@ export declare function buildSshConfigBlock(alias: string, keyPath: string, p: P
|
|
|
66
83
|
* losing to the user's settings exactly as it did before, and a directive added to the
|
|
67
84
|
* main block later cannot silently inherit precedence it was never reviewed for. Keep
|
|
68
85
|
* this stanza to ControlPath alone.
|
|
86
|
+
*
|
|
87
|
+
* On Windows the same precedence is what carries ENG-2676, and it has to go further: its
|
|
88
|
+
* `none` must beat EVERY ControlPath the candidate has, preamble included, because there
|
|
89
|
+
* any ControlPath is fatal rather than merely too long. So the Windows stanza goes at the
|
|
90
|
+
* very top of the file (`upsertSshConfigContent`) and closes with a bare `Host *`, which
|
|
91
|
+
* hands everything after it — the candidate's preamble — back to every host exactly as
|
|
92
|
+
* before. Nothing of theirs moves; our one directive is simply read first.
|
|
93
|
+
*/
|
|
94
|
+
export declare function buildControlPathBlock(alias: string, p: ParsedSsh, platform?: NodeJS.Platform): string;
|
|
95
|
+
/**
|
|
96
|
+
* The ControlPath we write: `~/.ssh/cm-%C` (ENG-1936), except `none` on Windows.
|
|
97
|
+
*
|
|
98
|
+
* ENG-2676. Win32-OpenSSH has no Unix-domain socket to multiplex over, so ANY ControlPath
|
|
99
|
+
* — with or without a ControlMaster — kills the connection before it offers a key:
|
|
100
|
+
*
|
|
101
|
+
* debug1: getpeername failed: Not a socket
|
|
102
|
+
* getsockname failed: Not a socket
|
|
103
|
+
* Read from remote host <cid>.vm.litmushiring.com: Unknown error
|
|
104
|
+
* Transferred: sent 0, received 0 bytes, in 0.0 seconds
|
|
105
|
+
*
|
|
106
|
+
* VS Code shows it as "Could not establish connection". Because the client dies before
|
|
107
|
+
* the bastion ever sees a key, nothing on our side logs it: no `resolve-ssh` call, no
|
|
108
|
+
* backend error, a healthy container. `none` switches sharing off outright, which is
|
|
109
|
+
* the only setting that works there. The socket-length problem `%C` exists for is a
|
|
110
|
+
* macOS one and cannot arise on Windows, since there is no socket.
|
|
69
111
|
*/
|
|
70
|
-
export declare function
|
|
112
|
+
export declare function controlPathFor(platform: NodeJS.Platform): string;
|
|
71
113
|
/**
|
|
72
114
|
* Replace (or insert) this alias's regions in the ~/.ssh/config content. Idempotent:
|
|
73
115
|
* re-running `connect` swaps them and never touches the rest of the user's config.
|
|
@@ -82,8 +124,14 @@ export declare function buildControlPathBlock(alias: string, p: ParsedSsh): stri
|
|
|
82
124
|
* (or in an `Include`d file pulled in before their first stanza) is still read first and
|
|
83
125
|
* still wins. Beating it would mean reordering the candidate's own config, which is not
|
|
84
126
|
* a thing `litmus connect` should do to fix its own socket path.
|
|
127
|
+
*
|
|
128
|
+
* Except on Windows (ENG-2676), where that residual is a dead connection rather than a
|
|
129
|
+
* long path. There the control block goes at the very top instead, which reorders
|
|
130
|
+
* nothing: it closes with `Host *`, so the preamble after it still applies to every
|
|
131
|
+
* host, and only our one ControlPath is read ahead of it. `platform` must match the one
|
|
132
|
+
* `buildControlPathBlock` was given; both default to the running platform.
|
|
85
133
|
*/
|
|
86
|
-
export declare function upsertSshConfigContent(existing: string, alias: string, block: string, controlBlock?: string): string;
|
|
134
|
+
export declare function upsertSshConfigContent(existing: string, alias: string, block: string, controlBlock?: string, platform?: NodeJS.Platform): string;
|
|
87
135
|
/** Short, stable alias from the workspace hostname (or token as a fallback). */
|
|
88
136
|
export declare function aliasFor(sshHostname: string | null | undefined, token: string): string;
|
|
89
137
|
/**
|
|
@@ -575,6 +623,13 @@ export declare function resolveIde(flag: string | undefined, log?: (line: string
|
|
|
575
623
|
ide: IdeChoice;
|
|
576
624
|
chosen: boolean;
|
|
577
625
|
}>;
|
|
626
|
+
/**
|
|
627
|
+
* Whether the post-start poll should keep going. The ordinary window covers every
|
|
628
|
+
* status; only a workspace the backend still reports `starting` earns the longer one,
|
|
629
|
+
* so a start that was answered `queued` (the row is back to `not_created`) is not held
|
|
630
|
+
* for eight minutes against something that is not coming.
|
|
631
|
+
*/
|
|
632
|
+
export declare function keepWaitingForWorkspace(elapsedMs: number, status: string | undefined): boolean;
|
|
578
633
|
/**
|
|
579
634
|
* The candidate's own assessment page.
|
|
580
635
|
*
|
|
@@ -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
|
@@ -10,8 +10,10 @@ import { fetchInitMetadata, startWorkspace, getConnection, fetchEditorChatConfig
|
|
|
10
10
|
import { applyEditorChatConfig } from "../lib/chat-model-config.js";
|
|
11
11
|
import { resolveApiBase } from "../lib/api-base.js";
|
|
12
12
|
import { editorInstallCandidates, editorSpawnSpec } from "../lib/editor-binary.js";
|
|
13
|
+
import { isWsl, whichOnPath, windowsSideEditor, wslEditorHint } from "../lib/wsl.js";
|
|
13
14
|
import { parseEditorVersion, parseWorkspaceProbe, remoteProbeCommand, seedVerdict, verifyVerdict, VERIFY_SCHEDULE_MS, } from "../lib/editor-server.js";
|
|
14
15
|
import { fatal, success, info, warn, setErrorContext } from "../lib/errors.js";
|
|
16
|
+
import { oldWindowsSshHint, parseOpenSshVersion, proxyJumpBroken, readEditorSettings, readSshVersionBanner, sshExecutableFor, } from "../lib/ssh-client.js";
|
|
15
17
|
import { editorEndingForewarning } from "../lib/session-end.js";
|
|
16
18
|
import { announceCursorAutoRestart, attemptQuit, platformSupportsGracefulCursorRestart, realCursorProcessIo, renderCursorRestartOutcome, } from "../lib/cursor-restart.js";
|
|
17
19
|
/**
|
|
@@ -114,7 +116,8 @@ export function parseSshCommand(cmd) {
|
|
|
114
116
|
* This does NOT turn multiplexing on; with no ControlMaster the directive is inert.
|
|
115
117
|
* It only makes sure that a candidate who HAS enabled it multiplexes over a path that
|
|
116
118
|
* fits. Every stanza gets it, because a ProxyJump opens its own connection to the
|
|
117
|
-
* bastion and that one is the long path.
|
|
119
|
+
* bastion and that one is the long path. "Inert" holds everywhere EXCEPT Windows, where
|
|
120
|
+
* any ControlPath at all breaks the connection — see `controlPathFor` (ENG-2676).
|
|
118
121
|
*
|
|
119
122
|
* WRITING THE DIRECTIVE IS ONLY HALF OF IT. ssh takes the FIRST value it sees for a
|
|
120
123
|
* keyword, so a global `Host *` sitting earlier in the file beats anything we write
|
|
@@ -124,12 +127,12 @@ export function parseSshCommand(cmd) {
|
|
|
124
127
|
* copy here is what applies when there is nothing global to lose to, and keeps this
|
|
125
128
|
* block correct on its own.
|
|
126
129
|
*/
|
|
127
|
-
export function buildSshConfigBlock(alias, keyPath, p) {
|
|
130
|
+
export function buildSshConfigBlock(alias, keyPath, p, platform = process.platform) {
|
|
128
131
|
const common = [
|
|
129
|
-
` IdentityFile ${keyPath}`,
|
|
132
|
+
` IdentityFile ${quoteConfigArg(keyPath)}`,
|
|
130
133
|
` IdentitiesOnly yes`,
|
|
131
134
|
` StrictHostKeyChecking accept-new`,
|
|
132
|
-
` ControlPath
|
|
135
|
+
` ControlPath ${controlPathFor(platform)}`,
|
|
133
136
|
];
|
|
134
137
|
const out = [CONNECT_BEGIN(alias)];
|
|
135
138
|
if (p.jumpHost) {
|
|
@@ -142,6 +145,24 @@ export function buildSshConfigBlock(alias, keyPath, p) {
|
|
|
142
145
|
out.push(CONNECT_END(alias));
|
|
143
146
|
return out.join("\n") + "\n";
|
|
144
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* ENG-2681 — an ssh_config argument, quoted so a space cannot split it.
|
|
150
|
+
*
|
|
151
|
+
* `keyPath` is built from the candidate's home directory, and a home with a space in it
|
|
152
|
+
* (`C:\Users\<two-word account name>`, common for Windows local accounts) split the unquoted line into
|
|
153
|
+
* two arguments. ssh does not skip a bad line; it rejects the whole file:
|
|
154
|
+
*
|
|
155
|
+
* line 6: keyword identityfile extra arguments at end of line
|
|
156
|
+
* terminating, 1 bad configuration options
|
|
157
|
+
*
|
|
158
|
+
* so every ssh the candidate ran, `git push` to GitHub included, failed until our block
|
|
159
|
+
* was deleted. Measured with `ssh -G`: the quoted form resolves to the path exactly as
|
|
160
|
+
* written, spaces and Windows backslashes (`\U`, `\.`) intact. A double quote cannot
|
|
161
|
+
* appear in a Windows path and has no portable escape in ssh_config, so it is not handled.
|
|
162
|
+
*/
|
|
163
|
+
export function quoteConfigArg(value) {
|
|
164
|
+
return `"${value}"`;
|
|
165
|
+
}
|
|
145
166
|
/** A line that opens a conditional block, i.e. the point after which anything we write
|
|
146
167
|
* is no longer first for the keywords a preceding stanza already set. */
|
|
147
168
|
const OPENS_A_STANZA = /^[ \t]*(Host|Match)[ \t=]/i;
|
|
@@ -160,18 +181,57 @@ const OPENS_A_STANZA = /^[ \t]*(Host|Match)[ \t=]/i;
|
|
|
160
181
|
* losing to the user's settings exactly as it did before, and a directive added to the
|
|
161
182
|
* main block later cannot silently inherit precedence it was never reviewed for. Keep
|
|
162
183
|
* this stanza to ControlPath alone.
|
|
184
|
+
*
|
|
185
|
+
* On Windows the same precedence is what carries ENG-2676, and it has to go further: its
|
|
186
|
+
* `none` must beat EVERY ControlPath the candidate has, preamble included, because there
|
|
187
|
+
* any ControlPath is fatal rather than merely too long. So the Windows stanza goes at the
|
|
188
|
+
* very top of the file (`upsertSshConfigContent`) and closes with a bare `Host *`, which
|
|
189
|
+
* hands everything after it — the candidate's preamble — back to every host exactly as
|
|
190
|
+
* before. Nothing of theirs moves; our one directive is simply read first.
|
|
163
191
|
*/
|
|
164
|
-
export function buildControlPathBlock(alias, p) {
|
|
192
|
+
export function buildControlPathBlock(alias, p, platform = process.platform) {
|
|
165
193
|
const hosts = p.jumpHost ? `${alias} ${alias}-bastion` : alias;
|
|
194
|
+
if (platform === "win32") {
|
|
195
|
+
return [
|
|
196
|
+
CONTROL_BEGIN(alias),
|
|
197
|
+
`# ControlPath only — first in the file because Windows OpenSSH cannot share connections,`,
|
|
198
|
+
`# and any ControlPath (yours or ours) breaks it. The Host * line hands everything after`,
|
|
199
|
+
`# it back to every host, so your own settings apply exactly as before.`,
|
|
200
|
+
`Host ${hosts}`,
|
|
201
|
+
` ControlPath ${controlPathFor(platform)}`,
|
|
202
|
+
`Host *`,
|
|
203
|
+
CONTROL_END(alias),
|
|
204
|
+
].join("\n") + "\n";
|
|
205
|
+
}
|
|
166
206
|
return [
|
|
167
207
|
CONTROL_BEGIN(alias),
|
|
168
208
|
`# ControlPath only — ahead of your own config so a global ControlPath cannot build`,
|
|
169
209
|
`# an over-long socket path out of the workspace id. Nothing else is overridden.`,
|
|
170
210
|
`Host ${hosts}`,
|
|
171
|
-
` ControlPath
|
|
211
|
+
` ControlPath ${controlPathFor(platform)}`,
|
|
172
212
|
CONTROL_END(alias),
|
|
173
213
|
].join("\n") + "\n";
|
|
174
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* The ControlPath we write: `~/.ssh/cm-%C` (ENG-1936), except `none` on Windows.
|
|
217
|
+
*
|
|
218
|
+
* ENG-2676. Win32-OpenSSH has no Unix-domain socket to multiplex over, so ANY ControlPath
|
|
219
|
+
* — with or without a ControlMaster — kills the connection before it offers a key:
|
|
220
|
+
*
|
|
221
|
+
* debug1: getpeername failed: Not a socket
|
|
222
|
+
* getsockname failed: Not a socket
|
|
223
|
+
* Read from remote host <cid>.vm.litmushiring.com: Unknown error
|
|
224
|
+
* Transferred: sent 0, received 0 bytes, in 0.0 seconds
|
|
225
|
+
*
|
|
226
|
+
* VS Code shows it as "Could not establish connection". Because the client dies before
|
|
227
|
+
* the bastion ever sees a key, nothing on our side logs it: no `resolve-ssh` call, no
|
|
228
|
+
* backend error, a healthy container. `none` switches sharing off outright, which is
|
|
229
|
+
* the only setting that works there. The socket-length problem `%C` exists for is a
|
|
230
|
+
* macOS one and cannot arise on Windows, since there is no socket.
|
|
231
|
+
*/
|
|
232
|
+
export function controlPathFor(platform) {
|
|
233
|
+
return platform === "win32" ? "none" : "~/.ssh/cm-%C";
|
|
234
|
+
}
|
|
175
235
|
/** Drop one marker-delimited region, wherever in the file it sits. */
|
|
176
236
|
function withoutRegion(existing, begin, end) {
|
|
177
237
|
const bi = existing.indexOf(begin);
|
|
@@ -196,6 +256,13 @@ function insertAhead(existing, block) {
|
|
|
196
256
|
const after = lines.slice(at).join("\n");
|
|
197
257
|
return (preamble.trim().length ? preamble + "\n\n" : "") + block + "\n" + after;
|
|
198
258
|
}
|
|
259
|
+
/** Put `block` at the very top of the file. Only for a block that closes with `Host *`
|
|
260
|
+
* (the Windows ControlPath stanza), which is what keeps the preamble below it global. */
|
|
261
|
+
function prepend(existing, block) {
|
|
262
|
+
if (!existing.trim().length)
|
|
263
|
+
return block;
|
|
264
|
+
return block + "\n" + existing.replace(/^\n+/, "");
|
|
265
|
+
}
|
|
199
266
|
/** Exactly one blank line before the block, however the prior content ended. Normalised
|
|
200
267
|
* rather than merely separated, so re-running never accretes blank lines where a
|
|
201
268
|
* removed region used to be — the output has to be a fixed point. */
|
|
@@ -218,12 +285,20 @@ function append(existing, block) {
|
|
|
218
285
|
* (or in an `Include`d file pulled in before their first stanza) is still read first and
|
|
219
286
|
* still wins. Beating it would mean reordering the candidate's own config, which is not
|
|
220
287
|
* a thing `litmus connect` should do to fix its own socket path.
|
|
288
|
+
*
|
|
289
|
+
* Except on Windows (ENG-2676), where that residual is a dead connection rather than a
|
|
290
|
+
* long path. There the control block goes at the very top instead, which reorders
|
|
291
|
+
* nothing: it closes with `Host *`, so the preamble after it still applies to every
|
|
292
|
+
* host, and only our one ControlPath is read ahead of it. `platform` must match the one
|
|
293
|
+
* `buildControlPathBlock` was given; both default to the running platform.
|
|
221
294
|
*/
|
|
222
|
-
export function upsertSshConfigContent(existing, alias, block, controlBlock) {
|
|
295
|
+
export function upsertSshConfigContent(existing, alias, block, controlBlock, platform = process.platform) {
|
|
223
296
|
let out = withoutRegion(existing, CONNECT_BEGIN(alias), CONNECT_END(alias));
|
|
224
297
|
out = withoutRegion(out, CONTROL_BEGIN(alias), CONTROL_END(alias));
|
|
225
298
|
out = append(out, block);
|
|
226
|
-
|
|
299
|
+
if (!controlBlock)
|
|
300
|
+
return out;
|
|
301
|
+
return platform === "win32" ? prepend(out, controlBlock) : insertAhead(out, controlBlock);
|
|
227
302
|
}
|
|
228
303
|
/** Short, stable alias from the workspace hostname (or token as a fallback). */
|
|
229
304
|
export function aliasFor(sshHostname, token) {
|
|
@@ -1045,6 +1120,28 @@ export async function resolveIde(flag, log = console.log, reentry) {
|
|
|
1045
1120
|
announceCursorFullQuit(log, reentry);
|
|
1046
1121
|
return { ide, chosen: answered.trim() !== "" };
|
|
1047
1122
|
}
|
|
1123
|
+
// How long `litmus connect` waits for a started workspace to become reachable.
|
|
1124
|
+
const WORKSPACE_READY_WAIT_MS = 120000;
|
|
1125
|
+
// ENG-2578: and how long while the backend still says `starting`. A host that has not
|
|
1126
|
+
// pulled the pinned image yet pulls it inside the first provision it gets (measured at
|
|
1127
|
+
// 221-264s for one variant after a pin moved), so `/workspace/start` answers `starting`
|
|
1128
|
+
// at its own 120s deadline and the backend keeps finishing the provision in the
|
|
1129
|
+
// background for `_LATE_PROVISION_TIMEOUT` more (backend/services/shared_host_workspace.py,
|
|
1130
|
+
// 480s). Wait that long, plus a margin for the write that lands it. Nothing enforces the
|
|
1131
|
+
// pair across the language boundary; keep them in step.
|
|
1132
|
+
const SERVER_LATE_PROVISION_MS = 480000;
|
|
1133
|
+
const WORKSPACE_STARTING_WAIT_MS = SERVER_LATE_PROVISION_MS + 30000;
|
|
1134
|
+
/**
|
|
1135
|
+
* Whether the post-start poll should keep going. The ordinary window covers every
|
|
1136
|
+
* status; only a workspace the backend still reports `starting` earns the longer one,
|
|
1137
|
+
* so a start that was answered `queued` (the row is back to `not_created`) is not held
|
|
1138
|
+
* for eight minutes against something that is not coming.
|
|
1139
|
+
*/
|
|
1140
|
+
export function keepWaitingForWorkspace(elapsedMs, status) {
|
|
1141
|
+
if (elapsedMs < WORKSPACE_READY_WAIT_MS)
|
|
1142
|
+
return true;
|
|
1143
|
+
return status === "starting" && elapsedMs < WORKSPACE_STARTING_WAIT_MS;
|
|
1144
|
+
}
|
|
1048
1145
|
/**
|
|
1049
1146
|
* The candidate's own assessment page.
|
|
1050
1147
|
*
|
|
@@ -1410,10 +1507,11 @@ export async function runConnect(token, ideOpt, opts = {}) {
|
|
|
1410
1507
|
});
|
|
1411
1508
|
return;
|
|
1412
1509
|
}
|
|
1413
|
-
// 4. Poll until healthy (cold provision is ~5s; give it up to ~2 min
|
|
1510
|
+
// 4. Poll until healthy (cold provision is ~5s; give it up to ~2 min, longer while
|
|
1511
|
+
// the backend still reports it `starting` — see `keepWaitingForWorkspace`).
|
|
1414
1512
|
let conn = null;
|
|
1415
|
-
const
|
|
1416
|
-
while (Date.now()
|
|
1513
|
+
const pollStartedAt = Date.now();
|
|
1514
|
+
while (keepWaitingForWorkspace(Date.now() - pollStartedAt, conn?.workspace_status)) {
|
|
1417
1515
|
try {
|
|
1418
1516
|
conn = await getConnection(backendUrl, token);
|
|
1419
1517
|
}
|
|
@@ -1427,7 +1525,9 @@ export async function runConnect(token, ideOpt, opts = {}) {
|
|
|
1427
1525
|
const ready = conn?.healthy && (ide === "browser" || !!conn.ssh_command);
|
|
1428
1526
|
if (ready)
|
|
1429
1527
|
break;
|
|
1430
|
-
boot.text =
|
|
1528
|
+
boot.text = Date.now() - pollStartedAt < WORKSPACE_READY_WAIT_MS
|
|
1529
|
+
? `Starting your workspace... (${conn?.workspace_status ?? "provisioning"})`
|
|
1530
|
+
: "Still starting your workspace... the first start on a fresh host can take a few minutes";
|
|
1431
1531
|
await sleep(2000);
|
|
1432
1532
|
}
|
|
1433
1533
|
if (!conn?.healthy || (ide !== "browser" && !conn.ssh_command)) {
|
|
@@ -1473,6 +1573,20 @@ export async function runConnect(token, ideOpt, opts = {}) {
|
|
|
1473
1573
|
// Prune stale host keys first so a re-provisioned container's new key is
|
|
1474
1574
|
// accepted, not refused (ENG-1221).
|
|
1475
1575
|
const parsed = parseSshCommand(conn.ssh_command);
|
|
1576
|
+
// ENG-2682 — Windows' own OpenSSH below 8.1 cannot run the ProxyJump every bastion
|
|
1577
|
+
// route depends on (see `lib/ssh-client.ts`). Refuse before writing anything, with the
|
|
1578
|
+
// version named and a way out, rather than hand an editor a connection it will lose
|
|
1579
|
+
// before it reaches us. The client judged is the one this route will run: the editor's
|
|
1580
|
+
// `remote.SSH.path` when it names one, PATH's ssh otherwise.
|
|
1581
|
+
if (process.platform === "win32" && parsed.jumpHost && ide !== "browser") {
|
|
1582
|
+
const executable = sshExecutableFor(ide, ide === "ssh" ? null : readEditorSettings(ide));
|
|
1583
|
+
const client = parseOpenSshVersion(readSshVersionBanner(executable) ?? "");
|
|
1584
|
+
if (client && proxyJumpBroken(client)) {
|
|
1585
|
+
const where = executable === "ssh" ? "" : ` at ${executable}, set in remote.SSH.path`;
|
|
1586
|
+
fatal(`Your Windows SSH (${client.label}${where}) is too old to reach the workspace.`, oldWindowsSshHint(Boolean(conn.browser_ide_url)), { severity: "warning" });
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1476
1590
|
const alias = aliasFor(conn.ssh_hostname, token);
|
|
1477
1591
|
pruneStaleHostKeys(parsed);
|
|
1478
1592
|
const block = buildSshConfigBlock(alias, KEY_PATH, parsed);
|
|
@@ -1496,6 +1610,17 @@ export async function runConnect(token, ideOpt, opts = {}) {
|
|
|
1496
1610
|
// installed" on a machine where the binary was sitting in the application bundle the
|
|
1497
1611
|
// whole time. Null here means we genuinely could not find a way to drive it.
|
|
1498
1612
|
const resolved = resolveEditorCommand(editor);
|
|
1613
|
+
// ENG-2683 — inside WSL, the editor on PATH is usually the WINDOWS app, and its
|
|
1614
|
+
// Remote-SSH reads Windows' ssh config, not the WSL one written above (see
|
|
1615
|
+
// `lib/wsl.ts`). Launching it would fail in the editor with nothing on our side, so say
|
|
1616
|
+
// so here, where both the editor and the alias are known.
|
|
1617
|
+
if (resolved && isWsl()) {
|
|
1618
|
+
const program = whichOnPath(resolved.command, process.env.PATH);
|
|
1619
|
+
if (program && windowsSideEditor(program)) {
|
|
1620
|
+
fatal(`${editor.label} here is the Windows app, but \`litmus connect\` is running inside WSL.`, wslEditorHint(editor.label, alias, Boolean(conn.browser_ide_url)), { severity: "warning" });
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1499
1624
|
// ENG-2152 — resolve WHAT THAT COMMAND ACTUALLY IS before anything acts on the choice.
|
|
1500
1625
|
// `bin` is a name on PATH, and on a machine where Cursor is the default editor `code`
|
|
1501
1626
|
// can be Cursor's own CLI. The verdict is taken here, above the Copilot Chat step,
|