@covro/covro 0.0.0-stage → 0.1.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -2
  3. package/README.zh-CN.md +174 -0
  4. package/bin/covro.mjs +8 -0
  5. package/dist/cli/control.js +66 -0
  6. package/dist/cli/control.js.map +7 -0
  7. package/dist/cli/daemon.js +118 -0
  8. package/dist/cli/daemon.js.map +7 -0
  9. package/dist/cli/main.js +152 -0
  10. package/dist/cli/main.js.map +7 -0
  11. package/dist/cli/paths.js +60 -0
  12. package/dist/cli/paths.js.map +7 -0
  13. package/dist/cli/service.js +87 -0
  14. package/dist/cli/service.js.map +7 -0
  15. package/dist/codex/adapter.js +1195 -0
  16. package/dist/codex/adapter.js.map +7 -0
  17. package/dist/codex/auto-review.js +102 -0
  18. package/dist/codex/auto-review.js.map +7 -0
  19. package/dist/codex/compatibility.js +7 -0
  20. package/dist/codex/compatibility.js.map +7 -0
  21. package/dist/codex/mapping.js +210 -0
  22. package/dist/codex/mapping.js.map +7 -0
  23. package/dist/codex/permissions.js +51 -0
  24. package/dist/codex/permissions.js.map +7 -0
  25. package/dist/codex/thread-title-policy.js +52 -0
  26. package/dist/codex/thread-title-policy.js.map +7 -0
  27. package/dist/codex/thread-titles.js +259 -0
  28. package/dist/codex/thread-titles.js.map +7 -0
  29. package/dist/codex/transport.js +236 -0
  30. package/dist/codex/transport.js.map +7 -0
  31. package/dist/codex/turn-token-usage.js +46 -0
  32. package/dist/codex/turn-token-usage.js.map +7 -0
  33. package/dist/codex/types.js +1 -0
  34. package/dist/codex/types.js.map +7 -0
  35. package/dist/drizzle/0000_past_wildside.sql +45 -0
  36. package/dist/drizzle/0001_shocking_ender_wiggin.sql +2 -0
  37. package/dist/drizzle/0002_allow_parallel_threads.sql +1 -0
  38. package/dist/drizzle/0003_project_registrations.sql +11 -0
  39. package/dist/drizzle/0004_turn_usage.sql +6 -0
  40. package/dist/drizzle/meta/0000_snapshot.json +309 -0
  41. package/dist/drizzle/meta/0001_snapshot.json +323 -0
  42. package/dist/drizzle/meta/0002_snapshot.json +315 -0
  43. package/dist/drizzle/meta/0003_snapshot.json +381 -0
  44. package/dist/drizzle/meta/0004_snapshot.json +420 -0
  45. package/dist/drizzle/meta/_journal.json +41 -0
  46. package/dist/server/auth.js +47 -0
  47. package/dist/server/auth.js.map +7 -0
  48. package/dist/server/compaction.js +40 -0
  49. package/dist/server/compaction.js.map +7 -0
  50. package/dist/server/config-store.js +147 -0
  51. package/dist/server/config-store.js.map +7 -0
  52. package/dist/server/config.js +124 -0
  53. package/dist/server/config.js.map +7 -0
  54. package/dist/server/db/index.js +21 -0
  55. package/dist/server/db/index.js.map +7 -0
  56. package/dist/server/db/migrate.js +26 -0
  57. package/dist/server/db/migrate.js.map +7 -0
  58. package/dist/server/db/operations.js +201 -0
  59. package/dist/server/db/operations.js.map +7 -0
  60. package/dist/server/db/schema.js +99 -0
  61. package/dist/server/db/schema.js.map +7 -0
  62. package/dist/server/db/turn-usage.js +31 -0
  63. package/dist/server/db/turn-usage.js.map +7 -0
  64. package/dist/server/events.js +123 -0
  65. package/dist/server/events.js.map +7 -0
  66. package/dist/server/http.js +305 -0
  67. package/dist/server/http.js.map +7 -0
  68. package/dist/server/images.js +49 -0
  69. package/dist/server/images.js.map +7 -0
  70. package/dist/server/instance-lock.js +25 -0
  71. package/dist/server/instance-lock.js.map +7 -0
  72. package/dist/server/local-files.js +100 -0
  73. package/dist/server/local-files.js.map +7 -0
  74. package/dist/server/main.js +125 -0
  75. package/dist/server/main.js.map +7 -0
  76. package/dist/server/markdown.js +28 -0
  77. package/dist/server/markdown.js.map +7 -0
  78. package/dist/server/projection.js +348 -0
  79. package/dist/server/projection.js.map +7 -0
  80. package/dist/server/projects.js +230 -0
  81. package/dist/server/projects.js.map +7 -0
  82. package/dist/server/service.js +1221 -0
  83. package/dist/server/service.js.map +7 -0
  84. package/dist/server/snapshot.js +98 -0
  85. package/dist/server/snapshot.js.map +7 -0
  86. package/dist/server/thread-export.js +97 -0
  87. package/dist/server/thread-export.js.map +7 -0
  88. package/dist/server/websocket.js +126 -0
  89. package/dist/server/websocket.js.map +7 -0
  90. package/dist/shared/async-questions.js +39 -0
  91. package/dist/shared/async-questions.js.map +7 -0
  92. package/dist/shared/codex-compatibility.js +56 -0
  93. package/dist/shared/codex-compatibility.js.map +7 -0
  94. package/dist/shared/elicitation.js +314 -0
  95. package/dist/shared/elicitation.js.map +7 -0
  96. package/dist/shared/errors.js +24 -0
  97. package/dist/shared/errors.js.map +7 -0
  98. package/dist/shared/gateway-config.js +1 -0
  99. package/dist/shared/gateway-config.js.map +7 -0
  100. package/dist/shared/images.js +21 -0
  101. package/dist/shared/images.js.map +7 -0
  102. package/dist/shared/package.js +5 -0
  103. package/dist/shared/package.js.map +7 -0
  104. package/dist/shared/permissions.js +63 -0
  105. package/dist/shared/permissions.js.map +7 -0
  106. package/dist/shared/thread-name.js +14 -0
  107. package/dist/shared/thread-name.js.map +7 -0
  108. package/dist/shared/thread-summary.js +24 -0
  109. package/dist/shared/thread-summary.js.map +7 -0
  110. package/dist/shared/types.js +1 -0
  111. package/dist/shared/types.js.map +7 -0
  112. package/dist/shared/updates.js +7 -0
  113. package/dist/shared/updates.js.map +7 -0
  114. package/dist/shared/validation.js +116 -0
  115. package/dist/shared/validation.js.map +7 -0
  116. package/dist/shared/versions.js +36 -0
  117. package/dist/shared/versions.js.map +7 -0
  118. package/dist/update-worker.cjs +21968 -0
  119. package/dist/updates/files.js +54 -0
  120. package/dist/updates/files.js.map +7 -0
  121. package/dist/updates/manager.js +315 -0
  122. package/dist/updates/manager.js.map +7 -0
  123. package/dist/updates/registry.js +71 -0
  124. package/dist/updates/registry.js.map +7 -0
  125. package/dist/updates/transaction.js +96 -0
  126. package/dist/updates/transaction.js.map +7 -0
  127. package/dist/updates/worker.js +345 -0
  128. package/dist/updates/worker.js.map +7 -0
  129. package/dist/web/assets/index-BW9afH_Y.js +83 -0
  130. package/dist/web/assets/index-L3S1ZiO0.css +1 -0
  131. package/dist/web/covro.svg +6 -0
  132. package/dist/web/index.html +15 -0
  133. package/package.json +88 -4
@@ -0,0 +1,348 @@
1
+ import { endCompactions, mergeCompaction, settleCompactions } from "./compaction.js";
2
+ const isActive = (status) => status === "running" || status === "waitingApproval" || status === "waitingInput";
3
+ const isTerminal = (status) => status === "completed" || status === "failed" || status === "interrupted";
4
+ class Projection {
5
+ constructor(maxItemBytes = 2 * 1024 * 1024) {
6
+ this.maxItemBytes = maxItemBytes;
7
+ }
8
+ maxItemBytes;
9
+ threads = /* @__PURE__ */ new Map();
10
+ actions = /* @__PURE__ */ new Map();
11
+ revisions = /* @__PURE__ */ new Map();
12
+ // Notifications may precede thread/turn history. Keep only a bounded, per-thread inbox.
13
+ earlyAutoReviews = /* @__PURE__ */ new Map();
14
+ earlyAutoReviewBytes = 0;
15
+ get(threadId) {
16
+ return this.threads.get(threadId);
17
+ }
18
+ values() {
19
+ return [...this.threads.values()];
20
+ }
21
+ revision(threadId) {
22
+ return this.revisions.get(threadId) ?? 0;
23
+ }
24
+ touch(threadId) {
25
+ this.revisions.set(threadId, this.revision(threadId) + 1);
26
+ }
27
+ thread(incoming, preserveCurrent = false, preserveRuntimeSettings = preserveCurrent) {
28
+ const current = this.threads.get(incoming.id);
29
+ if (!current) {
30
+ const thread = structuredClone(incoming);
31
+ for (const turn of thread.turns) turn.items = turn.items.map((item) => this.limit(item));
32
+ thread.actions = [...this.actions.values()].filter((action) => action.threadId === thread.id);
33
+ this.threads.set(thread.id, thread);
34
+ for (const turn of thread.turns) {
35
+ this.flushAutoReviews(thread.id, turn.id);
36
+ this.finishReconnect(turn);
37
+ settleCompactions(turn);
38
+ if (isTerminal(turn.status)) this.finishAutoReviews(turn);
39
+ }
40
+ } else {
41
+ const oldTurns = current.turns;
42
+ const oldStatus = current.status;
43
+ const oldRuntimeSettings = current.runtimeSettings;
44
+ const oldTitle = current.title;
45
+ Object.assign(current, incoming, { turns: oldTurns, actions: current.actions });
46
+ if (oldRuntimeSettings && incoming.runtimeSettings)
47
+ current.runtimeSettings = { ...oldRuntimeSettings, ...incoming.runtimeSettings };
48
+ if (preserveCurrent) {
49
+ current.status = oldStatus;
50
+ current.title = oldTitle;
51
+ }
52
+ if (preserveRuntimeSettings) current.runtimeSettings = oldRuntimeSettings;
53
+ for (const turn of incoming.turns) this.turn(incoming.id, turn, preserveCurrent);
54
+ }
55
+ this.touch(incoming.id);
56
+ return this.threads.get(incoming.id);
57
+ }
58
+ turn(threadId, incoming, preserveCurrent = false) {
59
+ const thread = this.threads.get(threadId);
60
+ if (!thread) return;
61
+ thread.hasMessages = true;
62
+ const current = thread.turns.find((turn) => turn.id === incoming.id);
63
+ if (!current)
64
+ thread.turns.push({
65
+ ...structuredClone(incoming),
66
+ items: incoming.items.map((item) => this.limit(item))
67
+ });
68
+ else {
69
+ const items = current.items;
70
+ if (!preserveCurrent) Object.assign(current, incoming, { items });
71
+ for (const item of incoming.items) this.item(threadId, incoming.id, item, preserveCurrent);
72
+ }
73
+ if (!preserveCurrent && thread.turns.at(-1)?.id === incoming.id)
74
+ thread.status = incoming.status;
75
+ this.flushAutoReviews(threadId, incoming.id);
76
+ const updated = thread.turns.find((turn) => turn.id === incoming.id);
77
+ this.finishReconnect(updated);
78
+ settleCompactions(updated);
79
+ if (isTerminal(updated.status)) this.finishAutoReviews(updated);
80
+ this.touch(threadId);
81
+ }
82
+ turnError(threadId, turnId, message, willRetry) {
83
+ const turn = this.threads.get(threadId)?.turns.find((entry) => entry.id === turnId);
84
+ if (!turn || !isActive(turn.status)) return false;
85
+ const detail = message.slice(0, 4096);
86
+ if (willRetry) turn.reconnect = { status: "retrying", message: detail };
87
+ else {
88
+ turn.error = detail;
89
+ endCompactions(turn, "failed", detail);
90
+ if (turn.reconnect) turn.reconnect = { status: "failed", message: detail };
91
+ }
92
+ this.touch(threadId);
93
+ return true;
94
+ }
95
+ compactionCompleted(threadId, turnId) {
96
+ const turn = this.threads.get(threadId)?.turns.find((entry) => entry.id === turnId);
97
+ if (!turn || !endCompactions(turn, "completed")) return false;
98
+ this.touch(threadId);
99
+ return true;
100
+ }
101
+ endThreadCompactions(threadId) {
102
+ for (const turn of this.threads.get(threadId)?.turns ?? []) endCompactions(turn, "unknown");
103
+ this.touch(threadId);
104
+ }
105
+ turnProgress(threadId, turnId) {
106
+ const turn = this.threads.get(threadId)?.turns.find((entry) => entry.id === turnId);
107
+ if (!turn || !isActive(turn.status) || turn.reconnect?.status !== "retrying") return false;
108
+ delete turn.reconnect;
109
+ this.touch(threadId);
110
+ return true;
111
+ }
112
+ finishReconnect(turn) {
113
+ if (!turn.reconnect || !isTerminal(turn.status)) return;
114
+ if (turn.status === "failed") {
115
+ turn.reconnect = {
116
+ status: "failed",
117
+ message: (turn.error ?? turn.reconnect.message).slice(0, 4096)
118
+ };
119
+ } else delete turn.reconnect;
120
+ }
121
+ autoReview(threadId, turnId, review) {
122
+ const item = this.limit({
123
+ id: `auto-review:${review.reviewId}`,
124
+ type: "autoApprovalReview",
125
+ status: review.status,
126
+ autoReview: review
127
+ });
128
+ const turn = this.threads.get(threadId)?.turns.find((entry) => entry.id === turnId);
129
+ if (turn) return this.applyAutoReview(threadId, turn, item);
130
+ const key = JSON.stringify([threadId, turnId, review.reviewId]);
131
+ const current = this.earlyAutoReviews.get(key);
132
+ if (current && !this.reviewCanReplace(current.item, item)) return false;
133
+ const bytes = Buffer.byteLength(JSON.stringify({ threadId, turnId, item }));
134
+ if (current) this.earlyAutoReviewBytes -= current.bytes;
135
+ this.earlyAutoReviews.set(key, { threadId, turnId, item, bytes });
136
+ this.earlyAutoReviewBytes += bytes;
137
+ while (this.earlyAutoReviews.size > 128 || this.earlyAutoReviewBytes > 4 * 1024 * 1024) {
138
+ const oldest = this.earlyAutoReviews.keys().next().value;
139
+ this.earlyAutoReviewBytes -= this.earlyAutoReviews.get(oldest).bytes;
140
+ this.earlyAutoReviews.delete(oldest);
141
+ }
142
+ return false;
143
+ }
144
+ reviewCanReplace(current, incoming) {
145
+ const previous = current.autoReview;
146
+ const next = incoming.autoReview;
147
+ if (previous.status !== "inProgress" && next.status === "inProgress") return false;
148
+ if (previous.completedAtMs !== void 0 && (next.completedAtMs === void 0 || previous.completedAtMs > next.completedAtMs))
149
+ return false;
150
+ return JSON.stringify(current) !== JSON.stringify(incoming);
151
+ }
152
+ applyAutoReview(threadId, turn, item) {
153
+ const index = turn.items.findIndex(
154
+ (entry) => entry.type === "autoApprovalReview" && entry.autoReview?.reviewId === item.autoReview.reviewId
155
+ );
156
+ if (index >= 0 && !this.reviewCanReplace(turn.items[index], item)) return false;
157
+ if (isTerminal(turn.status) && item.autoReview.status === "inProgress") {
158
+ item.autoReview.status = "unknown";
159
+ item.status = "unknown";
160
+ }
161
+ if (index < 0) turn.items.push(item);
162
+ else {
163
+ turn.items[index] = item;
164
+ }
165
+ this.touch(threadId);
166
+ return true;
167
+ }
168
+ flushAutoReviews(threadId, turnId) {
169
+ const turn = this.threads.get(threadId)?.turns.find((entry) => entry.id === turnId);
170
+ if (!turn) return;
171
+ for (const [key, entry] of this.earlyAutoReviews) {
172
+ if (entry.threadId !== threadId || entry.turnId !== turnId) continue;
173
+ this.earlyAutoReviews.delete(key);
174
+ this.earlyAutoReviewBytes -= entry.bytes;
175
+ this.applyAutoReview(threadId, turn, entry.item);
176
+ }
177
+ }
178
+ finishAutoReviews(turn) {
179
+ for (const item of turn.items) {
180
+ if (item.autoReview?.status !== "inProgress") continue;
181
+ item.autoReview.status = "unknown";
182
+ item.status = "unknown";
183
+ }
184
+ }
185
+ item(threadId, turnId, item, preserveCurrent = false) {
186
+ const turn = this.threads.get(threadId)?.turns.find((entry) => entry.id === turnId);
187
+ if (!turn) return;
188
+ const index = turn.items.findIndex((entry) => entry.id === item.id);
189
+ if (index < 0) turn.items.push(this.limit(item));
190
+ else if (!preserveCurrent)
191
+ turn.items[index] = this.limit(mergeCompaction(item, turn.items[index]));
192
+ settleCompactions(turn);
193
+ this.touch(threadId);
194
+ }
195
+ delta(threadId, turnId, itemId, field, delta, itemType) {
196
+ const turn = this.threads.get(threadId)?.turns.find((entry) => entry.id === turnId);
197
+ if (!turn) return;
198
+ let item = turn.items.find((entry) => entry.id === itemId);
199
+ if (!item) {
200
+ item = {
201
+ id: itemId,
202
+ type: itemType ?? (field === "text" ? "agentMessage" : "commandExecution")
203
+ };
204
+ turn.items.push(item);
205
+ }
206
+ if (item.truncated) return;
207
+ item[field] = (item[field] ?? "") + delta;
208
+ Object.assign(item, this.limit(item));
209
+ this.touch(threadId);
210
+ }
211
+ diff(threadId, turnId, diff) {
212
+ const turn = this.threads.get(threadId)?.turns.find((entry) => entry.id === turnId);
213
+ if (turn) turn.diff = this.clip(diff).value;
214
+ this.touch(threadId);
215
+ }
216
+ action(action) {
217
+ const owner = action.threadId ? this.threads.get(action.threadId)?.turns.find((turn) => turn.id === action.turnId) : void 0;
218
+ if (owner && isTerminal(owner.status) && ["pending", "responding"].includes(action.status))
219
+ action = { ...action, status: "expired" };
220
+ this.actions.set(action.actionId, action);
221
+ const thread = action.threadId ? this.threads.get(action.threadId) : void 0;
222
+ if (thread) {
223
+ thread.actions = [...this.actions.values()].filter((entry) => entry.threadId === thread.id);
224
+ this.updateActionStatus(thread);
225
+ this.touch(thread.id);
226
+ }
227
+ }
228
+ resolveAction(actionId, status) {
229
+ const action = this.actions.get(actionId);
230
+ if (!action) return void 0;
231
+ action.status = status;
232
+ const thread = action.threadId ? this.threads.get(action.threadId) : void 0;
233
+ if (thread) {
234
+ this.updateActionStatus(thread);
235
+ this.touch(thread.id);
236
+ }
237
+ return action;
238
+ }
239
+ expireTurn(threadId, turnId) {
240
+ for (const action of this.actions.values())
241
+ if (action.threadId === threadId && action.turnId === turnId && (action.status === "pending" || action.status === "responding"))
242
+ this.resolveAction(action.actionId, "expired");
243
+ }
244
+ runtimeReset() {
245
+ for (const action of this.actions.values())
246
+ if (action.status === "pending" || action.status === "responding")
247
+ this.resolveAction(action.actionId, "expired");
248
+ for (const thread of this.threads.values()) {
249
+ thread.managed = false;
250
+ delete thread.resumeError;
251
+ if (thread.runtimeSettings) {
252
+ const { model, reasoningEffort } = thread.runtimeSettings;
253
+ thread.runtimeSettings = { model, reasoningEffort };
254
+ }
255
+ if (isActive(thread.status)) thread.status = "unknown";
256
+ for (const turn of thread.turns) {
257
+ if (turn.reconnect?.status === "retrying") delete turn.reconnect;
258
+ if (isActive(turn.status)) turn.status = "unknown";
259
+ endCompactions(turn, "unknown");
260
+ this.finishAutoReviews(turn);
261
+ }
262
+ this.touch(thread.id);
263
+ }
264
+ for (const entry of this.earlyAutoReviews.values()) {
265
+ if (entry.item.autoReview?.status === "inProgress") {
266
+ this.earlyAutoReviewBytes -= entry.bytes;
267
+ entry.item.autoReview.status = "unknown";
268
+ entry.item.status = "unknown";
269
+ entry.bytes = Buffer.byteLength(
270
+ JSON.stringify({ threadId: entry.threadId, turnId: entry.turnId, item: entry.item })
271
+ );
272
+ this.earlyAutoReviewBytes += entry.bytes;
273
+ }
274
+ }
275
+ }
276
+ updateActionStatus(thread) {
277
+ const active = thread.actions.filter(
278
+ (action) => action.blocking !== false && (action.status === "pending" || action.status === "responding")
279
+ );
280
+ const turn = [...thread.turns].reverse().find((entry) => isActive(entry.status));
281
+ if (!turn) return;
282
+ const status = active.some(
283
+ (action) => action.kind === "questions" || action.kind === "elicitation"
284
+ ) ? "waitingInput" : active.length > 0 ? "waitingApproval" : "running";
285
+ thread.status = status;
286
+ turn.status = status;
287
+ }
288
+ limit(item) {
289
+ const result = structuredClone(item);
290
+ if (result.images) {
291
+ let bytes = 0;
292
+ result.imageCount ??= result.images.length;
293
+ result.images = result.images.filter((image) => {
294
+ bytes += Buffer.byteLength(image);
295
+ return bytes <= this.maxItemBytes;
296
+ });
297
+ }
298
+ for (const field of ["text", "output", "command"]) {
299
+ const value = result[field];
300
+ if (value) {
301
+ const clipped = this.clip(value);
302
+ result[field] = clipped.value;
303
+ if (clipped.truncated) result.truncated = true;
304
+ }
305
+ }
306
+ if (result.changes)
307
+ result.changes = result.changes.map((change) => {
308
+ const clipped = this.clip(change.diff);
309
+ if (clipped.truncated) result.truncated = true;
310
+ return { ...change, diff: clipped.value };
311
+ });
312
+ if (result.details !== void 0) {
313
+ const clipped = this.clip(JSON.stringify(result.details));
314
+ if (clipped.truncated) {
315
+ result.details = clipped.value;
316
+ result.truncated = true;
317
+ }
318
+ }
319
+ if (result.autoReview) {
320
+ for (const field of ["actionLabel", "actionSummary", "cwd", "rationale"]) {
321
+ const value = result.autoReview[field];
322
+ if (value === void 0) continue;
323
+ const clipped = this.clip(value);
324
+ result.autoReview[field] = clipped.value;
325
+ if (clipped.truncated) result.truncated = true;
326
+ }
327
+ }
328
+ return result;
329
+ }
330
+ clip(value) {
331
+ if (Buffer.byteLength(value) <= this.maxItemBytes) return { value, truncated: false };
332
+ let kept = "";
333
+ let bytes = 0;
334
+ for (const point of value) {
335
+ bytes += Buffer.byteLength(point);
336
+ if (bytes > this.maxItemBytes) break;
337
+ kept += point;
338
+ }
339
+ return { value: `${kept}
340
+ [\u8F93\u51FA\u5DF2\u622A\u65AD]`, truncated: true };
341
+ }
342
+ }
343
+ export {
344
+ Projection,
345
+ isActive,
346
+ isTerminal
347
+ };
348
+ //# sourceMappingURL=projection.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/server/projection.ts"],
4
+ "sourcesContent": ["import { endCompactions, mergeCompaction, settleCompactions } from './compaction.js';\nimport type {\n AutoReviewView,\n ItemView,\n PendingAction,\n RunStatus,\n ThreadView,\n TurnView,\n} from '../shared/types.js';\n\nexport const isActive = (status: RunStatus): boolean =>\n status === 'running' || status === 'waitingApproval' || status === 'waitingInput';\nexport const isTerminal = (status: RunStatus): boolean =>\n status === 'completed' || status === 'failed' || status === 'interrupted';\n\nexport class Projection {\n private readonly threads = new Map<string, ThreadView>();\n readonly actions = new Map<string, PendingAction>();\n private readonly revisions = new Map<string, number>();\n // Notifications may precede thread/turn history. Keep only a bounded, per-thread inbox.\n private readonly earlyAutoReviews = new Map<\n string,\n { threadId: string; turnId: string; item: ItemView; bytes: number }\n >();\n private earlyAutoReviewBytes = 0;\n constructor(private readonly maxItemBytes = 2 * 1024 * 1024) {}\n\n get(threadId: string): ThreadView | undefined {\n return this.threads.get(threadId);\n }\n values(): ThreadView[] {\n return [...this.threads.values()];\n }\n revision(threadId: string): number {\n return this.revisions.get(threadId) ?? 0;\n }\n touch(threadId: string): void {\n this.revisions.set(threadId, this.revision(threadId) + 1);\n }\n\n thread(\n incoming: ThreadView,\n preserveCurrent = false,\n preserveRuntimeSettings = preserveCurrent,\n ): ThreadView {\n const current = this.threads.get(incoming.id);\n if (!current) {\n const thread = structuredClone(incoming);\n for (const turn of thread.turns) turn.items = turn.items.map((item) => this.limit(item));\n thread.actions = [...this.actions.values()].filter((action) => action.threadId === thread.id);\n this.threads.set(thread.id, thread);\n for (const turn of thread.turns) {\n this.flushAutoReviews(thread.id, turn.id);\n this.finishReconnect(turn);\n settleCompactions(turn);\n if (isTerminal(turn.status)) this.finishAutoReviews(turn);\n }\n } else {\n const oldTurns = current.turns;\n const oldStatus = current.status;\n const oldRuntimeSettings = current.runtimeSettings;\n const oldTitle = current.title;\n Object.assign(current, incoming, { turns: oldTurns, actions: current.actions });\n if (oldRuntimeSettings && incoming.runtimeSettings)\n current.runtimeSettings = { ...oldRuntimeSettings, ...incoming.runtimeSettings };\n if (preserveCurrent) {\n current.status = oldStatus;\n current.title = oldTitle;\n }\n if (preserveRuntimeSettings) current.runtimeSettings = oldRuntimeSettings;\n for (const turn of incoming.turns) this.turn(incoming.id, turn, preserveCurrent);\n }\n this.touch(incoming.id);\n return this.threads.get(incoming.id)!;\n }\n\n turn(threadId: string, incoming: TurnView, preserveCurrent = false): void {\n const thread = this.threads.get(threadId);\n if (!thread) return;\n thread.hasMessages = true;\n const current = thread.turns.find((turn) => turn.id === incoming.id);\n if (!current)\n thread.turns.push({\n ...structuredClone(incoming),\n items: incoming.items.map((item) => this.limit(item)),\n });\n else {\n const items = current.items;\n if (!preserveCurrent) Object.assign(current, incoming, { items });\n for (const item of incoming.items) this.item(threadId, incoming.id, item, preserveCurrent);\n }\n // Tool cancellation can finish after a new turn starts. Only the latest\n // turn owns the thread status; older events still update their own history.\n if (!preserveCurrent && thread.turns.at(-1)?.id === incoming.id)\n thread.status = incoming.status;\n this.flushAutoReviews(threadId, incoming.id);\n const updated = thread.turns.find((turn) => turn.id === incoming.id)!;\n this.finishReconnect(updated);\n settleCompactions(updated);\n if (isTerminal(updated.status)) this.finishAutoReviews(updated);\n this.touch(threadId);\n }\n\n turnError(threadId: string, turnId: string, message: string, willRetry: boolean): boolean {\n const turn = this.threads.get(threadId)?.turns.find((entry) => entry.id === turnId);\n if (!turn || !isActive(turn.status)) return false;\n // Keep display-only diagnostics bounded, without changing the execution reservation.\n const detail = message.slice(0, 4096);\n if (willRetry) turn.reconnect = { status: 'retrying', message: detail };\n else {\n turn.error = detail;\n endCompactions(turn, 'failed', detail);\n if (turn.reconnect) turn.reconnect = { status: 'failed', message: detail };\n }\n this.touch(threadId);\n return true;\n }\n\n compactionCompleted(threadId: string, turnId: string): boolean {\n const turn = this.threads.get(threadId)?.turns.find((entry) => entry.id === turnId);\n if (!turn || !endCompactions(turn, 'completed')) return false;\n this.touch(threadId);\n return true;\n }\n\n endThreadCompactions(threadId: string): void {\n for (const turn of this.threads.get(threadId)?.turns ?? []) endCompactions(turn, 'unknown');\n this.touch(threadId);\n }\n\n turnProgress(threadId: string, turnId: string): boolean {\n const turn = this.threads.get(threadId)?.turns.find((entry) => entry.id === turnId);\n if (!turn || !isActive(turn.status) || turn.reconnect?.status !== 'retrying') return false;\n delete turn.reconnect;\n this.touch(threadId);\n return true;\n }\n\n private finishReconnect(turn: TurnView): void {\n if (!turn.reconnect || !isTerminal(turn.status)) return;\n if (turn.status === 'failed') {\n turn.reconnect = {\n status: 'failed',\n message: (turn.error ?? turn.reconnect.message).slice(0, 4096),\n };\n } else delete turn.reconnect;\n }\n\n autoReview(threadId: string, turnId: string, review: AutoReviewView): boolean {\n const item = this.limit({\n id: `auto-review:${review.reviewId}`,\n type: 'autoApprovalReview',\n status: review.status,\n autoReview: review,\n });\n const turn = this.threads.get(threadId)?.turns.find((entry) => entry.id === turnId);\n if (turn) return this.applyAutoReview(threadId, turn, item);\n const key = JSON.stringify([threadId, turnId, review.reviewId]);\n const current = this.earlyAutoReviews.get(key);\n if (current && !this.reviewCanReplace(current.item, item)) return false;\n const bytes = Buffer.byteLength(JSON.stringify({ threadId, turnId, item }));\n if (current) this.earlyAutoReviewBytes -= current.bytes;\n this.earlyAutoReviews.set(key, { threadId, turnId, item, bytes });\n this.earlyAutoReviewBytes += bytes;\n while (this.earlyAutoReviews.size > 128 || this.earlyAutoReviewBytes > 4 * 1024 * 1024) {\n const oldest = this.earlyAutoReviews.keys().next().value!;\n this.earlyAutoReviewBytes -= this.earlyAutoReviews.get(oldest)!.bytes;\n this.earlyAutoReviews.delete(oldest);\n }\n return false;\n }\n\n private reviewCanReplace(current: ItemView, incoming: ItemView): boolean {\n const previous = current.autoReview!;\n const next = incoming.autoReview!;\n if (previous.status !== 'inProgress' && next.status === 'inProgress') return false;\n if (\n previous.completedAtMs !== undefined &&\n (next.completedAtMs === undefined || previous.completedAtMs > next.completedAtMs)\n )\n return false;\n return JSON.stringify(current) !== JSON.stringify(incoming);\n }\n\n private applyAutoReview(threadId: string, turn: TurnView, item: ItemView): boolean {\n const index = turn.items.findIndex(\n (entry) =>\n entry.type === 'autoApprovalReview' &&\n entry.autoReview?.reviewId === item.autoReview!.reviewId,\n );\n // Check the original event before converting an unfinished late review to unknown.\n if (index >= 0 && !this.reviewCanReplace(turn.items[index]!, item)) return false;\n if (isTerminal(turn.status) && item.autoReview!.status === 'inProgress') {\n item.autoReview!.status = 'unknown';\n item.status = 'unknown';\n }\n if (index < 0) turn.items.push(item);\n else {\n turn.items[index] = item;\n }\n this.touch(threadId);\n return true;\n }\n\n private flushAutoReviews(threadId: string, turnId: string): void {\n const turn = this.threads.get(threadId)?.turns.find((entry) => entry.id === turnId);\n if (!turn) return;\n for (const [key, entry] of this.earlyAutoReviews) {\n if (entry.threadId !== threadId || entry.turnId !== turnId) continue;\n this.earlyAutoReviews.delete(key);\n this.earlyAutoReviewBytes -= entry.bytes;\n this.applyAutoReview(threadId, turn, entry.item);\n }\n }\n\n private finishAutoReviews(turn: TurnView): void {\n for (const item of turn.items) {\n if (item.autoReview?.status !== 'inProgress') continue;\n item.autoReview.status = 'unknown';\n item.status = 'unknown';\n }\n }\n\n item(threadId: string, turnId: string, item: ItemView, preserveCurrent = false): void {\n const turn = this.threads.get(threadId)?.turns.find((entry) => entry.id === turnId);\n if (!turn) return;\n const index = turn.items.findIndex((entry) => entry.id === item.id);\n if (index < 0) turn.items.push(this.limit(item));\n else if (!preserveCurrent)\n turn.items[index] = this.limit(mergeCompaction(item, turn.items[index]));\n settleCompactions(turn);\n this.touch(threadId);\n }\n\n delta(\n threadId: string,\n turnId: string,\n itemId: string,\n field: 'text' | 'output',\n delta: string,\n itemType?: string,\n ): void {\n const turn = this.threads.get(threadId)?.turns.find((entry) => entry.id === turnId);\n if (!turn) return;\n let item = turn.items.find((entry) => entry.id === itemId);\n if (!item) {\n item = {\n id: itemId,\n type: itemType ?? (field === 'text' ? 'agentMessage' : 'commandExecution'),\n };\n turn.items.push(item);\n }\n if (item.truncated) return;\n item[field] = (item[field] ?? '') + delta;\n Object.assign(item, this.limit(item));\n this.touch(threadId);\n }\n\n diff(threadId: string, turnId: string, diff: string): void {\n const turn = this.threads.get(threadId)?.turns.find((entry) => entry.id === turnId);\n if (turn) turn.diff = this.clip(diff).value;\n this.touch(threadId);\n }\n\n action(action: PendingAction): void {\n const owner = action.threadId\n ? this.threads.get(action.threadId)?.turns.find((turn) => turn.id === action.turnId)\n : undefined;\n if (owner && isTerminal(owner.status) && ['pending', 'responding'].includes(action.status))\n action = { ...action, status: 'expired' };\n this.actions.set(action.actionId, action);\n const thread = action.threadId ? this.threads.get(action.threadId) : undefined;\n if (thread) {\n thread.actions = [...this.actions.values()].filter((entry) => entry.threadId === thread.id);\n this.updateActionStatus(thread);\n this.touch(thread.id);\n }\n }\n\n resolveAction(actionId: string, status: PendingAction['status']): PendingAction | undefined {\n const action = this.actions.get(actionId);\n if (!action) return undefined;\n action.status = status;\n const thread = action.threadId ? this.threads.get(action.threadId) : undefined;\n if (thread) {\n this.updateActionStatus(thread);\n this.touch(thread.id);\n }\n return action;\n }\n\n expireTurn(threadId: string, turnId: string): void {\n for (const action of this.actions.values())\n if (\n action.threadId === threadId &&\n action.turnId === turnId &&\n (action.status === 'pending' || action.status === 'responding')\n )\n this.resolveAction(action.actionId, 'expired');\n }\n\n runtimeReset(): void {\n for (const action of this.actions.values())\n if (action.status === 'pending' || action.status === 'responding')\n this.resolveAction(action.actionId, 'expired');\n for (const thread of this.threads.values()) {\n thread.managed = false;\n delete thread.resumeError;\n if (thread.runtimeSettings) {\n const { model, reasoningEffort } = thread.runtimeSettings;\n thread.runtimeSettings = { model, reasoningEffort };\n }\n if (isActive(thread.status)) thread.status = 'unknown';\n for (const turn of thread.turns) {\n if (turn.reconnect?.status === 'retrying') delete turn.reconnect;\n if (isActive(turn.status)) turn.status = 'unknown';\n endCompactions(turn, 'unknown');\n this.finishAutoReviews(turn);\n }\n this.touch(thread.id);\n }\n for (const entry of this.earlyAutoReviews.values()) {\n if (entry.item.autoReview?.status === 'inProgress') {\n this.earlyAutoReviewBytes -= entry.bytes;\n entry.item.autoReview.status = 'unknown';\n entry.item.status = 'unknown';\n entry.bytes = Buffer.byteLength(\n JSON.stringify({ threadId: entry.threadId, turnId: entry.turnId, item: entry.item }),\n );\n this.earlyAutoReviewBytes += entry.bytes;\n }\n }\n }\n\n private updateActionStatus(thread: ThreadView): void {\n const active = thread.actions.filter(\n (action) =>\n action.blocking !== false &&\n (action.status === 'pending' || action.status === 'responding'),\n );\n const turn = [...thread.turns].reverse().find((entry) => isActive(entry.status));\n if (!turn) return;\n const status = active.some(\n (action) => action.kind === 'questions' || action.kind === 'elicitation',\n )\n ? 'waitingInput'\n : active.length > 0\n ? 'waitingApproval'\n : 'running';\n thread.status = status;\n turn.status = status;\n }\n\n private limit(item: ItemView): ItemView {\n const result = structuredClone(item);\n if (result.images) {\n let bytes = 0;\n result.imageCount ??= result.images.length;\n result.images = result.images.filter((image) => {\n bytes += Buffer.byteLength(image);\n return bytes <= this.maxItemBytes;\n });\n }\n for (const field of ['text', 'output', 'command'] as const) {\n const value = result[field];\n if (value) {\n const clipped = this.clip(value);\n result[field] = clipped.value;\n if (clipped.truncated) result.truncated = true;\n }\n }\n if (result.changes)\n result.changes = result.changes.map((change) => {\n const clipped = this.clip(change.diff);\n if (clipped.truncated) result.truncated = true;\n return { ...change, diff: clipped.value };\n });\n if (result.details !== undefined) {\n const clipped = this.clip(JSON.stringify(result.details));\n if (clipped.truncated) {\n result.details = clipped.value;\n result.truncated = true;\n }\n }\n if (result.autoReview) {\n for (const field of ['actionLabel', 'actionSummary', 'cwd', 'rationale'] as const) {\n const value = result.autoReview[field];\n if (value === undefined) continue;\n const clipped = this.clip(value);\n result.autoReview[field] = clipped.value;\n if (clipped.truncated) result.truncated = true;\n }\n }\n return result;\n }\n\n private clip(value: string): { value: string; truncated: boolean } {\n if (Buffer.byteLength(value) <= this.maxItemBytes) return { value, truncated: false };\n // Iterating code points avoids a half UTF-8 character at the truncation boundary.\n let kept = '';\n let bytes = 0;\n for (const point of value) {\n bytes += Buffer.byteLength(point);\n if (bytes > this.maxItemBytes) break;\n kept += point;\n }\n return { value: `${kept}\\n[\u8F93\u51FA\u5DF2\u622A\u65AD]`, truncated: true };\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,230 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { accessSync, constants, realpathSync, statSync } from "node:fs";
4
+ import { basename, isAbsolute, relative, resolve, sep } from "node:path";
5
+ import { asc } from "drizzle-orm";
6
+ import { AppError } from "../shared/errors.js";
7
+ import { createProjectSchema, renameProjectSchema } from "../shared/validation.js";
8
+ import { projectRegistrations } from "./db/schema.js";
9
+ function containsPath(root, candidate) {
10
+ const difference = relative(root, candidate);
11
+ return difference === "" || difference !== ".." && !difference.startsWith(`..${sep}`) && !isAbsolute(difference);
12
+ }
13
+ function checkoutRoot(cwd) {
14
+ try {
15
+ const output = execFileSync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
16
+ encoding: "utf8",
17
+ timeout: 3e3,
18
+ stdio: ["ignore", "pipe", "ignore"]
19
+ }).trim();
20
+ return output ? realpathSync(output) : void 0;
21
+ } catch {
22
+ return void 0;
23
+ }
24
+ }
25
+ class ProjectRegistry {
26
+ constructor(configs, database) {
27
+ this.database = database;
28
+ for (const row of database?.select().from(projectRegistrations).orderBy(asc(projectRegistrations.createdAt), asc(projectRegistrations.id)).all() ?? [])
29
+ this.registered.set(row.id, row);
30
+ const seeds = [];
31
+ for (const config of configs) {
32
+ const path = resolve(config.path);
33
+ const existing = this.registered.get(config.id);
34
+ if (existing) {
35
+ if (existing.path !== path)
36
+ throw new AppError(
37
+ "PROJECT_ID_CONFLICT",
38
+ `\u9879\u76EE ${config.id} \u5DF2\u7ED1\u5B9A\u5176\u4ED6\u76EE\u5F55\uFF1B\u9879\u76EE ID \u548C\u76EE\u5F55\u4E0D\u53EF\u4FEE\u6539\uFF0C\u8BF7\u4F7F\u7528\u65B0 ID`,
39
+ 409,
40
+ { projectId: config.id }
41
+ );
42
+ continue;
43
+ }
44
+ let canonicalRoot = null;
45
+ try {
46
+ canonicalRoot = this.directory(path);
47
+ } catch {
48
+ }
49
+ const now = this.nextCreatedAt();
50
+ const row = {
51
+ ...config,
52
+ path,
53
+ canonicalRoot,
54
+ removedAt: null,
55
+ createdAt: now,
56
+ updatedAt: now
57
+ };
58
+ this.registered.set(config.id, row);
59
+ seeds.push(row);
60
+ }
61
+ if (seeds.length) database?.insert(projectRegistrations).values(seeds).run();
62
+ }
63
+ database;
64
+ registered = /* @__PURE__ */ new Map();
65
+ nextCreatedAt() {
66
+ return [...this.registered.values()].reduce(
67
+ (next, row) => Math.max(next, row.createdAt + 1),
68
+ Date.now()
69
+ );
70
+ }
71
+ directory(path) {
72
+ if (!isAbsolute(path) || path.includes("\0"))
73
+ throw new AppError("INVALID_PROJECT_PATH", "\u8BF7\u8F93\u5165\u8FD0\u884C Codex \u7684\u7535\u8111\u4E0A\u7684\u7EDD\u5BF9\u76EE\u5F55\u8DEF\u5F84", 400);
74
+ try {
75
+ const canonicalRoot = realpathSync(path);
76
+ if (!statSync(canonicalRoot).isDirectory())
77
+ throw new AppError("PROJECT_UNAVAILABLE", "\u9879\u76EE\u8DEF\u5F84\u4E0D\u662F\u76EE\u5F55", 409);
78
+ accessSync(canonicalRoot, constants.R_OK | constants.X_OK);
79
+ return canonicalRoot;
80
+ } catch (error) {
81
+ if (error instanceof AppError) throw error;
82
+ const code = error.code;
83
+ throw new AppError(
84
+ "PROJECT_UNAVAILABLE",
85
+ code === "ENOENT" || code === "ENOTDIR" ? "\u9879\u76EE\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1B\u8BF7\u8F93\u5165\u8FD0\u884C Codex \u7684\u7535\u8111\u4E0A\u7684\u76EE\u5F55" : code === "EACCES" || code === "EPERM" ? "\u9879\u76EE\u76EE\u5F55\u4E0D\u53EF\u8BBF\u95EE\uFF0C\u9700\u8981\u8BFB\u53D6\u548C\u8FDB\u5165\u76EE\u5F55\u7684\u6743\u9650" : "\u9879\u76EE\u76EE\u5F55\u4E0D\u53EF\u7528\uFF0C\u8BF7\u68C0\u67E5\u8DEF\u5F84\u548C\u8BBF\u95EE\u6743\u9650",
86
+ 409
87
+ );
88
+ }
89
+ }
90
+ save(row) {
91
+ this.database?.insert(projectRegistrations).values(row).onConflictDoUpdate({ target: projectRegistrations.id, set: row }).run();
92
+ this.registered.set(row.id, row);
93
+ }
94
+ registration(id) {
95
+ const row = this.registered.get(id);
96
+ if (!row || row.removedAt !== null)
97
+ throw new AppError("PROJECT_NOT_FOUND", "\u9879\u76EE\u672A\u6CE8\u518C\u6216\u5DF2\u79FB\u9664", 404);
98
+ return row;
99
+ }
100
+ inspect(config) {
101
+ const rootPath = config.path;
102
+ try {
103
+ const canonicalRoot = this.directory(rootPath);
104
+ if (config.canonicalRoot !== null && canonicalRoot !== config.canonicalRoot)
105
+ throw new AppError(
106
+ "PROJECT_UNAVAILABLE",
107
+ "\u9879\u76EE\u76EE\u5F55\u7684\u771F\u5B9E\u8DEF\u5F84\u5DF2\u6539\u53D8\uFF0C\u4E0D\u80FD\u6CBF\u7528\u539F\u9879\u76EE\u8EAB\u4EFD",
108
+ 409
109
+ );
110
+ if (config.canonicalRoot === null)
111
+ this.save({ ...config, canonicalRoot, updatedAt: Date.now() });
112
+ return {
113
+ id: config.id,
114
+ name: config.name,
115
+ rootPath,
116
+ canonicalRoot,
117
+ workspaceKey: checkoutRoot(canonicalRoot) ?? canonicalRoot,
118
+ available: true
119
+ };
120
+ } catch (error) {
121
+ return {
122
+ id: config.id,
123
+ name: config.name,
124
+ rootPath,
125
+ canonicalRoot: config.canonicalRoot ?? rootPath,
126
+ workspaceKey: config.canonicalRoot ?? rootPath,
127
+ available: false,
128
+ error: error instanceof Error ? error.message : "\u9879\u76EE\u4E0D\u53EF\u7528"
129
+ };
130
+ }
131
+ }
132
+ list() {
133
+ return [...this.registered.values()].filter((project) => project.removedAt === null).map((project) => this.inspect(project));
134
+ }
135
+ /** Also usable when a saved directory is missing, so it can still be managed. */
136
+ describe(id) {
137
+ return this.inspect(this.registration(id));
138
+ }
139
+ add(input) {
140
+ const parsed = createProjectSchema.parse(input);
141
+ const canonicalRoot = this.directory(parsed.path);
142
+ this.list();
143
+ for (const row2 of this.registered.values()) {
144
+ if (row2.canonicalRoot !== null || row2.removedAt === null) continue;
145
+ try {
146
+ this.save({ ...row2, canonicalRoot: this.directory(row2.path), updatedAt: Date.now() });
147
+ } catch {
148
+ }
149
+ }
150
+ const matching = [...this.registered.values()].filter(
151
+ (row2) => row2.canonicalRoot === canonicalRoot
152
+ );
153
+ const removed = matching.find((row2) => row2.removedAt !== null);
154
+ const now = Date.now();
155
+ if (removed) {
156
+ const restored = { ...removed, removedAt: null, updatedAt: now };
157
+ const project = this.inspect(restored);
158
+ if (!project.available)
159
+ throw new AppError("PROJECT_UNAVAILABLE", project.error ?? "\u539F\u9879\u76EE\u76EE\u5F55\u4E0D\u53EF\u7528", 409);
160
+ this.save({ ...restored, name: parsed.name ?? removed.name });
161
+ return this.get(removed.id);
162
+ }
163
+ const existing = matching[0];
164
+ if (existing)
165
+ throw new AppError("PROJECT_EXISTS", "\u8BE5\u76EE\u5F55\u5DF2\u6DFB\u52A0\uFF0C\u8BF7\u9009\u62E9\u5DF2\u6709\u9879\u76EE", 409, {
166
+ projectId: existing.id
167
+ });
168
+ const row = {
169
+ id: randomUUID(),
170
+ name: parsed.name ?? (basename(canonicalRoot) || canonicalRoot),
171
+ path: canonicalRoot,
172
+ canonicalRoot,
173
+ removedAt: null,
174
+ createdAt: this.nextCreatedAt(),
175
+ updatedAt: now
176
+ };
177
+ this.save(row);
178
+ return this.get(row.id);
179
+ }
180
+ rename(id, input) {
181
+ const { name } = renameProjectSchema.parse(input);
182
+ this.save({ ...this.registration(id), name, updatedAt: Date.now() });
183
+ return this.describe(id);
184
+ }
185
+ remove(id) {
186
+ const now = Date.now();
187
+ this.save({ ...this.registration(id), removedAt: now, updatedAt: now });
188
+ }
189
+ get(id) {
190
+ const project = this.describe(id);
191
+ if (!project.available)
192
+ throw new AppError("PROJECT_UNAVAILABLE", "\u9879\u76EE\u76EE\u5F55\u4E0D\u53EF\u7528", 409, { projectId: id });
193
+ return project;
194
+ }
195
+ assertOwnership(projectId, cwd) {
196
+ const project = this.get(projectId);
197
+ let canonicalCwd;
198
+ try {
199
+ canonicalCwd = realpathSync(cwd);
200
+ if (!statSync(canonicalCwd).isDirectory()) throw new Error("not a directory");
201
+ } catch {
202
+ throw new AppError("PROJECT_UNAVAILABLE", "\u4F1A\u8BDD\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u53EF\u7528", 409);
203
+ }
204
+ if (!containsPath(project.canonicalRoot, canonicalCwd))
205
+ throw new AppError("PROJECT_OWNERSHIP", "\u4F1A\u8BDD\u4E0D\u5C5E\u4E8E\u6240\u9009\u9879\u76EE\u76EE\u5F55", 403);
206
+ return {
207
+ project,
208
+ cwd: canonicalCwd,
209
+ workspaceKey: checkoutRoot(canonicalCwd) ?? project.canonicalRoot
210
+ };
211
+ }
212
+ findOwner(cwd) {
213
+ let canonicalCwd;
214
+ try {
215
+ canonicalCwd = realpathSync(cwd);
216
+ } catch {
217
+ throw new AppError("PROJECT_UNAVAILABLE", "\u4F1A\u8BDD\u76EE\u5F55\u4E0D\u53EF\u7528", 409);
218
+ }
219
+ const project = [...this.registered.values()].map((entry) => ({ project: this.inspect(entry), removed: entry.removedAt !== null })).filter((entry) => containsPath(entry.project.canonicalRoot, canonicalCwd)).sort(
220
+ (a, b) => b.project.canonicalRoot.length - a.project.canonicalRoot.length || Number(a.removed) - Number(b.removed)
221
+ )[0]?.project;
222
+ if (!project) throw new AppError("PROJECT_OWNERSHIP", "\u4F1A\u8BDD\u4E0D\u5C5E\u4E8E\u5DF2\u6CE8\u518C\u9879\u76EE", 403);
223
+ return this.assertOwnership(project.id, canonicalCwd);
224
+ }
225
+ }
226
+ export {
227
+ ProjectRegistry,
228
+ containsPath
229
+ };
230
+ //# sourceMappingURL=projects.js.map