@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.
- package/LICENSE +21 -0
- package/README.md +173 -2
- package/README.zh-CN.md +174 -0
- package/bin/covro.mjs +8 -0
- package/dist/cli/control.js +66 -0
- package/dist/cli/control.js.map +7 -0
- package/dist/cli/daemon.js +118 -0
- package/dist/cli/daemon.js.map +7 -0
- package/dist/cli/main.js +152 -0
- package/dist/cli/main.js.map +7 -0
- package/dist/cli/paths.js +60 -0
- package/dist/cli/paths.js.map +7 -0
- package/dist/cli/service.js +87 -0
- package/dist/cli/service.js.map +7 -0
- package/dist/codex/adapter.js +1195 -0
- package/dist/codex/adapter.js.map +7 -0
- package/dist/codex/auto-review.js +102 -0
- package/dist/codex/auto-review.js.map +7 -0
- package/dist/codex/compatibility.js +7 -0
- package/dist/codex/compatibility.js.map +7 -0
- package/dist/codex/mapping.js +210 -0
- package/dist/codex/mapping.js.map +7 -0
- package/dist/codex/permissions.js +51 -0
- package/dist/codex/permissions.js.map +7 -0
- package/dist/codex/thread-title-policy.js +52 -0
- package/dist/codex/thread-title-policy.js.map +7 -0
- package/dist/codex/thread-titles.js +259 -0
- package/dist/codex/thread-titles.js.map +7 -0
- package/dist/codex/transport.js +236 -0
- package/dist/codex/transport.js.map +7 -0
- package/dist/codex/turn-token-usage.js +46 -0
- package/dist/codex/turn-token-usage.js.map +7 -0
- package/dist/codex/types.js +1 -0
- package/dist/codex/types.js.map +7 -0
- package/dist/drizzle/0000_past_wildside.sql +45 -0
- package/dist/drizzle/0001_shocking_ender_wiggin.sql +2 -0
- package/dist/drizzle/0002_allow_parallel_threads.sql +1 -0
- package/dist/drizzle/0003_project_registrations.sql +11 -0
- package/dist/drizzle/0004_turn_usage.sql +6 -0
- package/dist/drizzle/meta/0000_snapshot.json +309 -0
- package/dist/drizzle/meta/0001_snapshot.json +323 -0
- package/dist/drizzle/meta/0002_snapshot.json +315 -0
- package/dist/drizzle/meta/0003_snapshot.json +381 -0
- package/dist/drizzle/meta/0004_snapshot.json +420 -0
- package/dist/drizzle/meta/_journal.json +41 -0
- package/dist/server/auth.js +47 -0
- package/dist/server/auth.js.map +7 -0
- package/dist/server/compaction.js +40 -0
- package/dist/server/compaction.js.map +7 -0
- package/dist/server/config-store.js +147 -0
- package/dist/server/config-store.js.map +7 -0
- package/dist/server/config.js +124 -0
- package/dist/server/config.js.map +7 -0
- package/dist/server/db/index.js +21 -0
- package/dist/server/db/index.js.map +7 -0
- package/dist/server/db/migrate.js +26 -0
- package/dist/server/db/migrate.js.map +7 -0
- package/dist/server/db/operations.js +201 -0
- package/dist/server/db/operations.js.map +7 -0
- package/dist/server/db/schema.js +99 -0
- package/dist/server/db/schema.js.map +7 -0
- package/dist/server/db/turn-usage.js +31 -0
- package/dist/server/db/turn-usage.js.map +7 -0
- package/dist/server/events.js +123 -0
- package/dist/server/events.js.map +7 -0
- package/dist/server/http.js +305 -0
- package/dist/server/http.js.map +7 -0
- package/dist/server/images.js +49 -0
- package/dist/server/images.js.map +7 -0
- package/dist/server/instance-lock.js +25 -0
- package/dist/server/instance-lock.js.map +7 -0
- package/dist/server/local-files.js +100 -0
- package/dist/server/local-files.js.map +7 -0
- package/dist/server/main.js +125 -0
- package/dist/server/main.js.map +7 -0
- package/dist/server/markdown.js +28 -0
- package/dist/server/markdown.js.map +7 -0
- package/dist/server/projection.js +348 -0
- package/dist/server/projection.js.map +7 -0
- package/dist/server/projects.js +230 -0
- package/dist/server/projects.js.map +7 -0
- package/dist/server/service.js +1221 -0
- package/dist/server/service.js.map +7 -0
- package/dist/server/snapshot.js +98 -0
- package/dist/server/snapshot.js.map +7 -0
- package/dist/server/thread-export.js +97 -0
- package/dist/server/thread-export.js.map +7 -0
- package/dist/server/websocket.js +126 -0
- package/dist/server/websocket.js.map +7 -0
- package/dist/shared/async-questions.js +39 -0
- package/dist/shared/async-questions.js.map +7 -0
- package/dist/shared/codex-compatibility.js +56 -0
- package/dist/shared/codex-compatibility.js.map +7 -0
- package/dist/shared/elicitation.js +314 -0
- package/dist/shared/elicitation.js.map +7 -0
- package/dist/shared/errors.js +24 -0
- package/dist/shared/errors.js.map +7 -0
- package/dist/shared/gateway-config.js +1 -0
- package/dist/shared/gateway-config.js.map +7 -0
- package/dist/shared/images.js +21 -0
- package/dist/shared/images.js.map +7 -0
- package/dist/shared/package.js +5 -0
- package/dist/shared/package.js.map +7 -0
- package/dist/shared/permissions.js +63 -0
- package/dist/shared/permissions.js.map +7 -0
- package/dist/shared/thread-name.js +14 -0
- package/dist/shared/thread-name.js.map +7 -0
- package/dist/shared/thread-summary.js +24 -0
- package/dist/shared/thread-summary.js.map +7 -0
- package/dist/shared/types.js +1 -0
- package/dist/shared/types.js.map +7 -0
- package/dist/shared/updates.js +7 -0
- package/dist/shared/updates.js.map +7 -0
- package/dist/shared/validation.js +116 -0
- package/dist/shared/validation.js.map +7 -0
- package/dist/shared/versions.js +36 -0
- package/dist/shared/versions.js.map +7 -0
- package/dist/update-worker.cjs +21968 -0
- package/dist/updates/files.js +54 -0
- package/dist/updates/files.js.map +7 -0
- package/dist/updates/manager.js +315 -0
- package/dist/updates/manager.js.map +7 -0
- package/dist/updates/registry.js +71 -0
- package/dist/updates/registry.js.map +7 -0
- package/dist/updates/transaction.js +96 -0
- package/dist/updates/transaction.js.map +7 -0
- package/dist/updates/worker.js +345 -0
- package/dist/updates/worker.js.map +7 -0
- package/dist/web/assets/index-BW9afH_Y.js +83 -0
- package/dist/web/assets/index-L3S1ZiO0.css +1 -0
- package/dist/web/covro.svg +6 -0
- package/dist/web/index.html +15 -0
- 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
|