@nexrall/code-core 1.4.59 → 1.4.60

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.
@@ -0,0 +1,284 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports._sharedTasksInternal = void 0;
37
+ exports.listSharedTasks = listSharedTasks;
38
+ exports.createSharedTask = createSharedTask;
39
+ exports.claimTask = claimTask;
40
+ exports.updateSharedTaskStatus = updateSharedTaskStatus;
41
+ /**
42
+ * Shared task list — lets MULTIPLE independent sessions (two CLI windows, or
43
+ * CLI + VS Code + Desktop, all working the same project) coordinate who is
44
+ * doing what, mirroring Claude Code's agent-teams task list
45
+ * (`~/.claude/tasks/{team-name}/`, `pending/in_progress/completed/blocked`
46
+ * status, atomic claim-by-write).
47
+ *
48
+ * WHERE THIS FITS relative to the other two isolation mechanisms already in
49
+ * this codebase — they solve DIFFERENT problems and this module deliberately
50
+ * does not try to replace either:
51
+ * - crossProcessLock.ts: prevents two processes from corrupting the SAME
52
+ * FILE if they write it at the same instant. Reactive, byte-level.
53
+ * - worktree.ts: prevents two sessions from touching the same
54
+ * directory tree AT ALL. Preventive, physical.
55
+ * - sharedTasks.ts (here): lets sessions agree in ADVANCE on who is
56
+ * working on what, so they mostly never NEED the other two mechanisms
57
+ * to fire — this is social coordination, not a hard technical
58
+ * guarantee. A session can still ignore the task list; nothing here
59
+ * enforces claims the way worktreeEnforcement.ts enforces isolation.
60
+ * That's intentional and matches Claude Code's own docs: the task list
61
+ * is a coordination aid ("Assign different folders/files to different
62
+ * agents" is still the human's job), not a lock.
63
+ *
64
+ * STORAGE
65
+ * -------
66
+ * ~/.nexrall/peers/tasks/<workDirHash>/<taskId>.json
67
+ *
68
+ * One file per task. `workDirHash` scopes tasks to the project they belong
69
+ * to (same reasoning as crossProcessLock's opaque-key scoping) so two
70
+ * unrelated projects on the same machine never see each other's task lists.
71
+ *
72
+ * CLAIMING (the one operation that must be race-free)
73
+ * ----------------------------------------------------
74
+ * `claimTask` reads the task, checks it is claimable (pending, not blocked
75
+ * by an incomplete task), and writes `status: in_progress` + `claimedBy` —
76
+ * all while holding crossProcessLock.ts's OS-level lock for that exact task
77
+ * file. Two sessions racing to claim the same task: exactly one lock winner
78
+ * proceeds through the read-check-write; the loser's lock acquisition
79
+ * unblocks AFTER the winner has already written `in_progress`, so the
80
+ * loser's own claimability check (re-read after acquiring the lock) sees the
81
+ * task is no longer pending and returns 'already-claimed'. This is the exact
82
+ * race Claude Code's docs describe being solved "via file locking at the
83
+ * write layer" for their own task list.
84
+ *
85
+ * STALE CLAIMS
86
+ * ------------
87
+ * Claude Code's own docs list "status can become stale... requiring manual
88
+ * intervention" as a known limitation of their task list. This module goes
89
+ * one step further with a TTL + liveness check (same pattern as
90
+ * crossProcessLock's stale-lock detection): a claim whose owning session's
91
+ * process is no longer running, or whose claim is older than
92
+ * CLAIM_STALE_MS with no heartbeat, is treated as abandoned and can be
93
+ * reclaimed — still requiring the reclaiming session to go through the
94
+ * same race-free claim path above.
95
+ */
96
+ const fs = __importStar(require("fs"));
97
+ const os = __importStar(require("os"));
98
+ const path = __importStar(require("path"));
99
+ const crypto = __importStar(require("crypto"));
100
+ const crossProcessLock_1 = require("./crossProcessLock");
101
+ // A claim older than this with no update is treated the same as a dead-pid
102
+ // claim — reclaimable. Longer than crossProcessLock's STALE_MS (that guards a
103
+ // single tool call; this guards an entire unit of work a session might
104
+ // legitimately spend minutes on).
105
+ const CLAIM_STALE_MS = 30 * 60 * 1000; // 30 minutes
106
+ // Minimum age a claim must have before a DEAD-PID signal is trusted to mean
107
+ // "abandoned" (as opposed to CLAIM_STALE_MS above, which is the fallback for
108
+ // a claim whose pid is still alive but has gone quiet). Real bug this closes
109
+ // (caught by sharedTasks.test.mjs's genuine two-process claim race, which
110
+ // failed intermittently until this existed): a session can legitimately exit
111
+ // within milliseconds of successfully claiming a task — e.g. a short-lived
112
+ // worker that claims, does a tiny unit of work, and exits — and
113
+ // `process.kill(pid, 0)` reports that pid as dead the instant it's reaped, NOT
114
+ // only once it's been dead a while. A second session racing to claim the same
115
+ // task, arriving microseconds later, would see "pid dead" and treat a claim
116
+ // that is completely valid (its owner exited normally, having done exactly
117
+ // what it claimed the task to do) as abandoned trash to steal. A REAL crash
118
+ // is still caught by this — just after MIN_CLAIM_AGE_FOR_DEATH_CHECK_MS have
119
+ // passed, not instantly — which is an acceptable trade because the failure
120
+ // mode of "wait a couple seconds longer to reclaim a genuinely dead claim" is
121
+ // far cheaper than "two sessions think they both own the same unit of work".
122
+ const MIN_CLAIM_AGE_FOR_DEATH_CHECK_MS = 2000;
123
+ function tasksRoot() {
124
+ return process.env.NEXRALL_SHARED_TASKS_DIR || path.join(os.homedir(), '.nexrall', 'peers', 'tasks');
125
+ }
126
+ function scopeDir(workDir) {
127
+ const hash = crypto.createHash('sha256').update(path.resolve(workDir)).digest('hex').slice(0, 24);
128
+ return path.join(tasksRoot(), hash);
129
+ }
130
+ function taskPath(workDir, taskId) {
131
+ return path.join(scopeDir(workDir), `${taskId}.json`);
132
+ }
133
+ function readTask(workDir, taskId) {
134
+ try {
135
+ const raw = JSON.parse(fs.readFileSync(taskPath(workDir, taskId), 'utf-8'));
136
+ if (raw && typeof raw.id === 'string' && typeof raw.status === 'string')
137
+ return raw;
138
+ return null;
139
+ }
140
+ catch {
141
+ return null;
142
+ }
143
+ }
144
+ function writeTask(workDir, task) {
145
+ const dir = scopeDir(workDir);
146
+ fs.mkdirSync(dir, { recursive: true });
147
+ const p = taskPath(workDir, task.id);
148
+ // Same atomic-write-via-temp-then-rename reasoning as core's
149
+ // atomicWritePreservingMode: a crash mid-write must never leave a
150
+ // half-written JSON file that the next reader chokes on.
151
+ const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
152
+ fs.writeFileSync(tmp, JSON.stringify(task));
153
+ fs.renameSync(tmp, p);
154
+ }
155
+ function isPidAlive(pid) {
156
+ try {
157
+ process.kill(pid, 0);
158
+ return true;
159
+ }
160
+ catch {
161
+ return false;
162
+ }
163
+ }
164
+ /** A claimed task whose owning process is gone, or whose claim has gone stale with no update, is treated as abandoned. */
165
+ function isClaimAbandoned(task) {
166
+ if (task.status !== 'in_progress')
167
+ return false;
168
+ const age = typeof task.claimedAt === 'number' ? Date.now() - task.claimedAt : Infinity;
169
+ // See MIN_CLAIM_AGE_FOR_DEATH_CHECK_MS's own comment for why a dead-pid
170
+ // verdict is withheld until the claim has existed for a minimum age —
171
+ // without this guard, a session that legitimately claims-then-exits
172
+ // quickly can have its still-valid claim stolen out from under it.
173
+ if (age >= MIN_CLAIM_AGE_FOR_DEATH_CHECK_MS && typeof task.claimedByPid === 'number' && !isPidAlive(task.claimedByPid))
174
+ return true;
175
+ if (age > CLAIM_STALE_MS)
176
+ return true;
177
+ return false;
178
+ }
179
+ /** Whether every task ID in `blockedBy` is `completed` — i.e. this task is actually claimable right now. */
180
+ function isUnblocked(workDir, task) {
181
+ if (!task.blockedBy?.length)
182
+ return true;
183
+ return task.blockedBy.every((id) => readTask(workDir, id)?.status === 'completed');
184
+ }
185
+ /** List every task registered for `workDir`, newest first isn't guaranteed — sort by createdAt if display order matters to the caller. */
186
+ function listSharedTasks(workDir) {
187
+ let entries;
188
+ try {
189
+ entries = fs.readdirSync(scopeDir(workDir));
190
+ }
191
+ catch {
192
+ return [];
193
+ }
194
+ const out = [];
195
+ for (const name of entries) {
196
+ if (!name.endsWith('.json'))
197
+ continue;
198
+ const id = name.slice(0, -'.json'.length);
199
+ const t = readTask(workDir, id);
200
+ if (t)
201
+ out.push(t);
202
+ }
203
+ return out;
204
+ }
205
+ /** Create a new pending task. `id` auto-generated (short random) unless the caller supplies a stable one (e.g. re-registering from a static plan). */
206
+ function createSharedTask(workDir, content, opts = {}) {
207
+ const id = opts.id ?? crypto.randomBytes(6).toString('hex');
208
+ const now = Date.now();
209
+ const task = {
210
+ id, content, status: 'pending',
211
+ blockedBy: opts.blockedBy?.length ? opts.blockedBy : undefined,
212
+ createdAt: now, updatedAt: now,
213
+ };
214
+ writeTask(workDir, task);
215
+ return task;
216
+ }
217
+ /**
218
+ * Atomically claim `taskId` for `claimedBy` (a peer/session name). Race-free
219
+ * against another process calling this at the same instant — see this
220
+ * module's header comment for exactly why.
221
+ */
222
+ async function claimTask(workDir, taskId, claimedBy) {
223
+ // Lock scoped to (workDir, taskId) specifically — via crossProcessLock's
224
+ // opaque-key + workDir scoping (see its own header) — so claiming task A
225
+ // never blocks a concurrent claim of unrelated task B in the same project.
226
+ return (0, crossProcessLock_1.withCrossProcessLock)(`shared-task:${taskId}`, async () => {
227
+ const task = readTask(workDir, taskId);
228
+ if (!task)
229
+ return { ok: false, reason: 'not-found' };
230
+ // A task is claimable when it is 'pending' (the only status
231
+ // createSharedTask ever sets — 'blocked' is reserved for a future
232
+ // producer that wants to record the reason explicitly, but is treated
233
+ // identically to 'pending' here) OR an abandoned 'in_progress' claim
234
+ // (dead pid / stale timestamp — see isClaimAbandoned).
235
+ const claimable = task.status === 'pending' || task.status === 'blocked' || isClaimAbandoned(task);
236
+ if (!claimable) {
237
+ return { ok: false, reason: task.status === 'completed' ? 'not-pending' : 'already-claimed' };
238
+ }
239
+ if (!isUnblocked(workDir, task))
240
+ return { ok: false, reason: 'blocked' };
241
+ const now = Date.now();
242
+ const updated = {
243
+ ...task,
244
+ status: 'in_progress',
245
+ claimedBy,
246
+ claimedByPid: process.pid,
247
+ claimedAt: now,
248
+ updatedAt: now,
249
+ };
250
+ writeTask(workDir, updated);
251
+ return { ok: true, task: updated };
252
+ }, workDir);
253
+ }
254
+ /**
255
+ * Update a task's status. Used for both normal progress (in_progress →
256
+ * completed) and releasing a claim early (in_progress → pending, e.g. a
257
+ * session that claimed a task then decided not to do it after all). Also
258
+ * lock-guarded — a status update racing a claim attempt must not corrupt
259
+ * either operation's view of the task.
260
+ */
261
+ async function updateSharedTaskStatus(workDir, taskId, status, opts = {}) {
262
+ return (0, crossProcessLock_1.withCrossProcessLock)(`shared-task:${taskId}`, async () => {
263
+ const task = readTask(workDir, taskId);
264
+ if (!task)
265
+ return { ok: false, reason: 'not-found' };
266
+ // A task can only be marked done/reopened by whoever holds the claim —
267
+ // prevents session B from marking session A's in-progress task
268
+ // completed out from under it. Not enforced for a task with NO current
269
+ // claim (status pending/blocked → anyone may set it, e.g. un-blocking).
270
+ if (task.status === 'in_progress' && task.claimedBy && opts.claimedBy && task.claimedBy !== opts.claimedBy) {
271
+ return { ok: false, reason: 'already-claimed' };
272
+ }
273
+ const updated = {
274
+ ...task,
275
+ status,
276
+ updatedAt: Date.now(),
277
+ ...(status === 'pending' || status === 'completed' ? { claimedBy: undefined, claimedByPid: undefined, claimedAt: undefined } : {}),
278
+ };
279
+ writeTask(workDir, updated);
280
+ return { ok: true, task: updated };
281
+ }, workDir);
282
+ }
283
+ exports._sharedTasksInternal = { tasksRoot, scopeDir, taskPath, isClaimAbandoned, isUnblocked, CLAIM_STALE_MS, MIN_CLAIM_AGE_FOR_DEATH_CHECK_MS };
284
+ //# sourceMappingURL=sharedTasks.js.map
@@ -0,0 +1,111 @@
1
+ export interface WorktreeState {
2
+ /** Absolute path to the isolated worktree directory the session is confined to. */
3
+ worktreePath: string;
4
+ /** Absolute path to the main checkout / original workDir the isolation protects. */
5
+ mainCheckout: string;
6
+ /** Which backend created this worktree — determines how EnterWorktree enforcement and cleanup behave. */
7
+ backend: 'git' | 'copy' | 'hook';
8
+ /** Branch name, only meaningful for the git backend. */
9
+ branch?: string;
10
+ /** Human-facing name (also the directory's basename under .nexrall/worktrees/). */
11
+ name: string;
12
+ }
13
+ declare function isGitRepo(dir: string): {
14
+ ok: boolean;
15
+ root?: string;
16
+ };
17
+ /** Generate a readable random name, same shape Claude Code uses ("bright-running-fox"). */
18
+ export declare function randomWorktreeName(): string;
19
+ declare function safeName(name: string): string;
20
+ /**
21
+ * Look for a project-supplied override hook: `.nexrall/hooks/worktree-<phase>`
22
+ * (an executable script/command), same escape hatch Claude Code's docs
23
+ * describe for non-git version control. Returns the command string to run,
24
+ * or null if the project hasn't defined one for this phase.
25
+ */
26
+ declare function findWorktreeHook(workDir: string, phase: 'create' | 'enter' | 'exit'): string | null;
27
+ declare function copyRecursive(src: string, dest: string): void;
28
+ export interface CreateWorktreeResult {
29
+ ok: boolean;
30
+ state?: WorktreeState;
31
+ error?: string;
32
+ }
33
+ /**
34
+ * Create an isolated worktree for `workDir`, choosing the best backend
35
+ * automatically:
36
+ * 1. A project-supplied `.nexrall/hooks` override for 'create', if defined
37
+ * (works for ANY VCS, or a fully custom isolation scheme).
38
+ * 2. Git-native `git worktree add`, if workDir is inside a git repo.
39
+ * 3. Plain recursive copy (skips COPY_SKIP_DIRS), as a zero-config
40
+ * fallback that always works.
41
+ */
42
+ export declare function createWorktree(workDir: string, requestedName?: string): CreateWorktreeResult;
43
+ export interface RemoveWorktreeOptions {
44
+ /** Force removal even if the worktree has uncommitted/untracked changes. Mirrors Claude Code's own confirm-before-destroy default. */
45
+ force?: boolean;
46
+ }
47
+ export interface RemoveWorktreeResult {
48
+ ok: boolean;
49
+ /** True when the worktree had real work in it (dirty git tree, or ANY file, for copy-based) and removal was skipped without `force`. */
50
+ hadWork?: boolean;
51
+ error?: string;
52
+ }
53
+ /** Whether a worktree has anything in it worth not silently discarding. */
54
+ export declare function worktreeHasWork(state: WorktreeState): boolean;
55
+ export declare function removeWorktree(state: WorktreeState, opts?: RemoveWorktreeOptions): RemoveWorktreeResult;
56
+ /**
57
+ * Called when a session ENTERS an already-created worktree (including
58
+ * re-entering across a resume). Fires the project's 'enter' hook if defined
59
+ * — informational only for the git/copy backends (nothing to actually do:
60
+ * the directory already exists and is already the session's cwd), but
61
+ * REQUIRED for the 'hook' backend, which may need to check out/refresh
62
+ * VCS-specific state each time.
63
+ */
64
+ export declare function enterWorktree(state: WorktreeState): {
65
+ ok: boolean;
66
+ error?: string;
67
+ };
68
+ /**
69
+ * True when `absPath` resolves to somewhere inside the MAIN CHECKOUT that
70
+ * isolation is meant to protect, while NOT being inside the currently active
71
+ * worktree — the check `blockOutsideWorktree` (in executor.ts) uses to
72
+ * refuse a write/bash-cwd that targets the wrong tree. This is exactly the
73
+ * check Claude Code's own worktree docs describe: "blocks an Edit, Write, or
74
+ * NotebookEdit that targets a path in the main checkout" — NOT "any path
75
+ * outside the worktree" (a write to some unrelated directory elsewhere on
76
+ * disk isn't the hazard this specific check exists to prevent; ordinary
77
+ * sandbox/permission rules still govern that separately).
78
+ *
79
+ * The git/copy backends nest the worktree INSIDE the main checkout
80
+ * (`<mainCheckout>/.nexrall/worktrees/<name>`), so "inside mainCheckout" on
81
+ * its own would wrongly flag every path inside a perfectly legitimate
82
+ * worktree — this function excludes the active worktree's own subtree first.
83
+ *
84
+ * Deliberately resolves symlinks (fs.realpathSync) so a symlink planted
85
+ * INSIDE the worktree that points back at the main checkout can't be used to
86
+ * defeat the check — the same class of bypass Claude Code's own worktree
87
+ * docs call out for git redirects.
88
+ */
89
+ export declare function pathEscapesWorktree(absPath: string, state: WorktreeState): boolean;
90
+ declare function metadataPath(worktreePath: string): string;
91
+ /**
92
+ * List every worktree registered under `workDir`'s `.nexrall/worktrees/`,
93
+ * reconstructed from each one's metadata file. Skips (rather than throws on)
94
+ * a directory with no readable metadata — e.g. one a user created by hand,
95
+ * or debris from an older version — since one bad entry must not hide every
96
+ * legitimate one from a session picker.
97
+ */
98
+ export declare function listWorktrees(workDir: string): WorktreeState[];
99
+ /** Find a previously created worktree by name, for resuming into it in a fresh process. */
100
+ export declare function findWorktreeByName(workDir: string, name: string): WorktreeState | null;
101
+ export declare const _worktreeInternal: {
102
+ isGitRepo: typeof isGitRepo;
103
+ safeName: typeof safeName;
104
+ copyRecursive: typeof copyRecursive;
105
+ findWorktreeHook: typeof findWorktreeHook;
106
+ COPY_SKIP_DIRS: Set<string>;
107
+ WORKTREE_ROOT_DIRNAME: string;
108
+ metadataPath: typeof metadataPath;
109
+ };
110
+ export {};
111
+ //# sourceMappingURL=worktree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree.d.ts","sourceRoot":"","sources":["../../src/agent/worktree.ts"],"names":[],"mappings":"AAgDA,MAAM,WAAW,aAAa;IAC5B,mFAAmF;IACnF,YAAY,EAAE,MAAM,CAAC;IACrB,oFAAoF;IACpF,YAAY,EAAE,MAAM,CAAC;IACrB,yGAAyG;IACzG,OAAO,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;IACjC,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;CACd;AAiBD,iBAAS,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAQ9D;AAED,2FAA2F;AAC3F,wBAAgB,kBAAkB,IAAI,MAAM,CAO3C;AAED,iBAAS,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMtC;AAED;;;;;GAKG;AACH,iBAAS,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAW5F;AAuBD,iBAAS,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CActD;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,oBAAoB,CAiD5F;AAED,MAAM,WAAW,qBAAqB;IACpC,sIAAsI;IACtI,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,OAAO,CAAC;IACZ,wIAAwI;IACxI,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,2EAA2E;AAC3E,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAmC7D;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,IAAI,GAAE,qBAA0B,GAAG,oBAAoB,CAgD3G;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAMnF;AA6CD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,OAAO,CAMlF;AAgBD,iBAAS,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAElD;AA2BD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,EAAE,CAY9D;AAED,2FAA2F;AAC3F,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAEtF;AAED,eAAO,MAAM,iBAAiB;;;;;;;;CAAgH,CAAC"}