litmus-cli 1.4.27 → 1.4.29

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.
Files changed (47) hide show
  1. package/README.md +4 -2
  2. package/dist/commands/connect.d.ts +349 -7
  3. package/dist/commands/connect.d.ts.map +1 -1
  4. package/dist/commands/connect.js +751 -58
  5. package/dist/commands/connect.js.map +1 -1
  6. package/dist/commands/doctor.d.ts +52 -0
  7. package/dist/commands/doctor.d.ts.map +1 -1
  8. package/dist/commands/doctor.js +76 -7
  9. package/dist/commands/doctor.js.map +1 -1
  10. package/dist/commands/init.d.ts +2 -0
  11. package/dist/commands/init.d.ts.map +1 -1
  12. package/dist/commands/init.js +30 -4
  13. package/dist/commands/init.js.map +1 -1
  14. package/dist/commands/pause.d.ts +13 -8
  15. package/dist/commands/pause.d.ts.map +1 -1
  16. package/dist/commands/pause.js +18 -10
  17. package/dist/commands/pause.js.map +1 -1
  18. package/dist/commands/push.d.ts.map +1 -1
  19. package/dist/commands/push.js +6 -2
  20. package/dist/commands/push.js.map +1 -1
  21. package/dist/commands/submit.js +7 -4
  22. package/dist/commands/submit.js.map +1 -1
  23. package/dist/index.js +2 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/lib/ai-tracking.d.ts +19 -0
  26. package/dist/lib/ai-tracking.d.ts.map +1 -1
  27. package/dist/lib/ai-tracking.js +282 -1
  28. package/dist/lib/ai-tracking.js.map +1 -1
  29. package/dist/lib/api-base.d.ts +80 -0
  30. package/dist/lib/api-base.d.ts.map +1 -1
  31. package/dist/lib/api-base.js +83 -0
  32. package/dist/lib/api-base.js.map +1 -1
  33. package/dist/lib/editor-binary.d.ts +110 -0
  34. package/dist/lib/editor-binary.d.ts.map +1 -0
  35. package/dist/lib/editor-binary.js +72 -0
  36. package/dist/lib/editor-binary.js.map +1 -0
  37. package/dist/lib/editor-ide.d.ts +11 -0
  38. package/dist/lib/editor-ide.d.ts.map +1 -0
  39. package/dist/lib/editor-ide.js +2 -0
  40. package/dist/lib/editor-ide.js.map +1 -0
  41. package/dist/lib/editor-server.d.ts +253 -0
  42. package/dist/lib/editor-server.d.ts.map +1 -0
  43. package/dist/lib/editor-server.js +286 -0
  44. package/dist/lib/editor-server.js.map +1 -0
  45. package/dist/lib/watcher.js +18 -1
  46. package/dist/lib/watcher.js.map +1 -1
  47. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { FALLBACK_URL } from "./errors.js";
2
+ import { isWorkspaceInstall } from "./workspace-token.js";
2
3
  /**
3
4
  * Resolve the frontend base URL that `litmus init` talks to — the CLI's single
4
5
  * routing decision. `LITMUS_API_URL` (which preview deployments surface to the
@@ -14,4 +15,86 @@ import { FALLBACK_URL } from "./errors.js";
14
15
  export function resolveApiBase(apiUrl) {
15
16
  return apiUrl ?? FALLBACK_URL;
16
17
  }
18
+ /**
19
+ * The origin to send a `/workspace/*` or `/cli/*` request to, for the install
20
+ * this CLI is running from (ENG-2321).
21
+ *
22
+ * TWO ORIGINS SERVE THE SAME ROUTES AND ONLY ONE OF THEM IS REACHABLE FROM A
23
+ * CONTAINER. `litmus init` learns `backendUrl` from the server — it is
24
+ * `NEXT_PUBLIC_PYTHON_API_URL`, the Railway origin the Python backend is
25
+ * published at — and every later workspace call used it. That is correct on a
26
+ * laptop and wrong inside a Codespaces v2 workspace, whose egress deliberately
27
+ * does NOT carve out the orchestrator's own origin: `/internal/host/*` (hostd's
28
+ * register / lease / result / heartbeat channel) is served from the same host,
29
+ * so allowing it would hand the candidate the control plane —
30
+ * `infra/hostd/FLEET.md` says so in as many words, and the carve-out
31
+ * (`EGRESS_GATEWAY_ALLOW`) pins the APEX instead.
32
+ *
33
+ * The apex already proxies every container-facing prefix to that same backend
34
+ * (`frontend/next.config.mjs` rewrites `/vm/*`, `/cli/*`, `/workspace/*`,
35
+ * `/proxy/*`, `/mcp/*` and `/broker/*`, driven through Next's own matcher in
36
+ * `next.config.rewrites.test.ts`), and it is the origin the container is
37
+ * allowed to reach — which is exactly why ENG-2122 put the secret broker there
38
+ * rather than giving it a hostname of its own. So there is nothing to build:
39
+ * a workspace install simply has to address the route it is already entitled
40
+ * to use.
41
+ *
42
+ * WHAT THE BUG LOOKED LIKE, because the fingerprint is worth recognising:
43
+ * `litmus push` makes two hops. Hop 1 is `GET {apex}/api/cli/init` and prints
44
+ * `Connected — <assessment>`. Hop 2 was `POST {backendUrl}/workspace/submit`
45
+ * and threw `fetch failed` at the transport. **"Connected" and then "fetch
46
+ * failed" is this and nothing else** — a token problem answers with a status
47
+ * code, and an apex problem never reaches "Connected".
48
+ *
49
+ * THE PREDICATE IS `isWorkspaceInstall`, read from the caller's own module path
50
+ * for the reasons its docstring gives, and it is the same one ENG-2130 uses to
51
+ * decide which submit command a candidate is told to run. Nothing
52
+ * security-relevant hangs off it here either: both origins serve the same
53
+ * routes behind the same bearer check, so a wrong answer costs reachability,
54
+ * never authority.
55
+ *
56
+ * `apexBase` is the caller's already-resolved `resolveApiBase(...)` value — the
57
+ * deployment this CLI talks to — so a preview-provisioned workspace stays on
58
+ * its preview apex rather than being sent to production.
59
+ *
60
+ * THE AUDIT, so the next reader does not have to redo it. Every `backendUrl`
61
+ * the CLI holds comes from one of two places, and both are covered:
62
+ *
63
+ * * FROM THE INIT METADATA, used immediately — `push` (`/workspace/submit`)
64
+ * and `pause` (`/workspace/pause`), the two commands a candidate runs from
65
+ * a workspace terminal. Both resolve here.
66
+ * * PERSISTED INTO `.litmus/config.json` — `init`, `doctor`'s config rebuild
67
+ * and the watcher's self-heal. Those resolve here too, at the WRITE, which
68
+ * is what makes every later reader of a CLI-written config correct with no
69
+ * change of its own: the watcher's `/cli/activity` and `/cli/snapshot`
70
+ * uploads, `hook-logger.cjs`'s uploads, and `submit`'s `/cli/submit`.
71
+ * `doctor` additionally resolves at its own READ, because the config in a
72
+ * real container is the one hostd staged and names the Railway origin.
73
+ *
74
+ * DELIBERATELY NOT RESOLVED, each for a reason rather than an omission:
75
+ *
76
+ * * `connect` (`/workspace/start`, `/workspace/connection`,
77
+ * `/workspace/editor-chat-config`). It configures the LAPTOP — it writes
78
+ * `~/.ssh/config` and launches a local editor — so it has no meaning inside
79
+ * the container. And leaving it unreachable there is the SAFE state rather
80
+ * than a gap: `connect` calls `ensureSshKey`, which MINTS a keypair when the
81
+ * machine it runs on has none, which inside a fresh container it does not;
82
+ * `/workspace/start` then hands that key to `Ops.resume`, whose
83
+ * `_apply_authorized_keys` REWRITES `authorized_keys` rather than appending
84
+ * (ENG-2001). So a stray `litmus connect` in a workspace terminal would
85
+ * replace the candidate's own laptop key with one that exists only inside
86
+ * the container they ran it from. Failing at the transport costs nothing;
87
+ * succeeding would cost them their way back in.
88
+ * * `submit` (`/cli/submit`). ENG-2130 refuses it in a workspace before any
89
+ * network call at all; the origin it would have used never matters.
90
+ * * `watcher.ts`'s and `hook-logger.cjs`'s READS of the persisted config. In
91
+ * a real container that file is hostd's `_stage_litmus_config`, which
92
+ * carries DELIBERATELY no token, so both lanes return before uploading
93
+ * anything — they are inert there today, and the honest fix for whoever
94
+ * turns the real-time in-workspace activity lane on is hostd's write, not a
95
+ * second copy of this predicate in a module that imports nothing.
96
+ */
97
+ export function resolveBackendBase(backendUrl, apexBase, moduleUrl) {
98
+ return isWorkspaceInstall(moduleUrl) ? apexBase : backendUrl;
99
+ }
17
100
  //# sourceMappingURL=api-base.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-base.js","sourceRoot":"","sources":["../../src/lib/api-base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE1C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc,CAAC,MAA0B;IACvD,OAAO,MAAM,IAAI,YAAY,CAAA;AAC/B,CAAC"}
1
+ {"version":3,"file":"api-base.js","sourceRoot":"","sources":["../../src/lib/api-base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAEzD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc,CAAC,MAA0B;IACvD,OAAO,MAAM,IAAI,YAAY,CAAA;AAC/B,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8EG;AACH,MAAM,UAAU,kBAAkB,CAChC,UAAkB,EAClB,QAAgB,EAChB,SAAiB;IAEjB,OAAO,kBAAkB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAA;AAC9D,CAAC"}
@@ -0,0 +1,110 @@
1
+ import type { NativeIde } from "./editor-ide.js";
2
+ /**
3
+ * ENG-2332 — finding the editor's CLI when it is not on PATH, and building an argv that
4
+ * can actually drive it on the platform we are running on.
5
+ *
6
+ * ## Why resolution rather than a PATH probe
7
+ *
8
+ * Both editors ship their CLI inside the application bundle and put it on PATH only if
9
+ * the candidate has run "Shell Command: Install …" from the palette, which most people
10
+ * never have. Before this, `litmus connect` answered that by printing:
11
+ *
12
+ * Warning: Cursor `cursor` command not found on PATH.
13
+ * Connect manually:
14
+ * cursor --remote ssh-remote+<alias> /home/candidate/workspace
15
+ *
16
+ * — a remediation that is the very binary just reported missing (reported 2026-09-15,
17
+ * ENG-2332). The binary was present the whole time at
18
+ * `/Applications/Cursor.app/Contents/Resources/app/bin/cursor`. So look there.
19
+ *
20
+ * ## A resolved path is still not an identity
21
+ *
22
+ * ENG-2152's rule holds unchanged: a command is a NAME, never a claim about which editor
23
+ * it is. That is true of an absolute path too — `Cursor.app` could in principle be
24
+ * anything, and on the PATH branch `code` routinely IS Cursor. So the caller runs the
25
+ * `--help` banner probe against whatever this resolves to, not against `ed.bin`.
26
+ *
27
+ * ## The list is best-effort, and knowingly incomplete on Linux
28
+ *
29
+ * macOS and Windows have installers that write to known locations. Linux does not, for
30
+ * Cursor especially: it ships as an AppImage a candidate can leave anywhere. The entries
31
+ * below are the ones a package or the vendor's own installer produces; a miss there
32
+ * degrades to the manual-instructions branch, which is where the candidate was already.
33
+ */
34
+ /** A resolved way to reach the editor: a bare PATH name, or an absolute path we found. */
35
+ export interface ResolvedEditorCommand {
36
+ /** What to hand `editorSpawnSpec`. */
37
+ command: string;
38
+ /** How we got it — the caller's output differs, and so does the advice on failure. */
39
+ source: "path" | "install-location";
40
+ }
41
+ /**
42
+ * Where each editor's CLI lives when its installer put it there, per platform.
43
+ *
44
+ * Paths are built with the TARGET's separator (`path.win32` / `path.posix`) and never
45
+ * with the bare `path`, which is bound to the host: a Windows expectation assembled with
46
+ * a POSIX `path` passes on a POSIX host precisely when the shipped code is wrong there.
47
+ * That is `chat-model-config.ts`'s `pathFor` rule, and the tests beside this assert
48
+ * literal strings for the same reason.
49
+ */
50
+ export declare function editorInstallCandidates(ide: NativeIde, platform: NodeJS.Platform, env: NodeJS.ProcessEnv, home: string): string[];
51
+ /**
52
+ * How to actually run a resolved command. Returned rather than spawned so the argv is
53
+ * assertable on any host — a Windows spawn cannot be exercised from a POSIX CI runner,
54
+ * and this is the part that is wrong if Windows is wrong.
55
+ *
56
+ * ON WINDOWS, ALWAYS A SHELL. Both editors ship their CLI as `code.cmd` / `cursor.cmd`,
57
+ * and CreateProcess resolves `.exe` only, so a shim-less spawn cannot run either — not
58
+ * the `.cmd` on PATH and not the one inside the install directory. That is ENG-2134's
59
+ * finding for the `--help` probe, and it is why every Windows candidate has landed on
60
+ * the manual-instructions branch since this command existed. The condition is the
61
+ * PLATFORM and not the file extension: narrowing it to `.cmd` would leave a bare `code`
62
+ * on a Windows PATH un-shelled, which is the pre-ENG-2134 behaviour arrived at by
63
+ * accident.
64
+ *
65
+ * Node does NOT quote anything when `shell: true`, so the FILE is quoted here — always,
66
+ * and not only when it carries whitespace. Whitespace is the spelling that made this
67
+ * visible (`Microsoft VS Code`), but it is not the only one that breaks, and the command
68
+ * is not a literal we own: `editorInstallCandidates` builds it out of `%LOCALAPPDATA%` /
69
+ * `%ProgramFiles%`, so a candidate whose Windows account is `a&b` resolves
70
+ * `C:\Users\a&b\AppData\Local\Programs\cursor\resources\app\bin\cursor.cmd` — no whitespace
71
+ * anywhere in it, handed to `cmd.exe` unquoted, and `&` ends the command there and starts
72
+ * a new one. Inside double quotes `&` is an ordinary character, so quoting unconditionally
73
+ * costs the bare-PATH case nothing (`cmd /d /s /c ""code" --version"` resolves `code`
74
+ * through PATHEXT exactly as an unquoted one does) and closes the whole family at once.
75
+ *
76
+ * Quoting the FILE is sufficient only because nothing else on the line needs it, and BOTH
77
+ * halves of that are asserted rather than assumed. The command is held to the narrower
78
+ * thing a quoted `cmd.exe` token can carry: a `"` would close the quoting just opened,
79
+ * and `%` is still expanded INSIDE quotes, so either refuses rather than being wrapped.
80
+ * Arguments are not quoted at all, so they are held to strictly less than that — `%`
81
+ * included, for the same reason and more directly, since there is not even a quote
82
+ * between it and `cmd.exe`.
83
+ *
84
+ * THE TWO GUARDS FAIL DIFFERENTLY, BECAUSE THEIR INPUTS COME FROM DIFFERENT PLACES, and
85
+ * an earlier cut of this had them both throw — which made a candidate's own filesystem
86
+ * able to abort the connection (Greptile P1 on #2442).
87
+ *
88
+ * An unsafe ARGUMENT is OUR bug. Every argument this is given is a literal we own or an
89
+ * alias `aliasFor` has already reduced to `[A-Za-z0-9-]`, so one that needs quoting means
90
+ * a later caller threaded something candidate-supplied into an argv, and throwing is the
91
+ * point — the invariant stated as a check rather than as a comment somebody has to keep
92
+ * true.
93
+ *
94
+ * An unquotable COMMAND is DATA, and it is reachable. `editorInstallCandidates` builds
95
+ * the command out of `%LOCALAPPDATA%` / `%ProgramFiles%`, and `%` is legal in a Windows
96
+ * account name, so `C:\Users\50%\AppData\Local\Programs\cursor\…\cursor.cmd` is a path a
97
+ * real machine can present. Throwing on it aborted `litmus connect` from inside
98
+ * `probeEditorCli` — after the workspace was already up — for a candidate whose only
99
+ * fault was their username. So this answers NULL instead: "there is no argv that drives
100
+ * this command on this platform". Every caller has to handle that, which the compiler
101
+ * enforces rather than a remembered try/catch at five call sites, and each one degrades
102
+ * the way it already degrades for a command that cannot be run at all.
103
+ */
104
+ export interface EditorSpawnSpec {
105
+ command: string;
106
+ args: string[];
107
+ shell: boolean;
108
+ }
109
+ export declare function editorSpawnSpec(command: string, args: string[], platform: NodeJS.Platform): EditorSpawnSpec | null;
110
+ //# sourceMappingURL=editor-binary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor-binary.d.ts","sourceRoot":"","sources":["../../src/lib/editor-binary.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,0FAA0F;AAC1F,MAAM,WAAW,qBAAqB;IACpC,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAA;IACf,sFAAsF;IACtF,MAAM,EAAE,MAAM,GAAG,kBAAkB,CAAA;CACpC;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,SAAS,EACd,QAAQ,EAAE,MAAM,CAAC,QAAQ,EACzB,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,IAAI,EAAE,MAAM,GACX,MAAM,EAAE,CAmCV;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,KAAK,EAAE,OAAO,CAAA;CACf;AAaD,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,QAAQ,EAAE,MAAM,CAAC,QAAQ,GACxB,eAAe,GAAG,IAAI,CASxB"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=editor-ide.js.map
@@ -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"}