@bud.sh/app 0.5.1 → 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-Dn2pfmXf.js → index-DtISWZp9.js} +2 -2
  10. package/dist/client/assets/pages-BJ-tNJ4J.js +1 -0
  11. package/dist/client/assets/{project-branches-CMGRmrqr.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-ZbKDh78d.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-BPpc4Gd7.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-BYTOp1p7.js → canvas-AII16n9i.js} +97 -6
  22. package/dist/hosted/assets/{comment-data-Cn5y2VxV.js → comment-data-BXfMElfx.js} +1998 -381
  23. package/dist/hosted/assets/dist-BCSTeNf4.js +4785 -0
  24. package/dist/hosted/assets/{files-B1P2_24q.js → files-D_1yeAVO.js} +10 -5
  25. package/dist/hosted/assets/{pages-B0ZlVdci.js → pages-CN_SbKWA.js} +3 -3
  26. package/dist/hosted/assets/{project-branches-DlCWmfRo.js → project-branches-Bxp0DCwg.js} +1 -1
  27. package/dist/hosted/assets/{project-thread-links-BZXQ200n.js → project-thread-links-KqQoKBLE.js} +76 -4
  28. package/dist/{node-host/assets/screen-DapOVrga.js → hosted/assets/screen-DpAcDUKR.js} +25 -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 -8
  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-CkMK4hoN.js → node-host/assets/canvas-BeDoufy1.js} +97 -7
  49. package/dist/node-host/assets/{comment-data-yfWEdDQL.js → comment-data-DE2T9yTj.js} +1974 -355
  50. package/dist/node-host/assets/dist-BCSTeNf4.js +4785 -0
  51. package/dist/node-host/assets/{files-anHi-apm.js → files-Dl-PTS09.js} +11 -6
  52. package/dist/node-host/assets/{pages-CW4aF6dK.js → pages-DL2sk6_U.js} +3 -3
  53. package/dist/node-host/assets/{project-branches-D7rVCxQX.js → project-branches-DGp5ELct.js} +1 -1
  54. package/dist/{server/assets/project-thread-links-lLS_rq7c.js → node-host/assets/project-thread-links-CgBV3Jcm.js} +76 -5
  55. package/dist/{hosted/assets/screen-CfdC3NbR.js → node-host/assets/screen-B55yyVfb.js} +25 -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-DxmcFZCh.js → server/assets/canvas-CJWQ-jjv.js} +98 -6
  61. package/dist/server/assets/{comment-data-NLeHG89b.js → comment-data-Rg42ToTW.js} +1660 -26
  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-CRdl489E.js → files-CPJqKeMo.js} +12 -7
  65. package/dist/server/assets/{listener-BAYn9Q-u.js → listener-D61O6PzF.js} +708 -12
  66. package/dist/server/assets/{pages-DXVlJlBC.js → pages-KJY_Ba92.js} +4 -4
  67. package/dist/server/assets/{project-branches-CwTsf6WQ.js → project-branches-qG9ACZvo.js} +2 -2
  68. package/dist/{node-host/assets/project-thread-links-m-OLpsD9.js → server/assets/project-thread-links-CqK0lqCF.js} +77 -4
  69. package/dist/{node-host/assets/tasks-request-B0aeNocI.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-B1zH_1O3.js → screen-CBrHPFug.js} +26 -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-8GXJIuDK.js +0 -1
  98. package/dist/client/assets/difference-worker-C99ychYw.js +0 -8
  99. package/dist/client/assets/files-DkWt1l9H.js +0 -1
  100. package/dist/client/assets/i18n-hH_Ba5QO.js +0 -1
  101. package/dist/client/assets/index-tpHYVXma.css +0 -2
  102. package/dist/client/assets/pages-BVd6MQmu.js +0 -1
  103. package/dist/client/assets/project-comments-D1IIQwsW.js +0 -1
  104. package/dist/client/assets/project-thread-links-DKwe_SYb.js +0 -1
  105. package/dist/client/assets/project-work-D4DZqBCZ.js +0 -1
  106. package/dist/client/assets/tasks-Cn0VOZ9Z.js +0 -1
  107. package/dist/client/assets/tasks-request-BLyOzokZ.js +0 -1
  108. package/dist/client/assets/workspace-DGErsSg6.js +0 -2
  109. package/dist/hosted/assets/tasks-_yKh2Z6q.js +0 -649
  110. package/dist/hosted/assets/tasks-request-CFPLvLc7.js +0 -113
  111. package/dist/node-host/assets/tasks-BBK_CybR.js +0 -651
  112. package/dist/server/assets/project-work-CSZlCwH-.js +0 -88
  113. package/dist/server/assets/tasks-CGdz-uox.js +0 -652
  114. package/dist/server/assets/tasks-request-D2NBjCTl.js +0 -115
  115. package/dist/types/assets/picker-D1Bw3ndx.d.ts +0 -14
@@ -1,8 +1,7 @@
1
- import { A as concat, At as catch_, B as mapError$1, Bn as effectDiscard, C as ProcessRunner, Cn as tap, Ct as as, D as as$1, E as FileSystem, En as timeoutOrElse, Et as cached, F as fromEffect, Fn as currentTimeMillis, Ft as fail, Gn as mergeAll, Gt as map, H as runForEach, Ht as forkScoped, It as filterOrFail, Jn as succeed$1, K as makeUnsafe, Kn as provide, Kt as mapError, Lt as flatMap, M as drop, Mn as void_, Mt as context, N as empty, Nt as die, On as try_, Ot as catchIf, Pt as ensuring, R as make, Rn as effect, Sn as sync, St as andThen, T as Path, Tt as asVoid, Ut as gen, W as unwrap, Wn as merge, Wt as ignore, Xt as orElseSucceed, Yn as succeedContext, Zn as unwrap$1, _n as serviceOption, b as nodeLayer, c as previewsLayer, cr as add, ct as String$1, d as gitLayer, dt as Union, f as Project, fr as getOrUndefined, ft as decodeEffect, gn as scoped, gr as TaggedError, gt as optional, ht as isPattern, i as projectFlows, j as debounce, k as changes, kt as catchTag, lt as Struct, mt as isNonEmpty, nn as retry, ot as ArraySchema, pt as isMaxLength, qn as provideMerge, r as branchPreviewsLayer, s as Previews, sr as Service, st as Record, t as BranchPreviews, tn as result, u as Git, ur as make$1, ut as Undefined, w as RuntimePolicy, xn as suspend, xt as all, yn as succeed, yt as acquireUseRelease, z as mapEffect, zn as effectContext, zt as forEach } from "./branch-previews-ClkoJ0lM.js";
2
- import { A as adapterRegistryLayer, D as publishChanges, E as liveChannelLayer, F as WorkAdapter, I as WorkError, M as memoryAdapterLayer, N as parseThreadImport, O as AdapterRegistry, P as ThreadAdapter, R as bundledScreensLayer, T as LiveChannel, _ as storeActivityLayer, c as FileReview, d as fileSearchLayer, f as Files, g as StoreActivity, i as fileSavesLayer, j as defineAdapter, k as adapterModuleLoaderLayer, l as fileReviewLayer, n as fileReviewActionsLayer, o as fileMutationsLayer, p as filesLayer, s as fileWriteLockLayer, v as storeLiveLayer, z as sourceScreensLayer } from "./server-DxK-UB_Z.js";
3
- import { A as parseAnchor, B as parseThreadStatus, D as MAX_THREADS, Dn as parseMessage, En as messageDeletionRefusals, I as parseThreadInput, L as parseThreadLinkInput, Mn as parseWorkSchema, N as parseThreadDeleteInput, On as parseMessageAuthor, P as parseThreadFilter, Pn as WorkValidationError, R as parseThreadReplyInput, Sn as parseWorkUpdate, Tn as messageDeletionRefusal, V as parseThreadStatusInput, a as CommentMergeError, bn as parseWorkSync, c as parseCommentInput, cn as parseSectionData, d as parseCommentThread, dn as parseWorkConnection, f as revision, fn as parseWorkCreate, gn as parseWorkMutationResult, j as parseThread, kn as withoutMessages, l as conversationId, ln as parseWorkActionInput, mn as parseWorkItem, o as mergeThreads, p as text$2, pn as parseWorkDetail, s as stableJson, u as object$1, un as parseWorkActivity, vn as parseWorkQuery, xn as parseWorkSyncResult, yn as parseWorkSnapshot, z as parseThreadSnapshot } from "./events-iHpLGr3k.js";
4
- import { n as PICKER_MODULE_URL, t as pickerPageScript } from "./bridge-B_Blt55E.js";
5
- import { createHash, randomUUID } from "node:crypto";
1
+ import { $ as unwrap, $n as provide, $t as map, A as ProcessRunner, An as tap, At as andThen, Bt as context, Cn as scoped, Cr as TaggedError, Ct as isPattern, D as nodeLayer, Dt as acquireUseRelease, En as succeed, F as changes, Fn as try_, Gn as effect, Gt as flatMap, Hn as currentTimeMillis, Ht as ensuring, I as concat, It as catchIf, J as mapError$1, K as make, Kn as effectContext, L as debounce, Lt as catchTag, M as Path, N as FileSystem, Nn as timeoutOrElse, Nt as asVoid, On as suspend, P as as$1, Pt as cached, Qn as mergeAll, Qt as ignore, R as drop, Rt as catch_, St as isNonEmpty, Ut as fail, Vt as die, Wt as filterOrFail, X as runForEach, Xt as forkScoped, Yn as isLayer, Zn as merge, Zt as gen, _r as make$1, _t as Struct, bt as decodeEffect, c as previewsLayer, d as pickerPageScript, en as mapError, er as provideMerge, f as PICKER_MODULE_URL, g as Project, gt as String$1, h as gitLayer, hr as add, ht as Record, i as projectFlows, in as orElseSucceed, ir as unwrap$1, j as RuntimePolicy, jt as as, kn as sync, kt as all, ln as result, m as Git, mr as Service, nr as succeedContext, q as mapEffect, qn as effectDiscard, qt as forEach, r as branchPreviewsLayer, s as Previews, t as BranchPreviews, tr as succeed$1, tt as makeUnsafe, vt as Undefined, wn as serviceOption, wt as optional, xt as isMaxLength, y as configPlugins, yr as getOrUndefined, yt as Union, z as empty, zn as void_ } from "./branch-previews-DedrpnVR.js";
2
+ import { A as liveChannelLayer, B as ThreadAdapter, F as AdapterRegistry, G as sourceScreensLayer, H as WorkError, I as AdapterStartupError, L as adapterRegistryLayer, N as attachmentsLayer, P as directoryAttachmentStore, R as defineAdapter, S as storeLiveLayer, V as WorkAdapter, W as bundledScreensLayer, _ as factoryRunsLayer, b as StoreActivity, c as FileReview, d as fileSearchLayer, f as Files, i as fileSavesLayer, j as publishChanges, k as LiveChannel, l as fileReviewLayer, n as fileReviewActionsLayer, o as fileMutationsLayer, p as filesLayer, s as fileWriteLockLayer, x as storeActivityLayer, y as runExecutorLayer, z as parseThreadImport } from "./server-Dkbp5stG.js";
3
+ import { A as parseAnchor, B as parseThreadStatusInput, Cn as workRunSummary, D as MAX_THREADS, Dn as parseMessageAuthor, En as parseMessage, F as parseThreadInput, I as parseThreadLinkInput, L as parseThreadReplyInput, M as parseThreadDeleteInput, N as parseThreadFilter, Nn as WorkValidationError, On as withoutMessages, R as parseThreadSnapshot, Tn as messageDeletionRefusals, _n as parseWorkQuery, bn as parseWorkSyncResult, cn as parseWorkActionInput, dn as parseWorkCreate, fn as parseWorkDetail, hn as parseWorkMutationResult, j as parseThread, jn as parseWorkSchema, ln as parseWorkActivity, pn as parseWorkItem, sn as parseSectionData, un as parseWorkConnection, vn as parseWorkSnapshot, wn as messageDeletionRefusal, xn as parseWorkUpdate, yn as parseWorkSync, z as parseThreadStatus } from "./events-BH_rAu7O.js";
4
+ import { createHash } from "node:crypto";
6
5
  import { join, relative, sep } from "node:path";
7
6
  //#region ../beads/src/lib/contracts.ts
8
7
  var MAX_BEADS = 1e3;
@@ -995,6 +994,99 @@ var Beads = class extends Service()("@bud.sh/beads/Beads") {};
995
994
  */
996
995
  var BeadsStore = class extends Service()("@bud.sh/beads/Store") {};
997
996
  //#endregion
997
+ //#region ../beads/src/lib/custom-statuses.ts
998
+ var categories = [
999
+ "active",
1000
+ "wip",
1001
+ "done",
1002
+ "frozen"
1003
+ ];
1004
+ var statusName = /^[a-z][a-z0-9_]{0,63}$/;
1005
+ /**
1006
+ * Reads Beads' `status.custom` setting, such as `needs_approval:wip,merged:done`.
1007
+ * @param value - The setting; empty or absent for none.
1008
+ * @returns Its statuses in order, without duplicates.
1009
+ */
1010
+ function parseCustomStatuses(value) {
1011
+ let statuses = [];
1012
+ for (let entry of (value ?? "").split(",")) {
1013
+ let [name = "", category = ""] = entry.trim().split(":");
1014
+ if (!name) continue;
1015
+ if (!statusName.test(name)) throw new TypeError(`Invalid custom status ${name}.`);
1016
+ if (category && !categories.includes(category)) throw new TypeError(`Invalid category for custom status ${name}.`);
1017
+ if (statuses.some((status) => status.name === name)) continue;
1018
+ statuses.push({
1019
+ name,
1020
+ category: category || null
1021
+ });
1022
+ }
1023
+ return statuses;
1024
+ }
1025
+ /**
1026
+ * Writes statuses as Beads' `status.custom` setting.
1027
+ * @param statuses - The statuses in order.
1028
+ * @returns The setting.
1029
+ */
1030
+ function formatCustomStatuses(statuses) {
1031
+ return statuses.map((status) => status.category ? `${status.name}:${status.category}` : status.name).join(",");
1032
+ }
1033
+ /**
1034
+ * Adds statuses to the ones a project already has, keeping theirs and their order.
1035
+ * @param current - The configured custom statuses.
1036
+ * @param added - Statuses to add; ones already present, built in or custom, are left as they are.
1037
+ * @param builtIn - Beads' own status names, which can't be configured again.
1038
+ * @returns The combined custom statuses.
1039
+ */
1040
+ function mergeCustomStatuses(current, added, builtIn = []) {
1041
+ let merged = [...current];
1042
+ for (let status of added) {
1043
+ if (!statusName.test(status.name)) throw new TypeError(`Invalid custom status ${status.name}.`);
1044
+ if (builtIn.includes(status.name) || merged.some((entry) => entry.name === status.name)) continue;
1045
+ merged.push(status);
1046
+ }
1047
+ return merged;
1048
+ }
1049
+ var anchors = {
1050
+ active: {
1051
+ name: "open",
1052
+ after: true
1053
+ },
1054
+ wip: {
1055
+ name: "in_progress",
1056
+ after: true
1057
+ },
1058
+ frozen: {
1059
+ name: "deferred",
1060
+ after: true
1061
+ },
1062
+ done: {
1063
+ name: "closed",
1064
+ after: false
1065
+ }
1066
+ };
1067
+ /**
1068
+ * Places custom statuses beside the built-in status of their category, so a board shows a
1069
+ * workflow in order: in-progress ones after `in_progress`, done ones before `closed`.
1070
+ * @param statuses - Built-in statuses followed by custom ones, as Beads lists them.
1071
+ * @returns The same statuses in board order; custom ones keep their order within a category.
1072
+ */
1073
+ function orderStatuses(statuses) {
1074
+ let ordered = statuses.filter((status) => !status.custom);
1075
+ let placed = /* @__PURE__ */ new Map();
1076
+ for (let status of statuses.filter((entry) => entry.custom)) {
1077
+ let anchor = categories.includes(status.category ?? "") ? anchors[status.category] : null;
1078
+ let index = anchor ? ordered.findIndex((entry) => entry.name === anchor.name) : -1;
1079
+ if (!anchor || index === -1) {
1080
+ ordered.push(status);
1081
+ continue;
1082
+ }
1083
+ let offset = placed.get(anchor.name) ?? 0;
1084
+ ordered.splice(anchor.after ? index + 1 + offset : index, 0, status);
1085
+ placed.set(anchor.name, offset + 1);
1086
+ }
1087
+ return ordered;
1088
+ }
1089
+ //#endregion
998
1090
  //#region ../beads/src/server/cli-store.ts
999
1091
  function output(stdout) {
1000
1092
  return try_({
@@ -1105,7 +1197,40 @@ function writer(session) {
1105
1197
  ...(change.removeLabels ?? []).map((label) => `--remove-label=${label}`)
1106
1198
  ]),
1107
1199
  setMetadata: (id, json) => asVoid(session.metadata(id, json)),
1108
- importJsonl: (jsonl) => write(["import", "-"], jsonl)
1200
+ importJsonl: (jsonl) => write(["import", "-"], jsonl),
1201
+ addStatuses: (statuses) => gen(function* () {
1202
+ let { statuses: known } = yield* session.catalog;
1203
+ let builtIn = known.filter((status) => !status.custom).map((status) => status.name);
1204
+ let setting = yield* session.read([
1205
+ "config",
1206
+ "get",
1207
+ "status.custom"
1208
+ ], (value) => {
1209
+ let raw = value && typeof value === "object" && "value" in value ? value.value : "";
1210
+ return typeof raw === "string" ? raw : "";
1211
+ });
1212
+ let merged = yield* try_({
1213
+ try: () => {
1214
+ let current = parseCustomStatuses(setting);
1215
+ return {
1216
+ current,
1217
+ next: mergeCustomStatuses(current, statuses, builtIn)
1218
+ };
1219
+ },
1220
+ catch: (cause) => new BeadsError({
1221
+ reason: "input",
1222
+ message: "Invalid custom status.",
1223
+ cause
1224
+ })
1225
+ });
1226
+ if (merged.next.length === merged.current.length) return;
1227
+ yield* write([
1228
+ "config",
1229
+ "set",
1230
+ "status.custom",
1231
+ formatCustomStatuses(merged.next)
1232
+ ]);
1233
+ })
1109
1234
  };
1110
1235
  }
1111
1236
  /**
@@ -1137,7 +1262,7 @@ var beadsWorkspaceStoreLayer = effect(BeadsStore, gen(function* () {
1137
1262
  //#endregion
1138
1263
  //#region ../beads/src/server/service.ts
1139
1264
  var nonEmpty = String$1.check(isNonEmpty(), isMaxLength(32768), isPattern(/^[^\0]*$/));
1140
- var optionsSchema$1 = Struct({
1265
+ var optionsSchema = Struct({
1141
1266
  executable: optional(nonEmpty),
1142
1267
  environment: optional(Record(String$1, Union([String$1, Undefined])))
1143
1268
  });
@@ -1215,7 +1340,7 @@ function unsupportedCommand(error) {
1215
1340
  */
1216
1341
  function beadsLayer(options = {}) {
1217
1342
  return effectContext(gen(function* () {
1218
- let settings = yield* decodeEffect(optionsSchema$1, { onExcessProperty: "error" })(options).pipe(mapError((cause) => new BeadsError({
1343
+ let settings = yield* decodeEffect(optionsSchema, { onExcessProperty: "error" })(options).pipe(mapError((cause) => new BeadsError({
1219
1344
  reason: "input",
1220
1345
  message: "Invalid Beads adapter options.",
1221
1346
  cause
@@ -2084,7 +2209,7 @@ function beadsWorkSchema(snapshot, additional = []) {
2084
2209
  label: label$2(value)
2085
2210
  }))
2086
2211
  } : field),
2087
- statuses: [...statuses.values()].map((status) => ({
2212
+ statuses: orderStatuses([...statuses.values()]).map((status) => ({
2088
2213
  value: status.name,
2089
2214
  label: label$2(status.name),
2090
2215
  category: category(status)
@@ -2217,7 +2342,7 @@ function text(value) {
2217
2342
  function field(value, key) {
2218
2343
  return value && typeof value === "object" && !Array.isArray(value) ? value[key] : value;
2219
2344
  }
2220
- function changed$1(key, before, after) {
2345
+ function changed(key, before, after) {
2221
2346
  let name = fields[key] ?? label$1(key).toLowerCase();
2222
2347
  if (key === "labels") {
2223
2348
  let labels = (value) => Array.isArray(value) ? value.map(text).filter(Boolean) : text(value).split(",").map((v) => v.trim()).filter(Boolean);
@@ -2288,11 +2413,11 @@ function beadActivitySummary(event) {
2288
2413
  let value = field(after, key);
2289
2414
  return after && typeof after === "object" && !Array.isArray(after) && value !== void 0 && JSON.stringify(field(before, key)) !== JSON.stringify(value);
2290
2415
  });
2291
- return keys.length ? keys.map((key) => changed$1(key, field(before, key), field(after, key))).join("; ") : "updated this task";
2416
+ return keys.length ? keys.map((key) => changed(key, field(before, key), field(after, key))).join("; ") : "updated this task";
2292
2417
  }
2293
2418
  default: {
2294
2419
  let key = event.type.replace(/_changed$/, "");
2295
- if (Object.hasOwn(fields, key)) return changed$1(key, field(before, key), field(after, key));
2420
+ if (Object.hasOwn(fields, key)) return changed(key, field(before, key), field(after, key));
2296
2421
  return `recorded ${label$1(event.type).toLowerCase()}`;
2297
2422
  }
2298
2423
  }
@@ -2324,31 +2449,37 @@ function beadsWorkConnection(connection, schema) {
2324
2449
  message: connection.message
2325
2450
  });
2326
2451
  }
2327
- var discussionKey = "budDiscussion";
2452
+ var budKeys = [
2453
+ "budDiscussion",
2454
+ "budRun",
2455
+ "budContext"
2456
+ ];
2457
+ function runSummary(metadata) {
2458
+ let run = metadata && typeof metadata === "object" && !Array.isArray(metadata) ? workRunSummary(metadata.budRun) : void 0;
2459
+ return run ? { run } : {};
2460
+ }
2328
2461
  /**
2329
- * Removes Bud's native discussion record from task metadata.
2462
+ * Removes Bud's own records from task metadata.
2330
2463
  * @param metadata - The issue's stored metadata.
2331
2464
  * @returns The metadata a person sees and edits, or null when nothing else is stored.
2332
2465
  */
2333
2466
  function taskMetadata(metadata) {
2334
2467
  if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) return metadata;
2335
- if (!(discussionKey in metadata)) return metadata;
2336
- let { [discussionKey]: _discussion, ...rest } = metadata;
2468
+ if (!budKeys.some((key) => key in metadata)) return metadata;
2469
+ let rest = Object.fromEntries(Object.entries(metadata).filter(([key]) => !budKeys.includes(key)));
2337
2470
  return Object.keys(rest).length ? rest : null;
2338
2471
  }
2339
2472
  /**
2340
- * Applies edited task metadata while keeping the stored discussion record.
2341
- * @param edited - The metadata from a task edit, which may not set the discussion key.
2473
+ * Applies edited task metadata while keeping Bud's stored records.
2474
+ * @param edited - The metadata from a task edit, which may not set Bud's keys.
2342
2475
  * @param stored - The issue's current metadata.
2343
2476
  * @returns The metadata to write.
2344
2477
  */
2345
2478
  function withDiscussionMetadata(edited, stored) {
2346
- let { [discussionKey]: _ignored, ...rest } = edited;
2347
- let kept = stored !== null && typeof stored === "object" && !Array.isArray(stored) ? stored[discussionKey] : void 0;
2348
- return kept === void 0 ? rest : {
2349
- ...rest,
2350
- [discussionKey]: kept
2351
- };
2479
+ let rest = Object.fromEntries(Object.entries(edited).filter(([key]) => !budKeys.includes(key)));
2480
+ if (stored === null || typeof stored !== "object" || Array.isArray(stored)) return rest;
2481
+ for (let key of budKeys) if (stored[key] !== void 0) rest[key] = stored[key];
2482
+ return rest;
2352
2483
  }
2353
2484
  /**
2354
2485
  * Retains every native field, directed relationship, brief count and opaque revision.
@@ -2414,6 +2545,7 @@ function beadsWorkItem(issue, schema, ready) {
2414
2545
  counts,
2415
2546
  ...ready === void 0 ? {} : { ready },
2416
2547
  ...issue.epicCloseable === null ? {} : { closeable: issue.epicCloseable },
2548
+ ...runSummary(issue.metadata),
2417
2549
  contentLoaded: issue.contentLoaded,
2418
2550
  extra: taskMetadata(issue.metadata)
2419
2551
  }, schema);
@@ -2859,10 +2991,48 @@ var beadsStoreWorkAdapterLayer = effect(WorkAdapter, gen(function* () {
2859
2991
  }));
2860
2992
  }).pipe(mapError(beadsWorkError));
2861
2993
  }
2994
+ let beadCategories = {
2995
+ backlog: "active",
2996
+ unstarted: "active",
2997
+ started: "wip",
2998
+ blocked: "wip",
2999
+ deferred: "frozen",
3000
+ done: "done",
3001
+ cancelled: "done"
3002
+ };
3003
+ function addStatuses(root, statuses) {
3004
+ return store.change(root, (session) => session.addStatuses(statuses.map((status) => ({
3005
+ name: status.value,
3006
+ category: beadCategories[status.category]
3007
+ })))).pipe(mapError(beadsWorkError));
3008
+ }
3009
+ function budRecord(metadata, key) {
3010
+ return metadata !== null && typeof metadata === "object" && !Array.isArray(metadata) ? metadata[key] : void 0;
3011
+ }
3012
+ function readItemData(root, id, key) {
3013
+ return store.read(root, (session) => map(session.issue(id), (issue) => ({
3014
+ value: budRecord(issue.metadata, key) ?? null,
3015
+ revision: beadsWorkRevision(issue)
3016
+ }))).pipe(mapError(beadsWorkError));
3017
+ }
3018
+ function writeItemData(root, id, key, value, expectedRevision) {
3019
+ return store.change(root, (session) => gen(function* () {
3020
+ let current = yield* session.issue(id);
3021
+ yield* revision(current, expectedRevision);
3022
+ let stored = current.metadata;
3023
+ let metadata = stored !== null && typeof stored === "object" && !Array.isArray(stored) ? { ...stored } : {};
3024
+ metadata[key] = value;
3025
+ yield* session.setMetadata(id, JSON.stringify(metadata));
3026
+ return { revision: beadsWorkRevision(yield* session.issue(id)) };
3027
+ })).pipe(mapError(beadsWorkError));
3028
+ }
2862
3029
  return {
2863
3030
  read,
2864
3031
  describe,
2865
3032
  schema,
3033
+ readItemData,
3034
+ writeItemData,
3035
+ addStatuses,
2866
3036
  list,
2867
3037
  get,
2868
3038
  section: loadSection,
@@ -3025,7 +3195,7 @@ function canonical(value) {
3025
3195
  * @param count - The native message count.
3026
3196
  * @returns An opaque content revision shared by list and detail.
3027
3197
  */
3028
- function threadRevision$1(row, count) {
3198
+ function threadRevision(row, count) {
3029
3199
  let { issue, record } = row;
3030
3200
  return createHash("sha256").update(JSON.stringify(canonical({
3031
3201
  id: issue.id,
@@ -3154,7 +3324,7 @@ function threadView(row, comments, loaded) {
3154
3324
  issue.updatedAt,
3155
3325
  ...all.map((message) => message.createdAt)
3156
3326
  ].reduce((latest, at) => Date.parse(at) > Date.parse(latest) ? at : latest),
3157
- revision: threadRevision$1(row, count),
3327
+ revision: threadRevision(row, count),
3158
3328
  metadata: {
3159
3329
  creationId: record.creationId,
3160
3330
  ...record.source === void 0 ? {} : {
@@ -3303,7 +3473,8 @@ function threadChange(session, operation, identity) {
3303
3473
  reopen: (id, reason) => tracked(session.reopen(id, reason)),
3304
3474
  update: (id, change) => tracked(session.update(id, change)),
3305
3475
  setMetadata: (id, json) => tracked(session.setMetadata(id, json)),
3306
- importJsonl: (jsonl) => tracked(session.importJsonl(jsonl))
3476
+ importJsonl: (jsonl) => tracked(session.importJsonl(jsonl)),
3477
+ addStatuses: (statuses) => tracked(session.addStatuses(statuses))
3307
3478
  }).pipe(mapError((cause) => confirmed ? new WorkError({
3308
3479
  reason: "uncertain",
3309
3480
  message: `The discussion may have changed. Refresh and inspect Beads before trying again.${identity ? ` Creation identity: ${identity}.` : ""}`,
@@ -3772,7 +3943,7 @@ function changeItemDiscussion(session, id, expectedRevision, change) {
3772
3943
  }
3773
3944
  //#endregion
3774
3945
  //#region ../beads/src/server/thread-import.ts
3775
- function identity$1(parts) {
3946
+ function identity(parts) {
3776
3947
  return createHash("sha256").update(JSON.stringify(parts)).digest("hex").slice(0, 32);
3777
3948
  }
3778
3949
  function same(left, right) {
@@ -3815,7 +3986,7 @@ function importThread(session, value) {
3815
3986
  });
3816
3987
  yield* validateLinks(session, input.thread.itemIds);
3817
3988
  let { thread, source, sourceId } = input;
3818
- let creationId = identity$1([source, sourceId]);
3989
+ let creationId = identity([source, sourceId]);
3819
3990
  let id = `${session.connection.project.prefix ?? "bud"}-thread-${creationId}`;
3820
3991
  if ((yield* session.show(id, (value) => value).pipe(catchIf((cause) => cause.reason === "not-found", () => succeed(null)))) !== null) return yield* new WorkError({
3821
3992
  reason: "conflict",
@@ -3833,7 +4004,7 @@ function importThread(session, value) {
3833
4004
  createdAt: thread.createdAt,
3834
4005
  updatedAt: thread.updatedAt,
3835
4006
  bindings: thread.messages.map((message) => ({
3836
- nativeId: identity$1([
4007
+ nativeId: identity([
3837
4008
  source,
3838
4009
  sourceId,
3839
4010
  message.id
@@ -3908,7 +4079,7 @@ var adapter = {
3908
4079
  label: "Beads",
3909
4080
  version: "1"
3910
4081
  };
3911
- var capabilities$1 = {
4082
+ var capabilities = {
3912
4083
  create: true,
3913
4084
  reply: true,
3914
4085
  status: true,
@@ -3941,7 +4112,7 @@ function listThreads(session, filter) {
3941
4112
  state: "ready",
3942
4113
  adapter
3943
4114
  },
3944
- capabilities: capabilities$1,
4115
+ capabilities,
3945
4116
  threads: threads.slice(0, MAX_THREADS),
3946
4117
  truncated: rows.length > 1e3 || items.truncated || threads.length > 1e3
3947
4118
  });
@@ -4200,957 +4371,39 @@ var beadsStoreThreadAdapterLayer = effect(ThreadAdapter, gen(function* () {
4200
4371
  }));
4201
4372
  provide(beadsStoreThreadAdapterLayer, beadsWorkspaceStoreLayer);
4202
4373
  //#endregion
4203
- //#region ../comments/src/server/records.ts
4204
- /**
4205
- * Validates a persisted thread record of any version.
4206
- * @param value - Untrusted data.
4207
- * @returns The record.
4208
- */
4209
- function parseStoredThread(value) {
4210
- let input = object$1(value);
4211
- if (input.version !== 3) return parseCommentThread(value);
4212
- for (let key of Object.keys(input)) if (![
4213
- "version",
4214
- "id",
4215
- "project",
4216
- "branch",
4217
- "createdCommit",
4218
- "anchor",
4219
- "itemIds",
4220
- "comments",
4221
- "status",
4222
- "createdAt",
4223
- "updatedAt",
4224
- "metadata"
4225
- ].includes(key)) throw new Error(`Unsupported discussion field ${key}.`);
4226
- let project = text$2(input.project, "Project", 4096);
4227
- if (project !== "." && (project.startsWith("/") || project.includes("\\") || project.split("/").some((part) => !part || part === ".." || part === "."))) throw new Error("Invalid discussion project path.");
4228
- let createdCommit = input.createdCommit === "" ? "" : revision(input.createdCommit);
4229
- if (createdCommit === null) throw new Error("Invalid source commit.");
4230
- if (!Array.isArray(input.comments)) throw new Error("A discussion needs messages.");
4231
- let comments = input.comments.map(parseMessage);
4232
- let parents = new Map(comments.map((message) => [message.id, message.parentId]));
4233
- if (parents.size !== comments.length) throw new Error("Duplicate discussion message ID.");
4234
- for (let message of comments) {
4235
- let parent = message.parentId;
4236
- let seen = /* @__PURE__ */ new Set([message.id]);
4237
- while (parent !== null) {
4238
- if (seen.has(parent) || !parents.has(parent)) throw new Error("Invalid discussion reply ancestry.");
4239
- seen.add(parent);
4240
- parent = parents.get(parent);
4241
- }
4242
- }
4243
- let thread = parseThread({
4244
- id: conversationId(input.id),
4245
- status: input.status,
4246
- anchor: input.anchor,
4247
- itemIds: input.itemIds,
4248
- messages: comments.slice(0, 200),
4249
- messageCount: comments.length,
4250
- messagesLoaded: true,
4251
- messagesTruncated: comments.length > 200,
4252
- createdAt: input.createdAt,
4253
- updatedAt: input.updatedAt,
4254
- revision: null,
4255
- ...input.metadata === void 0 ? {} : { metadata: input.metadata }
4256
- });
4257
- return {
4258
- version: 3,
4259
- id: thread.id,
4260
- project,
4261
- branch: text$2(input.branch, "Source branch", 1024),
4262
- createdCommit,
4263
- anchor: thread.anchor,
4264
- itemIds: thread.itemIds,
4265
- comments,
4266
- status: thread.status,
4267
- createdAt: thread.createdAt,
4268
- updatedAt: thread.updatedAt,
4269
- ...thread.metadata === void 0 ? {} : { metadata: thread.metadata }
4270
- };
4271
- }
4272
- /**
4273
- * Tracks this record's content, independent of writes to other threads.
4274
- * @param record - The validated persisted discussion.
4275
- * @returns An opaque revision for compare-and-swap updates.
4276
- */
4277
- function threadRevision(record) {
4278
- return createHash("sha256").update(stableJson(record)).digest("hex");
4279
- }
4280
- /**
4281
- * Turns a persisted record into the `@bud.sh/work` thread the app shows.
4282
- * @param record - The validated persisted record.
4283
- * @param loaded - Whether to include its messages, up to the limit, or only the first.
4284
- * @returns The thread.
4285
- */
4286
- function projectThread(record, loaded = true) {
4287
- let thread = {
4288
- id: record.id,
4289
- status: record.status,
4290
- anchor: record.anchor,
4291
- itemIds: record.version === 3 ? record.itemIds : [],
4292
- messages: record.comments.slice(0, loaded ? 200 : 1),
4293
- messageCount: record.comments.length,
4294
- messagesLoaded: loaded,
4295
- messagesTruncated: loaded && record.comments.length > 200,
4296
- createdAt: record.createdAt,
4297
- updatedAt: record.updatedAt,
4298
- revision: threadRevision(record),
4299
- ...record.version === 3 ? record.metadata === void 0 ? {} : { metadata: record.metadata } : { metadata: { git: {
4300
- project: record.project,
4301
- branch: record.branch,
4302
- createdCommit: record.createdCommit
4303
- } } }
4304
- };
4305
- return parseThread(thread);
4306
- }
4307
- /**
4308
- * Promotes a legacy record without changing its capture or messages.
4309
- * @param record - The current stored record.
4310
- * @returns Its version 3 representation; persistence removes the old path in the same commit.
4311
- */
4312
- function promoteThread(record) {
4313
- if (record.version === 3) return record;
4314
- return {
4315
- ...record,
4316
- version: 3,
4317
- itemIds: [],
4318
- metadata: projectThread(record).metadata
4319
- };
4320
- }
4321
- //#endregion
4322
- //#region ../comments/src/server/merge.ts
4323
- function changed(local, remote, base, field, hasBase = base !== void 0) {
4324
- if (stableJson(local) === stableJson(remote)) return local;
4325
- if (hasBase && stableJson(local) === stableJson(base)) return remote;
4326
- if (hasBase && stableJson(remote) === stableJson(base)) return local;
4327
- throw new CommentMergeError(`Conflicting discussion ${field}. Resolve the record in Git before syncing.`);
4328
- }
4329
- /**
4330
- * Merges new records and mixed-version history while preserving captures.
4331
- * @param local - The locally stored record.
4332
- * @param remote - The validated incoming record.
4333
- * @param base - The common ancestor, when one exists.
4334
- * @returns The merged record, or a conflict when both sides changed the same value.
4335
- */
4336
- function mergeStoredThreads(local, remote, base) {
4337
- if (local.version !== 3 && remote.version !== 3 && base?.version !== 3) return mergeThreads(local, remote, base);
4338
- let left = promoteThread(local);
4339
- let right = promoteThread(remote);
4340
- let ancestor = base && promoteThread(base);
4341
- function identity(record) {
4342
- let { comments, status, updatedAt, itemIds, metadata, ...identity } = record;
4343
- return identity;
4344
- }
4345
- 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.");
4346
- let messages = /* @__PURE__ */ new Map();
4347
- for (let message of [...left.comments, ...right.comments]) {
4348
- let existing = messages.get(message.id);
4349
- if (existing && stableJson(existing) !== stableJson(message)) throw new CommentMergeError("Conflicting versions of a message. Resolve the record in Git before syncing.");
4350
- messages.set(message.id, message);
4351
- }
4352
- let itemIds = [.../* @__PURE__ */ new Set([
4353
- ...left.itemIds,
4354
- ...right.itemIds,
4355
- ...ancestor?.itemIds ?? []
4356
- ])].filter((id) => changed(left.itemIds.includes(id), right.itemIds.includes(id), ancestor?.itemIds.includes(id), "links")).sort();
4357
- let metadata = changed(left.metadata, right.metadata, ancestor?.metadata, "metadata", ancestor !== void 0);
4358
- return parseStoredThread({
4359
- ...left,
4360
- status: changed(left.status, right.status, ancestor?.status, "status"),
4361
- itemIds,
4362
- updatedAt: left.updatedAt > right.updatedAt ? left.updatedAt : right.updatedAt,
4363
- comments: [...messages.values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)),
4364
- metadata
4365
- });
4366
- }
4367
- //#endregion
4368
- //#region ../comments/src/server/errors.ts
4369
- /** Safe domain messages; nested diagnostics stay on the trusted server. */
4370
- var CommentsError = class extends TaggedError("CommentsError") {};
4371
- //#endregion
4372
- //#region ../comments/src/server/persistence.ts
4373
- var commentsRef = "refs/heads/bud/comments";
4374
- var threadPath = /^comments\/v(\d+)\/threads\/([a-f0-9-]{36})\/thread\.json$/;
4375
- var optionsSchema = Struct({
4376
- root: String$1.check(isNonEmpty(), isPattern(/^[^\0]*$/)),
4377
- remote: String$1.check(isPattern(/^[A-Za-z0-9][A-Za-z0-9._-]*$/)),
4378
- routes: ArraySchema(Struct({
4379
- id: String$1.check(isNonEmpty()),
4380
- path: String$1.check(isNonEmpty())
4381
- }))
4382
- });
4383
- function fromGit(cause) {
4384
- if (cause._tag === "GitIdentityError") return new CommentsError({
4385
- reason: "identity",
4386
- message: "Set git config user.name and user.email in this project before commenting.",
4387
- cause
4388
- });
4389
- if (cause._tag === "GitRepositoryError") return new CommentsError({
4390
- reason: "repository",
4391
- message: "Initialize a Git repository for this project to save comments.",
4392
- cause
4393
- });
4394
- if (cause._tag === "GitConflictError") return new CommentsError({
4395
- reason: "conflict",
4396
- message: "Comments changed elsewhere. Reload the conversation and try again.",
4397
- cause
4398
- });
4399
- return new CommentsError({
4400
- reason: "storage",
4401
- message: "The Git operation failed. Your existing comments remain saved. Check the repository and Git configuration.",
4402
- cause
4403
- });
4404
- }
4405
- function attempt$1(tryValue, reason = "input", message) {
4406
- return try_({
4407
- try: tryValue,
4408
- catch: (cause) => new CommentsError({
4409
- reason,
4410
- message: message ?? (cause instanceof Error ? cause.message : "Invalid comment."),
4411
- cause
4412
- })
4413
- });
4414
- }
4415
- function failure(reason, message) {
4416
- return (cause) => new CommentsError({
4417
- reason,
4418
- message,
4419
- cause
4420
- });
4421
- }
4422
- /**
4423
- * Captures shared persistence for legacy comments and the thread adapter.
4424
- * @param options - The trusted project, remote and configured routes.
4425
- * @returns Scoped operations; repository access starts only when an operation runs.
4426
- */
4427
- function createCommentStorage(options) {
4428
- return gen(function* () {
4429
- let input = yield* decodeEffect(optionsSchema, { onExcessProperty: "error" })(options).pipe(mapError(failure("input", "Invalid comment store configuration.")));
4430
- let git = yield* Git;
4431
- let fs = yield* FileSystem;
4432
- let paths = yield* Path;
4433
- function location() {
4434
- return gen(function* () {
4435
- let repo = yield* git.discover(input.root).pipe(mapError(fromGit));
4436
- if (repo.info.bare) return yield* new CommentsError({
4437
- reason: "repository",
4438
- message: "Open a source worktree before editing comments."
4439
- });
4440
- if ((yield* repo.symbolicRef.pipe(mapError(fromGit))) === "refs/heads/bud/comments") return yield* new CommentsError({
4441
- reason: "repository",
4442
- message: "Switch to your source branch before editing comments."
4443
- });
4444
- let root = yield* fs.realPath(input.root).pipe(mapError(failure("repository", "Unable to resolve the project directory.")));
4445
- let projectId = paths.relative(repo.info.root, root).replaceAll("\\", "/") || ".";
4446
- if (projectId === ".." || projectId.startsWith("../") || paths.isAbsolute(projectId)) return yield* new CommentsError({
4447
- reason: "repository",
4448
- message: "The project must be inside its Git repository."
4449
- });
4450
- return {
4451
- repo,
4452
- projectId
4453
- };
4454
- });
4455
- }
4456
- function headAt(repo, ref = commentsRef) {
4457
- return repo.resolve(ref).pipe(mapError(fromGit));
4458
- }
4459
- function author(repo) {
4460
- return repo.identity.pipe(mapError(fromGit), map(({ name, email }) => ({
4461
- name,
4462
- email,
4463
- id: createHash("sha256").update(`${name}\0${email}`).digest("hex").slice(0, 16)
4464
- })));
4465
- }
4466
- function source(repo) {
4467
- return gen(function* () {
4468
- return {
4469
- branch: (yield* repo.symbolicRef.pipe(mapError(fromGit)))?.replace(/^refs\/heads\//, "") ?? "detached",
4470
- createdCommit: (yield* headAt(repo, "HEAD")) ?? ""
4471
- };
4472
- });
4473
- }
4474
- function recordsAt(repo, head) {
4475
- return gen(function* () {
4476
- let records = /* @__PURE__ */ new Map();
4477
- let blobs = /* @__PURE__ */ new Map();
4478
- if (!head) return {
4479
- threads: records,
4480
- blobs
4481
- };
4482
- let entries = yield* repo.listTree(head, "comments").pipe(mapError(fromGit));
4483
- for (let entry of entries) {
4484
- let match = threadPath.exec(entry.path);
4485
- if (!match) continue;
4486
- if (entry.type !== "blob") return yield* new CommentsError({
4487
- reason: "conflict",
4488
- message: "The comments branch contains an unsupported or invalid thread."
4489
- });
4490
- let bytes = yield* repo.readBlob(entry.oid).pipe(mapError(fromGit));
4491
- let value = yield* attempt$1(() => JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)), "conflict", "The comments branch contains invalid JSON. Repair that record before syncing.");
4492
- let thread = yield* attempt$1(() => {
4493
- let thread = parseStoredThread(value);
4494
- if (thread.id !== match[2] || String(thread.version) !== match[1]) throw new Error("Mismatched thread ID or storage version.");
4495
- if (records.has(thread.id)) throw new Error("Duplicate thread across versions.");
4496
- return thread;
4497
- }, "conflict", "The comments branch contains an unsupported or invalid thread.");
4498
- records.set(thread.id, thread);
4499
- blobs.set(thread.id, bytes);
4500
- }
4501
- return {
4502
- threads: records,
4503
- blobs
4504
- };
4505
- });
4506
- }
4507
- function snapshot(repo, projectId) {
4508
- return gen(function* () {
4509
- let head = yield* headAt(repo);
4510
- return {
4511
- head,
4512
- 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))
4513
- };
4514
- });
4515
- }
4516
- function commitRecords(repo, parent, records, message, previous) {
4517
- return gen(function* () {
4518
- let files = yield* attempt$1(() => records.flatMap((record) => {
4519
- let validated = parseStoredThread(record);
4520
- let contents = stableJson(validated);
4521
- let existing = previous.records.threads.get(validated.id);
4522
- if (existing && stableJson(existing) === contents) return [];
4523
- let remote = previous.remote?.threads.get(validated.id);
4524
- let original = remote && stableJson(remote) === contents ? previous.remote?.blobs.get(validated.id) : void 0;
4525
- return [...existing && existing.version !== validated.version ? [{
4526
- path: `comments/v${existing.version}/threads/${existing.id}/thread.json`,
4527
- contents: null
4528
- }] : [], {
4529
- path: `comments/v${validated.version}/threads/${validated.id}/thread.json`,
4530
- contents: original ?? `${contents}\n`
4531
- }];
4532
- }));
4533
- return yield* repo.commitSnapshot({
4534
- ref: commentsRef,
4535
- expectedHead: parent,
4536
- files,
4537
- message,
4538
- extraParents: previous.extraParent && previous.extraParent !== parent ? [previous.extraParent] : void 0
4539
- }).pipe(mapError(fromGit));
4540
- });
4541
- }
4542
- function list() {
4543
- return gen(function* () {
4544
- let { repo, projectId } = yield* location();
4545
- return yield* snapshot(repo, projectId);
4546
- });
4547
- }
4548
- function write(expectedHead, value) {
4549
- return gen(function* () {
4550
- let command = yield* attempt$1(() => {
4551
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid comment change.");
4552
- return parseCommentInput({
4553
- ...value,
4554
- head: expectedHead
4555
- }, input.routes, value.id);
4556
- });
4557
- let { repo, projectId } = yield* location();
4558
- let head = yield* headAt(repo);
4559
- if (head !== command.head) return yield* new CommentsError({
4560
- reason: "conflict",
4561
- message: "Comments changed elsewhere. Reload the conversation and try again."
4562
- });
4563
- let records = yield* recordsAt(repo, head);
4564
- let change = command.change;
4565
- let identity = yield* author(repo);
4566
- let now = new Date(yield* currentTimeMillis).toISOString();
4567
- let comment = change.body ? {
4568
- id: randomUUID(),
4569
- body: change.body,
4570
- author: identity,
4571
- createdAt: now,
4572
- parentId: change.parentId ?? null
4573
- } : void 0;
4574
- let thread;
4575
- if (change.id) {
4576
- let previous = records.threads.get(change.id);
4577
- if (!previous || previous.project !== projectId || previous.version === 3) return yield* new CommentsError({
4578
- reason: "not-found",
4579
- message: "This conversation no longer exists."
4580
- });
4581
- if (change.parentId && !previous.comments.some((comment) => comment.id === change.parentId)) return yield* new CommentsError({
4582
- reason: "input",
4583
- message: "Reply to a comment in this conversation."
4584
- });
4585
- thread = {
4586
- ...previous,
4587
- comments: comment ? [...previous.comments, comment] : previous.comments,
4588
- status: change.status ?? previous.status,
4589
- updatedAt: now
4590
- };
4591
- } else {
4592
- let { branch, createdCommit } = yield* source(repo);
4593
- thread = yield* attempt$1(() => parseCommentThread({
4594
- version: "type" in change.anchor && change.anchor.type === "code" ? 2 : 1,
4595
- id: randomUUID(),
4596
- project: projectId,
4597
- branch,
4598
- createdCommit,
4599
- anchor: change.anchor,
4600
- comments: [comment],
4601
- status: "open",
4602
- createdAt: now,
4603
- updatedAt: now
4604
- }));
4605
- }
4606
- yield* commitRecords(repo, head, [thread], change.id ? "bud: update conversation" : "bud: start conversation", { records });
4607
- return yield* snapshot(repo, projectId);
4608
- });
4609
- }
4610
- function sync(direction = "both") {
4611
- return gen(function* () {
4612
- let { repo, projectId } = yield* location();
4613
- let remote = input.remote;
4614
- if (!(yield* repo.remoteUrl(remote).pipe(mapError(fromGit)))) return yield* new CommentsError({
4615
- reason: "remote-missing",
4616
- message: `Add the Git remote “${remote}” before syncing comments.`
4617
- });
4618
- 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.")));
4619
- let localHead = yield* headAt(repo);
4620
- let localRecords = yield* recordsAt(repo, localHead);
4621
- if (remoteExists && direction !== "push") {
4622
- 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.")));
4623
- if (!remoteHead) return yield* new CommentsError({
4624
- reason: "remote-fetch",
4625
- message: "Remote comments changed during fetch. Try syncing again."
4626
- });
4627
- let remoteRecords = yield* recordsAt(repo, remoteHead);
4628
- if (localHead !== remoteHead) {
4629
- if (!localHead) yield* repo.compareAndSwapRef(commentsRef, remoteHead, null).pipe(mapError(fromGit));
4630
- else {
4631
- let base = yield* repo.mergeBase(localHead, remoteHead).pipe(mapError(fromGit));
4632
- if (base === localHead) {
4633
- yield* attempt$1(() => {
4634
- for (let [id, local] of localRecords.threads) {
4635
- let remote = remoteRecords.threads.get(id);
4636
- if (remote) mergeStoredThreads(local, remote, local);
4637
- }
4638
- }, "conflict");
4639
- yield* repo.compareAndSwapRef(commentsRef, remoteHead, localHead).pipe(mapError(fromGit));
4640
- } else if (base !== remoteHead) {
4641
- let baseRecords = yield* recordsAt(repo, base);
4642
- yield* commitRecords(repo, localHead, yield* attempt$1(() => [.../* @__PURE__ */ new Set([...localRecords.threads.keys(), ...remoteRecords.threads.keys()])].map((id) => {
4643
- let local = localRecords.threads.get(id);
4644
- let remote = remoteRecords.threads.get(id);
4645
- return local && remote ? mergeStoredThreads(local, remote, baseRecords.threads.get(id)) : local ?? remote;
4646
- }), "conflict"), "bud: sync conversations", {
4647
- records: localRecords,
4648
- remote: remoteRecords,
4649
- extraParent: remoteHead
4650
- });
4651
- }
4652
- }
4653
- }
4654
- }
4655
- let publishHead = yield* headAt(repo);
4656
- if (publishHead && direction !== "pull") {
4657
- yield* recordsAt(repo, publishHead);
4658
- 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.")));
4659
- }
4660
- return yield* snapshot(repo, projectId);
4661
- });
4662
- }
4663
- function seed(seeds) {
4664
- return gen(function* () {
4665
- yield* attempt$1(() => {
4666
- if (!Array.isArray(seeds) || seeds.length > 1e3) throw new Error("Invalid seed conversations.");
4667
- if (new Set(seeds.map((seed) => seed.id)).size !== seeds.length) throw new Error("Duplicate seed conversation ID.");
4668
- });
4669
- let { repo, projectId } = yield* location();
4670
- let head = yield* headAt(repo);
4671
- let records = yield* recordsAt(repo, head);
4672
- let missing = yield* attempt$1(() => seeds.filter((seed) => {
4673
- let existing = records.threads.get(seed.id);
4674
- if (existing && existing.project !== projectId) throw new Error("That seed conversation belongs to another project.");
4675
- return !existing;
4676
- }), "conflict");
4677
- if (!missing.length) return yield* snapshot(repo, projectId);
4678
- let identity = yield* author(repo);
4679
- let now = new Date(yield* currentTimeMillis).toISOString();
4680
- let { branch, createdCommit } = yield* source(repo);
4681
- yield* commitRecords(repo, head, yield* attempt$1(() => missing.map((seed) => {
4682
- 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.");
4683
- return parseCommentThread({
4684
- version: 1,
4685
- id: seed.id,
4686
- project: projectId,
4687
- branch,
4688
- createdCommit,
4689
- anchor: seed.anchor,
4690
- comments: seed.messages.map((body) => ({
4691
- id: randomUUID(),
4692
- body,
4693
- author: identity,
4694
- createdAt: now,
4695
- parentId: null
4696
- })),
4697
- status: "open",
4698
- createdAt: now,
4699
- updatedAt: now
4700
- });
4701
- })), "bud: seed development conversations", { records });
4702
- return yield* snapshot(repo, projectId);
4703
- });
4704
- }
4705
- return {
4706
- list: list(),
4707
- write,
4708
- sync,
4709
- seed,
4710
- root: input.root,
4711
- paths,
4712
- location,
4713
- source,
4714
- author,
4715
- headAt,
4716
- recordsAt,
4717
- snapshot,
4718
- commitRecords
4719
- };
4720
- });
4721
- }
4722
- Service()("@bud.sh/comments/Comments");
4723
- //#endregion
4724
- //#region ../comments/src/server/adapter.ts
4725
- var capabilities = {
4726
- create: true,
4727
- reply: true,
4728
- status: true,
4729
- link: true,
4730
- sync: true,
4731
- edit: false,
4732
- delete: false
4733
- };
4734
- var disabled = {
4735
- create: false,
4736
- reply: false,
4737
- status: false,
4738
- link: false,
4739
- sync: false,
4740
- edit: false,
4741
- delete: false
4742
- };
4743
- var identity = {
4744
- id: "git-comments",
4745
- label: "Git comments",
4746
- version: "3"
4747
- };
4748
- function fromComments(cause) {
4749
- let reason = cause.reason === "repository" || cause.reason.startsWith("remote-") ? "unavailable" : cause.reason === "identity" ? "refused" : [
4750
- "input",
4751
- "not-found",
4752
- "conflict"
4753
- ].includes(cause.reason) ? cause.reason : "storage";
4754
- return new WorkError({
4755
- reason,
4756
- message: cause.message,
4757
- cause
4758
- });
4759
- }
4760
- function attempt(parse, reason = "input") {
4761
- return try_({
4762
- try: parse,
4763
- catch: (cause) => new WorkError({
4764
- reason,
4765
- message: cause instanceof Error ? cause.message : "Invalid discussion data.",
4766
- cause
4767
- })
4768
- });
4769
- }
4770
- function snapshot(records, filter) {
4771
- let selected = records.filter((record) => {
4772
- if (filter.itemId && (record.version !== 3 || !record.itemIds.includes(filter.itemId))) return false;
4773
- let kind = record.anchor ? "type" in record.anchor ? "code" : "canvas" : null;
4774
- if (filter.anchorKind && filter.anchorKind !== kind) return false;
4775
- return !filter.pagePath || record.anchor && !("type" in record.anchor) && record.anchor.pagePath === filter.pagePath;
4776
- });
4777
- return parseThreadSnapshot({
4778
- connection: {
4779
- state: "ready",
4780
- adapter: identity
4781
- },
4782
- capabilities,
4783
- threads: selected.slice(0, MAX_THREADS).map((record) => projectThread(record, false)),
4784
- truncated: selected.length > MAX_THREADS
4785
- });
4786
- }
4787
- function metadataSource(value, source, sourceId) {
4788
- return !!value && typeof value === "object" && !Array.isArray(value) && value.source === source && value.sourceId === sourceId;
4789
- }
4790
- /**
4791
- * Adapts versioned Git comments to discussions without touching the source index or branch.
4792
- * @param options - The trusted project root, remote and configured canvas routes.
4793
- * @returns A thread service backed by temporary-index commits and compare-and-swap ref updates.
4794
- */
4795
- function gitThreadAdapterLayer(options) {
4796
- return effect(ThreadAdapter, gen(function* () {
4797
- let store = yield* createCommentStorage(options).pipe(mapError(fromComments));
4798
- let fs = yield* FileSystem;
4799
- function root(value, program) {
4800
- return attempt(() => {
4801
- if (typeof value !== "string" || !value || value.includes("\0") || store.paths.resolve(value) !== store.paths.resolve(store.root)) throw new Error("The discussion root must match this companion’s project.");
4802
- }).pipe(andThen(program));
4803
- }
4804
- function current() {
4805
- return gen(function* () {
4806
- let location = yield* store.location();
4807
- let head = yield* store.headAt(location.repo);
4808
- let records = yield* store.recordsAt(location.repo, head);
4809
- return {
4810
- ...location,
4811
- head,
4812
- records
4813
- };
4814
- }).pipe(mapError(fromComments));
4815
- }
4816
- function record(state, id) {
4817
- return gen(function* () {
4818
- yield* attempt(() => conversationId(id));
4819
- let record = state.records.threads.get(id);
4820
- if (!record || record.project !== state.projectId) return yield* new WorkError({
4821
- reason: "not-found",
4822
- message: "This discussion no longer exists."
4823
- });
4824
- return record;
4825
- });
4826
- }
4827
- function expected(record, revision) {
4828
- let currentRevision = threadRevision(record);
4829
- return currentRevision === revision ? void_ : fail(new WorkError({
4830
- reason: "conflict",
4831
- message: "This discussion changed. Reload it before saving your draft.",
4832
- currentRevision
4833
- }));
4834
- }
4835
- function save(state, value, message) {
4836
- return gen(function* () {
4837
- let next = yield* attempt(() => parseStoredThread(value));
4838
- let projected = yield* attempt(() => projectThread(next), "storage");
4839
- yield* store.commitRecords(state.repo, state.head, [next], message, { records: state.records }).pipe(mapError((cause) => cause.reason === "conflict" ? cause : fromComments(cause)));
4840
- return projected;
4841
- });
4842
- }
4843
- function fresh(program) {
4844
- return program.pipe(retry({
4845
- times: 2,
4846
- while: (error) => error._tag === "CommentsError"
4847
- }), mapError((error) => error._tag === "CommentsError" ? fromComments(error) : error));
4848
- }
4849
- function change(id, revision, update) {
4850
- return fresh(gen(function* () {
4851
- let state = yield* current();
4852
- let previous = yield* record(state, id);
4853
- yield* expected(previous, revision);
4854
- let now = yield* currentTimeMillis;
4855
- let at = new Date(Math.max(now, Date.parse(previous.updatedAt))).toISOString();
4856
- return yield* save(state, yield* attempt(() => update(previous, at)), "bud: update discussion");
4857
- }));
4858
- }
4859
- function link(project, id, itemIds, revision, remove) {
4860
- return root(project, gen(function* () {
4861
- let input = yield* attempt(() => parseThreadLinkInput({
4862
- itemIds,
4863
- expectedRevision: revision
4864
- }));
4865
- return yield* change(id, input.expectedRevision, (previous, at) => {
4866
- let next = promoteThread(previous);
4867
- let linked = remove ? next.itemIds.filter((id) => !input.itemIds.includes(id)) : [.../* @__PURE__ */ new Set([...next.itemIds, ...input.itemIds])];
4868
- if (linked.length > 100) throw new Error("This discussion has too many linked work items.");
4869
- if (!next.anchor && !linked.length) throw new Error("An unanchored discussion needs a linked work item.");
4870
- return {
4871
- ...next,
4872
- itemIds: linked,
4873
- updatedAt: at
4874
- };
4875
- });
4876
- }));
4877
- }
4878
- function watch(project) {
4879
- return unwrap(root(project, gen(function* () {
4880
- let { repo } = yield* store.location().pipe(mapError(fromComments));
4881
- let head = store.headAt(repo).pipe(orElseSucceed(() => null));
4882
- return concat(fromEffect(head), fs.watch(repo.info.commonDirectory, { recursive: true }).pipe(debounce("100 millis"), mapEffect(() => head))).pipe(changes, drop(1), as$1(void 0), mapError$1((cause) => new WorkError({
4883
- reason: "unavailable",
4884
- message: "Bud could not watch the comments ref.",
4885
- cause
4886
- })));
4887
- })));
4888
- }
4889
- return {
4890
- watch,
4891
- list: (project, value) => root(project, gen(function* () {
4892
- let filter = yield* attempt(() => parseThreadFilter(value));
4893
- return yield* store.list.pipe(flatMap((result) => attempt(() => snapshot(result.threads, filter), "storage")), catchTag("CommentsError", (cause) => cause.reason === "repository" ? succeed({
4894
- connection: {
4895
- state: "not-initialized",
4896
- adapter: null,
4897
- message: cause.message
4898
- },
4899
- capabilities: disabled,
4900
- threads: [],
4901
- truncated: false
4902
- }) : fail(fromComments(cause))));
4903
- })),
4904
- get: (project, id) => root(project, gen(function* () {
4905
- let stored = yield* record(yield* current(), id);
4906
- return yield* attempt(() => projectThread(stored), "storage");
4907
- })),
4908
- create: (project, value, author) => root(project, gen(function* () {
4909
- let input = yield* attempt(() => parseThreadInput(value));
4910
- let trusted = yield* attempt(() => parseMessageAuthor(author));
4911
- let anchor = input.anchor;
4912
- if (anchor && !("type" in anchor) && !options.routes.some((route) => route.id === anchor.pageId && route.path === anchor.pagePath)) return yield* new WorkError({
4913
- reason: "input",
4914
- message: "The discussion must belong to a configured route."
4915
- });
4916
- return yield* fresh(gen(function* () {
4917
- let state = yield* current();
4918
- let source = yield* store.source(state.repo).pipe(mapError(fromComments));
4919
- let now = new Date(yield* currentTimeMillis).toISOString();
4920
- return yield* save(state, {
4921
- version: 3,
4922
- id: randomUUID(),
4923
- project: state.projectId,
4924
- ...source,
4925
- anchor: input.anchor,
4926
- itemIds: input.itemIds,
4927
- status: "open",
4928
- createdAt: now,
4929
- updatedAt: now,
4930
- comments: [{
4931
- id: randomUUID(),
4932
- body: input.body,
4933
- author: trusted,
4934
- parentId: null,
4935
- createdAt: now
4936
- }]
4937
- }, "bud: start discussion");
4938
- }));
4939
- })),
4940
- reply: (project, id, value, author) => root(project, gen(function* () {
4941
- let input = yield* attempt(() => parseThreadReplyInput(value));
4942
- let trusted = yield* attempt(() => parseMessageAuthor(author));
4943
- return yield* change(id, input.expectedRevision, (previous, at) => {
4944
- if (input.parentId && !previous.comments.some((message) => message.id === input.parentId)) throw new Error("Reply to a message in this discussion.");
4945
- return {
4946
- ...promoteThread(previous),
4947
- updatedAt: at,
4948
- comments: [...previous.comments, {
4949
- id: randomUUID(),
4950
- body: input.body,
4951
- author: trusted,
4952
- parentId: input.parentId,
4953
- createdAt: at
4954
- }]
4955
- };
4956
- });
4957
- })),
4958
- setStatus: (project, id, status, revision) => root(project, gen(function* () {
4959
- let input = yield* attempt(() => parseThreadStatusInput({
4960
- status,
4961
- expectedRevision: revision
4962
- }));
4963
- return yield* change(id, input.expectedRevision, (previous, at) => ({
4964
- ...promoteThread(previous),
4965
- status: input.status,
4966
- updatedAt: at
4967
- }));
4968
- })),
4969
- link: (project, id, ids, revision) => link(project, id, ids, revision, false),
4970
- unlink: (project, id, ids, revision) => link(project, id, ids, revision, true),
4971
- sync: (project, direction = "both") => root(project, gen(function* () {
4972
- let input = yield* attempt(() => parseWorkSync({ direction }));
4973
- let result = yield* store.sync(input.direction).pipe(mapError(fromComments));
4974
- return yield* attempt(() => snapshot(result.threads, {}), "storage");
4975
- })),
4976
- import: (project, value) => root(project, gen(function* () {
4977
- let input = yield* attempt(() => parseThreadImport(value));
4978
- return yield* fresh(gen(function* () {
4979
- let state = yield* current();
4980
- let existing = [...state.records.threads.values()].filter((record) => record.project === state.projectId && record.version === 3 && metadataSource(record.metadata, input.source, input.sourceId));
4981
- if (existing.length > 1) return yield* new WorkError({
4982
- reason: "conflict",
4983
- message: "This source discussion has duplicate imports. Repair the records before migrating."
4984
- });
4985
- if (existing.length) return {
4986
- outcome: "existing",
4987
- thread: projectThread(existing[0])
4988
- };
4989
- let source = yield* store.source(state.repo).pipe(mapError(fromComments));
4990
- let thread = input.thread;
4991
- return {
4992
- outcome: "created",
4993
- thread: yield* save(state, {
4994
- version: 3,
4995
- id: randomUUID(),
4996
- project: state.projectId,
4997
- ...source,
4998
- anchor: thread.anchor,
4999
- itemIds: thread.itemIds,
5000
- comments: thread.messages,
5001
- status: thread.status,
5002
- createdAt: thread.createdAt,
5003
- updatedAt: thread.updatedAt,
5004
- metadata: {
5005
- source: input.source,
5006
- sourceId: input.sourceId,
5007
- createdAt: thread.createdAt,
5008
- original: thread.metadata ?? null
5009
- }
5010
- }, "bud: import discussion")
5011
- };
5012
- }));
5013
- }))
5014
- };
5015
- }));
5016
- }
5017
- //#endregion
5018
- //#region ../comments/src/server/migration.ts
5019
- /**
5020
- * Reads complete migration history under the Git runtime's existing tree and blob byte limits.
5021
- * @param options - The trusted source project, remote and configured canvas routes.
5022
- * @returns Complete source records and their immutable ref head, without writing any Git object or
5023
- * ref.
5024
- */
5025
- function readGitThreadImports(options) {
5026
- return gen(function* () {
5027
- let snapshot = yield* (yield* createCommentStorage(options)).list;
5028
- let threads = yield* try_({
5029
- try: () => snapshot.threads.map((record) => parseThreadImport({
5030
- source: "bud-git",
5031
- sourceId: record.id,
5032
- thread: {
5033
- ...projectThread(record),
5034
- messages: record.comments,
5035
- messagesTruncated: false
5036
- }
5037
- })),
5038
- catch: (cause) => new CommentsError({
5039
- reason: "storage",
5040
- message: "The Git discussion history could not be validated for migration.",
5041
- cause
5042
- })
5043
- });
5044
- return {
5045
- head: snapshot.head,
5046
- threads
5047
- };
5048
- });
5049
- }
5050
- //#endregion
5051
4374
  //#region app/server/built-in-adapters.ts
5052
- var memorySchema = {
5053
- adapter: {
5054
- id: "memory",
5055
- label: "Memory",
5056
- version: "1"
5057
- },
5058
- fields: [
5059
- {
5060
- id: "id",
5061
- label: "Identifier",
5062
- kind: "text",
5063
- role: "identifier",
5064
- editable: false,
5065
- required: true
5066
- },
5067
- {
5068
- id: "title",
5069
- label: "Title",
5070
- kind: "text",
5071
- role: "title",
5072
- editable: true,
5073
- required: true
5074
- },
5075
- {
5076
- id: "status",
5077
- label: "Status",
5078
- kind: "status",
5079
- role: "status",
5080
- editable: true
5081
- },
5082
- {
5083
- id: "description",
5084
- label: "Description",
5085
- kind: "markdown",
5086
- role: "description",
5087
- editable: true
5088
- },
5089
- {
5090
- id: "createdAt",
5091
- label: "Created",
5092
- kind: "datetime",
5093
- role: "created",
5094
- editable: false,
5095
- required: true
5096
- },
5097
- {
5098
- id: "updatedAt",
5099
- label: "Updated",
5100
- kind: "datetime",
5101
- role: "updated",
5102
- editable: false,
5103
- required: true
5104
- },
5105
- {
5106
- id: "revision",
5107
- label: "Revision",
5108
- kind: "text",
5109
- role: "revision",
5110
- editable: false,
5111
- required: true
5112
- }
5113
- ],
5114
- statuses: [
5115
- {
5116
- value: "open",
5117
- label: "Open",
5118
- category: "unstarted"
5119
- },
5120
- {
5121
- value: "in_progress",
5122
- label: "In progress",
5123
- category: "started"
5124
- },
5125
- {
5126
- value: "closed",
5127
- label: "Closed",
5128
- category: "done"
5129
- }
5130
- ],
5131
- relations: [],
5132
- actions: [],
5133
- sections: [],
5134
- capabilities: {
5135
- create: true,
5136
- update: true,
5137
- sync: false,
5138
- discussion: true,
5139
- threads: true,
5140
- history: false,
5141
- activity: false,
5142
- ready: false
5143
- }
5144
- };
5145
4375
  /**
5146
- * Supplies built-ins with the companion's exact native services, including its shared Beads queue.
4376
+ * Supplies built-ins and the project's plugin stores with the companion's exact native services,
4377
+ * including its shared Beads queue.
5147
4378
  * @param project - The trusted consuming project.
5148
- * @returns Fully provided definitions; only selected adapter layers are acquired by the registry.
4379
+ * @returns Fully provided definitions by plugin name; only selected ones are acquired by the
4380
+ * registry.
5149
4381
  */
5150
4382
  function builtInAdapters(project) {
5151
4383
  return gen(function* () {
5152
4384
  let native = succeedContext(yield* context());
5153
- let memory = memoryAdapterLayer({ schema: memorySchema });
4385
+ let store = (plugin, value) => try_({
4386
+ try: () => {
4387
+ if (value === void 0) return void 0;
4388
+ let layer = typeof value === "function" ? value({ project }) : value;
4389
+ if (!isLayer(layer)) throw new Error("Not an Effect layer.");
4390
+ return provide(layer, native);
4391
+ },
4392
+ catch: (cause) => new AdapterStartupError({
4393
+ message: `Plugin ${plugin.name} must give its stores as Effect layers, or functions making them.`,
4394
+ cause
4395
+ })
4396
+ });
4397
+ let plugins = {};
4398
+ for (let plugin of configPlugins(project.config)) {
4399
+ if (plugin.work === void 0 && plugin.threads === void 0) continue;
4400
+ plugins[plugin.name] = defineAdapter({
4401
+ id: plugin.name.replace(/^bud:/, ""),
4402
+ label: plugin.label ?? plugin.name,
4403
+ work: yield* store(plugin, plugin.work),
4404
+ threads: yield* store(plugin, plugin.threads)
4405
+ });
4406
+ }
5154
4407
  return {
5155
4408
  "bud:beads": defineAdapter({
5156
4409
  id: "beads",
@@ -5158,24 +4411,7 @@ function builtInAdapters(project) {
5158
4411
  work: provide(beadsStoreWorkAdapterLayer, native),
5159
4412
  threads: provide(beadsStoreThreadAdapterLayer, native)
5160
4413
  }),
5161
- "bud:git-comments": defineAdapter({
5162
- id: "git-comments",
5163
- label: "Git comments",
5164
- threads: provide(gitThreadAdapterLayer({
5165
- root: project.root,
5166
- remote: project.config.remote,
5167
- routes: project.info.routes.map(({ id, path }) => ({
5168
- id,
5169
- path
5170
- }))
5171
- }), native)
5172
- }),
5173
- "bud:memory": defineAdapter({
5174
- id: "memory",
5175
- label: "Memory",
5176
- work: memory,
5177
- threads: memory
5178
- })
4414
+ ...plugins
5179
4415
  };
5180
4416
  });
5181
4417
  }
@@ -5332,6 +4568,7 @@ function domainLayer(project, options) {
5332
4568
  flows: projectFlows(project.root, project.config.flows),
5333
4569
  warm: options.warmPreviews
5334
4570
  });
4571
+ let attachments = attachmentsLayer(directoryAttachmentStore(join(project.root, "node_modules/.cache/bud")));
5335
4572
  let files = mergeAll(filesLayer({ root: project.root }), fileWriteLockLayer);
5336
4573
  let review = fileReviewLayer({ root: project.root });
5337
4574
  let channel = liveChannelLayer();
@@ -5342,7 +4579,7 @@ function domainLayer(project, options) {
5342
4579
  pollMs: Number(process.env.BUD_BRANCH_POLL_MS) || void 0
5343
4580
  });
5344
4581
  let live = mergeAll(provideMerge(publishLiveChanges, mergeAll(previews, branches, files, channel)), provide(fileLiveLayer, mergeAll(provideMerge(review, files), channel)), provideMerge(storeLiveLayer(project.root), merge(storeActivityLayer, channel)), provide(relocatePins(anchors), mergeAll(previews, branches, storeActivityLayer)));
5345
- return mergeAll(options.reloadPreviews ? provideMerge(reloadPreviewsOnChange, live) : live, provideMerge(fileReviewActionsLayer({ root: project.root }), provideMerge(mergeAll(review, fileMutationsLayer({ root: project.root }), fileSavesLayer({ root: project.root }), fileSearchLayer()), files)), options.development ? sourceScreensLayer(project) : bundledScreensLayer(project));
4582
+ return mergeAll(options.reloadPreviews ? provideMerge(reloadPreviewsOnChange, live) : live, provideMerge(fileReviewActionsLayer({ root: project.root }), provideMerge(mergeAll(review, fileMutationsLayer({ root: project.root }), fileSavesLayer({ root: project.root }), fileSearchLayer()), files)), options.development ? sourceScreensLayer(project) : bundledScreensLayer(project), attachments, provide(factoryRunsLayer(project), mergeAll(storeActivityLayer, runExecutorLayer(project), branches, attachments)));
5346
4583
  }
5347
4584
  /**
5348
4585
  * Adds explicitly selected storage adapters to the companion's shared service graph.
@@ -5361,7 +4598,7 @@ function configuredApplicationLayer(project, definitions, options = {}) {
5361
4598
  * @returns The registry and shared native services used by the application and migration CLI.
5362
4599
  */
5363
4600
  function storageLayer(project, definitions) {
5364
- return provideMerge(definitions ? adapterRegistryLayer(project, definitions) : unwrap$1(map(builtInAdapters(project), (definitions) => adapterRegistryLayer(project, definitions))), mergeAll(provideMerge(beadsLayer(), mergeAll(succeed$1(Project, project), provideMerge(gitLayer(), nodeLayer()))), adapterModuleLoaderLayer));
4601
+ return provideMerge(definitions ? adapterRegistryLayer(project, definitions) : unwrap$1(map(builtInAdapters(project), (definitions) => adapterRegistryLayer(project, definitions))), provideMerge(beadsLayer(), mergeAll(succeed$1(Project, project), provideMerge(gitLayer(), nodeLayer()))));
5365
4602
  }
5366
4603
  //#endregion
5367
- export { commentsRef as i, storageLayer as n, readGitThreadImports as r, applicationLayer as t };
4604
+ export { storageLayer as n, applicationLayer as t };