litmus-cli 1.4.28 → 1.4.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/connect.d.ts +360 -14
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +767 -70
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/doctor.d.ts +52 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +76 -7
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +6 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/pause.d.ts +13 -8
- package/dist/commands/pause.d.ts.map +1 -1
- package/dist/commands/pause.js +18 -10
- package/dist/commands/pause.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +6 -2
- package/dist/commands/push.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/api-base.d.ts +80 -0
- package/dist/lib/api-base.d.ts.map +1 -1
- package/dist/lib/api-base.js +83 -0
- package/dist/lib/api-base.js.map +1 -1
- package/dist/lib/editor-binary.d.ts +110 -0
- package/dist/lib/editor-binary.d.ts.map +1 -0
- package/dist/lib/editor-binary.js +72 -0
- package/dist/lib/editor-binary.js.map +1 -0
- package/dist/lib/editor-ide.d.ts +11 -0
- package/dist/lib/editor-ide.d.ts.map +1 -0
- package/dist/lib/editor-ide.js +2 -0
- package/dist/lib/editor-ide.js.map +1 -0
- package/dist/lib/editor-server.d.ts +253 -0
- package/dist/lib/editor-server.d.ts.map +1 -0
- package/dist/lib/editor-server.js +286 -0
- package/dist/lib/editor-server.js.map +1 -0
- package/dist/lib/watcher.js +18 -1
- package/dist/lib/watcher.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
/**
|
|
3
|
+
* Where each editor's CLI lives when its installer put it there, per platform.
|
|
4
|
+
*
|
|
5
|
+
* Paths are built with the TARGET's separator (`path.win32` / `path.posix`) and never
|
|
6
|
+
* with the bare `path`, which is bound to the host: a Windows expectation assembled with
|
|
7
|
+
* a POSIX `path` passes on a POSIX host precisely when the shipped code is wrong there.
|
|
8
|
+
* That is `chat-model-config.ts`'s `pathFor` rule, and the tests beside this assert
|
|
9
|
+
* literal strings for the same reason.
|
|
10
|
+
*/
|
|
11
|
+
export function editorInstallCandidates(ide, platform, env, home) {
|
|
12
|
+
if (platform === "darwin") {
|
|
13
|
+
const bundle = ide === "cursor" ? "Cursor.app" : "Visual Studio Code.app";
|
|
14
|
+
const bin = ide === "cursor" ? "cursor" : "code";
|
|
15
|
+
// A user-local install under ~/Applications is what a drag-install without admin
|
|
16
|
+
// rights produces, and is invisible to anything that only looks at /Applications.
|
|
17
|
+
return ["/Applications", path.posix.join(home, "Applications")].map((root) => path.posix.join(root, bundle, "Contents", "Resources", "app", "bin", bin));
|
|
18
|
+
}
|
|
19
|
+
if (platform === "win32") {
|
|
20
|
+
const local = env.LOCALAPPDATA;
|
|
21
|
+
const programFiles = env.ProgramFiles;
|
|
22
|
+
const programFilesX86 = env["ProgramFiles(x86)"];
|
|
23
|
+
const out = [];
|
|
24
|
+
if (ide === "cursor") {
|
|
25
|
+
// Cursor's Windows installer is per-user by default; the machine-wide option
|
|
26
|
+
// lands under Program Files with the same tree beneath it.
|
|
27
|
+
if (local)
|
|
28
|
+
out.push(path.win32.join(local, "Programs", "cursor", "resources", "app", "bin", "cursor.cmd"));
|
|
29
|
+
if (programFiles)
|
|
30
|
+
out.push(path.win32.join(programFiles, "cursor", "resources", "app", "bin", "cursor.cmd"));
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
if (local)
|
|
34
|
+
out.push(path.win32.join(local, "Programs", "Microsoft VS Code", "bin", "code.cmd"));
|
|
35
|
+
if (programFiles)
|
|
36
|
+
out.push(path.win32.join(programFiles, "Microsoft VS Code", "bin", "code.cmd"));
|
|
37
|
+
if (programFilesX86)
|
|
38
|
+
out.push(path.win32.join(programFilesX86, "Microsoft VS Code", "bin", "code.cmd"));
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
// Linux. Package installs and the vendor tarballs; an AppImage left in ~/Downloads is
|
|
43
|
+
// not reachable from here and is the documented residual.
|
|
44
|
+
const bin = ide === "cursor" ? "cursor" : "code";
|
|
45
|
+
const roots = ide === "cursor"
|
|
46
|
+
? ["/usr/share/cursor/bin", "/opt/Cursor/bin", "/opt/cursor/bin", path.posix.join(home, ".local", "bin")]
|
|
47
|
+
: ["/usr/share/code/bin", "/opt/visual-studio-code/bin", "/snap/bin", path.posix.join(home, ".local", "bin")];
|
|
48
|
+
return roots.map((root) => path.posix.join(root, bin));
|
|
49
|
+
}
|
|
50
|
+
const SHELL_SAFE_ARG = /^[A-Za-z0-9_@+=:,./\\-]+$/;
|
|
51
|
+
/**
|
|
52
|
+
* What a quoted `cmd.exe` token cannot carry. A `"` closes the quoting this opens, `%`
|
|
53
|
+
* is expanded inside quotes, and a control character can end the line outright.
|
|
54
|
+
* Everything else — spaces, `&`, `|`, `^`, parentheses — is literal once quoted, which
|
|
55
|
+
* is why the command may hold them and an unquoted ARGUMENT may not.
|
|
56
|
+
*/
|
|
57
|
+
// eslint-disable-next-line no-control-regex
|
|
58
|
+
const WIN32_UNQUOTABLE_COMMAND = /["%\u0000-\u001f]/;
|
|
59
|
+
export function editorSpawnSpec(command, args, platform) {
|
|
60
|
+
if (platform !== "win32")
|
|
61
|
+
return { command, args, shell: false };
|
|
62
|
+
for (const a of args) {
|
|
63
|
+
if (!SHELL_SAFE_ARG.test(a))
|
|
64
|
+
throw new Error(`refusing to build a shell command line around: ${a}`);
|
|
65
|
+
}
|
|
66
|
+
// Not an error — see the block comment. A command we cannot quote is a command we
|
|
67
|
+
// cannot drive, which is the same answer as a command that will not run.
|
|
68
|
+
if (WIN32_UNQUOTABLE_COMMAND.test(command))
|
|
69
|
+
return null;
|
|
70
|
+
return { command: `"${command}"`, args, shell: true };
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=editor-binary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editor-binary.js","sourceRoot":"","sources":["../../src/lib/editor-binary.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native editors `litmus connect` can drive.
|
|
3
|
+
*
|
|
4
|
+
* It lives here rather than in `commands/connect.ts` only so that the lib modules the
|
|
5
|
+
* connect flow leans on — `editor-binary.ts`, and anything after it — can name an editor
|
|
6
|
+
* without importing the command back and closing a cycle. `connect.ts` re-exports it, so
|
|
7
|
+
* the record it belongs to (`NATIVE_EDITORS`, which carries every measured difference
|
|
8
|
+
* between the two) is still the one place that describes them.
|
|
9
|
+
*/
|
|
10
|
+
export type NativeIde = "vscode" | "cursor";
|
|
11
|
+
//# sourceMappingURL=editor-ide.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editor-ide.d.ts","sourceRoot":"","sources":["../../src/lib/editor-ide.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editor-ide.js","sourceRoot":"","sources":["../../src/lib/editor-ide.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ENG-2332 — whether a native editor can, and then DID, start its server inside the
|
|
3
|
+
* workspace. Everything here is pure or takes its one side effect as an argument, so the
|
|
4
|
+
* decisions are testable without a Cursor install and without a live container.
|
|
5
|
+
*
|
|
6
|
+
* ## The failure this exists to stop
|
|
7
|
+
*
|
|
8
|
+
* A VS Code-family editor opening a Remote-SSH window does not run anything of ours: it
|
|
9
|
+
* copies its OWN server build into the remote home and launches it there. Which build is
|
|
10
|
+
* decided by the CLIENT, by commit, and the two editors are pinned to each other — a
|
|
11
|
+
* 3.19.13 client will not talk to a 3.20.21 server, it fetches its own.
|
|
12
|
+
*
|
|
13
|
+
* A Codespaces v2 container has sealed egress. It cannot reach `api2.cursor.sh`,
|
|
14
|
+
* `downloads.cursor.com` or `github.com` (measured from inside a prod workspace,
|
|
15
|
+
* 2026-09-15: `curl` returns 000 after a 12s timeout). The only route a non-baked client
|
|
16
|
+
* has is the mirror (`infra/vscode-mirror/`, ENG-1062 for VS Code, ENG-1265 + ENG-1936
|
|
17
|
+
* for Cursor), and a mirror route that has stopped matching what a client asks for is
|
|
18
|
+
* indistinguishable, from the client's side, from no route at all.
|
|
19
|
+
*
|
|
20
|
+
* What the client does then is the whole problem: it gives up and opens the window
|
|
21
|
+
* anyway. MEASURED on macOS against a live prod workspace (ENG-2332):
|
|
22
|
+
*
|
|
23
|
+
* - `cursor --remote ssh-remote+<alias> <dir>` exits 0 and prints nothing.
|
|
24
|
+
* - A window appears, carrying the remote authority — `storage.json` records
|
|
25
|
+
* `"remoteAuthority": "ssh-remote+<alias>"` — and Cursor's ordinary agent home
|
|
26
|
+
* screen. It is indistinguishable from a working one.
|
|
27
|
+
* - The renderer log says `Started local extension host`, and every
|
|
28
|
+
* `$updateShellExecCapabilities` is refused as `non-authoritative host`.
|
|
29
|
+
* - Nothing under `~/.cursor-server` on the VM is touched. No `data/logs` is created.
|
|
30
|
+
* No server process exists.
|
|
31
|
+
*
|
|
32
|
+
* So the candidate works in what looks like their editor, on their own laptop's files,
|
|
33
|
+
* and NOTHING IS RECORDED. That is the worst outcome this product has, and until this
|
|
34
|
+
* module every signal the CLI printed said it had worked.
|
|
35
|
+
*
|
|
36
|
+
* ## The two questions, and why they are both asked
|
|
37
|
+
*
|
|
38
|
+
* `seedVerdict` asks BEFORE launching: is the client's commit the one the image baked?
|
|
39
|
+
* `verifyVerdict` asks AFTER: did a server actually come up? Neither replaces the other.
|
|
40
|
+
* The first cannot be conclusive (the mirror may well serve a drifted client, and does
|
|
41
|
+
* for VS Code), and the second cannot be prompt — it costs the candidate up to a minute
|
|
42
|
+
* of waiting and, on a refusal, they have already opened a window. Asking both means the
|
|
43
|
+
* common, knowable failure is refused in advance and every other one is still loud.
|
|
44
|
+
*
|
|
45
|
+
* ## Why the probe is one shell command, and what it costs
|
|
46
|
+
*
|
|
47
|
+
* Each probe is `ssh <alias> <command>`, which sshd serves by exec'ing the candidate's
|
|
48
|
+
* login shell — so `/etc/ld.so.preload`'s `libttee.so` tees it and it lands in
|
|
49
|
+
* `candidate_activity_logs` as a `terminal_command`. That is acceptable and is NOT
|
|
50
|
+
* silent: the probe carries no pty, so `libttee`'s `tty_origin()` stamps it
|
|
51
|
+
* `origin: "tool"` (ENG-2179), which the read side groups as machinery rather than as
|
|
52
|
+
* something the candidate typed, and which the idle clock (ENG-2167) reads as a
|
|
53
|
+
* connection and never as work. It is still a row per probe, so the schedule below is
|
|
54
|
+
* deliberately sparse and backs off rather than polling on a fixed short interval.
|
|
55
|
+
*/
|
|
56
|
+
/** What one probe of the workspace answered. Every field is three-valued on purpose. */
|
|
57
|
+
export interface WorkspaceProbe {
|
|
58
|
+
/**
|
|
59
|
+
* The server commit the IMAGE baked, from the cache pointer, or null when the pointer
|
|
60
|
+
* is absent or unreadable. Null is "we do not know", never "there is no bundle".
|
|
61
|
+
*/
|
|
62
|
+
seedCommit: string | null;
|
|
63
|
+
/** How many processes the workspace is running out of this editor's server tree. */
|
|
64
|
+
serverProcesses: number;
|
|
65
|
+
/**
|
|
66
|
+
* Newest entry under `<serverHome>/data/logs`, or null when that directory does not
|
|
67
|
+
* exist. The names are timestamps (`20260915T200622`), so lexicographic order is
|
|
68
|
+
* chronological order and no `stat` is needed.
|
|
69
|
+
*/
|
|
70
|
+
newestLogDir: string | null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The command a probe runs in the workspace. POSIX `sh` only, and built from two
|
|
74
|
+
* constants we own — never from anything a candidate or a company supplies.
|
|
75
|
+
*
|
|
76
|
+
* Four details are load-bearing, and three of them are about one thing: `ps` output is
|
|
77
|
+
* full of near-misses, and a false POSITIVE here certifies a window that records nothing.
|
|
78
|
+
*
|
|
79
|
+
* The LEADING DOT keeps the pattern off the browser IDE — `~/.openvscode-server` contains
|
|
80
|
+
* `vscode-server` but not `/.vscode-server/`, and the browser IDE's node process is
|
|
81
|
+
* running on every healthy container, so a pattern without the dot answers "connected"
|
|
82
|
+
* for a native editor that never attached. The `[.]` bracket keeps it off the `grep` in
|
|
83
|
+
* that same listing, and the ANCHOR keeps it off everything that merely names the path.
|
|
84
|
+
* See the comments in the body for the measurements behind each.
|
|
85
|
+
*
|
|
86
|
+
* THE HOME SPELLING IS NOT THE ONLY ONE A HEALTHY CONTAINER PRODUCES, and missing the
|
|
87
|
+
* other one is silent. `litmus-firstboot.sh`'s `prewarm_editor` installs the baked bundle
|
|
88
|
+
* as a SYMLINK — `~/.<editor>-server/cli/servers/Stable-<commit>/server` ->
|
|
89
|
+
* `/opt/<editor>-server-cache/<commit>` — and the VS Code-family server bootstrap
|
|
90
|
+
* resolves its own root with `readlink -f "$0"` before exec'ing node, so on a PREWARMED
|
|
91
|
+
* (commit-matching, i.e. healthy) container argv[0] can be the `/opt` path. This repo
|
|
92
|
+
* already records both spellings for exactly these trees:
|
|
93
|
+
* `frontend/lib/server/tool-command-origin.ts`'s `SYSTEM_EDITOR_MARKERS` lists
|
|
94
|
+
* `/opt/vscode-server-cache/` and `/opt/cursor-server-cache/` beside `/.vscode-server/`.
|
|
95
|
+
* So `serverProcessPattern` carries both as alternatives, and every property above still
|
|
96
|
+
* holds of the added one: it is anchored at argv[0], so a candidate's `ls
|
|
97
|
+
* /opt/cursor-server-cache` is still not a server; it still cannot self-match, because
|
|
98
|
+
* the probe's own argv[0] is `sh` and the cache root reaches the command only as a shell
|
|
99
|
+
* VARIABLE, never as a literal followed by a slash; and unlike the home spelling that
|
|
100
|
+
* tree is root-owned on a read-only rootfs, so a candidate cannot put a binary there to
|
|
101
|
+
* forge one.
|
|
102
|
+
*
|
|
103
|
+
* WHAT IS STILL UNMEASURED, stated rather than implied: the controls behind this pattern
|
|
104
|
+
* are synthetic argv lines, taken on a laptop and not against a real prewarmed container.
|
|
105
|
+
* `ps -eo args=` on a live attached workspace, for both editors, is still to be confirmed.
|
|
106
|
+
* Adding the alternative is the safe direction in the meantime — it can only restore a
|
|
107
|
+
* signal that would otherwise be silent on the HEALTHY path, and the failure it prevents
|
|
108
|
+
* is the false alarm `verifyVerdict` names: a candidate who re-runs `litmus connect` with
|
|
109
|
+
* a window already open joins the running server, writes no new log directory, and would
|
|
110
|
+
* be told their work is not recorded.
|
|
111
|
+
*
|
|
112
|
+
* The banner is printed FIRST and unconditionally. `ssh` mixes the remote command's
|
|
113
|
+
* stdout with anything the login shell's rc files print, and a candidate's own `.bashrc`
|
|
114
|
+
* can write whatever it likes; a run that did not reach the banner is not a reading.
|
|
115
|
+
*
|
|
116
|
+
* `ps -eo args=` and not `pgrep`: procps is what the container has (the browser IDE's own
|
|
117
|
+
* monitor loop runs `ps -ax -o …`, ENG-2143), and `pgrep` is not guaranteed. A missing
|
|
118
|
+
* `ps` yields `proc 0`, which is the safe direction — it can only withhold a "connected"
|
|
119
|
+
* verdict, never manufacture one.
|
|
120
|
+
*/
|
|
121
|
+
/**
|
|
122
|
+
* The argv[0] pattern the probe hands `grep -E`, as ONE derivation used two ways.
|
|
123
|
+
*
|
|
124
|
+
* `remoteProbeCommand` calls it with the shell EXPRESSIONS that expand to the two names
|
|
125
|
+
* (`${d#.}` and `${c}`), so the alternation reaches the container as a pattern built from
|
|
126
|
+
* shell variables; a test calls it with the literal names and gets the ERE the container
|
|
127
|
+
* will actually run. Keeping both behind one function is what stops the anchor or the
|
|
128
|
+
* alternation being right in one form and wrong in the other.
|
|
129
|
+
*/
|
|
130
|
+
export declare function serverProcessPattern(serverHomeName: string, cacheRoot: string): string;
|
|
131
|
+
/**
|
|
132
|
+
* The names under `<serverHome>/data/logs` this probe will consider, as an ERE.
|
|
133
|
+
*
|
|
134
|
+
* The maximum is taken lexicographically, which is chronological ONLY over timestamps —
|
|
135
|
+
* and in C collation every letter sorts above every digit, so one stray `.log`, lock file
|
|
136
|
+
* or future named subdirectory becomes the maximum forever. The direction that breaks in
|
|
137
|
+
* is the bad one: `newestLogDir` stops advancing, the log signal dies silently, and a
|
|
138
|
+
* genuinely attached editor reads as `not-connected` — the false alarm at a working
|
|
139
|
+
* candidate that the process signal exists to prevent. So the invariant the field's
|
|
140
|
+
* docstring states is ENFORCED here rather than assumed, and a directory holding nothing
|
|
141
|
+
* timestamp-shaped answers "no logs", which is a missing input.
|
|
142
|
+
*
|
|
143
|
+
* The prefix is anchored and the tail is not: a build that appends a suffix to the
|
|
144
|
+
* timestamp still sorts chronologically, while a name that does not START with one cannot
|
|
145
|
+
* poison the maximum.
|
|
146
|
+
*/
|
|
147
|
+
export declare const LOG_DIR_NAME_ERE = "^[0-9]{8}T[0-9]{6}";
|
|
148
|
+
export declare function remoteProbeCommand(serverHomeDir: string, seedCommitPath: string): string;
|
|
149
|
+
/**
|
|
150
|
+
* Read a probe's stdout. Returns null for anything that is not a complete reading —
|
|
151
|
+
* a missing banner, a missing field, a non-commit `seed` — because a partial probe that
|
|
152
|
+
* degrades into a WorkspaceProbe with plausible zeros is a probe that reports "no server
|
|
153
|
+
* is running" when what happened is that the probe did not run.
|
|
154
|
+
*/
|
|
155
|
+
export declare function parseWorkspaceProbe(stdout: string | null): WorkspaceProbe | null;
|
|
156
|
+
/** What `<editor> --version` says about itself. */
|
|
157
|
+
export interface EditorVersion {
|
|
158
|
+
version: string | null;
|
|
159
|
+
commit: string | null;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Read `code --version` / `cursor --version`. Both print exactly three lines — version,
|
|
163
|
+
* commit, arch — and NEITHER prints a product name (the measurement is in
|
|
164
|
+
* `NATIVE_EDITORS`' block comment in `connect.ts`, and is why identity is read off
|
|
165
|
+
* `--help` instead). The commit is found by SHAPE rather than by line number, so a build
|
|
166
|
+
* that adds a line does not silently shift which field we read; the version is the first
|
|
167
|
+
* line that is not the commit.
|
|
168
|
+
*/
|
|
169
|
+
export declare function parseEditorVersion(stdout: string | null): EditorVersion;
|
|
170
|
+
/**
|
|
171
|
+
* Whether the client will find its own server already in the workspace.
|
|
172
|
+
*
|
|
173
|
+
* `unknown` is the answer to every missing input, and the direction that fails in is the
|
|
174
|
+
* point. We can positively assert only one thing — "these two commits are both readable
|
|
175
|
+
* and they differ" — so an unreadable `--version`, an unreachable workspace, an image
|
|
176
|
+
* with no pointer and an editor that names its build some other way may only move the
|
|
177
|
+
* answer towards "we could not tell", never towards a refusal. Refusing on a probe that
|
|
178
|
+
* failed for its own reasons would take a working editor away from a candidate on the
|
|
179
|
+
* strength of nothing; proceeding leaves them where they were before this existed, and
|
|
180
|
+
* `verifyVerdict` below is what still makes that loud.
|
|
181
|
+
*/
|
|
182
|
+
export type SeedVerdict = {
|
|
183
|
+
kind: "match";
|
|
184
|
+
commit: string;
|
|
185
|
+
} | {
|
|
186
|
+
kind: "mismatch";
|
|
187
|
+
clientCommit: string;
|
|
188
|
+
clientVersion: string | null;
|
|
189
|
+
seedCommit: string;
|
|
190
|
+
} | {
|
|
191
|
+
kind: "unknown";
|
|
192
|
+
reason: "no-client-commit" | "no-workspace-reading" | "no-seed";
|
|
193
|
+
};
|
|
194
|
+
export declare function seedVerdict(client: EditorVersion, probe: WorkspaceProbe | null): SeedVerdict;
|
|
195
|
+
/**
|
|
196
|
+
* Did a server come up because of THIS launch?
|
|
197
|
+
*
|
|
198
|
+
* THE TWO SIGNALS ARE READ DIFFERENTLY, and which is which is the whole of this.
|
|
199
|
+
*
|
|
200
|
+
* A LOG DIRECTORY IS AN ARTIFACT ON DISK, so it must be NEW. The home mount persists
|
|
201
|
+
* across park, resume and re-provision, so `<serverHome>/data/logs` from a connection
|
|
202
|
+
* last Tuesday is still sitting there today; a verdict keyed on its mere existence
|
|
203
|
+
* answers "connected" for the exact candidate this ticket is about — one whose second
|
|
204
|
+
* `litmus connect` attaches to nothing. That is why the caller takes a BASELINE probe
|
|
205
|
+
* before launching and hands it back here.
|
|
206
|
+
*
|
|
207
|
+
* SO A MISSING BASELINE DISABLES THAT SIGNAL ENTIRELY, and reading it as "there was no
|
|
208
|
+
* log directory before" is the same false reassurance arriving by a different road. The
|
|
209
|
+
* baseline probe is one `ssh`, and every way it can fail — a flaky link, a key that
|
|
210
|
+
* wants a passphrase, a container mid-resume — answers null; a candidate who has
|
|
211
|
+
* connected to this workspace before still has last Tuesday's `data/logs` sitting in the
|
|
212
|
+
* persistent home, so a null baseline plus a dead launch would be certified "connected"
|
|
213
|
+
* on an artifact this launch did not create. A missing input may only move the answer
|
|
214
|
+
* towards "we could not tell", which is the posture every other verdict in this module
|
|
215
|
+
* already takes, so with no baseline only a RUNNING PROCESS may conclude anything and
|
|
216
|
+
* everything else is `unverified` — never `not-connected`, because we have no reading to
|
|
217
|
+
* call anything new against.
|
|
218
|
+
*
|
|
219
|
+
* And the comparison is `>` rather than `!==`: the names are timestamps, so a directory
|
|
220
|
+
* that merely CHANGED (an editor pruning its own logs, a name that sorts earlier) is not
|
|
221
|
+
* evidence that this launch wrote one.
|
|
222
|
+
*
|
|
223
|
+
* A RUNNING PROCESS IS A STATEMENT ABOUT NOW, and needs no baseline. Park is `docker
|
|
224
|
+
* stop`, so every process in the container dies with it and one that is running belongs
|
|
225
|
+
* to this container's current run — nothing stale can be seen here. Requiring it to be
|
|
226
|
+
* new as well is what an earlier cut of this did, and it is wrong in the direction that
|
|
227
|
+
* matters: a candidate who already has a window open and runs `litmus connect` again
|
|
228
|
+
* gets a second window attached to the SAME server, creating no process and no log
|
|
229
|
+
* directory, and would have been told their work was not being recorded. Telling a
|
|
230
|
+
* working candidate to abandon their editor is worse than the residual, which is narrow
|
|
231
|
+
* and named: a server already up that this particular window fails to join reads as
|
|
232
|
+
* connected. For Cursor the one known cause of that — a build mismatch — has already
|
|
233
|
+
* refused the launch before this is ever asked.
|
|
234
|
+
*/
|
|
235
|
+
export type VerifyVerdict = {
|
|
236
|
+
kind: "connected";
|
|
237
|
+
via: "process" | "logs";
|
|
238
|
+
} | {
|
|
239
|
+
kind: "not-connected";
|
|
240
|
+
} | {
|
|
241
|
+
kind: "unverified";
|
|
242
|
+
};
|
|
243
|
+
export declare function verifyVerdict(baseline: WorkspaceProbe | null, current: WorkspaceProbe | null): VerifyVerdict;
|
|
244
|
+
/**
|
|
245
|
+
* When to probe, in milliseconds after the launch, and why it is a list rather than an
|
|
246
|
+
* interval. Every entry costs one `terminal_command` row in the candidate's own activity
|
|
247
|
+
* record (see this module's header), so the schedule spends its probes where the answer
|
|
248
|
+
* changes — a cache hit attaches in seconds — and backs off rather than paying twenty
|
|
249
|
+
* rows to shave a second off a case that was going to fail anyway. The last entry IS the
|
|
250
|
+
* budget: ~60s, which is the ticket's bound and comfortably past a warm attach.
|
|
251
|
+
*/
|
|
252
|
+
export declare const VERIFY_SCHEDULE_MS: readonly number[];
|
|
253
|
+
//# sourceMappingURL=editor-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editor-server.d.ts","sourceRoot":"","sources":["../../src/lib/editor-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,wFAAwF;AACxF,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,oFAAoF;IACpF,eAAe,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEtF;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,gBAAgB,uBAAuB,CAAA;AAEpD,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAkDxF;AAKD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,cAAc,GAAG,IAAI,CAwBhF;AAED,mDAAmD;AACnD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,aAAa,CASvE;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC5F;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,kBAAkB,GAAG,sBAAsB,GAAG,SAAS,CAAA;CAAE,CAAA;AAExF,wBAAgB,WAAW,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,cAAc,GAAG,IAAI,GAAG,WAAW,CAW5F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,GAAG,EAAE,SAAS,GAAG,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,GACzB;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAA;AAE1B,wBAAgB,aAAa,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,GAAG,aAAa,CAU5G;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAA2D,CAAA"}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ENG-2332 — whether a native editor can, and then DID, start its server inside the
|
|
3
|
+
* workspace. Everything here is pure or takes its one side effect as an argument, so the
|
|
4
|
+
* decisions are testable without a Cursor install and without a live container.
|
|
5
|
+
*
|
|
6
|
+
* ## The failure this exists to stop
|
|
7
|
+
*
|
|
8
|
+
* A VS Code-family editor opening a Remote-SSH window does not run anything of ours: it
|
|
9
|
+
* copies its OWN server build into the remote home and launches it there. Which build is
|
|
10
|
+
* decided by the CLIENT, by commit, and the two editors are pinned to each other — a
|
|
11
|
+
* 3.19.13 client will not talk to a 3.20.21 server, it fetches its own.
|
|
12
|
+
*
|
|
13
|
+
* A Codespaces v2 container has sealed egress. It cannot reach `api2.cursor.sh`,
|
|
14
|
+
* `downloads.cursor.com` or `github.com` (measured from inside a prod workspace,
|
|
15
|
+
* 2026-09-15: `curl` returns 000 after a 12s timeout). The only route a non-baked client
|
|
16
|
+
* has is the mirror (`infra/vscode-mirror/`, ENG-1062 for VS Code, ENG-1265 + ENG-1936
|
|
17
|
+
* for Cursor), and a mirror route that has stopped matching what a client asks for is
|
|
18
|
+
* indistinguishable, from the client's side, from no route at all.
|
|
19
|
+
*
|
|
20
|
+
* What the client does then is the whole problem: it gives up and opens the window
|
|
21
|
+
* anyway. MEASURED on macOS against a live prod workspace (ENG-2332):
|
|
22
|
+
*
|
|
23
|
+
* - `cursor --remote ssh-remote+<alias> <dir>` exits 0 and prints nothing.
|
|
24
|
+
* - A window appears, carrying the remote authority — `storage.json` records
|
|
25
|
+
* `"remoteAuthority": "ssh-remote+<alias>"` — and Cursor's ordinary agent home
|
|
26
|
+
* screen. It is indistinguishable from a working one.
|
|
27
|
+
* - The renderer log says `Started local extension host`, and every
|
|
28
|
+
* `$updateShellExecCapabilities` is refused as `non-authoritative host`.
|
|
29
|
+
* - Nothing under `~/.cursor-server` on the VM is touched. No `data/logs` is created.
|
|
30
|
+
* No server process exists.
|
|
31
|
+
*
|
|
32
|
+
* So the candidate works in what looks like their editor, on their own laptop's files,
|
|
33
|
+
* and NOTHING IS RECORDED. That is the worst outcome this product has, and until this
|
|
34
|
+
* module every signal the CLI printed said it had worked.
|
|
35
|
+
*
|
|
36
|
+
* ## The two questions, and why they are both asked
|
|
37
|
+
*
|
|
38
|
+
* `seedVerdict` asks BEFORE launching: is the client's commit the one the image baked?
|
|
39
|
+
* `verifyVerdict` asks AFTER: did a server actually come up? Neither replaces the other.
|
|
40
|
+
* The first cannot be conclusive (the mirror may well serve a drifted client, and does
|
|
41
|
+
* for VS Code), and the second cannot be prompt — it costs the candidate up to a minute
|
|
42
|
+
* of waiting and, on a refusal, they have already opened a window. Asking both means the
|
|
43
|
+
* common, knowable failure is refused in advance and every other one is still loud.
|
|
44
|
+
*
|
|
45
|
+
* ## Why the probe is one shell command, and what it costs
|
|
46
|
+
*
|
|
47
|
+
* Each probe is `ssh <alias> <command>`, which sshd serves by exec'ing the candidate's
|
|
48
|
+
* login shell — so `/etc/ld.so.preload`'s `libttee.so` tees it and it lands in
|
|
49
|
+
* `candidate_activity_logs` as a `terminal_command`. That is acceptable and is NOT
|
|
50
|
+
* silent: the probe carries no pty, so `libttee`'s `tty_origin()` stamps it
|
|
51
|
+
* `origin: "tool"` (ENG-2179), which the read side groups as machinery rather than as
|
|
52
|
+
* something the candidate typed, and which the idle clock (ENG-2167) reads as a
|
|
53
|
+
* connection and never as work. It is still a row per probe, so the schedule below is
|
|
54
|
+
* deliberately sparse and backs off rather than polling on a fixed short interval.
|
|
55
|
+
*/
|
|
56
|
+
/** A probe line's shape, so a truncated or interleaved read is refused rather than parsed. */
|
|
57
|
+
const PROBE_BANNER = "litmus-probe 1";
|
|
58
|
+
/**
|
|
59
|
+
* The command a probe runs in the workspace. POSIX `sh` only, and built from two
|
|
60
|
+
* constants we own — never from anything a candidate or a company supplies.
|
|
61
|
+
*
|
|
62
|
+
* Four details are load-bearing, and three of them are about one thing: `ps` output is
|
|
63
|
+
* full of near-misses, and a false POSITIVE here certifies a window that records nothing.
|
|
64
|
+
*
|
|
65
|
+
* The LEADING DOT keeps the pattern off the browser IDE — `~/.openvscode-server` contains
|
|
66
|
+
* `vscode-server` but not `/.vscode-server/`, and the browser IDE's node process is
|
|
67
|
+
* running on every healthy container, so a pattern without the dot answers "connected"
|
|
68
|
+
* for a native editor that never attached. The `[.]` bracket keeps it off the `grep` in
|
|
69
|
+
* that same listing, and the ANCHOR keeps it off everything that merely names the path.
|
|
70
|
+
* See the comments in the body for the measurements behind each.
|
|
71
|
+
*
|
|
72
|
+
* THE HOME SPELLING IS NOT THE ONLY ONE A HEALTHY CONTAINER PRODUCES, and missing the
|
|
73
|
+
* other one is silent. `litmus-firstboot.sh`'s `prewarm_editor` installs the baked bundle
|
|
74
|
+
* as a SYMLINK — `~/.<editor>-server/cli/servers/Stable-<commit>/server` ->
|
|
75
|
+
* `/opt/<editor>-server-cache/<commit>` — and the VS Code-family server bootstrap
|
|
76
|
+
* resolves its own root with `readlink -f "$0"` before exec'ing node, so on a PREWARMED
|
|
77
|
+
* (commit-matching, i.e. healthy) container argv[0] can be the `/opt` path. This repo
|
|
78
|
+
* already records both spellings for exactly these trees:
|
|
79
|
+
* `frontend/lib/server/tool-command-origin.ts`'s `SYSTEM_EDITOR_MARKERS` lists
|
|
80
|
+
* `/opt/vscode-server-cache/` and `/opt/cursor-server-cache/` beside `/.vscode-server/`.
|
|
81
|
+
* So `serverProcessPattern` carries both as alternatives, and every property above still
|
|
82
|
+
* holds of the added one: it is anchored at argv[0], so a candidate's `ls
|
|
83
|
+
* /opt/cursor-server-cache` is still not a server; it still cannot self-match, because
|
|
84
|
+
* the probe's own argv[0] is `sh` and the cache root reaches the command only as a shell
|
|
85
|
+
* VARIABLE, never as a literal followed by a slash; and unlike the home spelling that
|
|
86
|
+
* tree is root-owned on a read-only rootfs, so a candidate cannot put a binary there to
|
|
87
|
+
* forge one.
|
|
88
|
+
*
|
|
89
|
+
* WHAT IS STILL UNMEASURED, stated rather than implied: the controls behind this pattern
|
|
90
|
+
* are synthetic argv lines, taken on a laptop and not against a real prewarmed container.
|
|
91
|
+
* `ps -eo args=` on a live attached workspace, for both editors, is still to be confirmed.
|
|
92
|
+
* Adding the alternative is the safe direction in the meantime — it can only restore a
|
|
93
|
+
* signal that would otherwise be silent on the HEALTHY path, and the failure it prevents
|
|
94
|
+
* is the false alarm `verifyVerdict` names: a candidate who re-runs `litmus connect` with
|
|
95
|
+
* a window already open joins the running server, writes no new log directory, and would
|
|
96
|
+
* be told their work is not recorded.
|
|
97
|
+
*
|
|
98
|
+
* The banner is printed FIRST and unconditionally. `ssh` mixes the remote command's
|
|
99
|
+
* stdout with anything the login shell's rc files print, and a candidate's own `.bashrc`
|
|
100
|
+
* can write whatever it likes; a run that did not reach the banner is not a reading.
|
|
101
|
+
*
|
|
102
|
+
* `ps -eo args=` and not `pgrep`: procps is what the container has (the browser IDE's own
|
|
103
|
+
* monitor loop runs `ps -ax -o …`, ENG-2143), and `pgrep` is not guaranteed. A missing
|
|
104
|
+
* `ps` yields `proc 0`, which is the safe direction — it can only withhold a "connected"
|
|
105
|
+
* verdict, never manufacture one.
|
|
106
|
+
*/
|
|
107
|
+
/**
|
|
108
|
+
* The argv[0] pattern the probe hands `grep -E`, as ONE derivation used two ways.
|
|
109
|
+
*
|
|
110
|
+
* `remoteProbeCommand` calls it with the shell EXPRESSIONS that expand to the two names
|
|
111
|
+
* (`${d#.}` and `${c}`), so the alternation reaches the container as a pattern built from
|
|
112
|
+
* shell variables; a test calls it with the literal names and gets the ERE the container
|
|
113
|
+
* will actually run. Keeping both behind one function is what stops the anchor or the
|
|
114
|
+
* alternation being right in one form and wrong in the other.
|
|
115
|
+
*/
|
|
116
|
+
export function serverProcessPattern(serverHomeName, cacheRoot) {
|
|
117
|
+
return `^[^ ]*(/[.]${serverHomeName}/|${cacheRoot}/)`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The names under `<serverHome>/data/logs` this probe will consider, as an ERE.
|
|
121
|
+
*
|
|
122
|
+
* The maximum is taken lexicographically, which is chronological ONLY over timestamps —
|
|
123
|
+
* and in C collation every letter sorts above every digit, so one stray `.log`, lock file
|
|
124
|
+
* or future named subdirectory becomes the maximum forever. The direction that breaks in
|
|
125
|
+
* is the bad one: `newestLogDir` stops advancing, the log signal dies silently, and a
|
|
126
|
+
* genuinely attached editor reads as `not-connected` — the false alarm at a working
|
|
127
|
+
* candidate that the process signal exists to prevent. So the invariant the field's
|
|
128
|
+
* docstring states is ENFORCED here rather than assumed, and a directory holding nothing
|
|
129
|
+
* timestamp-shaped answers "no logs", which is a missing input.
|
|
130
|
+
*
|
|
131
|
+
* The prefix is anchored and the tail is not: a build that appends a suffix to the
|
|
132
|
+
* timestamp still sorts chronologically, while a name that does not START with one cannot
|
|
133
|
+
* poison the maximum.
|
|
134
|
+
*/
|
|
135
|
+
export const LOG_DIR_NAME_ERE = "^[0-9]{8}T[0-9]{6}";
|
|
136
|
+
export function remoteProbeCommand(serverHomeDir, seedCommitPath) {
|
|
137
|
+
// Both arguments are module constants in `connect.ts`. Assert that rather than trust
|
|
138
|
+
// it: a probe body is a shell program, and the day one of these becomes derived from
|
|
139
|
+
// anything else, this is the line that has to fail instead of interpolating.
|
|
140
|
+
if (!/^\.[a-z0-9-]+$/.test(serverHomeDir))
|
|
141
|
+
throw new Error(`unsafe server home dir: ${serverHomeDir}`);
|
|
142
|
+
if (!/^\/[A-Za-z0-9/_.-]+$/.test(seedCommitPath))
|
|
143
|
+
throw new Error(`unsafe seed path: ${seedCommitPath}`);
|
|
144
|
+
// The baked bundle and its pointer are ONE tree: `/opt/<editor>-server-cache/COMMIT`
|
|
145
|
+
// sits inside `/opt/<editor>-server-cache/`. Deriving the root from the pointer rather
|
|
146
|
+
// than declaring it again keeps the record in `connect.ts` the only place either is
|
|
147
|
+
// spelled — a second literal is a copy that goes stale silently, which is exactly what
|
|
148
|
+
// `backend/tests/test_editor_server_paths.py` pins the first one against.
|
|
149
|
+
const cacheRoot = seedCommitPath.replace(/\/[^/]+$/, "");
|
|
150
|
+
if (!/^\/[A-Za-z0-9/_.-]+$/.test(cacheRoot))
|
|
151
|
+
throw new Error(`unsafe seed path: ${seedCommitPath}`);
|
|
152
|
+
// THE COMMAND MUST NOT CONTAIN THE STRING IT SEARCHES FOR. `ps -eo args=` lists this
|
|
153
|
+
// probe's own `sh -c <command>` process, so a body carrying a literal
|
|
154
|
+
// `/.cursor-server/` — which the obvious spelling of the logs path does — matches
|
|
155
|
+
// itself. MEASURED: the first cut of this returned `proc 4` on a laptop with no server
|
|
156
|
+
// of any kind running. The consequence is not a false "connected", because the BASELINE
|
|
157
|
+
// probe matches itself too and `verifyVerdict` then sees a process that was already
|
|
158
|
+
// there; it is worse than that — the process signal quietly stops being a signal, and
|
|
159
|
+
// the whole verification silently rests on the log directory alone.
|
|
160
|
+
//
|
|
161
|
+
// So the directory name is held in `$d` and never written next to a slash, and the
|
|
162
|
+
// pattern keeps `[.]` (which also stops it matching the `grep` in that same listing).
|
|
163
|
+
// `${d#.}` strips the leading dot so the bracket can supply it.
|
|
164
|
+
//
|
|
165
|
+
// AND THE PATTERN IS ANCHORED AT ARGV[0], because the dangerous direction here is a
|
|
166
|
+
// false POSITIVE: a probe that certifies a dead window is worse than one that says
|
|
167
|
+
// nothing. A candidate's own `ls ~/.cursor-server`, a `grep` over it, an editor's file
|
|
168
|
+
// watcher — any of them puts that path in a `ps` listing while proving nothing about a
|
|
169
|
+
// server. `^[^ ]*` requires the path to be the EXECUTABLE, which it is for the server
|
|
170
|
+
// (its node binary lives inside that tree) and is not for anything merely naming it.
|
|
171
|
+
// A server launched some other way loses this signal and keeps the log one, which is
|
|
172
|
+
// the safe way for it to be wrong.
|
|
173
|
+
const logs = `"$HOME/$d/data/logs"`;
|
|
174
|
+
// Both names reach the pattern as shell VARIABLES, never as literals followed by a
|
|
175
|
+
// slash, so the command text still cannot hold the string it searches for.
|
|
176
|
+
const pattern = serverProcessPattern("${d#.}", "${c}");
|
|
177
|
+
return [
|
|
178
|
+
`printf '${PROBE_BANNER}\\n';`,
|
|
179
|
+
`d=${serverHomeDir};`,
|
|
180
|
+
`c=${cacheRoot};`,
|
|
181
|
+
`if [ -r '${seedCommitPath}' ]; then`,
|
|
182
|
+
`printf 'seed %s\\n' "$(tr -d '\\r\\n' < '${seedCommitPath}' 2>/dev/null)";`,
|
|
183
|
+
`else printf 'seed -\\n'; fi;`,
|
|
184
|
+
`printf 'proc %s\\n' "$(ps -eo args= 2>/dev/null | grep -Ec "${pattern}" || true)";`,
|
|
185
|
+
`if [ -d ${logs} ]; then`,
|
|
186
|
+
`printf 'log %s\\n' "$(ls -1 ${logs} 2>/dev/null | grep -E '${LOG_DIR_NAME_ERE}' | sort | tail -n 1)";`,
|
|
187
|
+
`else printf 'log -\\n'; fi`,
|
|
188
|
+
].join(" ");
|
|
189
|
+
}
|
|
190
|
+
/** A 40-hex commit, the only shape either editor names a server build by. */
|
|
191
|
+
const COMMIT_RE = /^[0-9a-f]{40}$/;
|
|
192
|
+
/**
|
|
193
|
+
* Read a probe's stdout. Returns null for anything that is not a complete reading —
|
|
194
|
+
* a missing banner, a missing field, a non-commit `seed` — because a partial probe that
|
|
195
|
+
* degrades into a WorkspaceProbe with plausible zeros is a probe that reports "no server
|
|
196
|
+
* is running" when what happened is that the probe did not run.
|
|
197
|
+
*/
|
|
198
|
+
export function parseWorkspaceProbe(stdout) {
|
|
199
|
+
if (!stdout)
|
|
200
|
+
return null;
|
|
201
|
+
const lines = stdout.split(/\r?\n/).map((l) => l.trim());
|
|
202
|
+
if (!lines.includes(PROBE_BANNER))
|
|
203
|
+
return null;
|
|
204
|
+
const field = (name) => {
|
|
205
|
+
// LAST match wins: an rc file that echoes something shaped like a field cannot
|
|
206
|
+
// displace ours, because ours are printed after the shell has finished starting.
|
|
207
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
208
|
+
if (lines[i] === name || lines[i].startsWith(`${name} `))
|
|
209
|
+
return lines[i].slice(name.length).trim();
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
};
|
|
213
|
+
const seedRaw = field("seed");
|
|
214
|
+
const procRaw = field("proc");
|
|
215
|
+
const logRaw = field("log");
|
|
216
|
+
if (seedRaw === null || procRaw === null || logRaw === null)
|
|
217
|
+
return null;
|
|
218
|
+
const proc = Number.parseInt(procRaw, 10);
|
|
219
|
+
if (!Number.isFinite(proc) || proc < 0)
|
|
220
|
+
return null;
|
|
221
|
+
const seedCommit = COMMIT_RE.test(seedRaw) ? seedRaw : null;
|
|
222
|
+
return {
|
|
223
|
+
seedCommit,
|
|
224
|
+
serverProcesses: proc,
|
|
225
|
+
newestLogDir: logRaw === "-" || logRaw === "" ? null : logRaw,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Read `code --version` / `cursor --version`. Both print exactly three lines — version,
|
|
230
|
+
* commit, arch — and NEITHER prints a product name (the measurement is in
|
|
231
|
+
* `NATIVE_EDITORS`' block comment in `connect.ts`, and is why identity is read off
|
|
232
|
+
* `--help` instead). The commit is found by SHAPE rather than by line number, so a build
|
|
233
|
+
* that adds a line does not silently shift which field we read; the version is the first
|
|
234
|
+
* line that is not the commit.
|
|
235
|
+
*/
|
|
236
|
+
export function parseEditorVersion(stdout) {
|
|
237
|
+
if (!stdout)
|
|
238
|
+
return { version: null, commit: null };
|
|
239
|
+
const lines = stdout
|
|
240
|
+
.split(/\r?\n/)
|
|
241
|
+
.map((l) => l.trim())
|
|
242
|
+
.filter((l) => l.length > 0);
|
|
243
|
+
const commit = lines.find((l) => COMMIT_RE.test(l)) ?? null;
|
|
244
|
+
const version = lines.find((l) => !COMMIT_RE.test(l) && /^\d+\.\d+/.test(l)) ?? null;
|
|
245
|
+
return { version, commit };
|
|
246
|
+
}
|
|
247
|
+
export function seedVerdict(client, probe) {
|
|
248
|
+
if (!client.commit)
|
|
249
|
+
return { kind: "unknown", reason: "no-client-commit" };
|
|
250
|
+
if (!probe)
|
|
251
|
+
return { kind: "unknown", reason: "no-workspace-reading" };
|
|
252
|
+
if (!probe.seedCommit)
|
|
253
|
+
return { kind: "unknown", reason: "no-seed" };
|
|
254
|
+
if (probe.seedCommit === client.commit)
|
|
255
|
+
return { kind: "match", commit: client.commit };
|
|
256
|
+
return {
|
|
257
|
+
kind: "mismatch",
|
|
258
|
+
clientCommit: client.commit,
|
|
259
|
+
clientVersion: client.version,
|
|
260
|
+
seedCommit: probe.seedCommit,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
export function verifyVerdict(baseline, current) {
|
|
264
|
+
if (!current)
|
|
265
|
+
return { kind: "unverified" };
|
|
266
|
+
if (current.serverProcesses > 0)
|
|
267
|
+
return { kind: "connected", via: "process" };
|
|
268
|
+
if (!baseline)
|
|
269
|
+
return { kind: "unverified" };
|
|
270
|
+
if (current.newestLogDir !== null && current.newestLogDir > (baseline.newestLogDir ?? "")) {
|
|
271
|
+
return { kind: "connected", via: "logs" };
|
|
272
|
+
}
|
|
273
|
+
// No server running and a log directory that has not advanced is the measured failure:
|
|
274
|
+
// the client gave up before writing anything to the workspace at all.
|
|
275
|
+
return { kind: "not-connected" };
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* When to probe, in milliseconds after the launch, and why it is a list rather than an
|
|
279
|
+
* interval. Every entry costs one `terminal_command` row in the candidate's own activity
|
|
280
|
+
* record (see this module's header), so the schedule spends its probes where the answer
|
|
281
|
+
* changes — a cache hit attaches in seconds — and backs off rather than paying twenty
|
|
282
|
+
* rows to shave a second off a case that was going to fail anyway. The last entry IS the
|
|
283
|
+
* budget: ~60s, which is the ticket's bound and comfortably past a warm attach.
|
|
284
|
+
*/
|
|
285
|
+
export const VERIFY_SCHEDULE_MS = [4000, 9000, 15000, 23000, 33000, 45000, 60000];
|
|
286
|
+
//# sourceMappingURL=editor-server.js.map
|