@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.
Files changed (133) hide show
  1. package/dist/src/actions.d.ts +22 -0
  2. package/dist/src/actions.js +44 -0
  3. package/dist/src/actions.js.map +1 -0
  4. package/dist/src/agent/agent.d.ts +89 -0
  5. package/dist/src/agent/agent.js +22 -0
  6. package/dist/src/agent/agent.js.map +1 -0
  7. package/dist/src/agent/claude.d.ts +70 -0
  8. package/dist/src/agent/claude.js +404 -0
  9. package/dist/src/agent/claude.js.map +1 -0
  10. package/dist/src/agent/codex.d.ts +44 -0
  11. package/dist/src/agent/codex.js +303 -0
  12. package/dist/src/agent/codex.js.map +1 -0
  13. package/dist/src/agent/mock.d.ts +31 -0
  14. package/dist/src/agent/mock.js +155 -0
  15. package/dist/src/agent/mock.js.map +1 -0
  16. package/dist/src/agent/model-error.d.ts +8 -0
  17. package/dist/src/agent/model-error.js +25 -0
  18. package/dist/src/agent/model-error.js.map +1 -0
  19. package/dist/src/agent/openrouter.d.ts +38 -0
  20. package/dist/src/agent/openrouter.js +60 -0
  21. package/dist/src/agent/openrouter.js.map +1 -0
  22. package/dist/src/agent/workspace-scope.d.ts +25 -0
  23. package/dist/src/agent/workspace-scope.js +71 -0
  24. package/dist/src/agent/workspace-scope.js.map +1 -0
  25. package/dist/src/allowlist.d.ts +56 -0
  26. package/dist/src/allowlist.js +141 -0
  27. package/dist/src/allowlist.js.map +1 -0
  28. package/dist/src/chatlog.d.ts +50 -0
  29. package/dist/src/chatlog.js +179 -0
  30. package/dist/src/chatlog.js.map +1 -0
  31. package/dist/src/cli/paths.d.ts +40 -0
  32. package/dist/src/cli/paths.js +76 -0
  33. package/dist/src/cli/paths.js.map +1 -0
  34. package/dist/src/cli/service.d.ts +20 -0
  35. package/dist/src/cli/service.js +245 -0
  36. package/dist/src/cli/service.js.map +1 -0
  37. package/dist/src/cli.d.ts +2 -0
  38. package/dist/src/cli.js +227 -0
  39. package/dist/src/cli.js.map +1 -0
  40. package/dist/src/config.d.ts +33 -0
  41. package/dist/src/config.js +122 -0
  42. package/dist/src/config.js.map +1 -0
  43. package/dist/src/credentials.d.ts +38 -0
  44. package/dist/src/credentials.js +120 -0
  45. package/dist/src/credentials.js.map +1 -0
  46. package/dist/src/crypto.d.ts +49 -0
  47. package/dist/src/crypto.js +202 -0
  48. package/dist/src/crypto.js.map +1 -0
  49. package/dist/src/daemon-identity.d.ts +31 -0
  50. package/dist/src/daemon-identity.js +97 -0
  51. package/dist/src/daemon-identity.js.map +1 -0
  52. package/dist/src/envelope.d.ts +14 -0
  53. package/dist/src/envelope.js +28 -0
  54. package/dist/src/envelope.js.map +1 -0
  55. package/dist/src/events.d.ts +40 -0
  56. package/dist/src/events.js +24 -0
  57. package/dist/src/events.js.map +1 -0
  58. package/dist/src/forward.d.ts +51 -0
  59. package/dist/src/forward.js +170 -0
  60. package/dist/src/forward.js.map +1 -0
  61. package/dist/src/fs-scope.d.ts +17 -0
  62. package/dist/src/fs-scope.js +47 -0
  63. package/dist/src/fs-scope.js.map +1 -0
  64. package/dist/src/github.d.ts +70 -0
  65. package/dist/src/github.js +178 -0
  66. package/dist/src/github.js.map +1 -0
  67. package/dist/src/gitops.d.ts +113 -0
  68. package/dist/src/gitops.js +557 -0
  69. package/dist/src/gitops.js.map +1 -0
  70. package/dist/src/hub.d.ts +50 -0
  71. package/dist/src/hub.js +92 -0
  72. package/dist/src/hub.js.map +1 -0
  73. package/dist/src/index.d.ts +22 -0
  74. package/dist/src/index.js +56 -0
  75. package/dist/src/index.js.map +1 -0
  76. package/dist/src/jobs/scheduler.d.ts +94 -0
  77. package/dist/src/jobs/scheduler.js +373 -0
  78. package/dist/src/jobs/scheduler.js.map +1 -0
  79. package/dist/src/jobs/store.d.ts +33 -0
  80. package/dist/src/jobs/store.js +111 -0
  81. package/dist/src/jobs/store.js.map +1 -0
  82. package/dist/src/log.d.ts +61 -0
  83. package/dist/src/log.js +166 -0
  84. package/dist/src/log.js.map +1 -0
  85. package/dist/src/migrate-state-dir.d.ts +1 -0
  86. package/dist/src/migrate-state-dir.js +47 -0
  87. package/dist/src/migrate-state-dir.js.map +1 -0
  88. package/dist/src/models.d.ts +36 -0
  89. package/dist/src/models.js +71 -0
  90. package/dist/src/models.js.map +1 -0
  91. package/dist/src/permission-gate.d.ts +48 -0
  92. package/dist/src/permission-gate.js +93 -0
  93. package/dist/src/permission-gate.js.map +1 -0
  94. package/dist/src/permission-scope.d.ts +77 -0
  95. package/dist/src/permission-scope.js +244 -0
  96. package/dist/src/permission-scope.js.map +1 -0
  97. package/dist/src/ports.d.ts +71 -0
  98. package/dist/src/ports.js +216 -0
  99. package/dist/src/ports.js.map +1 -0
  100. package/dist/src/projects.d.ts +83 -0
  101. package/dist/src/projects.js +205 -0
  102. package/dist/src/projects.js.map +1 -0
  103. package/dist/src/protocol.d.ts +1226 -0
  104. package/dist/src/protocol.js +16 -0
  105. package/dist/src/protocol.js.map +1 -0
  106. package/dist/src/repo-sync.d.ts +49 -0
  107. package/dist/src/repo-sync.js +83 -0
  108. package/dist/src/repo-sync.js.map +1 -0
  109. package/dist/src/server.d.ts +33 -0
  110. package/dist/src/server.js +973 -0
  111. package/dist/src/server.js.map +1 -0
  112. package/dist/src/terminal.d.ts +32 -0
  113. package/dist/src/terminal.js +92 -0
  114. package/dist/src/terminal.js.map +1 -0
  115. package/dist/src/titler.d.ts +15 -0
  116. package/dist/src/titler.js +72 -0
  117. package/dist/src/titler.js.map +1 -0
  118. package/dist/src/types.d.ts +99 -0
  119. package/dist/src/types.js +3 -0
  120. package/dist/src/types.js.map +1 -0
  121. package/dist/src/usage-limit.d.ts +49 -0
  122. package/dist/src/usage-limit.js +83 -0
  123. package/dist/src/usage-limit.js.map +1 -0
  124. package/dist/src/usage.d.ts +23 -0
  125. package/dist/src/usage.js +63 -0
  126. package/dist/src/usage.js.map +1 -0
  127. package/dist/src/workspace.d.ts +303 -0
  128. package/dist/src/workspace.js +1086 -0
  129. package/dist/src/workspace.js.map +1 -0
  130. package/dist/src/worktree.d.ts +133 -0
  131. package/dist/src/worktree.js +561 -0
  132. package/dist/src/worktree.js.map +1 -0
  133. package/package.json +67 -0
@@ -0,0 +1,557 @@
1
+ /**
2
+ * Git review-and-merge operations for a workspace: status, commit, push, PR.
3
+ *
4
+ * These power the Conductor-style "review the diff → commit → push → open a
5
+ * PR" path from a pure-front-end client. Everything runs inside the worktree;
6
+ * the PR goes through the `gh` CLI so its keychain auth is reused (same stance
7
+ * as github.ts — no tokens on the wire or in source).
8
+ */
9
+ import { execFile } from "node:child_process";
10
+ import { promisify } from "node:util";
11
+ import { simpleGit } from "simple-git";
12
+ import { diffStat } from "./worktree.js";
13
+ const exec = promisify(execFile);
14
+ /**
15
+ * `git remote get-url origin` for a repo, cached by path. Used to tag project /
16
+ * workspace summaries with an origin URL so a client connected to several
17
+ * daemons can match the "same" project across them (the basis for moving a
18
+ * workspace between daemons). A repo with no origin remote caches `undefined`.
19
+ *
20
+ * Origin rarely changes for a given repo path, so the cache avoids a git spawn
21
+ * per workspace on every list broadcast; call `forgetOriginUrl(repo)` if a
22
+ * project's remote is ever rewired.
23
+ */
24
+ const originUrlCache = new Map();
25
+ export async function originUrl(repoPath) {
26
+ if (originUrlCache.has(repoPath))
27
+ return originUrlCache.get(repoPath);
28
+ let url;
29
+ try {
30
+ const out = await simpleGit(repoPath).raw(["remote", "get-url", "origin"]);
31
+ url = out.trim() || undefined;
32
+ }
33
+ catch {
34
+ url = undefined; // no origin remote (blank/local project)
35
+ }
36
+ originUrlCache.set(repoPath, url);
37
+ return url;
38
+ }
39
+ /** Drop a cached origin URL (e.g. after a remote is added/changed). */
40
+ export function forgetOriginUrl(repoPath) {
41
+ originUrlCache.delete(repoPath);
42
+ }
43
+ /**
44
+ * True if `ref` resolves in this worktree (branch, tag, MERGE_HEAD, …). Must
45
+ * check the *output*, not just whether the call threw: `git rev-parse --verify
46
+ * --quiet <missing>` exits non-zero with empty stdout, and simple-git resolves
47
+ * (does not reject) that — so a missing ref comes back as an empty string.
48
+ */
49
+ async function refExists(git, ref) {
50
+ try {
51
+ const out = await git.raw(["rev-parse", "--verify", "--quiet", ref]);
52
+ return out.trim().length > 0;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ /** A merge is unfinished exactly while MERGE_HEAD is present (incl. conflicts). */
59
+ async function mergeInProgress(git) {
60
+ return refExists(git, "MERGE_HEAD");
61
+ }
62
+ /**
63
+ * Whether the branch has an upstream configured (`branch.<name>.remote`, set by
64
+ * the first `push -u`). Unlike the remote-tracking ref, this config survives the
65
+ * remote branch being deleted — which GitHub does when a PR merges — so it is the
66
+ * reliable signal that the branch was ever pushed and may still carry a (now
67
+ * merged) PR worth querying.
68
+ */
69
+ async function hasUpstream(git, branch) {
70
+ try {
71
+ // Scope to this repo's config: reading global/system config could report a
72
+ // false upstream (and trigger a needless `gh` call). `--get` parses the key
73
+ // as the segment after the final dot, so a `.`-containing branch name in the
74
+ // subsection is handled correctly.
75
+ const out = await git.raw([
76
+ "config",
77
+ "--local",
78
+ "--get",
79
+ `branch.${branch}.remote`,
80
+ ]);
81
+ return out.trim().length > 0;
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
87
+ /**
88
+ * The ref this worktree is measured/reconciled against — the *remote* base
89
+ * (`origin/<base>`) when it exists, else the local base. Using the remote ref
90
+ * keeps ahead/behind pointed at the real merge-to branch, and it matches what
91
+ * `gitResolve` fetches + merges, so the behind count and the merge target can't
92
+ * drift into a "Resolve that does nothing" loop.
93
+ */
94
+ async function baseCompareRef(git, baseBranch) {
95
+ return (await refExists(git, `refs/remotes/origin/${baseBranch}`))
96
+ ? `origin/${baseBranch}`
97
+ : baseBranch;
98
+ }
99
+ /**
100
+ * Whether reconciling `mergeRef` into `headRef` would conflict — the signal that
101
+ * gates the "Resolve" affordance. Predicted with `git merge-tree --write-tree
102
+ * --quiet`, which performs the merge entirely in-memory (exit 0 = clean, 1 =
103
+ * conflicts) and never touches the worktree, so it is safe to run on every
104
+ * status poll. `mergeRef` is the same ref `gitResolve` fetches + merges
105
+ * (`baseCompareRef`), so the prediction matches what Resolve would actually do.
106
+ * Best effort: any failure other than the conflict exit (bad ref, ancient git)
107
+ * reports false, so a broken probe hides Resolve rather than misfiring.
108
+ */
109
+ async function baseConflicts(worktreePath, headRef, mergeRef) {
110
+ try {
111
+ await exec("git", ["merge-tree", "--write-tree", "--quiet", headRef, mergeRef], {
112
+ cwd: worktreePath,
113
+ });
114
+ return false; // exit 0 — clean merge
115
+ }
116
+ catch (err) {
117
+ // execFile rejects with the process exit code on `.code`; merge-tree uses 1
118
+ // exclusively for "merge has conflicts". Any other code is an error we don't
119
+ // want to read as a conflict.
120
+ return err.code === 1;
121
+ }
122
+ }
123
+ /** Ahead/behind + dirty-file summary against the workspace's merge-to ref. */
124
+ export async function gitStatus(record) {
125
+ const git = simpleGit(record.worktreePath);
126
+ const compareRef = await baseCompareRef(git, record.baseBranch);
127
+ let ahead = 0;
128
+ let behind = 0;
129
+ try {
130
+ const raw = await git.raw([
131
+ "rev-list",
132
+ "--left-right",
133
+ "--count",
134
+ `${compareRef}...HEAD`,
135
+ ]);
136
+ const [b, a] = raw.trim().split(/\s+/).map((n) => Number(n) || 0);
137
+ behind = b ?? 0;
138
+ ahead = a ?? 0;
139
+ }
140
+ catch {
141
+ /* base ref may be gone (e.g. HEAD in a fresh clone) — report 0/0 */
142
+ }
143
+ // Unpushed = commits on HEAD the remote branch doesn't have yet. When the
144
+ // branch has never been pushed there is no `origin/<branch>` ref, so every
145
+ // commit ahead of the base still needs pushing — fall back to `ahead`.
146
+ const hasRemote = await refExists(git, `refs/remotes/origin/${record.branch}`);
147
+ let unpushed = ahead;
148
+ if (hasRemote) {
149
+ try {
150
+ const raw = await git.raw(["rev-list", "--count", `refs/remotes/origin/${record.branch}..HEAD`]);
151
+ unpushed = Number(raw.trim()) || 0;
152
+ }
153
+ catch {
154
+ unpushed = ahead;
155
+ }
156
+ }
157
+ // A PR can only exist once the branch is pushed, so skip the `gh` call
158
+ // entirely for purely local branches. We gate on "ever pushed" — the
159
+ // remote-tracking ref OR an upstream config — rather than the remote-tracking
160
+ // ref alone: GitHub deletes the head branch when a PR merges, which prunes that
161
+ // ref, and gating on it there would blind us to the merged PR (dropping the
162
+ // "Archive" affordance). The upstream config outlives the branch deletion, so
163
+ // the merged PR is still queried. `hasPr` (an open PR) gates the "Create PR"
164
+ // button (hidden once one exists), "Merge", and "Resolve"; `prMerged` gates
165
+ // the "Archive" affordance once the PR has landed. `number`/`url` ride along so
166
+ // clients can surface a link to the PR (and the workspace label can follow the
167
+ // PR's head branch — see Workspace.gitStatus).
168
+ const everPushed = hasRemote || (await hasUpstream(git, record.branch));
169
+ const pr = everPushed ? await prInfo(record) : NO_PR;
170
+ const hasPr = pr.state === "OPEN";
171
+ const prMerged = pr.state === "MERGED";
172
+ // A conflict is only possible when the base has moved ahead of us; short-
173
+ // circuit otherwise so a merge-tree probe never runs on an up-to-date branch.
174
+ const conflictsWithBase = behind > 0 ? await baseConflicts(record.worktreePath, "HEAD", compareRef) : false;
175
+ const status = await git.status();
176
+ const merging = status.conflicted.length > 0 || (await mergeInProgress(git));
177
+ // diffStat runs `git add -AN` (intent-to-add) to count untracked lines; that
178
+ // mutates the index and, mid-merge, leaves conflicted entries in a state
179
+ // `git merge --abort` rejects ("Entry … not uptodate"). Skip it while a merge
180
+ // is in progress — line counts aren't shown in the conflict UI anyway.
181
+ const { added, removed } = merging
182
+ ? { added: 0, removed: 0 }
183
+ : await diffStat(record.worktreePath, compareRef);
184
+ return {
185
+ ws: record.id,
186
+ branch: record.branch,
187
+ baseBranch: record.baseBranch,
188
+ ahead,
189
+ behind,
190
+ hasRemote,
191
+ unpushed,
192
+ merging,
193
+ hasPr,
194
+ prMerged,
195
+ ...(pr.number != null ? { prNumber: pr.number } : {}),
196
+ ...(pr.url ? { prUrl: pr.url } : {}),
197
+ conflictsWithBase,
198
+ conflicted: [...status.conflicted].sort(),
199
+ changed: status.files.map((f) => f.path).sort(),
200
+ added,
201
+ removed,
202
+ };
203
+ }
204
+ /** Stage everything and commit. Fails soft ("nothing to commit") into ok:false. */
205
+ export async function gitCommit(record, message) {
206
+ const ws = record.id;
207
+ const trimmed = message.trim();
208
+ if (!trimmed)
209
+ return { ws, op: "commit", ok: false, message: "commit message is empty" };
210
+ const git = simpleGit(record.worktreePath);
211
+ try {
212
+ await git.add(["-A"]);
213
+ const res = await git.commit(trimmed);
214
+ if (!res.commit) {
215
+ return { ws, op: "commit", ok: false, message: "nothing to commit" };
216
+ }
217
+ return {
218
+ ws,
219
+ op: "commit",
220
+ ok: true,
221
+ message: `committed ${res.commit} (${res.summary.changes} change${res.summary.changes === 1 ? "" : "s"})`,
222
+ };
223
+ }
224
+ catch (err) {
225
+ return { ws, op: "commit", ok: false, message: errText(err) };
226
+ }
227
+ }
228
+ /**
229
+ * Push the workspace branch. When `autoSetupRemote` is true (the default and the
230
+ * git.worktreePushAutoSetupRemote config default), sets the upstream on first
231
+ * push with `-u`; when false, pushes without configuring the upstream.
232
+ */
233
+ export async function gitPush(record, autoSetupRemote = true) {
234
+ const ws = record.id;
235
+ const git = simpleGit(record.worktreePath);
236
+ try {
237
+ const args = autoSetupRemote
238
+ ? ["-u", "origin", record.branch]
239
+ : ["origin", record.branch];
240
+ await git.push(args);
241
+ return { ws, op: "push", ok: true, message: `pushed ${record.branch} to origin` };
242
+ }
243
+ catch (err) {
244
+ return { ws, op: "push", ok: false, message: errText(err) };
245
+ }
246
+ }
247
+ const NO_PR = { state: "NONE", number: null, url: null, headRef: null, title: null };
248
+ /** Default freshness window for a branch's cached PR state. */
249
+ export const DEFAULT_PR_STATE_TTL_MS = 60_000;
250
+ const prStateCache = new Map();
251
+ const prCacheKey = (record) => `${record.worktreePath}${record.branch}`;
252
+ async function fetchPrInfo(record) {
253
+ try {
254
+ const { stdout } = await exec("gh", ["pr", "view", record.branch, "--json", "state,number,url,headRefName,title"], { cwd: record.worktreePath });
255
+ const j = JSON.parse(stdout || "{}");
256
+ const s = j.state;
257
+ const state = s === "OPEN" || s === "MERGED" || s === "CLOSED" ? s : "NONE";
258
+ if (state === "NONE")
259
+ return NO_PR;
260
+ return {
261
+ state,
262
+ number: typeof j.number === "number" ? j.number : null,
263
+ url: j.url ?? null,
264
+ headRef: j.headRefName ?? null,
265
+ title: j.title ?? null,
266
+ };
267
+ }
268
+ catch {
269
+ // No PR, `gh` unauthenticated/absent, or offline — treat as "no PR" so the
270
+ // buttons fall back to their pre-PR state rather than getting stuck.
271
+ return NO_PR;
272
+ }
273
+ }
274
+ /**
275
+ * The workspace branch's PR (state + number/url/head) via `gh pr view <branch>`
276
+ * (reuses keychain auth, same stance as prCreate), cached per branch to bound
277
+ * `gh` invocations on the status-poll path: at most one lookup per `ttlMs`, and
278
+ * concurrent callers for the same branch (a merged-check, a hasPr probe, and the
279
+ * label-reconcile in the same poll) ride a single in-flight request. Best effort
280
+ * throughout — a failed lookup resolves to {@link NO_PR} and the throttle still
281
+ * advances, so an unreachable remote isn't retried every poll.
282
+ */
283
+ export async function prInfo(record, ttlMs = DEFAULT_PR_STATE_TTL_MS) {
284
+ const key = prCacheKey(record);
285
+ let e = prStateCache.get(key);
286
+ if (!e) {
287
+ e = { at: 0, inflight: null, value: NO_PR };
288
+ prStateCache.set(key, e);
289
+ }
290
+ if (e.inflight)
291
+ return e.inflight;
292
+ if (e.at !== 0 && Date.now() - e.at < ttlMs)
293
+ return e.value;
294
+ const entry = e;
295
+ entry.inflight = (async () => {
296
+ try {
297
+ entry.value = await fetchPrInfo(record);
298
+ return entry.value;
299
+ }
300
+ finally {
301
+ entry.at = Date.now();
302
+ entry.inflight = null;
303
+ }
304
+ })();
305
+ return entry.inflight;
306
+ }
307
+ /** Just the {@link prInfo} state enum — the common case for merge/archive gates. */
308
+ export async function prState(record, ttlMs = DEFAULT_PR_STATE_TTL_MS) {
309
+ return (await prInfo(record, ttlMs)).state;
310
+ }
311
+ /**
312
+ * Forget a branch's cached PR state so the next {@link prState} re-queries `gh`.
313
+ * Called after opening or merging a PR so the buttons re-key to the new state on
314
+ * the very next status poll instead of lingering until the TTL lapses.
315
+ */
316
+ export function invalidatePrState(record) {
317
+ prStateCache.delete(prCacheKey(record));
318
+ }
319
+ /** Testing/reset hook: forget all cached PR state. */
320
+ export function resetPrStateCache() {
321
+ prStateCache.clear();
322
+ }
323
+ /**
324
+ * Whether the workspace branch's PR has been merged on the remote. Best effort:
325
+ * any failure (no PR, `gh` unauthenticated/absent, network) reads as not-merged
326
+ * so the caller simply doesn't auto-archive.
327
+ */
328
+ export async function detectMerged(record) {
329
+ return (await prState(record)) === "MERGED";
330
+ }
331
+ /**
332
+ * Open a PR for the workspace branch via `gh pr create`. If one already exists
333
+ * for this branch, surface its URL instead of failing.
334
+ */
335
+ export async function prCreate(record, title, body) {
336
+ const ws = record.id;
337
+ const trimmed = title.trim();
338
+ if (!trimmed)
339
+ return { ws, op: "pr", ok: false, message: "PR title is empty" };
340
+ try {
341
+ const { stdout } = await exec("gh", [
342
+ "pr",
343
+ "create",
344
+ // Target the workspace's own base ref, not the repo default branch —
345
+ // a workspace branched off e.g. `release/2.0` must open its PR against
346
+ // `release/2.0`, matching how gitStatus computes ahead/behind.
347
+ "--base",
348
+ record.baseBranch,
349
+ "--head",
350
+ record.branch,
351
+ "--title",
352
+ trimmed,
353
+ "--body",
354
+ body ?? "",
355
+ ], { cwd: record.worktreePath });
356
+ const url = stdout.trim().split("\n").pop() ?? "";
357
+ // A PR now exists — drop the cached "no PR" so the Create-PR button hides on
358
+ // the next status poll instead of waiting out the TTL.
359
+ invalidatePrState(record);
360
+ return { ws, op: "pr", ok: true, message: "pull request opened", url };
361
+ }
362
+ catch (err) {
363
+ // `gh` exits non-zero when a PR for the branch already exists; recover its URL.
364
+ try {
365
+ const { stdout } = await exec("gh", ["pr", "view", record.branch, "--json", "url", "--jq", ".url"], { cwd: record.worktreePath });
366
+ const url = stdout.trim();
367
+ if (url) {
368
+ invalidatePrState(record);
369
+ return { ws, op: "pr", ok: true, message: "pull request already exists", url };
370
+ }
371
+ }
372
+ catch {
373
+ /* fall through to the original failure */
374
+ }
375
+ return { ws, op: "pr", ok: false, message: errText(err) };
376
+ }
377
+ }
378
+ /**
379
+ * Merge the workspace branch's open PR on the remote via `gh pr merge --merge`.
380
+ * Backs the "Merge" button, which surfaces only for an open PR that is clean
381
+ * (no conflicts with the base). On success we drop the cached PR state so the
382
+ * next status poll sees the PR as merged and swaps Merge for the Archive
383
+ * affordance. Best effort on the error path, same keychain-auth stance as
384
+ * prCreate.
385
+ */
386
+ export async function prMerge(record) {
387
+ const ws = record.id;
388
+ try {
389
+ await exec("gh", ["pr", "merge", record.branch, "--merge"], {
390
+ cwd: record.worktreePath,
391
+ });
392
+ invalidatePrState(record);
393
+ return { ws, op: "pr_merge", ok: true, message: "pull request merged" };
394
+ }
395
+ catch (err) {
396
+ return { ws, op: "pr_merge", ok: false, message: errText(err) };
397
+ }
398
+ }
399
+ /**
400
+ * Reconcile the worktree with its merge-to branch. Used by the "Resolve"
401
+ * action, which surfaces only when the base has advanced past this branch
402
+ * (`behind > 0`) and the tree is clean.
403
+ *
404
+ * We fetch `origin/<base>` first so we reconcile against what's actually on the
405
+ * remote, then merge that ref (falling back to the local base when there is no
406
+ * remote-tracking ref, e.g. offline or base never pushed). A clean merge lands
407
+ * a merge commit. A conflicting merge is *left in progress on purpose*: the
408
+ * client drops into its in-app conflict flow (see `gitMerge`) to pick a side
409
+ * per file and commit, or abort. We refuse up front if the tree is dirty, since
410
+ * `git merge` would bail anyway with a less friendly message.
411
+ */
412
+ export async function gitResolve(record) {
413
+ const ws = record.id;
414
+ const git = simpleGit(record.worktreePath);
415
+ try {
416
+ const pre = await git.status();
417
+ if (!pre.isClean()) {
418
+ return {
419
+ ws,
420
+ op: "resolve",
421
+ ok: false,
422
+ message: "commit or discard your changes before resolving",
423
+ };
424
+ }
425
+ // Best-effort refresh of the remote base; offline falls back to last-known.
426
+ try {
427
+ await git.fetch(["origin", record.baseBranch]);
428
+ }
429
+ catch {
430
+ /* offline / no remote — merge whatever we already have locally */
431
+ }
432
+ const mergeRef = await baseCompareRef(git, record.baseBranch);
433
+ // A conflicting merge may or may not reject (simple-git swallows some git
434
+ // non-zero exits), so the source of truth is whether conflicts are present
435
+ // afterward — not whether this threw.
436
+ let mergeErr = null;
437
+ try {
438
+ await git.raw(["merge", "--no-edit", mergeRef]);
439
+ }
440
+ catch (e) {
441
+ mergeErr = e;
442
+ }
443
+ const conflicted = (await git.status()).conflicted;
444
+ if (conflicted.length > 0) {
445
+ const shown = conflicted.slice(0, 5).join(", ");
446
+ const more = conflicted.length > 5 ? ", …" : "";
447
+ return {
448
+ ws,
449
+ op: "resolve",
450
+ ok: false,
451
+ message: `merge has conflicts in ${conflicted.length} file(s): ${shown}${more} — resolve them below or abort`,
452
+ };
453
+ }
454
+ if (mergeErr)
455
+ return { ws, op: "resolve", ok: false, message: errText(mergeErr) };
456
+ return {
457
+ ws,
458
+ op: "resolve",
459
+ ok: true,
460
+ message: `merged ${mergeRef} into ${record.branch}`,
461
+ };
462
+ }
463
+ catch (err) {
464
+ return { ws, op: "resolve", ok: false, message: errText(err) };
465
+ }
466
+ }
467
+ /**
468
+ * Drive an in-progress merge from the client's conflict UI:
469
+ * - `ours` — keep the workspace branch's version of the conflicting files
470
+ * - `theirs` — keep the merged-in base branch's version
471
+ * - `commit` — finalize the merge commit (refuses while conflicts remain)
472
+ * - `abort` — roll the merge back, restoring the pre-merge worktree
473
+ *
474
+ * `ours`/`theirs` default to every conflicting file; pass `paths` to resolve a
475
+ * subset (per-file resolution). After picking a side we `git add` the files so
476
+ * git marks them resolved.
477
+ */
478
+ export async function gitMerge(record, action, paths) {
479
+ const ws = record.id;
480
+ const git = simpleGit(record.worktreePath);
481
+ try {
482
+ if (action === "abort") {
483
+ await git.raw(["merge", "--abort"]);
484
+ return { ws, op: "merge", ok: true, message: "merge aborted" };
485
+ }
486
+ if (action === "commit") {
487
+ const s = await git.status();
488
+ if (s.conflicted.length > 0) {
489
+ return {
490
+ ws,
491
+ op: "merge",
492
+ ok: false,
493
+ message: `resolve ${s.conflicted.length} conflicting file(s) first`,
494
+ };
495
+ }
496
+ if (!(await mergeInProgress(git))) {
497
+ return { ws, op: "merge", ok: false, message: "no merge in progress" };
498
+ }
499
+ await git.raw(["commit", "--no-edit"]);
500
+ return { ws, op: "merge", ok: true, message: "merge committed" };
501
+ }
502
+ // ours / theirs: during a merge `--ours` is the workspace branch and
503
+ // `--theirs` is the base we merged in. Resolve the chosen files to one side.
504
+ const s = await git.status();
505
+ const targets = paths && paths.length > 0 ? paths : s.conflicted;
506
+ if (targets.length === 0) {
507
+ return { ws, op: "merge", ok: false, message: "no conflicts to resolve" };
508
+ }
509
+ await git.raw(["checkout", `--${action}`, "--", ...targets]);
510
+ await git.add(targets);
511
+ const left = (await git.status()).conflicted.length;
512
+ const side = action === "theirs" ? "base" : "your";
513
+ return {
514
+ ws,
515
+ op: "merge",
516
+ ok: true,
517
+ message: left > 0
518
+ ? `kept ${side} version for ${targets.length} file(s); ${left} conflict(s) left`
519
+ : `kept ${side} version — all conflicts resolved, commit the merge`,
520
+ };
521
+ }
522
+ catch (err) {
523
+ return { ws, op: "merge", ok: false, message: errText(err) };
524
+ }
525
+ }
526
+ /**
527
+ * Reveal the worktree on this Mac: Finder, Terminal, or the user's editor.
528
+ * Editor prefers the VS Code CLI when installed, then falls back to `open -a`.
529
+ */
530
+ export async function openWorktree(record, target) {
531
+ const path = record.worktreePath;
532
+ switch (target) {
533
+ case "finder":
534
+ await exec("open", [path]);
535
+ return;
536
+ case "terminal":
537
+ await exec("open", ["-a", "Terminal", path]);
538
+ return;
539
+ case "editor":
540
+ try {
541
+ await exec("code", [path]);
542
+ }
543
+ catch {
544
+ await exec("open", ["-a", "Visual Studio Code", path]);
545
+ }
546
+ return;
547
+ }
548
+ }
549
+ function errText(err) {
550
+ if (err instanceof Error) {
551
+ // execFile errors carry the CLI's stderr — usually the useful part.
552
+ const stderr = err.stderr;
553
+ return (stderr?.trim() || err.message).slice(0, 500);
554
+ }
555
+ return String(err).slice(0, 500);
556
+ }
557
+ //# sourceMappingURL=gitops.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitops.js","sourceRoot":"","sources":["../../src/gitops.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAEjC;;;;;;;;;GASG;AACH,MAAM,cAAc,GAAG,IAAI,GAAG,EAA8B,CAAC;AAE7D,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,QAAgB;IAC9C,IAAI,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACtE,IAAI,GAAuB,CAAC;IAC5B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC3E,GAAG,GAAG,GAAG,CAAC,IAAI,EAAE,IAAI,SAAS,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,GAAG,GAAG,SAAS,CAAC,CAAC,yCAAyC;IAC5D,CAAC;IACD,cAAc,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAClC,OAAO,GAAG,CAAC;AACb,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAClC,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,SAAS,CAAC,GAAiC,EAAE,GAAW;IACrE,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC;QACrE,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,mFAAmF;AACnF,KAAK,UAAU,eAAe,CAAC,GAAiC;IAC9D,OAAO,SAAS,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;AACtC,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,WAAW,CAAC,GAAiC,EAAE,MAAc;IAC1E,IAAI,CAAC;QACH,2EAA2E;QAC3E,4EAA4E;QAC5E,6EAA6E;QAC7E,mCAAmC;QACnC,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,GAAG,CAAC;YACxB,QAAQ;YACR,SAAS;YACT,OAAO;YACP,UAAU,MAAM,SAAS;SAC1B,CAAC,CAAC;QACH,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,cAAc,CAAC,GAAiC,EAAE,UAAkB;IACjF,OAAO,CAAC,MAAM,SAAS,CAAC,GAAG,EAAE,uBAAuB,UAAU,EAAE,CAAC,CAAC;QAChE,CAAC,CAAC,UAAU,UAAU,EAAE;QACxB,CAAC,CAAC,UAAU,CAAC;AACjB,CAAC;AAED;;;;;;;;;GASG;AACH,KAAK,UAAU,aAAa,CAC1B,YAAoB,EACpB,OAAe,EACf,QAAgB;IAEhB,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE;YAC9E,GAAG,EAAE,YAAY;SAClB,CAAC,CAAC;QACH,OAAO,KAAK,CAAC,CAAC,uBAAuB;IACvC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,4EAA4E;QAC5E,6EAA6E;QAC7E,8BAA8B;QAC9B,OAAQ,GAAyB,CAAC,IAAI,KAAK,CAAC,CAAC;IAC/C,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,MAAuB;IACrD,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IAC3C,MAAM,UAAU,GAAG,MAAM,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IAChE,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,GAAG,CAAC;YACxB,UAAU;YACV,cAAc;YACd,SAAS;YACT,GAAG,UAAU,SAAS;SACvB,CAAC,CAAC;QACH,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAClE,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC;QAChB,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAAC,MAAM,CAAC;QACP,oEAAoE;IACtE,CAAC;IACD,0EAA0E;IAC1E,2EAA2E;IAC3E,uEAAuE;IACvE,MAAM,SAAS,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,uBAAuB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IAC/E,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,SAAS,EAAE,CAAC;QACd,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,SAAS,EAAE,uBAAuB,MAAM,CAAC,MAAM,QAAQ,CAAC,CAAC,CAAC;YACjG,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,QAAQ,GAAG,KAAK,CAAC;QACnB,CAAC;IACH,CAAC;IACD,uEAAuE;IACvE,qEAAqE;IACrE,8EAA8E;IAC9E,gFAAgF;IAChF,4EAA4E;IAC5E,8EAA8E;IAC9E,6EAA6E;IAC7E,4EAA4E;IAC5E,gFAAgF;IAChF,+EAA+E;IAC/E,+CAA+C;IAC/C,MAAM,UAAU,GAAG,SAAS,IAAI,CAAC,MAAM,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IACxE,MAAM,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACrD,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,KAAK,MAAM,CAAC;IAClC,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,KAAK,QAAQ,CAAC;IACvC,0EAA0E;IAC1E,8EAA8E;IAC9E,MAAM,iBAAiB,GACrB,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,aAAa,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAEpF,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC;IAClC,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7E,6EAA6E;IAC7E,yEAAyE;IACzE,8EAA8E;IAC9E,uEAAuE;IACvE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO;QAChC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE;QAC1B,CAAC,CAAC,MAAM,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;IACpD,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,KAAK;QACL,MAAM;QACN,SAAS;QACT,QAAQ;QACR,OAAO;QACP,KAAK;QACL,QAAQ;QACR,GAAG,CAAC,EAAE,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrD,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpC,iBAAiB;QACjB,UAAU,EAAE,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,EAAE;QACzC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE;QAC/C,KAAK;QACL,OAAO;KACR,CAAC;AACJ,CAAC;AAED,mFAAmF;AACnF,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,MAAuB,EACvB,OAAe;IAEf,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC;IACrB,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAC/B,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,yBAAyB,EAAE,CAAC;IACzF,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IAC3C,IAAI,CAAC;QACH,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACtB,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACtC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;YAChB,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC;QACvE,CAAC;QACD,OAAO;YACL,EAAE;YACF,EAAE,EAAE,QAAQ;YACZ,EAAE,EAAE,IAAI;YACR,OAAO,EAAE,aAAa,GAAG,CAAC,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,OAAO,UAAU,GAAG,CAAC,OAAO,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG;SAC1G,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;IAChE,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,MAAuB,EACvB,eAAe,GAAG,IAAI;IAEtB,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC;IACrB,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IAC3C,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,eAAe;YAC1B,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC;YACjC,CAAC,CAAC,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QAC9B,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrB,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,MAAM,CAAC,MAAM,YAAY,EAAE,CAAC;IACpF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;IAC9D,CAAC;AACH,CAAC;AAqBD,MAAM,KAAK,GAAW,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAU7F,+DAA+D;AAC/D,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAC9C,MAAM,YAAY,GAAG,IAAI,GAAG,EAAwB,CAAC;AACrD,MAAM,UAAU,GAAG,CAAC,MAAuB,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,YAAY,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;AAU1F,KAAK,UAAU,WAAW,CAAC,MAAuB;IAChD,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,IAAI,CAC3B,IAAI,EACJ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,oCAAoC,CAAC,EAC7E,EAAE,GAAG,EAAE,MAAM,CAAC,YAAY,EAAE,CAC7B,CAAC;QACF,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAiB,CAAC;QACrD,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC;QAClB,MAAM,KAAK,GAAY,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QACrF,IAAI,KAAK,KAAK,MAAM;YAAE,OAAO,KAAK,CAAC;QACnC,OAAO;YACL,KAAK;YACL,MAAM,EAAE,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI;YACtD,GAAG,EAAE,CAAC,CAAC,GAAG,IAAI,IAAI;YAClB,OAAO,EAAE,CAAC,CAAC,WAAW,IAAI,IAAI;YAC9B,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI;SACvB,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,2EAA2E;QAC3E,qEAAqE;QACrE,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAC1B,MAAuB,EACvB,KAAK,GAAG,uBAAuB;IAE/B,MAAM,GAAG,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IAC/B,IAAI,CAAC,GAAG,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QAC5C,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IAC3B,CAAC;IACD,IAAI,CAAC,CAAC,QAAQ;QAAE,OAAO,CAAC,CAAC,QAAQ,CAAC;IAClC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,EAAE,GAAG,KAAK;QAAE,OAAO,CAAC,CAAC,KAAK,CAAC;IAC5D,MAAM,KAAK,GAAG,CAAC,CAAC;IAChB,KAAK,CAAC,QAAQ,GAAG,CAAC,KAAK,IAAI,EAAE;QAC3B,IAAI,CAAC;YACH,KAAK,CAAC,KAAK,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;YACxC,OAAO,KAAK,CAAC,KAAK,CAAC;QACrB,CAAC;gBAAS,CAAC;YACT,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACtB,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC;QACxB,CAAC;IACH,CAAC,CAAC,EAAE,CAAC;IACL,OAAO,KAAK,CAAC,QAAQ,CAAC;AACxB,CAAC;AAED,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,MAAuB,EACvB,KAAK,GAAG,uBAAuB;IAE/B,OAAO,CAAC,MAAM,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;AAC7C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAuB;IACvD,YAAY,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,iBAAiB;IAC/B,YAAY,CAAC,KAAK,EAAE,CAAC;AACvB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAuB;IACxD,OAAO,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,QAAQ,CAAC;AAC9C,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,MAAuB,EACvB,KAAa,EACb,IAAa;IAEb,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC;IACrB,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/E,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,IAAI,CAC3B,IAAI,EACJ;YACE,IAAI;YACJ,QAAQ;YACR,qEAAqE;YACrE,uEAAuE;YACvE,+DAA+D;YAC/D,QAAQ;YACR,MAAM,CAAC,UAAU;YACjB,QAAQ;YACR,MAAM,CAAC,MAAM;YACb,SAAS;YACT,OAAO;YACP,QAAQ;YACR,IAAI,IAAI,EAAE;SACX,EACD,EAAE,GAAG,EAAE,MAAM,CAAC,YAAY,EAAE,CAC7B,CAAC;QACF,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;QAClD,6EAA6E;QAC7E,uDAAuD;QACvD,iBAAiB,CAAC,MAAM,CAAC,CAAC;QAC1B,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,qBAAqB,EAAE,GAAG,EAAE,CAAC;IACzE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,gFAAgF;QAChF,IAAI,CAAC;YACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,IAAI,CAC3B,IAAI,EACJ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAC9D,EAAE,GAAG,EAAE,MAAM,CAAC,YAAY,EAAE,CAC7B,CAAC;YACF,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;YAC1B,IAAI,GAAG,EAAE,CAAC;gBACR,iBAAiB,CAAC,MAAM,CAAC,CAAC;gBAC1B,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,6BAA6B,EAAE,GAAG,EAAE,CAAC;YACjF,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,0CAA0C;QAC5C,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;IAC5D,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,MAAuB;IACnD,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC;IACrB,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE;YAC1D,GAAG,EAAE,MAAM,CAAC,YAAY;SACzB,CAAC,CAAC;QACH,iBAAiB,CAAC,MAAM,CAAC,CAAC;QAC1B,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC;IAC1E,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;IAClE,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,MAAuB;IACtD,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC;IACrB,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IAC3C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC;QAC/B,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC;YACnB,OAAO;gBACL,EAAE;gBACF,EAAE,EAAE,SAAS;gBACb,EAAE,EAAE,KAAK;gBACT,OAAO,EAAE,iDAAiD;aAC3D,CAAC;QACJ,CAAC;QACD,4EAA4E;QAC5E,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;QACjD,CAAC;QAAC,MAAM,CAAC;YACP,kEAAkE;QACpE,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;QAC9D,0EAA0E;QAC1E,2EAA2E;QAC3E,sCAAsC;QACtC,IAAI,QAAQ,GAAY,IAAI,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC;QAClD,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,QAAQ,GAAG,CAAC,CAAC;QACf,CAAC;QACD,MAAM,UAAU,GAAG,CAAC,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC;QACnD,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1B,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAChD,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YAChD,OAAO;gBACL,EAAE;gBACF,EAAE,EAAE,SAAS;gBACb,EAAE,EAAE,KAAK;gBACT,OAAO,EAAE,0BAA0B,UAAU,CAAC,MAAM,aAAa,KAAK,GAAG,IAAI,gCAAgC;aAC9G,CAAC;QACJ,CAAC;QACD,IAAI,QAAQ;YAAE,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QAClF,OAAO;YACL,EAAE;YACF,EAAE,EAAE,SAAS;YACb,EAAE,EAAE,IAAI;YACR,OAAO,EAAE,UAAU,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE;SACpD,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;IACjE,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,MAAuB,EACvB,MAA8C,EAC9C,KAAgB;IAEhB,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC;IACrB,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IAC3C,IAAI,CAAC;QACH,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;YACvB,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;YACpC,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC;QACjE,CAAC;QACD,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YACxB,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC;YAC7B,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC5B,OAAO;oBACL,EAAE;oBACF,EAAE,EAAE,OAAO;oBACX,EAAE,EAAE,KAAK;oBACT,OAAO,EAAE,WAAW,CAAC,CAAC,UAAU,CAAC,MAAM,4BAA4B;iBACpE,CAAC;YACJ,CAAC;YACD,IAAI,CAAC,CAAC,MAAM,eAAe,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;gBAClC,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,sBAAsB,EAAE,CAAC;YACzE,CAAC;YACD,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC;YACvC,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC;QACnE,CAAC;QACD,qEAAqE;QACrE,6EAA6E;QAC7E,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;QACjE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,yBAAyB,EAAE,CAAC;QAC5E,CAAC;QACD,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,KAAK,MAAM,EAAE,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC;QAC7D,MAAM,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACvB,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC;QACpD,MAAM,IAAI,GAAG,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;QACnD,OAAO;YACL,EAAE;YACF,EAAE,EAAE,OAAO;YACX,EAAE,EAAE,IAAI;YACR,OAAO,EACL,IAAI,GAAG,CAAC;gBACN,CAAC,CAAC,QAAQ,IAAI,gBAAgB,OAAO,CAAC,MAAM,aAAa,IAAI,mBAAmB;gBAChF,CAAC,CAAC,QAAQ,IAAI,qDAAqD;SACxE,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;IAC/D,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,MAAuB,EACvB,MAAwC;IAExC,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,CAAC;IACjC,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,QAAQ;YACX,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;YAC3B,OAAO;QACT,KAAK,UAAU;YACb,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC;YAC7C,OAAO;QACT,KAAK,QAAQ;YACX,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;YAC7B,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,oBAAoB,EAAE,IAAI,CAAC,CAAC,CAAC;YACzD,CAAC;YACD,OAAO;IACX,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CAAC,GAAY;IAC3B,IAAI,GAAG,YAAY,KAAK,EAAE,CAAC;QACzB,oEAAoE;QACpE,MAAM,MAAM,GAAI,GAA2B,CAAC,MAAM,CAAC;QACnD,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Connection hub. Single-user, but more than one device may be attached at once
3
+ * (phone + tablet + a desktop client), so daemon→client events broadcast to all
4
+ * sockets.
5
+ *
6
+ * This is also the single send seam. With E2E on, each device pairs its own
7
+ * keypair, so there is no one global cipher: every socket carries its OWN codec
8
+ * (cleartext until it completes the `hello` handshake, then a NaCl box sealed to
9
+ * that device's key). Sends and broadcasts therefore encode per-socket.
10
+ */
11
+ import { WebSocket } from "ws";
12
+ import type { Envelope, MessageType, PayloadFor } from "./protocol.js";
13
+ import { type Codec } from "./crypto.js";
14
+ export declare class Hub {
15
+ private requireSealed;
16
+ /** Each attached socket and the codec its frames are (de)coded with. */
17
+ private conns;
18
+ /**
19
+ * Sockets that asked to follow the daemon log (via `log.tail`). Live
20
+ * `log.event` frames go only here, so a phone that never opens a log view
21
+ * isn't fed a firehose. Membership is dropped when the socket detaches.
22
+ */
23
+ private logSubscribers;
24
+ /**
25
+ * When true (E2E on), a socket that hasn't completed the `hello` handshake is
26
+ * still on the cleartext codec, so fanning a broadcast out to it would leak the
27
+ * payload to the Cloudflare edge in clear. In that mode we skip unsealed
28
+ * sockets on the broadcast paths — a device receives events only once it has
29
+ * sealed its channel. Point-to-point handshake replies still go via `send`.
30
+ */
31
+ constructor(requireSealed?: boolean);
32
+ /** A socket whose channel is sealed to its device key (post-`hello`). */
33
+ private sealed;
34
+ add(socket: WebSocket): void;
35
+ remove(socket: WebSocket): void;
36
+ /** Follow the live daemon log on this socket (idempotent). */
37
+ subscribeLog(socket: WebSocket): void;
38
+ /** Push a daemon→client message to just the log subscribers. */
39
+ broadcastLog<T extends MessageType>(type: T, ws: string | null, payload: PayloadFor<T>): void;
40
+ /** The codec bound to a socket (cleartext if unknown/unpaired). */
41
+ codecFor(socket: WebSocket): Codec;
42
+ /** Bind a socket to a codec (called once the device's key is known). */
43
+ setCodec(socket: WebSocket, codec: Codec): void;
44
+ /** Send to one socket, sealed with that socket's own codec. */
45
+ send(socket: WebSocket, env: Envelope): void;
46
+ /** Build + send a daemon→client message to ONE socket (per-device streams). */
47
+ sendTo<T extends MessageType>(socket: WebSocket, type: T, ws: string | null, payload: PayloadFor<T>): void;
48
+ /** Build + broadcast a daemon→client message to every attached socket. */
49
+ broadcast<T extends MessageType>(type: T, ws: string | null, payload: PayloadFor<T>): void;
50
+ }