@gtc6244/immediacy-daemon 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/actions.d.ts +22 -0
- package/dist/src/actions.js +44 -0
- package/dist/src/actions.js.map +1 -0
- package/dist/src/agent/agent.d.ts +89 -0
- package/dist/src/agent/agent.js +22 -0
- package/dist/src/agent/agent.js.map +1 -0
- package/dist/src/agent/claude.d.ts +70 -0
- package/dist/src/agent/claude.js +404 -0
- package/dist/src/agent/claude.js.map +1 -0
- package/dist/src/agent/codex.d.ts +44 -0
- package/dist/src/agent/codex.js +303 -0
- package/dist/src/agent/codex.js.map +1 -0
- package/dist/src/agent/mock.d.ts +31 -0
- package/dist/src/agent/mock.js +155 -0
- package/dist/src/agent/mock.js.map +1 -0
- package/dist/src/agent/model-error.d.ts +8 -0
- package/dist/src/agent/model-error.js +25 -0
- package/dist/src/agent/model-error.js.map +1 -0
- package/dist/src/agent/openrouter.d.ts +38 -0
- package/dist/src/agent/openrouter.js +60 -0
- package/dist/src/agent/openrouter.js.map +1 -0
- package/dist/src/agent/workspace-scope.d.ts +25 -0
- package/dist/src/agent/workspace-scope.js +71 -0
- package/dist/src/agent/workspace-scope.js.map +1 -0
- package/dist/src/allowlist.d.ts +56 -0
- package/dist/src/allowlist.js +141 -0
- package/dist/src/allowlist.js.map +1 -0
- package/dist/src/chatlog.d.ts +50 -0
- package/dist/src/chatlog.js +179 -0
- package/dist/src/chatlog.js.map +1 -0
- package/dist/src/cli/paths.d.ts +40 -0
- package/dist/src/cli/paths.js +76 -0
- package/dist/src/cli/paths.js.map +1 -0
- package/dist/src/cli/service.d.ts +20 -0
- package/dist/src/cli/service.js +245 -0
- package/dist/src/cli/service.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +227 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +33 -0
- package/dist/src/config.js +122 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/credentials.d.ts +38 -0
- package/dist/src/credentials.js +120 -0
- package/dist/src/credentials.js.map +1 -0
- package/dist/src/crypto.d.ts +49 -0
- package/dist/src/crypto.js +202 -0
- package/dist/src/crypto.js.map +1 -0
- package/dist/src/daemon-identity.d.ts +31 -0
- package/dist/src/daemon-identity.js +97 -0
- package/dist/src/daemon-identity.js.map +1 -0
- package/dist/src/envelope.d.ts +14 -0
- package/dist/src/envelope.js +28 -0
- package/dist/src/envelope.js.map +1 -0
- package/dist/src/events.d.ts +40 -0
- package/dist/src/events.js +24 -0
- package/dist/src/events.js.map +1 -0
- package/dist/src/forward.d.ts +51 -0
- package/dist/src/forward.js +170 -0
- package/dist/src/forward.js.map +1 -0
- package/dist/src/fs-scope.d.ts +17 -0
- package/dist/src/fs-scope.js +47 -0
- package/dist/src/fs-scope.js.map +1 -0
- package/dist/src/github.d.ts +70 -0
- package/dist/src/github.js +178 -0
- package/dist/src/github.js.map +1 -0
- package/dist/src/gitops.d.ts +113 -0
- package/dist/src/gitops.js +557 -0
- package/dist/src/gitops.js.map +1 -0
- package/dist/src/hub.d.ts +50 -0
- package/dist/src/hub.js +92 -0
- package/dist/src/hub.js.map +1 -0
- package/dist/src/index.d.ts +22 -0
- package/dist/src/index.js +56 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/jobs/scheduler.d.ts +94 -0
- package/dist/src/jobs/scheduler.js +373 -0
- package/dist/src/jobs/scheduler.js.map +1 -0
- package/dist/src/jobs/store.d.ts +33 -0
- package/dist/src/jobs/store.js +111 -0
- package/dist/src/jobs/store.js.map +1 -0
- package/dist/src/log.d.ts +61 -0
- package/dist/src/log.js +166 -0
- package/dist/src/log.js.map +1 -0
- package/dist/src/migrate-state-dir.d.ts +1 -0
- package/dist/src/migrate-state-dir.js +47 -0
- package/dist/src/migrate-state-dir.js.map +1 -0
- package/dist/src/models.d.ts +36 -0
- package/dist/src/models.js +71 -0
- package/dist/src/models.js.map +1 -0
- package/dist/src/permission-gate.d.ts +48 -0
- package/dist/src/permission-gate.js +93 -0
- package/dist/src/permission-gate.js.map +1 -0
- package/dist/src/permission-scope.d.ts +77 -0
- package/dist/src/permission-scope.js +244 -0
- package/dist/src/permission-scope.js.map +1 -0
- package/dist/src/ports.d.ts +71 -0
- package/dist/src/ports.js +216 -0
- package/dist/src/ports.js.map +1 -0
- package/dist/src/projects.d.ts +83 -0
- package/dist/src/projects.js +205 -0
- package/dist/src/projects.js.map +1 -0
- package/dist/src/protocol.d.ts +1226 -0
- package/dist/src/protocol.js +16 -0
- package/dist/src/protocol.js.map +1 -0
- package/dist/src/repo-sync.d.ts +49 -0
- package/dist/src/repo-sync.js +83 -0
- package/dist/src/repo-sync.js.map +1 -0
- package/dist/src/server.d.ts +33 -0
- package/dist/src/server.js +973 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/terminal.d.ts +32 -0
- package/dist/src/terminal.js +92 -0
- package/dist/src/terminal.js.map +1 -0
- package/dist/src/titler.d.ts +15 -0
- package/dist/src/titler.js +72 -0
- package/dist/src/titler.js.map +1 -0
- package/dist/src/types.d.ts +99 -0
- package/dist/src/types.js +3 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/usage-limit.d.ts +49 -0
- package/dist/src/usage-limit.js +83 -0
- package/dist/src/usage-limit.js.map +1 -0
- package/dist/src/usage.d.ts +23 -0
- package/dist/src/usage.js +63 -0
- package/dist/src/usage.js.map +1 -0
- package/dist/src/workspace.d.ts +303 -0
- package/dist/src/workspace.js +1086 -0
- package/dist/src/workspace.js.map +1 -0
- package/dist/src/worktree.d.ts +133 -0
- package/dist/src/worktree.js +561 -0
- package/dist/src/worktree.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree-owned port discovery.
|
|
3
|
+
*
|
|
4
|
+
* When an agent runs `npm run dev` (or anything that binds a TCP port) inside
|
|
5
|
+
* its worktree, we want to surface that port to the operator so they can preview
|
|
6
|
+
* it in a local browser via a forwarded stream (see `forward.ts`).
|
|
7
|
+
*
|
|
8
|
+
* The authoritative signal is the OS, not the dev server's banner text: we ask
|
|
9
|
+
* `lsof` which processes are LISTENing on TCP, then correlate each listening
|
|
10
|
+
* process to a worktree by its working directory (`cwd`). A process whose `cwd`
|
|
11
|
+
* resolves inside a worktree path is attributed to that workspace — this catches
|
|
12
|
+
* servers launched in the interactive PTY and by the agent alike, and (because
|
|
13
|
+
* `cwd` survives it) a `npm run dev &` that gets reparented to launchd.
|
|
14
|
+
*
|
|
15
|
+
* The same attribution doubles as the security boundary: `forward.open` only
|
|
16
|
+
* dials ports that appear here, so a client cannot reach arbitrary loopback
|
|
17
|
+
* ports on the daemon box (the daemon's own WSS port, SSH, etc. never resolve to
|
|
18
|
+
* a worktree cwd, so they never appear).
|
|
19
|
+
*/
|
|
20
|
+
import { execFile } from "node:child_process";
|
|
21
|
+
import { realpathSync } from "node:fs";
|
|
22
|
+
import { logger } from "./log.js";
|
|
23
|
+
const SCAN_MS = Math.max(1000, Number(process.env.FORWARD_SCAN_MS) || 4000);
|
|
24
|
+
/** Hard cap on a single lsof invocation (independent of the scan interval). */
|
|
25
|
+
const LSOF_TIMEOUT_MS = 4000;
|
|
26
|
+
/**
|
|
27
|
+
* Run lsof and return stdout even on a non-zero exit. lsof exits 1 when it can't
|
|
28
|
+
* stat *some* of the entries it found (a process we don't own, a socket that
|
|
29
|
+
* just closed) while still printing the rest — that partial output is exactly
|
|
30
|
+
* what we want, so a non-zero exit is not treated as failure.
|
|
31
|
+
*/
|
|
32
|
+
function runLsof(args) {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
execFile("lsof", args, { timeout: LSOF_TIMEOUT_MS, maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => {
|
|
35
|
+
if (err && !stdout) {
|
|
36
|
+
// lsof missing, timed out, or produced nothing — degrade to "no ports".
|
|
37
|
+
logger.debug?.("forward", "lsof failed", {
|
|
38
|
+
data: { message: err instanceof Error ? err.message : String(err) },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
resolve(stdout ?? "");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** Parse `host:port` (incl. IPv6 `[::1]:port` / `*:port`) → {addr, port}. */
|
|
46
|
+
export function splitHostPort(name) {
|
|
47
|
+
const colon = name.lastIndexOf(":");
|
|
48
|
+
if (colon < 0)
|
|
49
|
+
return null;
|
|
50
|
+
const port = Number(name.slice(colon + 1));
|
|
51
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
52
|
+
return null;
|
|
53
|
+
let addr = name.slice(0, colon);
|
|
54
|
+
if (addr.startsWith("[") && addr.endsWith("]"))
|
|
55
|
+
addr = addr.slice(1, -1);
|
|
56
|
+
return { addr, port };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* All processes LISTENing on TCP, as {pid, port, addr}. `-n`/`-P` keep hosts and
|
|
60
|
+
* ports numeric; `-Fpn` gives machine-readable field output (`p<pid>` at the
|
|
61
|
+
* process level, `n<addr:port>` per socket).
|
|
62
|
+
*/
|
|
63
|
+
async function listeningSockets() {
|
|
64
|
+
const out = await runLsof(["-nP", "-iTCP", "-sTCP:LISTEN", "-Fpn"]);
|
|
65
|
+
const sockets = [];
|
|
66
|
+
let pid = 0;
|
|
67
|
+
for (const line of out.split("\n")) {
|
|
68
|
+
if (line.length < 2)
|
|
69
|
+
continue;
|
|
70
|
+
const tag = line[0];
|
|
71
|
+
const val = line.slice(1);
|
|
72
|
+
if (tag === "p") {
|
|
73
|
+
pid = Number(val) || 0;
|
|
74
|
+
}
|
|
75
|
+
else if (tag === "n" && pid) {
|
|
76
|
+
const hp = splitHostPort(val);
|
|
77
|
+
if (hp)
|
|
78
|
+
sockets.push({ pid, port: hp.port, addr: hp.addr });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return sockets;
|
|
82
|
+
}
|
|
83
|
+
/** cwd for each pid: `lsof -a -p <csv> -d cwd -Fpn` → pid→directory. */
|
|
84
|
+
async function cwdOf(pids) {
|
|
85
|
+
const map = new Map();
|
|
86
|
+
if (pids.length === 0)
|
|
87
|
+
return map;
|
|
88
|
+
const out = await runLsof(["-a", "-p", pids.join(","), "-d", "cwd", "-Fpn"]);
|
|
89
|
+
let pid = 0;
|
|
90
|
+
for (const line of out.split("\n")) {
|
|
91
|
+
if (line.length < 2)
|
|
92
|
+
continue;
|
|
93
|
+
const tag = line[0];
|
|
94
|
+
const val = line.slice(1);
|
|
95
|
+
if (tag === "p")
|
|
96
|
+
pid = Number(val) || 0;
|
|
97
|
+
else if (tag === "n" && pid)
|
|
98
|
+
map.set(pid, val);
|
|
99
|
+
}
|
|
100
|
+
return map;
|
|
101
|
+
}
|
|
102
|
+
/** True when `cwd` is `root` itself or nested inside it. */
|
|
103
|
+
function underPath(cwd, root) {
|
|
104
|
+
if (!cwd || !root)
|
|
105
|
+
return false;
|
|
106
|
+
const r = root.endsWith("/") ? root.slice(0, -1) : root;
|
|
107
|
+
return cwd === r || cwd.startsWith(r + "/");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* One discovery pass: which worktree-owned ports are listening right now.
|
|
111
|
+
* Deduped by (ws, port); a port that binds both IPv4 and IPv6 collapses to one
|
|
112
|
+
* entry, preferring an explicit loopback address for display.
|
|
113
|
+
*/
|
|
114
|
+
export async function scanOwnedPorts(worktrees) {
|
|
115
|
+
if (worktrees.length === 0)
|
|
116
|
+
return [];
|
|
117
|
+
const sockets = await listeningSockets();
|
|
118
|
+
if (sockets.length === 0)
|
|
119
|
+
return [];
|
|
120
|
+
const cwds = await cwdOf([...new Set(sockets.map((s) => s.pid))]);
|
|
121
|
+
// Canonicalize worktree roots: lsof reports a process's cwd as its real path
|
|
122
|
+
// (e.g. macOS resolves /var → /private/var, /tmp → /private/tmp), so we must
|
|
123
|
+
// compare against the resolved root or a legitimately-owned port never matches.
|
|
124
|
+
const roots = worktrees.map((w) => {
|
|
125
|
+
try {
|
|
126
|
+
return { ws: w.ws, root: realpathSync(w.worktreePath) };
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return { ws: w.ws, root: w.worktreePath };
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
const byKey = new Map();
|
|
133
|
+
for (const s of sockets) {
|
|
134
|
+
const cwd = cwds.get(s.pid);
|
|
135
|
+
if (!cwd)
|
|
136
|
+
continue;
|
|
137
|
+
const wt = roots.find((w) => underPath(cwd, w.root));
|
|
138
|
+
if (!wt)
|
|
139
|
+
continue;
|
|
140
|
+
const key = `${wt.ws}:${s.port}`;
|
|
141
|
+
const existing = byKey.get(key);
|
|
142
|
+
// Prefer a concrete loopback bind for the displayed addr.
|
|
143
|
+
if (!existing || (existing.addr !== "127.0.0.1" && s.addr === "127.0.0.1")) {
|
|
144
|
+
byKey.set(key, { ws: wt.ws, port: s.port, addr: s.addr });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return [...byKey.values()].sort((a, b) => a.port - b.port);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Polls `scanOwnedPorts` on an interval and fires `onChange` only when the set
|
|
151
|
+
* actually moves (a new server came up / one went away), so clients get an
|
|
152
|
+
* event on transitions rather than a steady heartbeat. Start it when at least
|
|
153
|
+
* one client is attached; stop it when the last one leaves.
|
|
154
|
+
*/
|
|
155
|
+
export class PortScanner {
|
|
156
|
+
worktrees;
|
|
157
|
+
onChange;
|
|
158
|
+
timer = null;
|
|
159
|
+
last = "";
|
|
160
|
+
scanning = false;
|
|
161
|
+
/** A refresh() that landed during an in-flight scan; honored before tick ends. */
|
|
162
|
+
rescanRequested = false;
|
|
163
|
+
constructor(worktrees, onChange) {
|
|
164
|
+
this.worktrees = worktrees;
|
|
165
|
+
this.onChange = onChange;
|
|
166
|
+
}
|
|
167
|
+
/** The most recent scan result (used to answer `forward.list` immediately). */
|
|
168
|
+
current = [];
|
|
169
|
+
start() {
|
|
170
|
+
if (this.timer)
|
|
171
|
+
return;
|
|
172
|
+
void this.tick();
|
|
173
|
+
this.timer = setInterval(() => void this.tick(), SCAN_MS);
|
|
174
|
+
this.timer.unref?.();
|
|
175
|
+
}
|
|
176
|
+
stop() {
|
|
177
|
+
if (this.timer)
|
|
178
|
+
clearInterval(this.timer);
|
|
179
|
+
this.timer = null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Force an out-of-band scan (e.g. a client just attached, or an agent turn
|
|
183
|
+
* finished). Guaranteed to yield a scan that started after this call: if one
|
|
184
|
+
* is already running, `rescanRequested` makes the in-flight loop run again
|
|
185
|
+
* rather than dropping the request.
|
|
186
|
+
*/
|
|
187
|
+
async refresh() {
|
|
188
|
+
this.rescanRequested = true;
|
|
189
|
+
await this.tick();
|
|
190
|
+
}
|
|
191
|
+
async tick() {
|
|
192
|
+
if (this.scanning)
|
|
193
|
+
return; // a scan owns the loop; rescanRequested re-runs it
|
|
194
|
+
this.scanning = true;
|
|
195
|
+
try {
|
|
196
|
+
do {
|
|
197
|
+
this.rescanRequested = false;
|
|
198
|
+
const ports = await scanOwnedPorts(this.worktrees());
|
|
199
|
+
const sig = ports.map((p) => `${p.ws}:${p.port}`).join(",");
|
|
200
|
+
this.current = ports;
|
|
201
|
+
if (sig !== this.last) {
|
|
202
|
+
this.last = sig;
|
|
203
|
+
this.onChange(ports);
|
|
204
|
+
}
|
|
205
|
+
} while (this.rescanRequested);
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
this.scanning = false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Is `port` currently owned by `ws`? The forward-open security check. */
|
|
212
|
+
isOwned(ws, port) {
|
|
213
|
+
return this.current.some((p) => p.ws === ws && p.port === port);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=ports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports.js","sourceRoot":"","sources":["../../src/ports.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectStore — the registry of projects (one repo each) that worktrees hang
|
|
3
|
+
* under. First-class and persisted (projects.json) so a project stays a stable
|
|
4
|
+
* "+ new worktree here" target even with zero live worktrees.
|
|
5
|
+
*
|
|
6
|
+
* Where a project lives on disk is inherent, never chosen by a client: blank
|
|
7
|
+
* and GitHub projects are materialized under the daemon-managed projects root
|
|
8
|
+
* (a path derived from name + id), and an adopted local repo stays wherever it
|
|
9
|
+
* already is. A project can therefore start from nothing — no existing repo,
|
|
10
|
+
* local or remote — via a fresh `git init` (see worktree.ts `initRepo`).
|
|
11
|
+
*/
|
|
12
|
+
import type { ProjectRecord } from "./types.js";
|
|
13
|
+
/** Managed root where blank + GitHub projects live; override with PROJECTS_ROOT. */
|
|
14
|
+
export declare const PROJECTS_ROOT: string;
|
|
15
|
+
/**
|
|
16
|
+
* How to populate a project's repo. Exactly one intent is expected:
|
|
17
|
+
* - `blank` — `git init` a brand-new repo (no existing repo anywhere);
|
|
18
|
+
* - `github` — clone a GitHub repo into the managed root;
|
|
19
|
+
* - `repo` — adopt an existing local git repo in place.
|
|
20
|
+
*/
|
|
21
|
+
export interface ProjectSource {
|
|
22
|
+
blank?: boolean;
|
|
23
|
+
repo?: string;
|
|
24
|
+
github?: {
|
|
25
|
+
fullName: string;
|
|
26
|
+
defaultBranch?: string;
|
|
27
|
+
};
|
|
28
|
+
name?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare class ProjectStore {
|
|
31
|
+
private projects;
|
|
32
|
+
load(): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* One-time cleanup: projects created before names were humanised stored the
|
|
35
|
+
* "owner__repo" clone-dir basename as their name. Rewrite those to the bare
|
|
36
|
+
* repo name. Only touches names still equal to the clone-dir basename, so a
|
|
37
|
+
* name the user has since edited is left untouched. Returns whether anything
|
|
38
|
+
* changed.
|
|
39
|
+
*/
|
|
40
|
+
private renameCachedClonesInPlace;
|
|
41
|
+
private persist;
|
|
42
|
+
list(): ProjectRecord[];
|
|
43
|
+
get(id: string): ProjectRecord | undefined;
|
|
44
|
+
/** The project whose repo resolves to the same absolute path, if any. */
|
|
45
|
+
findByRepo(repo: string): ProjectRecord | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* The project a source already maps to, if any. GitHub sources de-dup by full
|
|
48
|
+
* name, adopted local repos by resolved path; a blank project never de-dups —
|
|
49
|
+
* each `git init` is a distinct new repo.
|
|
50
|
+
*/
|
|
51
|
+
private findExisting;
|
|
52
|
+
/**
|
|
53
|
+
* Materialize a source into an on-disk repo path. GitHub and blank projects
|
|
54
|
+
* land in the managed root at a name+id-derived dir (never client-chosen); an
|
|
55
|
+
* adopted local repo stays where it already is and is validated to be a git
|
|
56
|
+
* repo. GitHub is cloned, blank is `git init`ed with an empty first commit.
|
|
57
|
+
*/
|
|
58
|
+
private materializeRepo;
|
|
59
|
+
/** Managed dir name: a readable slug plus a short id so names never collide. */
|
|
60
|
+
private slugFor;
|
|
61
|
+
/**
|
|
62
|
+
* Default project name from its source: an explicit name wins, else the bare
|
|
63
|
+
* GitHub repo name (no `owner/` prefix), else an adopted repo's path
|
|
64
|
+
* basename, else "project" (a blank repo).
|
|
65
|
+
*/
|
|
66
|
+
private defaultName;
|
|
67
|
+
/**
|
|
68
|
+
* Find-or-create the project for a source. GitHub/local sources are idempotent
|
|
69
|
+
* (see `findExisting`); a blank source always creates. The backbone of
|
|
70
|
+
* `project.create` and the legacy bare-repo workspace create path.
|
|
71
|
+
*/
|
|
72
|
+
ensureForSource(src: ProjectSource): Promise<ProjectRecord>;
|
|
73
|
+
/** Remove a project. The caller guards that no worktrees still reference it. */
|
|
74
|
+
remove(id: string): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Rewrite the stored project order. `list()` returns projects in Map insertion
|
|
77
|
+
* order, so we rebuild the Map in the requested order. Unknown ids are ignored;
|
|
78
|
+
* any known project absent from `ids` (e.g. one created after the client took
|
|
79
|
+
* its snapshot) is appended in its prior relative order so nothing is dropped.
|
|
80
|
+
* Persists only when the order actually changed.
|
|
81
|
+
*/
|
|
82
|
+
reorder(ids: string[]): Promise<void>;
|
|
83
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectStore — the registry of projects (one repo each) that worktrees hang
|
|
3
|
+
* under. First-class and persisted (projects.json) so a project stays a stable
|
|
4
|
+
* "+ new worktree here" target even with zero live worktrees.
|
|
5
|
+
*
|
|
6
|
+
* Where a project lives on disk is inherent, never chosen by a client: blank
|
|
7
|
+
* and GitHub projects are materialized under the daemon-managed projects root
|
|
8
|
+
* (a path derived from name + id), and an adopted local repo stays wherever it
|
|
9
|
+
* already is. A project can therefore start from nothing — no existing repo,
|
|
10
|
+
* local or remote — via a fresh `git init` (see worktree.ts `initRepo`).
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
17
|
+
import { cloneInto, refreshRepo, REPO_CACHE } from "./github.js";
|
|
18
|
+
import { initRepo } from "./worktree.js";
|
|
19
|
+
const PROJECTS_FILE = process.env.PROJECTS_FILE ?? join(homedir(), ".immediacy", "projects.json");
|
|
20
|
+
/** Managed root where blank + GitHub projects live; override with PROJECTS_ROOT. */
|
|
21
|
+
export const PROJECTS_ROOT = process.env.PROJECTS_ROOT ?? join(homedir(), ".immediacy", "projects");
|
|
22
|
+
/** True when `repoPath` is a clone under the GitHub repo cache. */
|
|
23
|
+
function isCachedClone(repoPath) {
|
|
24
|
+
const cache = resolve(REPO_CACHE);
|
|
25
|
+
const p = resolve(repoPath);
|
|
26
|
+
return p === cache || p.startsWith(cache + sep);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The bare repo name for a cached clone. `cacheDirFor` maps "owner/repo" to
|
|
30
|
+
* "owner__repo" (the "/" is the only separator turned into "__", since repo and
|
|
31
|
+
* owner names are already `[A-Za-z0-9._-]`), so the last "__"-segment is the
|
|
32
|
+
* repo — e.g. "GTC6244__immediacy" → "immediacy".
|
|
33
|
+
*/
|
|
34
|
+
function repoNameFromCacheDir(repoPath) {
|
|
35
|
+
const base = basename(repoPath);
|
|
36
|
+
return base.split("__").pop() || base;
|
|
37
|
+
}
|
|
38
|
+
export class ProjectStore {
|
|
39
|
+
projects = new Map();
|
|
40
|
+
async load() {
|
|
41
|
+
try {
|
|
42
|
+
const raw = await readFile(PROJECTS_FILE, "utf8");
|
|
43
|
+
const records = JSON.parse(raw);
|
|
44
|
+
for (const r of records)
|
|
45
|
+
this.projects.set(r.id, r);
|
|
46
|
+
console.log(`[projects] restored ${this.projects.size} project(s)`);
|
|
47
|
+
if (this.renameCachedClonesInPlace())
|
|
48
|
+
await this.persist();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* no prior state */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One-time cleanup: projects created before names were humanised stored the
|
|
56
|
+
* "owner__repo" clone-dir basename as their name. Rewrite those to the bare
|
|
57
|
+
* repo name. Only touches names still equal to the clone-dir basename, so a
|
|
58
|
+
* name the user has since edited is left untouched. Returns whether anything
|
|
59
|
+
* changed.
|
|
60
|
+
*/
|
|
61
|
+
renameCachedClonesInPlace() {
|
|
62
|
+
let changed = false;
|
|
63
|
+
for (const r of this.projects.values()) {
|
|
64
|
+
if (isCachedClone(r.repo) && r.name === basename(r.repo)) {
|
|
65
|
+
r.name = repoNameFromCacheDir(r.repo);
|
|
66
|
+
changed = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return changed;
|
|
70
|
+
}
|
|
71
|
+
async persist() {
|
|
72
|
+
await mkdir(dirname(PROJECTS_FILE), { recursive: true });
|
|
73
|
+
const records = [...this.projects.values()];
|
|
74
|
+
await writeFile(PROJECTS_FILE, JSON.stringify(records, null, 2), "utf8");
|
|
75
|
+
}
|
|
76
|
+
list() {
|
|
77
|
+
return [...this.projects.values()];
|
|
78
|
+
}
|
|
79
|
+
get(id) {
|
|
80
|
+
return this.projects.get(id);
|
|
81
|
+
}
|
|
82
|
+
/** The project whose repo resolves to the same absolute path, if any. */
|
|
83
|
+
findByRepo(repo) {
|
|
84
|
+
const key = resolve(repo);
|
|
85
|
+
return [...this.projects.values()].find((p) => resolve(p.repo) === key);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The project a source already maps to, if any. GitHub sources de-dup by full
|
|
89
|
+
* name, adopted local repos by resolved path; a blank project never de-dups —
|
|
90
|
+
* each `git init` is a distinct new repo.
|
|
91
|
+
*/
|
|
92
|
+
findExisting(src) {
|
|
93
|
+
if (src.blank)
|
|
94
|
+
return undefined;
|
|
95
|
+
if (src.github?.fullName) {
|
|
96
|
+
return [...this.projects.values()].find((p) => p.github?.fullName === src.github.fullName);
|
|
97
|
+
}
|
|
98
|
+
if (src.repo)
|
|
99
|
+
return this.findByRepo(src.repo);
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Materialize a source into an on-disk repo path. GitHub and blank projects
|
|
104
|
+
* land in the managed root at a name+id-derived dir (never client-chosen); an
|
|
105
|
+
* adopted local repo stays where it already is and is validated to be a git
|
|
106
|
+
* repo. GitHub is cloned, blank is `git init`ed with an empty first commit.
|
|
107
|
+
*/
|
|
108
|
+
async materializeRepo(src, id, name) {
|
|
109
|
+
// Adopt an existing local repo in place — the one non-managed source.
|
|
110
|
+
if (src.repo && !src.blank && !src.github?.fullName) {
|
|
111
|
+
const repoPath = resolve(src.repo);
|
|
112
|
+
if (!existsSync(join(repoPath, ".git"))) {
|
|
113
|
+
throw new Error(`not a git repo: ${repoPath}`);
|
|
114
|
+
}
|
|
115
|
+
return repoPath;
|
|
116
|
+
}
|
|
117
|
+
const dest = join(PROJECTS_ROOT, this.slugFor(name, id));
|
|
118
|
+
if (src.github?.fullName) {
|
|
119
|
+
await cloneInto(src.github.fullName, dest);
|
|
120
|
+
return dest;
|
|
121
|
+
}
|
|
122
|
+
return initRepo(dest);
|
|
123
|
+
}
|
|
124
|
+
/** Managed dir name: a readable slug plus a short id so names never collide. */
|
|
125
|
+
slugFor(name, id) {
|
|
126
|
+
const slug = name.replace(/[^a-zA-Z0-9._-]/g, "-").toLowerCase().replace(/^-+|-+$/g, "") ||
|
|
127
|
+
"project";
|
|
128
|
+
return `${slug}-${id.slice(0, 8)}`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Default project name from its source: an explicit name wins, else the bare
|
|
132
|
+
* GitHub repo name (no `owner/` prefix), else an adopted repo's path
|
|
133
|
+
* basename, else "project" (a blank repo).
|
|
134
|
+
*/
|
|
135
|
+
defaultName(src) {
|
|
136
|
+
const explicit = src.name?.trim();
|
|
137
|
+
if (explicit)
|
|
138
|
+
return explicit;
|
|
139
|
+
if (src.github?.fullName) {
|
|
140
|
+
return src.github.fullName.split("/").pop() || src.github.fullName;
|
|
141
|
+
}
|
|
142
|
+
if (src.repo && !src.blank)
|
|
143
|
+
return basename(resolve(src.repo)) || "project";
|
|
144
|
+
return "project";
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Find-or-create the project for a source. GitHub/local sources are idempotent
|
|
148
|
+
* (see `findExisting`); a blank source always creates. The backbone of
|
|
149
|
+
* `project.create` and the legacy bare-repo workspace create path.
|
|
150
|
+
*/
|
|
151
|
+
async ensureForSource(src) {
|
|
152
|
+
const existing = this.findExisting(src);
|
|
153
|
+
if (existing) {
|
|
154
|
+
// Freshen a reused GitHub clone; blank/adopted repos have nothing to pull.
|
|
155
|
+
if (existing.github)
|
|
156
|
+
await refreshRepo(existing.repo);
|
|
157
|
+
return existing;
|
|
158
|
+
}
|
|
159
|
+
const id = randomUUID();
|
|
160
|
+
const name = this.defaultName(src);
|
|
161
|
+
const repo = await this.materializeRepo(src, id, name);
|
|
162
|
+
const record = {
|
|
163
|
+
id,
|
|
164
|
+
name,
|
|
165
|
+
repo,
|
|
166
|
+
github: src.github?.fullName
|
|
167
|
+
? { fullName: src.github.fullName, defaultBranch: src.github.defaultBranch }
|
|
168
|
+
: undefined,
|
|
169
|
+
createdAt: Date.now(),
|
|
170
|
+
};
|
|
171
|
+
this.projects.set(record.id, record);
|
|
172
|
+
await this.persist();
|
|
173
|
+
return record;
|
|
174
|
+
}
|
|
175
|
+
/** Remove a project. The caller guards that no worktrees still reference it. */
|
|
176
|
+
async remove(id) {
|
|
177
|
+
if (this.projects.delete(id))
|
|
178
|
+
await this.persist();
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Rewrite the stored project order. `list()` returns projects in Map insertion
|
|
182
|
+
* order, so we rebuild the Map in the requested order. Unknown ids are ignored;
|
|
183
|
+
* any known project absent from `ids` (e.g. one created after the client took
|
|
184
|
+
* its snapshot) is appended in its prior relative order so nothing is dropped.
|
|
185
|
+
* Persists only when the order actually changed.
|
|
186
|
+
*/
|
|
187
|
+
async reorder(ids) {
|
|
188
|
+
const next = new Map();
|
|
189
|
+
for (const id of ids) {
|
|
190
|
+
const r = this.projects.get(id);
|
|
191
|
+
if (r && !next.has(id))
|
|
192
|
+
next.set(id, r);
|
|
193
|
+
}
|
|
194
|
+
for (const [id, r] of this.projects) {
|
|
195
|
+
if (!next.has(id))
|
|
196
|
+
next.set(id, r);
|
|
197
|
+
}
|
|
198
|
+
const prevOrder = [...this.projects.keys()];
|
|
199
|
+
const changed = [...next.keys()].some((id, i) => id !== prevOrder[i]);
|
|
200
|
+
this.projects = next;
|
|
201
|
+
if (changed)
|
|
202
|
+
await this.persist();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=projects.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projects.js","sourceRoot":"","sources":["../../src/projects.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAElE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,MAAM,aAAa,GACjB,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,EAAE,eAAe,CAAC,CAAC;AAE9E,oFAAoF;AACpF,MAAM,CAAC,MAAM,aAAa,GACxB,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;AAezE,mEAAmE;AACnE,SAAS,aAAa,CAAC,QAAgB;IACrC,MAAM,KAAK,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAClC,MAAM,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5B,OAAO,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,UAAU,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC;AAClD,CAAC;AAED;;;;;GAKG;AACH,SAAS,oBAAoB,CAAC,QAAgB;IAC5C,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAChC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC;AACxC,CAAC;AAED,MAAM,OAAO,YAAY;IACf,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;IAEpD,KAAK,CAAC,IAAI;QACR,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;YAClD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAoB,CAAC;YACnD,KAAK,MAAM,CAAC,IAAI,OAAO;gBAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;YACpD,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,CAAC,QAAQ,CAAC,IAAI,aAAa,CAAC,CAAC;YACpE,IAAI,IAAI,CAAC,yBAAyB,EAAE;gBAAE,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QAC7D,CAAC;QAAC,MAAM,CAAC;YACP,oBAAoB;QACtB,CAAC;IACH,CAAC;IAED;;;;;;OAMG;IACK,yBAAyB;QAC/B,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YACvC,IAAI,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;gBACzD,CAAC,CAAC,IAAI,GAAG,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;gBACtC,OAAO,GAAG,IAAI,CAAC;YACjB,CAAC;QACH,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAEO,KAAK,CAAC,OAAO;QACnB,MAAM,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzD,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;QAC5C,MAAM,SAAS,CAAC,aAAa,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC3E,CAAC;IAED,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACrC,CAAC;IAED,GAAG,CAAC,EAAU;QACZ,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC/B,CAAC;IAED,yEAAyE;IACzE,UAAU,CAAC,IAAY;QACrB,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1B,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;IAC1E,CAAC;IAED;;;;OAIG;IACK,YAAY,CAAC,GAAkB;QACrC,IAAI,GAAG,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QAChC,IAAI,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC;YACzB,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CACrC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,QAAQ,KAAK,GAAG,CAAC,MAAO,CAAC,QAAQ,CACnD,CAAC;QACJ,CAAC;QACD,IAAI,GAAG,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC/C,OAAO,SAAS,CAAC;IACnB,CAAC;IAED;;;;;OAKG;IACK,KAAK,CAAC,eAAe,CAC3B,GAAkB,EAClB,EAAU,EACV,IAAY;QAEZ,sEAAsE;QACtE,IAAI,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC;YACpD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACnC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC;gBACxC,MAAM,IAAI,KAAK,CAAC,mBAAmB,QAAQ,EAAE,CAAC,CAAC;YACjD,CAAC;YACD,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;QACzD,IAAI,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC;YACzB,MAAM,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YAC3C,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IAED,gFAAgF;IACxE,OAAO,CAAC,IAAY,EAAE,EAAU;QACtC,MAAM,IAAI,GACR,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;YAC3E,SAAS,CAAC;QACZ,OAAO,GAAG,IAAI,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;IACrC,CAAC;IAED;;;;OAIG;IACK,WAAW,CAAC,GAAkB;QACpC,MAAM,QAAQ,GAAG,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;QAClC,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAC9B,IAAI,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC;YACzB,OAAO,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC;QACrE,CAAC;QACD,IAAI,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK;YAAE,OAAO,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,SAAS,CAAC;QAC5E,OAAO,SAAS,CAAC;IACnB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,eAAe,CAAC,GAAkB;QACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QACxC,IAAI,QAAQ,EAAE,CAAC;YACb,2EAA2E;YAC3E,IAAI,QAAQ,CAAC,MAAM;gBAAE,MAAM,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YACtD,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QACnC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,GAAG,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;QACvD,MAAM,MAAM,GAAkB;YAC5B,EAAE;YACF,IAAI;YACJ,IAAI;YACJ,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ;gBAC1B,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,aAAa,EAAE;gBAC5E,CAAC,CAAC,SAAS;YACb,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;SACtB,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QACrC,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACrB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,gFAAgF;IAChF,KAAK,CAAC,MAAM,CAAC,EAAU;QACrB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAAE,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACrD,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,OAAO,CAAC,GAAa;QACzB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAyB,CAAC;QAC9C,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;YACrB,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAChC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QAC1C,CAAC;QACD,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACpC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QACrC,CAAC;QACD,MAAM,SAAS,GAAG,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;QAC5C,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QACtE,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,OAAO;YAAE,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACpC,CAAC;CACF"}
|