litmus-cli 1.4.7-eng1502.0 → 1.4.7

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 (55) hide show
  1. package/README.md +0 -15
  2. package/dist/commands/doctor.d.ts +1 -41
  3. package/dist/commands/doctor.d.ts.map +1 -1
  4. package/dist/commands/doctor.js +26 -188
  5. package/dist/commands/doctor.js.map +1 -1
  6. package/dist/commands/init.d.ts +1 -3
  7. package/dist/commands/init.d.ts.map +1 -1
  8. package/dist/commands/init.js +10 -74
  9. package/dist/commands/init.js.map +1 -1
  10. package/dist/commands/status.d.ts.map +1 -1
  11. package/dist/commands/status.js +25 -1
  12. package/dist/commands/status.js.map +1 -1
  13. package/dist/commands/submit.d.ts.map +1 -1
  14. package/dist/commands/submit.js +4 -38
  15. package/dist/commands/submit.js.map +1 -1
  16. package/dist/index.js +2 -3
  17. package/dist/index.js.map +1 -1
  18. package/dist/lib/ai-tracking.d.ts +10 -0
  19. package/dist/lib/ai-tracking.d.ts.map +1 -1
  20. package/dist/lib/ai-tracking.js +25 -0
  21. package/dist/lib/ai-tracking.js.map +1 -1
  22. package/dist/lib/backlog-flush.d.ts +118 -0
  23. package/dist/lib/backlog-flush.d.ts.map +1 -0
  24. package/dist/lib/backlog-flush.js +268 -0
  25. package/dist/lib/backlog-flush.js.map +1 -0
  26. package/dist/lib/config.d.ts +0 -2
  27. package/dist/lib/config.d.ts.map +1 -1
  28. package/dist/lib/config.js.map +1 -1
  29. package/dist/lib/hook-logger.cjs +136 -2
  30. package/dist/lib/keepalive.d.ts +33 -0
  31. package/dist/lib/keepalive.d.ts.map +1 -0
  32. package/dist/lib/keepalive.js +39 -0
  33. package/dist/lib/keepalive.js.map +1 -0
  34. package/dist/lib/shadow.d.ts +14 -0
  35. package/dist/lib/shadow.d.ts.map +1 -1
  36. package/dist/lib/shadow.js +33 -0
  37. package/dist/lib/shadow.js.map +1 -1
  38. package/dist/lib/tracker.d.ts.map +1 -1
  39. package/dist/lib/tracker.js +9 -1
  40. package/dist/lib/tracker.js.map +1 -1
  41. package/dist/lib/watcher.js +31 -7
  42. package/dist/lib/watcher.js.map +1 -1
  43. package/package.json +1 -1
  44. package/dist/lib/editor.d.ts +0 -73
  45. package/dist/lib/editor.d.ts.map +0 -1
  46. package/dist/lib/editor.js +0 -222
  47. package/dist/lib/editor.js.map +0 -1
  48. package/dist/lib/select.d.ts +0 -64
  49. package/dist/lib/select.d.ts.map +0 -1
  50. package/dist/lib/select.js +0 -170
  51. package/dist/lib/select.js.map +0 -1
  52. package/dist/lib/session.d.ts +0 -65
  53. package/dist/lib/session.d.ts.map +0 -1
  54. package/dist/lib/session.js +0 -212
  55. package/dist/lib/session.js.map +0 -1
@@ -1,170 +0,0 @@
1
- /**
2
- * A single-choice arrow-key picker for the candidate-facing prompts.
3
- *
4
- * Written by hand rather than pulled from a prompt library: the CLI ships to
5
- * candidates' machines, so every dependency is one more thing that can fail to
6
- * install on an unknown Node/npm. The surface here is one function.
7
- *
8
- * Non-interactive stdin (piped, CI, the watcher's auto-submit) never blocks —
9
- * `select` resolves to null immediately and the caller falls back to printing
10
- * instructions.
11
- */
12
- import { stdin, stdout } from "process";
13
- import chalk from "chalk";
14
- const ESC = "\u001b";
15
- const CTRL_C = "\u0003";
16
- /** The starts of sequences we understand — held back rather than acted on. */
17
- const SEQUENCE_PREFIXES = new Set([ESC, `${ESC}[`, `${ESC}O`]);
18
- /**
19
- * Escape sequence → intent. Exported for tests; there is no other way to
20
- * exercise key handling without a real TTY.
21
- *
22
- * A bare ESC deliberately means nothing. An arrow key IS an ESC followed by two
23
- * more bytes, and a terminal is free to deliver those bytes in separate reads —
24
- * so a lone ESC is far more likely to be half an arrow than a candidate asking
25
- * to dismiss the picker. Dismissing has its own row in the list; navigation
26
- * that closes the picker at random does not.
27
- */
28
- export function decodeKey(seq) {
29
- switch (seq) {
30
- case `${ESC}[A`: // arrow up, normal mode
31
- case `${ESC}OA`: // arrow up, application mode (tmux, some terminals)
32
- case "k":
33
- return "up";
34
- case `${ESC}[B`:
35
- case `${ESC}OB`:
36
- case "j":
37
- return "down";
38
- case "\r":
39
- case "\n":
40
- return "select";
41
- case CTRL_C: // raw mode swallows SIGINT, so we handle Ctrl-C ourselves
42
- return "abort";
43
- default:
44
- return null;
45
- }
46
- }
47
- /**
48
- * Pull every complete keypress out of the bytes read so far, and hand back
49
- * whatever trailing fragment is still the start of a sequence.
50
- *
51
- * Reads are not keypresses in either direction: holding an arrow key down
52
- * coalesces several presses into one read, and a slow link or a multiplexer can
53
- * split a single arrow across two. The caller carries `pending` into the next
54
- * read, so a split arrow still arrives as one "up".
55
- *
56
- * A fragment that turns out NOT to be a sequence start — ESC then a plain
57
- * letter — drops just the ESC and re-reads the rest, so the letter is not
58
- * swallowed along with it.
59
- */
60
- export function consumeKeys(buffer) {
61
- const keys = [];
62
- let rest = buffer;
63
- while (rest.length > 0) {
64
- const three = rest.length >= 3 ? decodeKey(rest.slice(0, 3)) : null;
65
- if (three) {
66
- keys.push(three);
67
- rest = rest.slice(3);
68
- continue;
69
- }
70
- if (SEQUENCE_PREFIXES.has(rest))
71
- break; // incomplete — wait for the rest
72
- const one = decodeKey(rest[0]);
73
- if (one)
74
- keys.push(one);
75
- rest = rest.slice(1);
76
- }
77
- return { keys, pending: rest };
78
- }
79
- /**
80
- * The rendered block, one string per terminal line. Pure, so tests can assert
81
- * which row carries the pointer without driving a TTY.
82
- */
83
- export function renderBlock(title, choices, active) {
84
- const rows = choices.map((c, i) => {
85
- const on = i === active;
86
- const pointer = on ? chalk.cyan("❯") : " ";
87
- const label = on ? chalk.cyan.bold(c.label) : c.label;
88
- const hint = c.hint ? " " + chalk.dim(c.hint) : "";
89
- return ` ${pointer} ${label}${hint}`;
90
- });
91
- return [
92
- ` ${chalk.bold(title)}`,
93
- "",
94
- ...rows,
95
- "",
96
- chalk.dim(" ↑/↓ to move · enter to choose"),
97
- ];
98
- }
99
- /** Whether we can drive a picker at all: both ends must be a TTY. */
100
- export function isInteractive() {
101
- return Boolean(stdin.isTTY && stdout.isTTY && typeof stdin.setRawMode === "function");
102
- }
103
- /**
104
- * Show `title` above `choices` and let the candidate move with ↑/↓ and confirm
105
- * with enter. Resolves to the chosen value, or to null when the picker can't
106
- * run at all (non-TTY). Ctrl-C exits with 130 — the same thing an un-swallowed
107
- * SIGINT would have done.
108
- *
109
- * The block is erased on exit, so the caller owns what the candidate is left
110
- * looking at.
111
- */
112
- export function select(title, choices) {
113
- if (!isInteractive() || choices.length === 0)
114
- return Promise.resolve(null);
115
- let active = 0;
116
- const lineCount = renderBlock(title, choices, active).length;
117
- const paint = (first) => {
118
- if (!first)
119
- stdout.write(`${ESC}[${lineCount}A`);
120
- for (const line of renderBlock(title, choices, active)) {
121
- stdout.write(`${ESC}[2K${line}\n`);
122
- }
123
- };
124
- const erase = () => {
125
- stdout.write(`${ESC}[${lineCount}A${ESC}[0J`);
126
- };
127
- return new Promise((resolve) => {
128
- const wasRaw = stdin.isRaw;
129
- stdout.write(`${ESC}[?25l`); // hide cursor
130
- stdin.setRawMode(true);
131
- stdin.resume();
132
- stdin.setEncoding("utf8");
133
- paint(true);
134
- const cleanup = () => {
135
- stdin.off("data", onData);
136
- stdin.setRawMode(Boolean(wasRaw));
137
- stdin.pause();
138
- stdout.write(`${ESC}[?25h`); // show cursor
139
- };
140
- // Carries a half-delivered escape sequence from one read to the next.
141
- let pending = "";
142
- const onData = (chunk) => {
143
- const read = consumeKeys(pending + chunk);
144
- pending = read.pending;
145
- for (const key of read.keys) {
146
- if (key === "up") {
147
- active = (active - 1 + choices.length) % choices.length;
148
- paint(false);
149
- }
150
- else if (key === "down") {
151
- active = (active + 1) % choices.length;
152
- paint(false);
153
- }
154
- else if (key === "select") {
155
- erase();
156
- cleanup();
157
- resolve(choices[active].value);
158
- return;
159
- }
160
- else if (key === "abort") {
161
- erase();
162
- cleanup();
163
- process.exit(130);
164
- }
165
- }
166
- };
167
- stdin.on("data", onData);
168
- });
169
- }
170
- //# sourceMappingURL=select.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"select.js","sourceRoot":"","sources":["../../src/lib/select.ts"],"names":[],"mappings":"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"}
@@ -1,65 +0,0 @@
1
- /**
2
- * The assessment shell — how `litmus init` puts the candidate *inside* their
3
- * assessment folder, and how `litmus submit` takes them back out.
4
- *
5
- * A process cannot change its parent shell's working directory, so "cd into the
6
- * folder for them" has exactly one honest implementation: init spawns an
7
- * interactive shell rooted in the assessment directory and waits on it. The
8
- * candidate works in that shell. When submit succeeds it hangs that shell up,
9
- * the shell exits, init's wait returns, and the candidate is back at the prompt
10
- * they started from, in the directory they started in — the `cd ..` they never
11
- * had to type.
12
- *
13
- * Two independent facts have to line up before we signal anything:
14
- * - LITMUS_ASSESSMENT_SHELL=1 in our own environment, which only a shell we
15
- * spawned can have set, and
16
- * - a .litmus/shell.pid written by that same init, for this same folder,
17
- * naming a process that is genuinely one of our ancestors.
18
- * Either alone could be stale; together they can only describe the shell we are
19
- * actually standing in.
20
- */
21
- /** Set in the spawned shell's environment; read back by `litmus submit`. */
22
- export declare const SHELL_ENV_MARKER = "LITMUS_ASSESSMENT_SHELL";
23
- /**
24
- * Whether init should hand the candidate a shell at all. Piped stdin (the e2e
25
- * suite, a CI script, `litmus init | tee`) must never get one — it would hang
26
- * forever on a shell nobody can type into.
27
- */
28
- export declare function shouldEnterShell(optIn: boolean | undefined): boolean;
29
- /** The candidate's own shell, falling back to something that certainly exists. */
30
- export declare function resolveShell(): {
31
- command: string;
32
- args: string[];
33
- };
34
- /**
35
- * Drop the candidate into `projectRoot` in their own shell and block until they
36
- * leave it — by typing exit, or by `litmus submit` hanging it up.
37
- *
38
- * Resolves to "submitted" when the folder is gone by the time the shell exits
39
- * (submit deleted it), "exited" when the candidate just left.
40
- *
41
- * Closing the shell has two triggers, because the two cases know different
42
- * things: an in-shell `litmus submit` signals it directly (instant, and ordered
43
- * against its own output), while a submit that happens elsewhere — the deadline
44
- * auto-submit in the watcher, or a second terminal — is noticed here by the
45
- * assessment folder disappearing.
46
- */
47
- export declare function enterAssessmentShell(projectRoot: string, folderName: string): Promise<"submitted" | "exited">;
48
- /** The pid init recorded for this folder's shell, if there is one. */
49
- export declare function readAssessmentShellPid(projectRoot: string): number | null;
50
- /**
51
- * Are we running inside the shell init opened for this folder? Both the
52
- * environment marker and an ancestry check have to agree — see the file header.
53
- *
54
- * Callers use this before printing anything about where the candidate will end
55
- * up, since leaving the shell moves them out of the folder for free.
56
- */
57
- export declare function inAssessmentShell(shellPid: number | null): boolean;
58
- /**
59
- * End the assessment shell. Returns true when the hang-up went out.
60
- *
61
- * Call this LAST and print nothing after it: the shell hangs up its jobs on the
62
- * way out, and this process is one of them.
63
- */
64
- export declare function hangUpAssessmentShell(shellPid: number | null): boolean;
65
- //# sourceMappingURL=session.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/lib/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAOH,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,4BAA4B,CAAA;AAQzD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAIpE;AAED,kFAAkF;AAClF,wBAAgB,YAAY,IAAI;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAOlE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,GAAG,QAAQ,CAAC,CAmEjC;AAED,sEAAsE;AACtE,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOzE;AA6BD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAIlE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAkBtE"}
@@ -1,212 +0,0 @@
1
- /**
2
- * The assessment shell — how `litmus init` puts the candidate *inside* their
3
- * assessment folder, and how `litmus submit` takes them back out.
4
- *
5
- * A process cannot change its parent shell's working directory, so "cd into the
6
- * folder for them" has exactly one honest implementation: init spawns an
7
- * interactive shell rooted in the assessment directory and waits on it. The
8
- * candidate works in that shell. When submit succeeds it hangs that shell up,
9
- * the shell exits, init's wait returns, and the candidate is back at the prompt
10
- * they started from, in the directory they started in — the `cd ..` they never
11
- * had to type.
12
- *
13
- * Two independent facts have to line up before we signal anything:
14
- * - LITMUS_ASSESSMENT_SHELL=1 in our own environment, which only a shell we
15
- * spawned can have set, and
16
- * - a .litmus/shell.pid written by that same init, for this same folder,
17
- * naming a process that is genuinely one of our ancestors.
18
- * Either alone could be stale; together they can only describe the shell we are
19
- * actually standing in.
20
- */
21
- import path from "path";
22
- import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
23
- import { spawn, execFileSync } from "child_process";
24
- import chalk from "chalk";
25
- /** Set in the spawned shell's environment; read back by `litmus submit`. */
26
- export const SHELL_ENV_MARKER = "LITMUS_ASSESSMENT_SHELL";
27
- const SHELL_PID_FILE = "shell.pid";
28
- function shellPidPath(projectRoot) {
29
- return path.join(projectRoot, ".litmus", SHELL_PID_FILE);
30
- }
31
- /**
32
- * Whether init should hand the candidate a shell at all. Piped stdin (the e2e
33
- * suite, a CI script, `litmus init | tee`) must never get one — it would hang
34
- * forever on a shell nobody can type into.
35
- */
36
- export function shouldEnterShell(optIn) {
37
- if (optIn === false)
38
- return false;
39
- if (process.env.LITMUS_NO_SHELL === "1")
40
- return false;
41
- return Boolean(process.stdin.isTTY && process.stdout.isTTY);
42
- }
43
- /** The candidate's own shell, falling back to something that certainly exists. */
44
- export function resolveShell() {
45
- if (process.platform === "win32") {
46
- return { command: process.env.ComSpec || "cmd.exe", args: [] };
47
- }
48
- // -i because stdio is inherited but the shell is not the session leader; zsh
49
- // and bash both need it to read their interactive rc files and print a prompt.
50
- return { command: process.env.SHELL || "/bin/sh", args: ["-i"] };
51
- }
52
- /**
53
- * Drop the candidate into `projectRoot` in their own shell and block until they
54
- * leave it — by typing exit, or by `litmus submit` hanging it up.
55
- *
56
- * Resolves to "submitted" when the folder is gone by the time the shell exits
57
- * (submit deleted it), "exited" when the candidate just left.
58
- *
59
- * Closing the shell has two triggers, because the two cases know different
60
- * things: an in-shell `litmus submit` signals it directly (instant, and ordered
61
- * against its own output), while a submit that happens elsewhere — the deadline
62
- * auto-submit in the watcher, or a second terminal — is noticed here by the
63
- * assessment folder disappearing.
64
- */
65
- export async function enterAssessmentShell(projectRoot, folderName) {
66
- const { command, args } = resolveShell();
67
- console.log(chalk.dim(` You're in ${folderName}/ — a shell opened here for you.`));
68
- console.log(chalk.dim(` Type ${chalk.reset(chalk.cyan("exit"))}${chalk.dim(" to leave it; your assessment keeps running either way.")}`));
69
- console.log();
70
- const child = spawn(command, args, {
71
- cwd: projectRoot,
72
- stdio: "inherit",
73
- env: {
74
- ...process.env,
75
- [SHELL_ENV_MARKER]: "1",
76
- LITMUS_ASSESSMENT_DIR: projectRoot,
77
- },
78
- });
79
- if (child.pid) {
80
- try {
81
- writeFileSync(shellPidPath(projectRoot), String(child.pid), "utf8");
82
- }
83
- catch {
84
- // Without the pid file submit just skips the hang-up and tells the
85
- // candidate to cd out themselves. Not worth failing init over.
86
- }
87
- }
88
- // Not every submit can hang this shell up itself. The deadline auto-submit
89
- // runs in the detached watcher, and a candidate can run `litmus submit` from
90
- // a second terminal; neither is standing in this shell, so neither is allowed
91
- // to signal it — and both delete the folder, which would leave the candidate
92
- // sitting in a shell whose working directory no longer exists.
93
- //
94
- // This process is the one that can always fix that: it spawned the shell, so
95
- // it knows the pid for certain, with no pid file and no ancestry to infer.
96
- // The folder going away IS the signal, whoever caused it.
97
- const folderWatch = setInterval(() => {
98
- if (existsSync(projectRoot))
99
- return;
100
- clearInterval(folderWatch);
101
- try {
102
- child.kill("SIGHUP");
103
- }
104
- catch {
105
- // Already gone — the in-shell submit path beat us to it.
106
- }
107
- }, 2000);
108
- // The shell is the foreground job; Ctrl-C at its prompt reaches this process
109
- // too. Ignoring it here means the candidate's ^C interrupts what they're
110
- // running, not the init that is holding their session open.
111
- const swallowSigint = () => { };
112
- process.on("SIGINT", swallowSigint);
113
- try {
114
- await new Promise((resolve) => {
115
- child.on("exit", () => resolve());
116
- child.on("error", () => resolve());
117
- });
118
- }
119
- finally {
120
- clearInterval(folderWatch);
121
- process.off("SIGINT", swallowSigint);
122
- }
123
- if (!existsSync(projectRoot))
124
- return "submitted";
125
- try {
126
- unlinkSync(shellPidPath(projectRoot));
127
- }
128
- catch {
129
- // already gone
130
- }
131
- return "exited";
132
- }
133
- /** The pid init recorded for this folder's shell, if there is one. */
134
- export function readAssessmentShellPid(projectRoot) {
135
- try {
136
- const pid = parseInt(readFileSync(shellPidPath(projectRoot), "utf8").trim(), 10);
137
- return Number.isFinite(pid) && pid > 1 ? pid : null;
138
- }
139
- catch {
140
- return null;
141
- }
142
- }
143
- /**
144
- * Is `target` one of our ancestors? Guards against a recycled pid: the pid file
145
- * outlives the shell that wrote it if init was killed outright.
146
- */
147
- function isAncestorPid(target) {
148
- if (process.platform === "win32") {
149
- // No cheap ancestor walk here; the env marker plus a per-folder pid file is
150
- // the gate on Windows.
151
- return true;
152
- }
153
- let pid = process.ppid;
154
- for (let depth = 0; depth < 8 && pid > 1; depth++) {
155
- if (pid === target)
156
- return true;
157
- try {
158
- const out = execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], {
159
- encoding: "utf8",
160
- timeout: 3000,
161
- });
162
- pid = parseInt(out.trim(), 10);
163
- }
164
- catch {
165
- return false;
166
- }
167
- if (!Number.isFinite(pid))
168
- return false;
169
- }
170
- return false;
171
- }
172
- /**
173
- * Are we running inside the shell init opened for this folder? Both the
174
- * environment marker and an ancestry check have to agree — see the file header.
175
- *
176
- * Callers use this before printing anything about where the candidate will end
177
- * up, since leaving the shell moves them out of the folder for free.
178
- */
179
- export function inAssessmentShell(shellPid) {
180
- if (process.env[SHELL_ENV_MARKER] !== "1")
181
- return false;
182
- if (shellPid === null)
183
- return false;
184
- return isAncestorPid(shellPid);
185
- }
186
- /**
187
- * End the assessment shell. Returns true when the hang-up went out.
188
- *
189
- * Call this LAST and print nothing after it: the shell hangs up its jobs on the
190
- * way out, and this process is one of them.
191
- */
192
- export function hangUpAssessmentShell(shellPid) {
193
- if (!inAssessmentShell(shellPid))
194
- return false;
195
- // zsh forwards SIGHUP to its jobs, and we are one. Ignore it so this process
196
- // finishes on its own terms instead of dying between two writes.
197
- process.on("SIGHUP", () => { });
198
- try {
199
- if (process.platform === "win32") {
200
- // No /T: that would take this process down with the shell.
201
- execFileSync("taskkill", ["/PID", String(shellPid), "/F"], { stdio: "ignore", timeout: 5000 });
202
- }
203
- else {
204
- process.kill(shellPid, "SIGHUP");
205
- }
206
- return true;
207
- }
208
- catch {
209
- return false;
210
- }
211
- }
212
- //# sourceMappingURL=session.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/lib/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,IAAI,MAAM,MAAM,CAAA;AACvB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,IAAI,CAAA;AACxE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,4EAA4E;AAC5E,MAAM,CAAC,MAAM,gBAAgB,GAAG,yBAAyB,CAAA;AAEzD,MAAM,cAAc,GAAG,WAAW,CAAA;AAElC,SAAS,YAAY,CAAC,WAAmB;IACvC,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,EAAE,cAAc,CAAC,CAAA;AAC1D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAA0B;IACzD,IAAI,KAAK,KAAK,KAAK;QAAE,OAAO,KAAK,CAAA;IACjC,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe,KAAK,GAAG;QAAE,OAAO,KAAK,CAAA;IACrD,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC7D,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,YAAY;IAC1B,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,SAAS,EAAE,IAAI,EAAE,EAAE,EAAE,CAAA;IAChE,CAAC;IACD,6EAA6E;IAC7E,+EAA+E;IAC/E,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,SAAS,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAA;AAClE,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,WAAmB,EACnB,UAAkB;IAElB,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,YAAY,EAAE,CAAA;IAExC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,eAAe,UAAU,kCAAkC,CAAC,CAAC,CAAA;IACnF,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,yDAAyD,CAAC,EAAE,CAAC,CAAC,CAAA;IAC1I,OAAO,CAAC,GAAG,EAAE,CAAA;IAEb,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE;QACjC,GAAG,EAAE,WAAW;QAChB,KAAK,EAAE,SAAS;QAChB,GAAG,EAAE;YACH,GAAG,OAAO,CAAC,GAAG;YACd,CAAC,gBAAgB,CAAC,EAAE,GAAG;YACvB,qBAAqB,EAAE,WAAW;SACnC;KACF,CAAC,CAAA;IAEF,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC;QACd,IAAI,CAAC;YACH,aAAa,CAAC,YAAY,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAA;QACrE,CAAC;QAAC,MAAM,CAAC;YACP,mEAAmE;YACnE,+DAA+D;QACjE,CAAC;IACH,CAAC;IAED,2EAA2E;IAC3E,6EAA6E;IAC7E,8EAA8E;IAC9E,6EAA6E;IAC7E,+DAA+D;IAC/D,EAAE;IACF,6EAA6E;IAC7E,2EAA2E;IAC3E,0DAA0D;IAC1D,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE;QACnC,IAAI,UAAU,CAAC,WAAW,CAAC;YAAE,OAAM;QACnC,aAAa,CAAC,WAAW,CAAC,CAAA;QAC1B,IAAI,CAAC;YACH,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACtB,CAAC;QAAC,MAAM,CAAC;YACP,yDAAyD;QAC3D,CAAC;IACH,CAAC,EAAE,IAAI,CAAC,CAAA;IAER,6EAA6E;IAC7E,yEAAyE;IACzE,4DAA4D;IAC5D,MAAM,aAAa,GAAG,GAAG,EAAE,GAAE,CAAC,CAAA;IAC9B,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAA;IACnC,IAAI,CAAC;QACH,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YAClC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAA;YACjC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAA;QACpC,CAAC,CAAC,CAAA;IACJ,CAAC;YAAS,CAAC;QACT,aAAa,CAAC,WAAW,CAAC,CAAA;QAC1B,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAA;IACtC,CAAC;IAED,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;QAAE,OAAO,WAAW,CAAA;IAChD,IAAI,CAAC;QACH,UAAU,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,CAAA;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,eAAe;IACjB,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,sBAAsB,CAAC,WAAmB;IACxD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,QAAQ,CAAC,YAAY,CAAC,YAAY,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAA;QAChF,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAA;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,aAAa,CAAC,MAAc;IACnC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,4EAA4E;QAC5E,uBAAuB;QACvB,OAAO,IAAI,CAAA;IACb,CAAC;IACD,IAAI,GAAG,GAAG,OAAO,CAAC,IAAI,CAAA;IACtB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;QAClD,IAAI,GAAG,KAAK,MAAM;YAAE,OAAO,IAAI,CAAA;QAC/B,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE;gBACjE,QAAQ,EAAE,MAAM;gBAChB,OAAO,EAAE,IAAI;aACd,CAAC,CAAA;YACF,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAA;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAA;QACd,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAA;IACzC,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAuB;IACvD,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,KAAK,GAAG;QAAE,OAAO,KAAK,CAAA;IACvD,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IACnC,OAAO,aAAa,CAAC,QAAQ,CAAC,CAAA;AAChC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAuB;IAC3D,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAA;IAE9C,6EAA6E;IAC7E,iEAAiE;IACjE,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IAE9B,IAAI,CAAC;QACH,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACjC,2DAA2D;YAC3D,YAAY,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;QAChG,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC,QAAkB,EAAE,QAAQ,CAAC,CAAA;QAC5C,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC"}