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.
- package/README.md +4 -2
- package/dist/commands/connect.d.ts +349 -7
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +751 -58
- 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 +2 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +30 -4
- 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/commands/submit.js +7 -4
- package/dist/commands/submit.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/ai-tracking.d.ts +19 -0
- package/dist/lib/ai-tracking.d.ts.map +1 -1
- package/dist/lib/ai-tracking.js +282 -1
- package/dist/lib/ai-tracking.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
package/dist/lib/api-base.js
CHANGED
|
@@ -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
|
package/dist/lib/api-base.js.map
CHANGED
|
@@ -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;
|
|
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 @@
|
|
|
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"}
|