@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.
- package/README.md +41 -31
- package/dist/client/assets/bud/design.js +1 -1
- package/dist/client/assets/canvas-Bih9EdKJ.js +1 -0
- package/dist/client/assets/difference-worker-Bzg-rizy.js +8 -0
- package/dist/client/assets/dist-CdE5wBkg.js +5 -0
- package/dist/client/assets/files-1dLuoM2f.js +1 -0
- package/dist/client/assets/i18n-CkO6VFI0.js +1 -0
- package/dist/client/assets/index-Cn2mxroU.css +2 -0
- package/dist/client/assets/{index-ChANGtl-.js → index-DtISWZp9.js} +2 -2
- package/dist/client/assets/pages-BJ-tNJ4J.js +1 -0
- package/dist/client/assets/{project-branches-DO897zM6.js → project-branches-Ce3rXrkc.js} +1 -1
- package/dist/client/assets/project-comments-CPUONfs8.js +1 -0
- package/dist/client/assets/project-thread-links-C-TGkq59.js +1 -0
- package/dist/client/assets/{project-threads-Lay351p4.js → project-threads-Bv7IDBgi.js} +105 -105
- package/dist/client/assets/project-work-DAE9-olg.js +1 -0
- package/dist/client/assets/{task-design-Avb2RO4w.js → task-design-CwySIAOl.js} +3 -3
- package/dist/client/assets/tasks-KWsCLi_h.js +3 -0
- package/dist/client/assets/wasm-inline-DSUyFn1L.js +1 -0
- package/dist/client/assets/workspace-CVdcfaaB.js +2 -0
- package/dist/client/index.html +3 -3
- package/dist/hosted/assets/{canvas-V36732ER.js → canvas-AII16n9i.js} +97 -6
- package/dist/hosted/assets/{comment-data-CF2ZV_3k.js → comment-data-BXfMElfx.js} +2004 -402
- package/dist/hosted/assets/dist-BCSTeNf4.js +4785 -0
- package/dist/hosted/assets/{files-DjSCjfU7.js → files-D_1yeAVO.js} +10 -5
- package/dist/hosted/assets/{pages-BzjzeuPu.js → pages-CN_SbKWA.js} +3 -3
- package/dist/hosted/assets/{project-branches-BT97J9i7.js → project-branches-Bxp0DCwg.js} +1 -1
- package/dist/hosted/assets/{project-thread-links-BRLgByEe.js → project-thread-links-KqQoKBLE.js} +76 -4
- package/dist/{node-host/assets/screen-CZXWJEds.js → hosted/assets/screen-DpAcDUKR.js} +29 -1
- package/dist/hosted/assets/tasks-D66LlwxM.js +1423 -0
- package/dist/hosted/assets/wasm-inline-Dl9tqabx.js +4 -0
- package/dist/hosted/hosted.js +7885 -6219
- package/dist/node/assets/Stream-CAm1eJ2n.js +16604 -0
- package/dist/{server/assets/bridge-B_Blt55E.js → node/assets/bridge-DQSBYHmw.js} +124 -290
- package/dist/node/assets/config-9_jWwNBh.js +1067 -0
- package/dist/node/assets/load-config.server-CMVtRVmO.js +261 -0
- package/dist/node/assets/module-3GgtT4Xs.js +308 -0
- package/dist/node/assets/server-DTQFuoX2.js +846 -0
- package/dist/node/assets/src-D-kG1GxB.js +2 -0
- package/dist/node/assets/{src-CerrKjID.js → src-Dwe9APUx.js} +93 -3
- package/dist/node/assets/src-DysjfbM8.js +8592 -0
- package/dist/node/bridge.js +1 -845
- package/dist/node/cli.js +206 -0
- package/dist/node/design.js +811 -13
- package/dist/node/index.js +2 -739
- package/dist/node/plugins.js +5711 -0
- package/dist/node/server.js +1 -6746
- package/dist/node/vite.js +121 -0
- package/dist/{server/assets/canvas-Dc1mYCwQ.js → node-host/assets/canvas-BeDoufy1.js} +97 -7
- package/dist/node-host/assets/{comment-data-DyIZNpfG.js → comment-data-DE2T9yTj.js} +1980 -376
- package/dist/node-host/assets/dist-BCSTeNf4.js +4785 -0
- package/dist/node-host/assets/{files-Dh2TiQZq.js → files-Dl-PTS09.js} +11 -6
- package/dist/node-host/assets/{pages-BxpjbYhs.js → pages-DL2sk6_U.js} +3 -3
- package/dist/node-host/assets/{project-branches-B7HUGiVK.js → project-branches-DGp5ELct.js} +1 -1
- package/dist/{server/assets/project-thread-links-CBOpf6zk.js → node-host/assets/project-thread-links-CgBV3Jcm.js} +76 -5
- package/dist/{hosted/assets/screen-CKp2C7kx.js → node-host/assets/screen-B55yyVfb.js} +29 -1
- package/dist/node-host/assets/tasks-DZRNkoxe.js +1425 -0
- package/dist/node-host/assets/wasm-inline-Dl9tqabx.js +4 -0
- package/dist/node-host/node-host.js +6127 -4438
- package/dist/server/assets/{branch-previews-ClkoJ0lM.js → branch-previews-DedrpnVR.js} +1555 -673
- package/dist/{node-host/assets/canvas-TRUfxfL5.js → server/assets/canvas-CJWQ-jjv.js} +98 -6
- package/dist/server/assets/{comment-data-CsjukwAW.js → comment-data-Rg42ToTW.js} +1666 -47
- package/dist/server/assets/dist-BCSTeNf4.js +4785 -0
- package/dist/server/assets/{events-iHpLGr3k.js → events-BH_rAu7O.js} +155 -20
- package/dist/server/assets/{files-E9mSFeoc.js → files-CPJqKeMo.js} +12 -7
- package/dist/server/assets/{listener-DN5TyOjJ.js → listener-D61O6PzF.js} +708 -12
- package/dist/server/assets/{pages-BBBXyJWV.js → pages-KJY_Ba92.js} +4 -4
- package/dist/server/assets/{project-branches-B0YIK6Ur.js → project-branches-qG9ACZvo.js} +2 -2
- package/dist/{node-host/assets/project-thread-links-CauJF0Cz.js → server/assets/project-thread-links-CqK0lqCF.js} +77 -4
- package/dist/{node-host/assets/tasks-request-ZWmbc8fE.js → server/assets/project-work-DoIwxWMm.js} +87 -2
- package/dist/server/assets/run-BJ8r5jSh.js +572 -0
- package/dist/server/assets/{screen-BIQA2mr4.js → screen-CBrHPFug.js} +30 -2
- package/dist/server/assets/{server-DxK-UB_Z.js → server-Dkbp5stG.js} +1521 -793
- package/dist/server/assets/{services-BRrgXYlT.js → services-BTfig8A8.js} +234 -997
- package/dist/server/assets/tasks-Bgu5Lg9Y.js +1427 -0
- package/dist/server/assets/wasm-inline-Dl9tqabx.js +4 -0
- package/dist/server/capture.js +1 -2
- package/dist/server/migration.js +569 -12
- package/dist/server/runtime.js +7 -5
- package/dist/server/server.js +3 -3
- package/dist/types/assets/{config-DIC2cf_S.d.ts → config-CvcYmE2s.d.ts} +188 -50
- package/dist/types/assets/{contract-CAT-XFNq.d.ts → contract-B7yuuqSn.d.ts} +44 -2
- package/dist/types/assets/{items-CaVkByHk.d.ts → items-BQdaVksf.d.ts} +12 -1
- package/dist/types/assets/memory-state-BBW-FAVA.d.ts +236 -0
- package/dist/types/assets/picker-Be5Gx4WR.d.ts +9 -0
- package/dist/types/assets/{task-design-xJOhRNWh.d.ts → task-design-BDHKFHpp.d.ts} +3 -1
- package/dist/types/bridge.d.ts +1 -1
- package/dist/types/companion.d.ts +3 -3
- package/dist/types/design.d.ts +2 -2
- package/dist/types/hosted.d.ts +3 -3
- package/dist/types/index.d.ts +2 -2
- package/dist/types/node-host.d.ts +2 -2
- package/dist/types/plugins.d.ts +445 -0
- package/dist/types/screen.d.ts +2 -2
- package/dist/types/server.d.ts +2 -228
- package/dist/types/vite.d.ts +16 -0
- package/package.json +19 -3
- package/dist/client/assets/canvas-Cu6R6xxT.js +0 -1
- package/dist/client/assets/difference-worker-jfTSn-Tw.js +0 -8
- package/dist/client/assets/files-js6ElfkU.js +0 -1
- package/dist/client/assets/i18n-DxqFr2mi.js +0 -1
- package/dist/client/assets/index-CFFxP0gS.css +0 -2
- package/dist/client/assets/pages-6_lhiQmz.js +0 -1
- package/dist/client/assets/project-comments-DFFKVpia.js +0 -1
- package/dist/client/assets/project-thread-links-NLFWQRD2.js +0 -1
- package/dist/client/assets/project-work-du0Y-L5Y.js +0 -1
- package/dist/client/assets/tasks-CA-E8bbK.js +0 -1
- package/dist/client/assets/tasks-request-C55IeuiP.js +0 -1
- package/dist/client/assets/workspace-Cw5HvX5d.js +0 -2
- package/dist/hosted/assets/tasks-Dj7AyBJB.js +0 -649
- package/dist/hosted/assets/tasks-request-DZob623r.js +0 -113
- package/dist/node-host/assets/tasks-Bs1IOpeU.js +0 -651
- package/dist/server/assets/project-work-qSKFO5yj.js +0 -88
- package/dist/server/assets/tasks-CiXodsLN.js +0 -652
- package/dist/server/assets/tasks-request-DhwCzpRi.js +0 -115
- package/dist/types/assets/picker-D1Bw3ndx.d.ts +0 -14
package/dist/server/migration.js
CHANGED
|
@@ -1,7 +1,562 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { j as parseThread,
|
|
4
|
-
import { n as storageLayer
|
|
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: "
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
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 {
|
package/dist/server/runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { n as listen, r as createApp, t as ServerError } from "./assets/listener-
|
|
4
|
-
import { t as applicationLayer } from "./assets/services-
|
|
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:
|
|
92
|
+
origin: listener.origin,
|
|
91
93
|
close
|
|
92
94
|
};
|
|
93
95
|
} catch (error) {
|
package/dist/server/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { n as listen, r as createApp } from "./assets/listener-
|
|
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") {};
|