@bud.sh/app 0.5.0 → 0.6.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 (115) hide show
  1. package/README.md +41 -31
  2. package/dist/client/assets/bud/design.js +1 -1
  3. package/dist/client/assets/canvas-Bih9EdKJ.js +1 -0
  4. package/dist/client/assets/difference-worker-Bzg-rizy.js +8 -0
  5. package/dist/client/assets/dist-CdE5wBkg.js +5 -0
  6. package/dist/client/assets/files-1dLuoM2f.js +1 -0
  7. package/dist/client/assets/i18n-CkO6VFI0.js +1 -0
  8. package/dist/client/assets/index-Cn2mxroU.css +2 -0
  9. package/dist/client/assets/{index-ChANGtl-.js → index-DtISWZp9.js} +2 -2
  10. package/dist/client/assets/pages-BJ-tNJ4J.js +1 -0
  11. package/dist/client/assets/{project-branches-DO897zM6.js → project-branches-Ce3rXrkc.js} +1 -1
  12. package/dist/client/assets/project-comments-CPUONfs8.js +1 -0
  13. package/dist/client/assets/project-thread-links-C-TGkq59.js +1 -0
  14. package/dist/client/assets/{project-threads-Lay351p4.js → project-threads-Bv7IDBgi.js} +105 -105
  15. package/dist/client/assets/project-work-DAE9-olg.js +1 -0
  16. package/dist/client/assets/{task-design-Avb2RO4w.js → task-design-CwySIAOl.js} +3 -3
  17. package/dist/client/assets/tasks-KWsCLi_h.js +3 -0
  18. package/dist/client/assets/wasm-inline-DSUyFn1L.js +1 -0
  19. package/dist/client/assets/workspace-CVdcfaaB.js +2 -0
  20. package/dist/client/index.html +3 -3
  21. package/dist/hosted/assets/{canvas-V36732ER.js → canvas-AII16n9i.js} +97 -6
  22. package/dist/hosted/assets/{comment-data-CF2ZV_3k.js → comment-data-BXfMElfx.js} +2004 -402
  23. package/dist/hosted/assets/dist-BCSTeNf4.js +4785 -0
  24. package/dist/hosted/assets/{files-DjSCjfU7.js → files-D_1yeAVO.js} +10 -5
  25. package/dist/hosted/assets/{pages-BzjzeuPu.js → pages-CN_SbKWA.js} +3 -3
  26. package/dist/hosted/assets/{project-branches-BT97J9i7.js → project-branches-Bxp0DCwg.js} +1 -1
  27. package/dist/hosted/assets/{project-thread-links-BRLgByEe.js → project-thread-links-KqQoKBLE.js} +76 -4
  28. package/dist/{node-host/assets/screen-CZXWJEds.js → hosted/assets/screen-DpAcDUKR.js} +29 -1
  29. package/dist/hosted/assets/tasks-D66LlwxM.js +1423 -0
  30. package/dist/hosted/assets/wasm-inline-Dl9tqabx.js +4 -0
  31. package/dist/hosted/hosted.js +7885 -6219
  32. package/dist/node/assets/Stream-CAm1eJ2n.js +16604 -0
  33. package/dist/{server/assets/bridge-B_Blt55E.js → node/assets/bridge-DQSBYHmw.js} +124 -290
  34. package/dist/node/assets/config-9_jWwNBh.js +1067 -0
  35. package/dist/node/assets/load-config.server-CMVtRVmO.js +261 -0
  36. package/dist/node/assets/module-3GgtT4Xs.js +308 -0
  37. package/dist/node/assets/server-DTQFuoX2.js +846 -0
  38. package/dist/node/assets/src-D-kG1GxB.js +2 -0
  39. package/dist/node/assets/{src-CerrKjID.js → src-Dwe9APUx.js} +93 -3
  40. package/dist/node/assets/src-DysjfbM8.js +8592 -0
  41. package/dist/node/bridge.js +1 -845
  42. package/dist/node/cli.js +206 -0
  43. package/dist/node/design.js +811 -13
  44. package/dist/node/index.js +2 -739
  45. package/dist/node/plugins.js +5711 -0
  46. package/dist/node/server.js +1 -6746
  47. package/dist/node/vite.js +121 -0
  48. package/dist/{server/assets/canvas-Dc1mYCwQ.js → node-host/assets/canvas-BeDoufy1.js} +97 -7
  49. package/dist/node-host/assets/{comment-data-DyIZNpfG.js → comment-data-DE2T9yTj.js} +1980 -376
  50. package/dist/node-host/assets/dist-BCSTeNf4.js +4785 -0
  51. package/dist/node-host/assets/{files-Dh2TiQZq.js → files-Dl-PTS09.js} +11 -6
  52. package/dist/node-host/assets/{pages-BxpjbYhs.js → pages-DL2sk6_U.js} +3 -3
  53. package/dist/node-host/assets/{project-branches-B7HUGiVK.js → project-branches-DGp5ELct.js} +1 -1
  54. package/dist/{server/assets/project-thread-links-CBOpf6zk.js → node-host/assets/project-thread-links-CgBV3Jcm.js} +76 -5
  55. package/dist/{hosted/assets/screen-CKp2C7kx.js → node-host/assets/screen-B55yyVfb.js} +29 -1
  56. package/dist/node-host/assets/tasks-DZRNkoxe.js +1425 -0
  57. package/dist/node-host/assets/wasm-inline-Dl9tqabx.js +4 -0
  58. package/dist/node-host/node-host.js +6127 -4438
  59. package/dist/server/assets/{branch-previews-ClkoJ0lM.js → branch-previews-DedrpnVR.js} +1555 -673
  60. package/dist/{node-host/assets/canvas-TRUfxfL5.js → server/assets/canvas-CJWQ-jjv.js} +98 -6
  61. package/dist/server/assets/{comment-data-CsjukwAW.js → comment-data-Rg42ToTW.js} +1666 -47
  62. package/dist/server/assets/dist-BCSTeNf4.js +4785 -0
  63. package/dist/server/assets/{events-iHpLGr3k.js → events-BH_rAu7O.js} +155 -20
  64. package/dist/server/assets/{files-E9mSFeoc.js → files-CPJqKeMo.js} +12 -7
  65. package/dist/server/assets/{listener-DN5TyOjJ.js → listener-D61O6PzF.js} +708 -12
  66. package/dist/server/assets/{pages-BBBXyJWV.js → pages-KJY_Ba92.js} +4 -4
  67. package/dist/server/assets/{project-branches-B0YIK6Ur.js → project-branches-qG9ACZvo.js} +2 -2
  68. package/dist/{node-host/assets/project-thread-links-CauJF0Cz.js → server/assets/project-thread-links-CqK0lqCF.js} +77 -4
  69. package/dist/{node-host/assets/tasks-request-ZWmbc8fE.js → server/assets/project-work-DoIwxWMm.js} +87 -2
  70. package/dist/server/assets/run-BJ8r5jSh.js +572 -0
  71. package/dist/server/assets/{screen-BIQA2mr4.js → screen-CBrHPFug.js} +30 -2
  72. package/dist/server/assets/{server-DxK-UB_Z.js → server-Dkbp5stG.js} +1521 -793
  73. package/dist/server/assets/{services-BRrgXYlT.js → services-BTfig8A8.js} +234 -997
  74. package/dist/server/assets/tasks-Bgu5Lg9Y.js +1427 -0
  75. package/dist/server/assets/wasm-inline-Dl9tqabx.js +4 -0
  76. package/dist/server/capture.js +1 -2
  77. package/dist/server/migration.js +569 -12
  78. package/dist/server/runtime.js +7 -5
  79. package/dist/server/server.js +3 -3
  80. package/dist/types/assets/{config-DIC2cf_S.d.ts → config-CvcYmE2s.d.ts} +188 -50
  81. package/dist/types/assets/{contract-CAT-XFNq.d.ts → contract-B7yuuqSn.d.ts} +44 -2
  82. package/dist/types/assets/{items-CaVkByHk.d.ts → items-BQdaVksf.d.ts} +12 -1
  83. package/dist/types/assets/memory-state-BBW-FAVA.d.ts +236 -0
  84. package/dist/types/assets/picker-Be5Gx4WR.d.ts +9 -0
  85. package/dist/types/assets/{task-design-xJOhRNWh.d.ts → task-design-BDHKFHpp.d.ts} +3 -1
  86. package/dist/types/bridge.d.ts +1 -1
  87. package/dist/types/companion.d.ts +3 -3
  88. package/dist/types/design.d.ts +2 -2
  89. package/dist/types/hosted.d.ts +3 -3
  90. package/dist/types/index.d.ts +2 -2
  91. package/dist/types/node-host.d.ts +2 -2
  92. package/dist/types/plugins.d.ts +445 -0
  93. package/dist/types/screen.d.ts +2 -2
  94. package/dist/types/server.d.ts +2 -228
  95. package/dist/types/vite.d.ts +16 -0
  96. package/package.json +19 -3
  97. package/dist/client/assets/canvas-Cu6R6xxT.js +0 -1
  98. package/dist/client/assets/difference-worker-jfTSn-Tw.js +0 -8
  99. package/dist/client/assets/files-js6ElfkU.js +0 -1
  100. package/dist/client/assets/i18n-DxqFr2mi.js +0 -1
  101. package/dist/client/assets/index-CFFxP0gS.css +0 -2
  102. package/dist/client/assets/pages-6_lhiQmz.js +0 -1
  103. package/dist/client/assets/project-comments-DFFKVpia.js +0 -1
  104. package/dist/client/assets/project-thread-links-NLFWQRD2.js +0 -1
  105. package/dist/client/assets/project-work-du0Y-L5Y.js +0 -1
  106. package/dist/client/assets/tasks-CA-E8bbK.js +0 -1
  107. package/dist/client/assets/tasks-request-C55IeuiP.js +0 -1
  108. package/dist/client/assets/workspace-Cw5HvX5d.js +0 -2
  109. package/dist/hosted/assets/tasks-Dj7AyBJB.js +0 -649
  110. package/dist/hosted/assets/tasks-request-DZob623r.js +0 -113
  111. package/dist/node-host/assets/tasks-Bs1IOpeU.js +0 -651
  112. package/dist/server/assets/project-work-qSKFO5yj.js +0 -88
  113. package/dist/server/assets/tasks-CiXodsLN.js +0 -652
  114. package/dist/server/assets/tasks-request-DhwCzpRi.js +0 -115
  115. package/dist/types/assets/picker-D1Bw3ndx.d.ts +0 -14
@@ -1,7 +1,562 @@
1
- import { Kt as mapError, Lt as flatMap, On as try_, Ut as gen, _ as resolveStorage, f as Project, p as createProjectContext, sn as runPromise, u as Git } from "./assets/branch-previews-ClkoJ0lM.js";
2
- import { H as make, I as WorkError, O as AdapterRegistry } from "./assets/server-DxK-UB_Z.js";
3
- import { j as parseThread, z as parseThreadSnapshot } from "./assets/events-iHpLGr3k.js";
4
- import { n as storageLayer, r as readGitThreadImports } from "./assets/services-BRrgXYlT.js";
1
+ import { $t as map, Cr as TaggedError, Ct as isPattern, Fn as try_, Gt as flatMap, Hn as currentTimeMillis, M as Path, N as FileSystem, St as isNonEmpty, Zt as gen, _ as createProjectContext, _t as Struct, b as withPlugins, bt as decodeEffect, en as mapError, g as Project, gt as String$1, m as Git, mn as runPromise, mr as Service, mt as ArraySchema, y as configPlugins } from "./assets/branch-previews-DedrpnVR.js";
2
+ import { F as AdapterRegistry, H as WorkError, J as make, z as parseThreadImport } from "./assets/server-Dkbp5stG.js";
3
+ import { En as parseMessage, R as parseThreadSnapshot, a as CommentMergeError, c as parseCommentInput, d as parseCommentThread, f as revision, j as parseThread, l as conversationId, o as mergeThreads, p as text, s as stableJson, u as object } from "./assets/events-BH_rAu7O.js";
4
+ import { n as storageLayer } from "./assets/services-BTfig8A8.js";
5
+ import { createHash, randomUUID } from "node:crypto";
6
+ //#region ../comments/src/server/records.ts
7
+ /**
8
+ * Validates a persisted thread record of any version.
9
+ * @param value - Untrusted data.
10
+ * @returns The record.
11
+ */
12
+ function parseStoredThread(value) {
13
+ let input = object(value);
14
+ if (input.version !== 3) return parseCommentThread(value);
15
+ for (let key of Object.keys(input)) if (![
16
+ "version",
17
+ "id",
18
+ "project",
19
+ "branch",
20
+ "createdCommit",
21
+ "anchor",
22
+ "itemIds",
23
+ "comments",
24
+ "status",
25
+ "createdAt",
26
+ "updatedAt",
27
+ "metadata"
28
+ ].includes(key)) throw new Error(`Unsupported discussion field ${key}.`);
29
+ let project = text(input.project, "Project", 4096);
30
+ if (project !== "." && (project.startsWith("/") || project.includes("\\") || project.split("/").some((part) => !part || part === ".." || part === "."))) throw new Error("Invalid discussion project path.");
31
+ let createdCommit = input.createdCommit === "" ? "" : revision(input.createdCommit);
32
+ if (createdCommit === null) throw new Error("Invalid source commit.");
33
+ if (!Array.isArray(input.comments)) throw new Error("A discussion needs messages.");
34
+ let comments = input.comments.map(parseMessage);
35
+ let parents = new Map(comments.map((message) => [message.id, message.parentId]));
36
+ if (parents.size !== comments.length) throw new Error("Duplicate discussion message ID.");
37
+ for (let message of comments) {
38
+ let parent = message.parentId;
39
+ let seen = /* @__PURE__ */ new Set([message.id]);
40
+ while (parent !== null) {
41
+ if (seen.has(parent) || !parents.has(parent)) throw new Error("Invalid discussion reply ancestry.");
42
+ seen.add(parent);
43
+ parent = parents.get(parent);
44
+ }
45
+ }
46
+ let thread = parseThread({
47
+ id: conversationId(input.id),
48
+ status: input.status,
49
+ anchor: input.anchor,
50
+ itemIds: input.itemIds,
51
+ messages: comments.slice(0, 200),
52
+ messageCount: comments.length,
53
+ messagesLoaded: true,
54
+ messagesTruncated: comments.length > 200,
55
+ createdAt: input.createdAt,
56
+ updatedAt: input.updatedAt,
57
+ revision: null,
58
+ ...input.metadata === void 0 ? {} : { metadata: input.metadata }
59
+ });
60
+ return {
61
+ version: 3,
62
+ id: thread.id,
63
+ project,
64
+ branch: text(input.branch, "Source branch", 1024),
65
+ createdCommit,
66
+ anchor: thread.anchor,
67
+ itemIds: thread.itemIds,
68
+ comments,
69
+ status: thread.status,
70
+ createdAt: thread.createdAt,
71
+ updatedAt: thread.updatedAt,
72
+ ...thread.metadata === void 0 ? {} : { metadata: thread.metadata }
73
+ };
74
+ }
75
+ /**
76
+ * Tracks this record's content, independent of writes to other threads.
77
+ * @param record - The validated persisted discussion.
78
+ * @returns An opaque revision for compare-and-swap updates.
79
+ */
80
+ function threadRevision(record) {
81
+ return createHash("sha256").update(stableJson(record)).digest("hex");
82
+ }
83
+ /**
84
+ * Turns a persisted record into the `@bud.sh/work` thread the app shows.
85
+ * @param record - The validated persisted record.
86
+ * @param loaded - Whether to include its messages, up to the limit, or only the first.
87
+ * @returns The thread.
88
+ */
89
+ function projectThread(record, loaded = true) {
90
+ let thread = {
91
+ id: record.id,
92
+ status: record.status,
93
+ anchor: record.anchor,
94
+ itemIds: record.version === 3 ? record.itemIds : [],
95
+ messages: record.comments.slice(0, loaded ? 200 : 1),
96
+ messageCount: record.comments.length,
97
+ messagesLoaded: loaded,
98
+ messagesTruncated: loaded && record.comments.length > 200,
99
+ createdAt: record.createdAt,
100
+ updatedAt: record.updatedAt,
101
+ revision: threadRevision(record),
102
+ ...record.version === 3 ? record.metadata === void 0 ? {} : { metadata: record.metadata } : { metadata: { git: {
103
+ project: record.project,
104
+ branch: record.branch,
105
+ createdCommit: record.createdCommit
106
+ } } }
107
+ };
108
+ return parseThread(thread);
109
+ }
110
+ /**
111
+ * Promotes a legacy record without changing its capture or messages.
112
+ * @param record - The current stored record.
113
+ * @returns Its version 3 representation; persistence removes the old path in the same commit.
114
+ */
115
+ function promoteThread(record) {
116
+ if (record.version === 3) return record;
117
+ return {
118
+ ...record,
119
+ version: 3,
120
+ itemIds: [],
121
+ metadata: projectThread(record).metadata
122
+ };
123
+ }
124
+ //#endregion
125
+ //#region ../comments/src/server/merge.ts
126
+ function changed(local, remote, base, field, hasBase = base !== void 0) {
127
+ if (stableJson(local) === stableJson(remote)) return local;
128
+ if (hasBase && stableJson(local) === stableJson(base)) return remote;
129
+ if (hasBase && stableJson(remote) === stableJson(base)) return local;
130
+ throw new CommentMergeError(`Conflicting discussion ${field}. Resolve the record in Git before syncing.`);
131
+ }
132
+ /**
133
+ * Merges new records and mixed-version history while preserving captures.
134
+ * @param local - The locally stored record.
135
+ * @param remote - The validated incoming record.
136
+ * @param base - The common ancestor, when one exists.
137
+ * @returns The merged record, or a conflict when both sides changed the same value.
138
+ */
139
+ function mergeStoredThreads(local, remote, base) {
140
+ if (local.version !== 3 && remote.version !== 3 && base?.version !== 3) return mergeThreads(local, remote, base);
141
+ let left = promoteThread(local);
142
+ let right = promoteThread(remote);
143
+ let ancestor = base && promoteThread(base);
144
+ function identity(record) {
145
+ let { comments, status, updatedAt, itemIds, metadata, ...identity } = record;
146
+ return identity;
147
+ }
148
+ if (stableJson(identity(left)) !== stableJson(identity(right)) || ancestor && stableJson(identity(left)) !== stableJson(identity(ancestor))) throw new CommentMergeError("Conflicting discussion anchors. Resolve the record in Git before syncing.");
149
+ let messages = /* @__PURE__ */ new Map();
150
+ for (let message of [...left.comments, ...right.comments]) {
151
+ let existing = messages.get(message.id);
152
+ if (existing && stableJson(existing) !== stableJson(message)) throw new CommentMergeError("Conflicting versions of a message. Resolve the record in Git before syncing.");
153
+ messages.set(message.id, message);
154
+ }
155
+ let itemIds = [.../* @__PURE__ */ new Set([
156
+ ...left.itemIds,
157
+ ...right.itemIds,
158
+ ...ancestor?.itemIds ?? []
159
+ ])].filter((id) => changed(left.itemIds.includes(id), right.itemIds.includes(id), ancestor?.itemIds.includes(id), "links")).sort();
160
+ let metadata = changed(left.metadata, right.metadata, ancestor?.metadata, "metadata", ancestor !== void 0);
161
+ return parseStoredThread({
162
+ ...left,
163
+ status: changed(left.status, right.status, ancestor?.status, "status"),
164
+ itemIds,
165
+ updatedAt: left.updatedAt > right.updatedAt ? left.updatedAt : right.updatedAt,
166
+ comments: [...messages.values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)),
167
+ metadata
168
+ });
169
+ }
170
+ //#endregion
171
+ //#region ../comments/src/server/errors.ts
172
+ /** Safe domain messages; nested diagnostics stay on the trusted server. */
173
+ var CommentsError = class extends TaggedError("CommentsError") {};
174
+ //#endregion
175
+ //#region ../comments/src/server/persistence.ts
176
+ var commentsRef = "refs/heads/bud/comments";
177
+ var threadPath = /^comments\/v(\d+)\/threads\/([a-f0-9-]{36})\/thread\.json$/;
178
+ var optionsSchema = Struct({
179
+ root: String$1.check(isNonEmpty(), isPattern(/^[^\0]*$/)),
180
+ remote: String$1.check(isPattern(/^[A-Za-z0-9][A-Za-z0-9._-]*$/)),
181
+ routes: ArraySchema(Struct({
182
+ id: String$1.check(isNonEmpty()),
183
+ path: String$1.check(isNonEmpty())
184
+ }))
185
+ });
186
+ function fromGit(cause) {
187
+ if (cause._tag === "GitIdentityError") return new CommentsError({
188
+ reason: "identity",
189
+ message: "Set git config user.name and user.email in this project before commenting.",
190
+ cause
191
+ });
192
+ if (cause._tag === "GitRepositoryError") return new CommentsError({
193
+ reason: "repository",
194
+ message: "Initialize a Git repository for this project to save comments.",
195
+ cause
196
+ });
197
+ if (cause._tag === "GitConflictError") return new CommentsError({
198
+ reason: "conflict",
199
+ message: "Comments changed elsewhere. Reload the conversation and try again.",
200
+ cause
201
+ });
202
+ return new CommentsError({
203
+ reason: "storage",
204
+ message: "The Git operation failed. Your existing comments remain saved. Check the repository and Git configuration.",
205
+ cause
206
+ });
207
+ }
208
+ function attempt(tryValue, reason = "input", message) {
209
+ return try_({
210
+ try: tryValue,
211
+ catch: (cause) => new CommentsError({
212
+ reason,
213
+ message: message ?? (cause instanceof Error ? cause.message : "Invalid comment."),
214
+ cause
215
+ })
216
+ });
217
+ }
218
+ function failure(reason, message) {
219
+ return (cause) => new CommentsError({
220
+ reason,
221
+ message,
222
+ cause
223
+ });
224
+ }
225
+ /**
226
+ * Captures shared persistence for legacy comments and the thread adapter.
227
+ * @param options - The trusted project, remote and configured routes.
228
+ * @returns Scoped operations; repository access starts only when an operation runs.
229
+ */
230
+ function createCommentStorage(options) {
231
+ return gen(function* () {
232
+ let input = yield* decodeEffect(optionsSchema, { onExcessProperty: "error" })(options).pipe(mapError(failure("input", "Invalid comment store configuration.")));
233
+ let git = yield* Git;
234
+ let fs = yield* FileSystem;
235
+ let paths = yield* Path;
236
+ function location() {
237
+ return gen(function* () {
238
+ let repo = yield* git.discover(input.root).pipe(mapError(fromGit));
239
+ if (repo.info.bare) return yield* new CommentsError({
240
+ reason: "repository",
241
+ message: "Open a source worktree before editing comments."
242
+ });
243
+ if ((yield* repo.symbolicRef.pipe(mapError(fromGit))) === "refs/heads/bud/comments") return yield* new CommentsError({
244
+ reason: "repository",
245
+ message: "Switch to your source branch before editing comments."
246
+ });
247
+ let root = yield* fs.realPath(input.root).pipe(mapError(failure("repository", "Unable to resolve the project directory.")));
248
+ let projectId = paths.relative(repo.info.root, root).replaceAll("\\", "/") || ".";
249
+ if (projectId === ".." || projectId.startsWith("../") || paths.isAbsolute(projectId)) return yield* new CommentsError({
250
+ reason: "repository",
251
+ message: "The project must be inside its Git repository."
252
+ });
253
+ return {
254
+ repo,
255
+ projectId
256
+ };
257
+ });
258
+ }
259
+ function headAt(repo, ref = commentsRef) {
260
+ return repo.resolve(ref).pipe(mapError(fromGit));
261
+ }
262
+ function author(repo) {
263
+ return repo.identity.pipe(mapError(fromGit), map(({ name, email }) => ({
264
+ name,
265
+ email,
266
+ id: createHash("sha256").update(`${name}\0${email}`).digest("hex").slice(0, 16)
267
+ })));
268
+ }
269
+ function source(repo) {
270
+ return gen(function* () {
271
+ return {
272
+ branch: (yield* repo.symbolicRef.pipe(mapError(fromGit)))?.replace(/^refs\/heads\//, "") ?? "detached",
273
+ createdCommit: (yield* headAt(repo, "HEAD")) ?? ""
274
+ };
275
+ });
276
+ }
277
+ function recordsAt(repo, head) {
278
+ return gen(function* () {
279
+ let records = /* @__PURE__ */ new Map();
280
+ let blobs = /* @__PURE__ */ new Map();
281
+ if (!head) return {
282
+ threads: records,
283
+ blobs
284
+ };
285
+ let entries = yield* repo.listTree(head, "comments").pipe(mapError(fromGit));
286
+ for (let entry of entries) {
287
+ let match = threadPath.exec(entry.path);
288
+ if (!match) continue;
289
+ if (entry.type !== "blob") return yield* new CommentsError({
290
+ reason: "conflict",
291
+ message: "The comments branch contains an unsupported or invalid thread."
292
+ });
293
+ let bytes = yield* repo.readBlob(entry.oid).pipe(mapError(fromGit));
294
+ let value = yield* attempt(() => JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)), "conflict", "The comments branch contains invalid JSON. Repair that record before syncing.");
295
+ let thread = yield* attempt(() => {
296
+ let thread = parseStoredThread(value);
297
+ if (thread.id !== match[2] || String(thread.version) !== match[1]) throw new Error("Mismatched thread ID or storage version.");
298
+ if (records.has(thread.id)) throw new Error("Duplicate thread across versions.");
299
+ return thread;
300
+ }, "conflict", "The comments branch contains an unsupported or invalid thread.");
301
+ records.set(thread.id, thread);
302
+ blobs.set(thread.id, bytes);
303
+ }
304
+ return {
305
+ threads: records,
306
+ blobs
307
+ };
308
+ });
309
+ }
310
+ function snapshot(repo, projectId) {
311
+ return gen(function* () {
312
+ let head = yield* headAt(repo);
313
+ return {
314
+ head,
315
+ threads: [...(yield* recordsAt(repo, head)).threads.values()].filter((thread) => thread.project === projectId).sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id))
316
+ };
317
+ });
318
+ }
319
+ function commitRecords(repo, parent, records, message, previous) {
320
+ return gen(function* () {
321
+ let files = yield* attempt(() => records.flatMap((record) => {
322
+ let validated = parseStoredThread(record);
323
+ let contents = stableJson(validated);
324
+ let existing = previous.records.threads.get(validated.id);
325
+ if (existing && stableJson(existing) === contents) return [];
326
+ let remote = previous.remote?.threads.get(validated.id);
327
+ let original = remote && stableJson(remote) === contents ? previous.remote?.blobs.get(validated.id) : void 0;
328
+ return [...existing && existing.version !== validated.version ? [{
329
+ path: `comments/v${existing.version}/threads/${existing.id}/thread.json`,
330
+ contents: null
331
+ }] : [], {
332
+ path: `comments/v${validated.version}/threads/${validated.id}/thread.json`,
333
+ contents: original ?? `${contents}\n`
334
+ }];
335
+ }));
336
+ return yield* repo.commitSnapshot({
337
+ ref: commentsRef,
338
+ expectedHead: parent,
339
+ files,
340
+ message,
341
+ extraParents: previous.extraParent && previous.extraParent !== parent ? [previous.extraParent] : void 0
342
+ }).pipe(mapError(fromGit));
343
+ });
344
+ }
345
+ function list() {
346
+ return gen(function* () {
347
+ let { repo, projectId } = yield* location();
348
+ return yield* snapshot(repo, projectId);
349
+ });
350
+ }
351
+ function write(expectedHead, value) {
352
+ return gen(function* () {
353
+ let command = yield* attempt(() => {
354
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid comment change.");
355
+ return parseCommentInput({
356
+ ...value,
357
+ head: expectedHead
358
+ }, input.routes, value.id);
359
+ });
360
+ let { repo, projectId } = yield* location();
361
+ let head = yield* headAt(repo);
362
+ if (head !== command.head) return yield* new CommentsError({
363
+ reason: "conflict",
364
+ message: "Comments changed elsewhere. Reload the conversation and try again."
365
+ });
366
+ let records = yield* recordsAt(repo, head);
367
+ let change = command.change;
368
+ let identity = yield* author(repo);
369
+ let now = new Date(yield* currentTimeMillis).toISOString();
370
+ let comment = change.body ? {
371
+ id: randomUUID(),
372
+ body: change.body,
373
+ author: identity,
374
+ createdAt: now,
375
+ parentId: change.parentId ?? null
376
+ } : void 0;
377
+ let thread;
378
+ if (change.id) {
379
+ let previous = records.threads.get(change.id);
380
+ if (!previous || previous.project !== projectId || previous.version === 3) return yield* new CommentsError({
381
+ reason: "not-found",
382
+ message: "This conversation no longer exists."
383
+ });
384
+ if (change.parentId && !previous.comments.some((comment) => comment.id === change.parentId)) return yield* new CommentsError({
385
+ reason: "input",
386
+ message: "Reply to a comment in this conversation."
387
+ });
388
+ thread = {
389
+ ...previous,
390
+ comments: comment ? [...previous.comments, comment] : previous.comments,
391
+ status: change.status ?? previous.status,
392
+ updatedAt: now
393
+ };
394
+ } else {
395
+ let { branch, createdCommit } = yield* source(repo);
396
+ thread = yield* attempt(() => parseCommentThread({
397
+ version: "type" in change.anchor && change.anchor.type === "code" ? 2 : 1,
398
+ id: randomUUID(),
399
+ project: projectId,
400
+ branch,
401
+ createdCommit,
402
+ anchor: change.anchor,
403
+ comments: [comment],
404
+ status: "open",
405
+ createdAt: now,
406
+ updatedAt: now
407
+ }));
408
+ }
409
+ yield* commitRecords(repo, head, [thread], change.id ? "bud: update conversation" : "bud: start conversation", { records });
410
+ return yield* snapshot(repo, projectId);
411
+ });
412
+ }
413
+ function sync(direction = "both") {
414
+ return gen(function* () {
415
+ let { repo, projectId } = yield* location();
416
+ let remote = input.remote;
417
+ if (!(yield* repo.remoteUrl(remote).pipe(mapError(fromGit)))) return yield* new CommentsError({
418
+ reason: "remote-missing",
419
+ message: `Add the Git remote “${remote}” before syncing comments.`
420
+ });
421
+ let remoteExists = yield* repo.remoteHead(remote, commentsRef).pipe(mapError(failure("remote-read", "Cannot reach the Git remote. Check your connection and Git credentials; local comments are saved.")));
422
+ let localHead = yield* headAt(repo);
423
+ let localRecords = yield* recordsAt(repo, localHead);
424
+ if (remoteExists && direction !== "push") {
425
+ let remoteHead = yield* repo.fetchRef(remote, commentsRef, `refs/bud/remotes/${remote}/comments`).pipe(mapError(failure("remote-fetch", "Could not fetch comments from the Git remote. Local comments are saved.")));
426
+ if (!remoteHead) return yield* new CommentsError({
427
+ reason: "remote-fetch",
428
+ message: "Remote comments changed during fetch. Try syncing again."
429
+ });
430
+ let remoteRecords = yield* recordsAt(repo, remoteHead);
431
+ if (localHead !== remoteHead) {
432
+ if (!localHead) yield* repo.compareAndSwapRef(commentsRef, remoteHead, null).pipe(mapError(fromGit));
433
+ else {
434
+ let base = yield* repo.mergeBase(localHead, remoteHead).pipe(mapError(fromGit));
435
+ if (base === localHead) {
436
+ yield* attempt(() => {
437
+ for (let [id, local] of localRecords.threads) {
438
+ let remote = remoteRecords.threads.get(id);
439
+ if (remote) mergeStoredThreads(local, remote, local);
440
+ }
441
+ }, "conflict");
442
+ yield* repo.compareAndSwapRef(commentsRef, remoteHead, localHead).pipe(mapError(fromGit));
443
+ } else if (base !== remoteHead) {
444
+ let baseRecords = yield* recordsAt(repo, base);
445
+ yield* commitRecords(repo, localHead, yield* attempt(() => [.../* @__PURE__ */ new Set([...localRecords.threads.keys(), ...remoteRecords.threads.keys()])].map((id) => {
446
+ let local = localRecords.threads.get(id);
447
+ let remote = remoteRecords.threads.get(id);
448
+ return local && remote ? mergeStoredThreads(local, remote, baseRecords.threads.get(id)) : local ?? remote;
449
+ }), "conflict"), "bud: sync conversations", {
450
+ records: localRecords,
451
+ remote: remoteRecords,
452
+ extraParent: remoteHead
453
+ });
454
+ }
455
+ }
456
+ }
457
+ }
458
+ let publishHead = yield* headAt(repo);
459
+ if (publishHead && direction !== "pull") {
460
+ yield* recordsAt(repo, publishHead);
461
+ yield* repo.pushRef(remote, commentsRef, publishHead).pipe(mapError(failure("remote-push", "Comments are saved locally. The remote rejected the push; check Git access or sync again to include newer comments.")));
462
+ }
463
+ return yield* snapshot(repo, projectId);
464
+ });
465
+ }
466
+ function seed(seeds) {
467
+ return gen(function* () {
468
+ yield* attempt(() => {
469
+ if (!Array.isArray(seeds) || seeds.length > 1e3) throw new Error("Invalid seed conversations.");
470
+ if (new Set(seeds.map((seed) => seed.id)).size !== seeds.length) throw new Error("Duplicate seed conversation ID.");
471
+ });
472
+ let { repo, projectId } = yield* location();
473
+ let head = yield* headAt(repo);
474
+ let records = yield* recordsAt(repo, head);
475
+ let missing = yield* attempt(() => seeds.filter((seed) => {
476
+ let existing = records.threads.get(seed.id);
477
+ if (existing && existing.project !== projectId) throw new Error("That seed conversation belongs to another project.");
478
+ return !existing;
479
+ }), "conflict");
480
+ if (!missing.length) return yield* snapshot(repo, projectId);
481
+ let identity = yield* author(repo);
482
+ let now = new Date(yield* currentTimeMillis).toISOString();
483
+ let { branch, createdCommit } = yield* source(repo);
484
+ yield* commitRecords(repo, head, yield* attempt(() => missing.map((seed) => {
485
+ if (seed.anchor.element !== null || !input.routes.some((route) => route.id === seed.anchor.pageId && route.path === seed.anchor.pagePath)) throw new Error("Seed comments need coordinates on a configured route.");
486
+ return parseCommentThread({
487
+ version: 1,
488
+ id: seed.id,
489
+ project: projectId,
490
+ branch,
491
+ createdCommit,
492
+ anchor: seed.anchor,
493
+ comments: seed.messages.map((body) => ({
494
+ id: randomUUID(),
495
+ body,
496
+ author: identity,
497
+ createdAt: now,
498
+ parentId: null
499
+ })),
500
+ status: "open",
501
+ createdAt: now,
502
+ updatedAt: now
503
+ });
504
+ })), "bud: seed development conversations", { records });
505
+ return yield* snapshot(repo, projectId);
506
+ });
507
+ }
508
+ return {
509
+ list: list(),
510
+ write,
511
+ sync,
512
+ seed,
513
+ root: input.root,
514
+ paths,
515
+ location,
516
+ source,
517
+ author,
518
+ headAt,
519
+ recordsAt,
520
+ snapshot,
521
+ commitRecords
522
+ };
523
+ });
524
+ }
525
+ Service()("@bud.sh/comments/Comments");
526
+ //#endregion
527
+ //#region ../comments/src/server/migration.ts
528
+ /**
529
+ * Reads complete migration history under the Git runtime's existing tree and blob byte limits.
530
+ * @param options - The trusted source project, remote and configured canvas routes.
531
+ * @returns Complete source records and their immutable ref head, without writing any Git object or
532
+ * ref.
533
+ */
534
+ function readGitThreadImports(options) {
535
+ return gen(function* () {
536
+ let snapshot = yield* (yield* createCommentStorage(options)).list;
537
+ let threads = yield* try_({
538
+ try: () => snapshot.threads.map((record) => parseThreadImport({
539
+ source: "bud-git",
540
+ sourceId: record.id,
541
+ thread: {
542
+ ...projectThread(record),
543
+ messages: record.comments,
544
+ messagesTruncated: false
545
+ }
546
+ })),
547
+ catch: (cause) => new CommentsError({
548
+ reason: "storage",
549
+ message: "The Git discussion history could not be validated for migration.",
550
+ cause
551
+ })
552
+ });
553
+ return {
554
+ head: snapshot.head,
555
+ threads
556
+ };
557
+ });
558
+ }
559
+ //#endregion
5
560
  //#region app/server/comment-migration.ts
6
561
  function identity(metadata) {
7
562
  if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) return null;
@@ -26,7 +581,7 @@ function migrateComments(dryRun = false) {
26
581
  let target = registry.threads;
27
582
  if (!target) return yield* new WorkError({
28
583
  reason: "unavailable",
29
- message: "Configure storage.threads or pass --to bud:beads before migrating comments."
584
+ message: "Discussions are off; pass --to bud:beads before migrating comments."
30
585
  });
31
586
  if (!target.import) return yield* new WorkError({
32
587
  reason: "refused",
@@ -105,18 +660,20 @@ function migrateComments(dryRun = false) {
105
660
  * @returns The migration summary after the runtime and all native processes have been released.
106
661
  */
107
662
  async function runCommentMigration(options) {
108
- let storage = resolveStorage({
109
- work: false,
110
- threads: options.to ?? options.config.storage.threads
111
- });
112
- let config = {
663
+ let plugins = configPlugins(options.config);
664
+ let threads = options.to ?? options.config.storage.threads;
665
+ if (threads !== "bud:beads" && !plugins.some((plugin) => plugin.name === threads && plugin.threads !== void 0)) throw new Error(threads ? `--to ${threads} names no plugin of this project that stores discussions; use bud:beads or one in plugins.` : "The project’s discussions are off; pass --to with the store to migrate into.");
666
+ let config = withPlugins({
113
667
  ...options.config,
114
- storage,
668
+ storage: {
669
+ work: false,
670
+ threads
671
+ },
115
672
  designs: {
116
673
  ...options.config.designs,
117
674
  tasks: "bud:beads"
118
675
  }
119
- };
676
+ }, plugins);
120
677
  let project = await runPromise(createProjectContext(options.root, config, config.targetOrigin ?? "http://localhost"), { signal: options.signal });
121
678
  let runtime = make(storageLayer(project));
122
679
  try {
@@ -1,7 +1,7 @@
1
- import { An as uninterruptible, Dn as tryPromise, Kt as mapError, Rn as effect, Ut as gen, Zt as promise, p as createProjectContext, qn as provideMerge, sn as runPromise, sr as Service, vt as acquireRelease, x as readText } from "./assets/branch-previews-ClkoJ0lM.js";
2
- import { B as sharedModules, H as make } from "./assets/server-DxK-UB_Z.js";
3
- import { n as listen, r as createApp, t as ServerError } from "./assets/listener-DN5TyOjJ.js";
4
- import { t as applicationLayer } from "./assets/services-BRrgXYlT.js";
1
+ import { Et as acquireRelease, Gn as effect, Ln as uninterruptible, O as readText, Pn as tryPromise, Zt as gen, _ as createProjectContext, an as promise, en as mapError, er as provideMerge, mn as runPromise, mr as Service } from "./assets/branch-previews-DedrpnVR.js";
2
+ import { J as make, K as sharedModules, v as rememberOrigin } from "./assets/server-Dkbp5stG.js";
3
+ import { n as listen, r as createApp, t as ServerError } from "./assets/listener-D61O6PzF.js";
4
+ import { t as applicationLayer } from "./assets/services-BTfig8A8.js";
5
5
  import { fileURLToPath } from "node:url";
6
6
  //#region app/server/runtime.ts
7
7
  var BudListener = class extends Service()("bud/Listener") {};
@@ -86,8 +86,10 @@ async function startBud(options) {
86
86
  };
87
87
  options.signal?.addEventListener("abort", abort, { once: true });
88
88
  try {
89
+ let listener = await runtime.runPromise(BudListener, { signal: options.signal });
90
+ rememberOrigin(project, listener.origin);
89
91
  return {
90
- origin: (await runtime.runPromise(BudListener, { signal: options.signal })).origin,
92
+ origin: listener.origin,
91
93
  close
92
94
  };
93
95
  } catch (error) {
@@ -1,6 +1,6 @@
1
- import { Kn as provide, Rn as effect, Ut as gen, b as nodeLayer, sr as Service, x as readText } from "./assets/branch-previews-ClkoJ0lM.js";
2
- import { H as make } from "./assets/server-DxK-UB_Z.js";
3
- import { n as listen, r as createApp } from "./assets/listener-DN5TyOjJ.js";
1
+ import { $n as provide, D as nodeLayer, Gn as effect, O as readText, Zt as gen, mr as Service } from "./assets/branch-previews-DedrpnVR.js";
2
+ import { J as make } from "./assets/server-Dkbp5stG.js";
3
+ import { n as listen, r as createApp } from "./assets/listener-D61O6PzF.js";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region server.ts
6
6
  var Listener = class extends Service()("bud/StandaloneListener") {};