@h-rig/server 0.0.6-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/src/bootstrap.js +161 -0
- package/dist/src/index.js +13153 -0
- package/dist/src/inspector/agent-runtime.js +1077 -0
- package/dist/src/inspector/analysis.js +41 -0
- package/dist/src/inspector/discovery.js +137 -0
- package/dist/src/inspector/journal.js +518 -0
- package/dist/src/inspector/mission.js +562 -0
- package/dist/src/inspector/prompt.js +97 -0
- package/dist/src/inspector/provider-session.js +65 -0
- package/dist/src/inspector/reconcile.js +118 -0
- package/dist/src/inspector/review.js +13 -0
- package/dist/src/inspector/service.js +1759 -0
- package/dist/src/inspector/skills.js +155 -0
- package/dist/src/inspector/tools.js +1592 -0
- package/dist/src/inspector/types.js +1 -0
- package/dist/src/inspector/upstream-sync.js +479 -0
- package/dist/src/orchestration.js +402 -0
- package/dist/src/remote.js +123 -0
- package/dist/src/scheduler.js +84 -0
- package/dist/src/server-helpers/broadcasters.js +161 -0
- package/dist/src/server-helpers/conversation-snapshot.js +382 -0
- package/dist/src/server-helpers/event-emitter.js +41 -0
- package/dist/src/server-helpers/github-auth-store.js +155 -0
- package/dist/src/server-helpers/github-credentials.js +38 -0
- package/dist/src/server-helpers/github-project-status-sync.js +196 -0
- package/dist/src/server-helpers/github-projects.js +147 -0
- package/dist/src/server-helpers/github-reconciler.js +89 -0
- package/dist/src/server-helpers/http-router.js +3781 -0
- package/dist/src/server-helpers/http-utils.js +135 -0
- package/dist/src/server-helpers/inspector-agent-lifecycle.js +104 -0
- package/dist/src/server-helpers/inspector-jobs.js +4145 -0
- package/dist/src/server-helpers/issue-analysis.js +362 -0
- package/dist/src/server-helpers/normalizers.js +31 -0
- package/dist/src/server-helpers/notifications.js +96 -0
- package/dist/src/server-helpers/orchestration-ops.js +287 -0
- package/dist/src/server-helpers/orchestration.js +39 -0
- package/dist/src/server-helpers/plugin-host-cache.js +86 -0
- package/dist/src/server-helpers/project-fs-ops.js +194 -0
- package/dist/src/server-helpers/project-registry.js +124 -0
- package/dist/src/server-helpers/queue-state.js +78 -0
- package/dist/src/server-helpers/remote-checkout.js +140 -0
- package/dist/src/server-helpers/remote-snapshots.js +119 -0
- package/dist/src/server-helpers/run-io.js +262 -0
- package/dist/src/server-helpers/run-mutations.js +1784 -0
- package/dist/src/server-helpers/run-steering.js +176 -0
- package/dist/src/server-helpers/run-writers.js +75 -0
- package/dist/src/server-helpers/server-paths.js +27 -0
- package/dist/src/server-helpers/snapshot-orchestrator.js +832 -0
- package/dist/src/server-helpers/snapshot-service.js +1143 -0
- package/dist/src/server-helpers/summaries.js +126 -0
- package/dist/src/server-helpers/task-config.js +50 -0
- package/dist/src/server-helpers/task-projection.js +98 -0
- package/dist/src/server-helpers/terminal-runtime.js +156 -0
- package/dist/src/server-helpers/terminal-sessions.js +22 -0
- package/dist/src/server-helpers/validation-failure.js +31 -0
- package/dist/src/server-helpers/ws-router.js +1308 -0
- package/dist/src/server.js +12628 -0
- package/dist/src/websocket.js +63 -0
- package/package.json +33 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/server/src/server-helpers/normalizers.ts
|
|
3
|
+
function normalizeString(value) {
|
|
4
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// packages/server/src/server-helpers/orchestration.ts
|
|
8
|
+
function createEmptyOrchestrationProject(input) {
|
|
9
|
+
return {
|
|
10
|
+
id: input.id,
|
|
11
|
+
title: input.title,
|
|
12
|
+
workspaceRoot: input.workspaceRoot,
|
|
13
|
+
defaultModel: input.defaultModel,
|
|
14
|
+
scripts: [],
|
|
15
|
+
createdAt: input.createdAt,
|
|
16
|
+
updatedAt: input.createdAt,
|
|
17
|
+
deletedAt: null
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function createEmptyOrchestrationThread(input) {
|
|
21
|
+
return {
|
|
22
|
+
id: input.id,
|
|
23
|
+
projectId: input.projectId,
|
|
24
|
+
title: input.title,
|
|
25
|
+
model: input.model,
|
|
26
|
+
runtimeMode: input.runtimeMode,
|
|
27
|
+
interactionMode: input.interactionMode,
|
|
28
|
+
branch: input.branch,
|
|
29
|
+
worktreePath: input.worktreePath,
|
|
30
|
+
latestTurn: null,
|
|
31
|
+
createdAt: input.createdAt,
|
|
32
|
+
updatedAt: input.createdAt,
|
|
33
|
+
deletedAt: null,
|
|
34
|
+
messages: [],
|
|
35
|
+
proposedPlans: [],
|
|
36
|
+
activities: [],
|
|
37
|
+
checkpoints: [],
|
|
38
|
+
session: null
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// packages/server/src/server-helpers/orchestration-ops.ts
|
|
43
|
+
function buildOrchestrationSnapshot(state) {
|
|
44
|
+
return {
|
|
45
|
+
snapshotSequence: Math.max(0, state.sequence),
|
|
46
|
+
projects: state.orchestration.projects,
|
|
47
|
+
threads: state.orchestration.threads,
|
|
48
|
+
updatedAt: new Date().toISOString()
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function findOrchestrationProject(state, projectId) {
|
|
52
|
+
return state.orchestration.projects.find((project) => project.id === projectId) ?? null;
|
|
53
|
+
}
|
|
54
|
+
function findOrchestrationThread(state, threadId) {
|
|
55
|
+
return state.orchestration.threads.find((thread) => thread.id === threadId) ?? null;
|
|
56
|
+
}
|
|
57
|
+
function appendOrchestrationActivity(thread, input) {
|
|
58
|
+
const activities = Array.isArray(thread.activities) ? [...thread.activities] : [];
|
|
59
|
+
activities.push({
|
|
60
|
+
id: `activity-${Date.now()}-${activities.length + 1}`,
|
|
61
|
+
tone: input.kind.includes("error") ? "error" : input.kind.includes("approval") ? "approval" : "info",
|
|
62
|
+
kind: input.kind,
|
|
63
|
+
summary: input.summary,
|
|
64
|
+
payload: input.payload ?? {},
|
|
65
|
+
turnId: input.turnId ?? null,
|
|
66
|
+
createdAt: input.createdAt
|
|
67
|
+
});
|
|
68
|
+
thread.activities = activities;
|
|
69
|
+
thread.updatedAt = input.createdAt;
|
|
70
|
+
}
|
|
71
|
+
function applyOrchestrationCommand(state, command) {
|
|
72
|
+
const type = normalizeString(command.type);
|
|
73
|
+
if (!type) {
|
|
74
|
+
throw new Error("command.type is required");
|
|
75
|
+
}
|
|
76
|
+
switch (type) {
|
|
77
|
+
case "project.create": {
|
|
78
|
+
const projectId = normalizeString(command.projectId);
|
|
79
|
+
const title = normalizeString(command.title);
|
|
80
|
+
const workspaceRoot = normalizeString(command.workspaceRoot);
|
|
81
|
+
const createdAt = normalizeString(command.createdAt) ?? new Date().toISOString();
|
|
82
|
+
if (!projectId || !title || !workspaceRoot) {
|
|
83
|
+
throw new Error("project.create requires projectId, title, and workspaceRoot");
|
|
84
|
+
}
|
|
85
|
+
const existing = findOrchestrationProject(state, projectId);
|
|
86
|
+
if (existing) {
|
|
87
|
+
existing.title = title;
|
|
88
|
+
existing.workspaceRoot = workspaceRoot;
|
|
89
|
+
existing.defaultModel = normalizeString(command.defaultModel);
|
|
90
|
+
existing.updatedAt = createdAt;
|
|
91
|
+
existing.deletedAt = null;
|
|
92
|
+
} else {
|
|
93
|
+
state.orchestration.projects.push(createEmptyOrchestrationProject({
|
|
94
|
+
id: projectId,
|
|
95
|
+
title,
|
|
96
|
+
workspaceRoot,
|
|
97
|
+
defaultModel: normalizeString(command.defaultModel),
|
|
98
|
+
createdAt
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
case "project.meta.update": {
|
|
104
|
+
const projectId = normalizeString(command.projectId);
|
|
105
|
+
const project = projectId ? findOrchestrationProject(state, projectId) : null;
|
|
106
|
+
if (!project)
|
|
107
|
+
return;
|
|
108
|
+
const nextTitle = normalizeString(command.title);
|
|
109
|
+
const nextWorkspaceRoot = normalizeString(command.workspaceRoot);
|
|
110
|
+
if (nextTitle)
|
|
111
|
+
project.title = nextTitle;
|
|
112
|
+
if (nextWorkspaceRoot)
|
|
113
|
+
project.workspaceRoot = nextWorkspaceRoot;
|
|
114
|
+
if ("defaultModel" in command)
|
|
115
|
+
project.defaultModel = normalizeString(command.defaultModel);
|
|
116
|
+
if (Array.isArray(command.scripts))
|
|
117
|
+
project.scripts = command.scripts;
|
|
118
|
+
project.updatedAt = new Date().toISOString();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
case "project.delete": {
|
|
122
|
+
const projectId = normalizeString(command.projectId);
|
|
123
|
+
if (!projectId)
|
|
124
|
+
return;
|
|
125
|
+
state.orchestration.projects = state.orchestration.projects.filter((project) => project.id !== projectId);
|
|
126
|
+
state.orchestration.threads = state.orchestration.threads.filter((thread) => thread.projectId !== projectId);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
case "thread.create": {
|
|
130
|
+
const threadId = normalizeString(command.threadId);
|
|
131
|
+
const projectId = normalizeString(command.projectId);
|
|
132
|
+
const title = normalizeString(command.title);
|
|
133
|
+
const model = normalizeString(command.model);
|
|
134
|
+
const runtimeMode = normalizeString(command.runtimeMode) ?? "full-access";
|
|
135
|
+
const interactionMode = normalizeString(command.interactionMode) ?? "default";
|
|
136
|
+
const createdAt = normalizeString(command.createdAt) ?? new Date().toISOString();
|
|
137
|
+
if (!threadId || !projectId || !title || !model) {
|
|
138
|
+
throw new Error("thread.create requires threadId, projectId, title, and model");
|
|
139
|
+
}
|
|
140
|
+
const existing = findOrchestrationThread(state, threadId);
|
|
141
|
+
if (existing) {
|
|
142
|
+
existing.title = title;
|
|
143
|
+
existing.model = model;
|
|
144
|
+
existing.runtimeMode = runtimeMode;
|
|
145
|
+
existing.interactionMode = interactionMode;
|
|
146
|
+
existing.branch = normalizeString(command.branch);
|
|
147
|
+
existing.worktreePath = normalizeString(command.worktreePath);
|
|
148
|
+
existing.updatedAt = createdAt;
|
|
149
|
+
existing.deletedAt = null;
|
|
150
|
+
} else {
|
|
151
|
+
state.orchestration.threads.push(createEmptyOrchestrationThread({
|
|
152
|
+
id: threadId,
|
|
153
|
+
projectId,
|
|
154
|
+
title,
|
|
155
|
+
model,
|
|
156
|
+
runtimeMode,
|
|
157
|
+
interactionMode,
|
|
158
|
+
branch: normalizeString(command.branch),
|
|
159
|
+
worktreePath: normalizeString(command.worktreePath),
|
|
160
|
+
createdAt
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
case "thread.meta.update": {
|
|
166
|
+
const threadId = normalizeString(command.threadId);
|
|
167
|
+
const thread = threadId ? findOrchestrationThread(state, threadId) : null;
|
|
168
|
+
if (!thread)
|
|
169
|
+
return;
|
|
170
|
+
const nextTitle = normalizeString(command.title);
|
|
171
|
+
const nextModel = normalizeString(command.model);
|
|
172
|
+
if (nextTitle)
|
|
173
|
+
thread.title = nextTitle;
|
|
174
|
+
if (nextModel)
|
|
175
|
+
thread.model = nextModel;
|
|
176
|
+
if ("branch" in command)
|
|
177
|
+
thread.branch = normalizeString(command.branch);
|
|
178
|
+
if ("worktreePath" in command)
|
|
179
|
+
thread.worktreePath = normalizeString(command.worktreePath);
|
|
180
|
+
thread.updatedAt = new Date().toISOString();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
case "thread.runtime-mode.set":
|
|
184
|
+
case "thread.interaction-mode.set":
|
|
185
|
+
case "thread.session.stop":
|
|
186
|
+
case "thread.turn.start":
|
|
187
|
+
case "thread.turn.interrupt":
|
|
188
|
+
case "thread.approval.respond":
|
|
189
|
+
case "thread.user-input.respond":
|
|
190
|
+
case "thread.checkpoint.revert": {
|
|
191
|
+
const threadId = normalizeString(command.threadId);
|
|
192
|
+
const thread = threadId ? findOrchestrationThread(state, threadId) : null;
|
|
193
|
+
if (!thread)
|
|
194
|
+
return;
|
|
195
|
+
const createdAt = normalizeString(command.createdAt) ?? new Date().toISOString();
|
|
196
|
+
if (type === "thread.runtime-mode.set") {
|
|
197
|
+
thread.runtimeMode = normalizeString(command.runtimeMode) ?? thread.runtimeMode;
|
|
198
|
+
}
|
|
199
|
+
if (type === "thread.interaction-mode.set") {
|
|
200
|
+
thread.interactionMode = normalizeString(command.interactionMode) ?? thread.interactionMode;
|
|
201
|
+
}
|
|
202
|
+
if (type === "thread.turn.start") {
|
|
203
|
+
const message = command.message && typeof command.message === "object" ? command.message : null;
|
|
204
|
+
const turnId = `turn-${Date.now()}`;
|
|
205
|
+
const messages = Array.isArray(thread.messages) ? [...thread.messages] : [];
|
|
206
|
+
if (message) {
|
|
207
|
+
messages.push({
|
|
208
|
+
id: normalizeString(message.messageId) ?? `message-${Date.now()}`,
|
|
209
|
+
role: "user",
|
|
210
|
+
text: typeof message.text === "string" ? message.text : "",
|
|
211
|
+
attachments: Array.isArray(message.attachments) ? message.attachments : [],
|
|
212
|
+
turnId,
|
|
213
|
+
streaming: false,
|
|
214
|
+
createdAt,
|
|
215
|
+
updatedAt: createdAt
|
|
216
|
+
});
|
|
217
|
+
thread.messages = messages;
|
|
218
|
+
}
|
|
219
|
+
thread.latestTurn = {
|
|
220
|
+
turnId,
|
|
221
|
+
state: "running",
|
|
222
|
+
requestedAt: createdAt,
|
|
223
|
+
startedAt: createdAt,
|
|
224
|
+
completedAt: null,
|
|
225
|
+
assistantMessageId: null
|
|
226
|
+
};
|
|
227
|
+
thread.session = {
|
|
228
|
+
threadId,
|
|
229
|
+
status: "running",
|
|
230
|
+
providerName: normalizeString(command.provider),
|
|
231
|
+
runtimeMode: normalizeString(command.runtimeMode) ?? thread.runtimeMode ?? "full-access",
|
|
232
|
+
activeTurnId: turnId,
|
|
233
|
+
lastError: null,
|
|
234
|
+
updatedAt: createdAt
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (type === "thread.turn.interrupt") {
|
|
238
|
+
if (thread.latestTurn && typeof thread.latestTurn === "object") {
|
|
239
|
+
thread.latestTurn = {
|
|
240
|
+
...thread.latestTurn,
|
|
241
|
+
state: "interrupted",
|
|
242
|
+
completedAt: createdAt
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (thread.session && typeof thread.session === "object") {
|
|
246
|
+
thread.session = {
|
|
247
|
+
...thread.session,
|
|
248
|
+
status: "interrupted",
|
|
249
|
+
updatedAt: createdAt
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (type === "thread.session.stop" && thread.session && typeof thread.session === "object") {
|
|
254
|
+
thread.session = {
|
|
255
|
+
...thread.session,
|
|
256
|
+
status: "stopped",
|
|
257
|
+
activeTurnId: null,
|
|
258
|
+
updatedAt: createdAt
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
appendOrchestrationActivity(thread, {
|
|
262
|
+
kind: type,
|
|
263
|
+
summary: type.replace(/^thread\./, "").replaceAll(".", " "),
|
|
264
|
+
createdAt,
|
|
265
|
+
turnId: thread.latestTurn && typeof thread.latestTurn === "object" ? normalizeString(thread.latestTurn.turnId) : null,
|
|
266
|
+
payload: command
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
case "thread.delete": {
|
|
271
|
+
const threadId = normalizeString(command.threadId);
|
|
272
|
+
if (!threadId)
|
|
273
|
+
return;
|
|
274
|
+
state.orchestration.threads = state.orchestration.threads.filter((thread) => thread.id !== threadId);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
default:
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
export {
|
|
282
|
+
findOrchestrationThread,
|
|
283
|
+
findOrchestrationProject,
|
|
284
|
+
buildOrchestrationSnapshot,
|
|
285
|
+
applyOrchestrationCommand,
|
|
286
|
+
appendOrchestrationActivity
|
|
287
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/server/src/server-helpers/orchestration.ts
|
|
3
|
+
function createEmptyOrchestrationProject(input) {
|
|
4
|
+
return {
|
|
5
|
+
id: input.id,
|
|
6
|
+
title: input.title,
|
|
7
|
+
workspaceRoot: input.workspaceRoot,
|
|
8
|
+
defaultModel: input.defaultModel,
|
|
9
|
+
scripts: [],
|
|
10
|
+
createdAt: input.createdAt,
|
|
11
|
+
updatedAt: input.createdAt,
|
|
12
|
+
deletedAt: null
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function createEmptyOrchestrationThread(input) {
|
|
16
|
+
return {
|
|
17
|
+
id: input.id,
|
|
18
|
+
projectId: input.projectId,
|
|
19
|
+
title: input.title,
|
|
20
|
+
model: input.model,
|
|
21
|
+
runtimeMode: input.runtimeMode,
|
|
22
|
+
interactionMode: input.interactionMode,
|
|
23
|
+
branch: input.branch,
|
|
24
|
+
worktreePath: input.worktreePath,
|
|
25
|
+
latestTurn: null,
|
|
26
|
+
createdAt: input.createdAt,
|
|
27
|
+
updatedAt: input.createdAt,
|
|
28
|
+
deletedAt: null,
|
|
29
|
+
messages: [],
|
|
30
|
+
proposedPlans: [],
|
|
31
|
+
activities: [],
|
|
32
|
+
checkpoints: [],
|
|
33
|
+
session: null
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
createEmptyOrchestrationThread,
|
|
38
|
+
createEmptyOrchestrationProject
|
|
39
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
4
|
+
// packages/server/src/server-helpers/plugin-host-cache.ts
|
|
5
|
+
import { existsSync, statSync } from "fs";
|
|
6
|
+
import { resolve } from "path";
|
|
7
|
+
var contextCache = new Map;
|
|
8
|
+
var taskListCache = new Map;
|
|
9
|
+
var DEFAULT_TASK_LIST_TTL_MS = 2000;
|
|
10
|
+
function getPluginHostConfigMtime(projectRoot) {
|
|
11
|
+
for (const name of ["rig.config.ts", "rig.config.json"]) {
|
|
12
|
+
const path = resolve(projectRoot, name);
|
|
13
|
+
if (existsSync(path)) {
|
|
14
|
+
try {
|
|
15
|
+
return statSync(path).mtimeMs;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
async function getCachedPluginHostContext(projectRoot) {
|
|
24
|
+
const mtimeMs = getPluginHostConfigMtime(projectRoot);
|
|
25
|
+
if (mtimeMs === null) {
|
|
26
|
+
contextCache.delete(projectRoot);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const cached = contextCache.get(projectRoot);
|
|
30
|
+
if (cached && cached.mtimeMs === mtimeMs && cached.ctx) {
|
|
31
|
+
return cached.ctx;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const { buildPluginHostContext } = await import("@rig/runtime/control-plane/plugin-host-context");
|
|
35
|
+
const ctx = await buildPluginHostContext(projectRoot);
|
|
36
|
+
if (!ctx) {
|
|
37
|
+
contextCache.delete(projectRoot);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
contextCache.set(projectRoot, { mtimeMs, ctx });
|
|
41
|
+
return ctx;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
contextCache.delete(projectRoot);
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function getCachedTaskSourceList(projectRoot, options = {}) {
|
|
48
|
+
const ctx = await getCachedPluginHostContext(projectRoot);
|
|
49
|
+
if (!ctx)
|
|
50
|
+
return null;
|
|
51
|
+
const sources = ctx.taskSourceRegistry.list();
|
|
52
|
+
if (sources.length === 0)
|
|
53
|
+
return null;
|
|
54
|
+
const source = sources[0];
|
|
55
|
+
const cacheKey = `${projectRoot}::${source.kind}`;
|
|
56
|
+
const ttl = Math.max(0, options.ttlMs ?? DEFAULT_TASK_LIST_TTL_MS);
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
if (!options.force) {
|
|
59
|
+
const cached = taskListCache.get(cacheKey);
|
|
60
|
+
if (cached && cached.expiresAt > now) {
|
|
61
|
+
return { kind: source.kind, tasks: cached.tasks };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const tasks = await source.list();
|
|
65
|
+
taskListCache.set(cacheKey, { expiresAt: now + ttl, tasks });
|
|
66
|
+
return { kind: source.kind, tasks };
|
|
67
|
+
}
|
|
68
|
+
function invalidatePluginHostCache(projectRoot) {
|
|
69
|
+
contextCache.delete(projectRoot);
|
|
70
|
+
for (const key of taskListCache.keys()) {
|
|
71
|
+
if (key.startsWith(`${projectRoot}::`)) {
|
|
72
|
+
taskListCache.delete(key);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function resetPluginHostCacheForTests() {
|
|
77
|
+
contextCache.clear();
|
|
78
|
+
taskListCache.clear();
|
|
79
|
+
}
|
|
80
|
+
export {
|
|
81
|
+
resetPluginHostCacheForTests,
|
|
82
|
+
invalidatePluginHostCache,
|
|
83
|
+
getPluginHostConfigMtime,
|
|
84
|
+
getCachedTaskSourceList,
|
|
85
|
+
getCachedPluginHostContext
|
|
86
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/server/src/server-helpers/project-fs-ops.ts
|
|
3
|
+
import { spawn, spawnSync } from "child_process";
|
|
4
|
+
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
5
|
+
import { dirname, isAbsolute, relative, resolve } from "path";
|
|
6
|
+
function normalizeRelativePath(cwd, relativePath) {
|
|
7
|
+
const safeRoot = resolve(cwd);
|
|
8
|
+
const resolvedPath = resolve(safeRoot, relativePath);
|
|
9
|
+
const relativeToRoot = relative(safeRoot, resolvedPath);
|
|
10
|
+
if (relativeToRoot.startsWith("..") || isAbsolute(relativeToRoot)) {
|
|
11
|
+
throw new Error("Path escapes workspace");
|
|
12
|
+
}
|
|
13
|
+
return resolvedPath;
|
|
14
|
+
}
|
|
15
|
+
function gitResult(projectRoot, args) {
|
|
16
|
+
const result = spawnSync("git", args, {
|
|
17
|
+
cwd: projectRoot,
|
|
18
|
+
encoding: "utf8"
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
stdout: result.stdout ?? "",
|
|
22
|
+
stderr: result.stderr ?? "",
|
|
23
|
+
exitCode: result.status ?? 1
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function listDirectoryEntries(input) {
|
|
27
|
+
const targetPath = normalizeRelativePath(input.cwd, input.relativePath);
|
|
28
|
+
const entries = readdirSync(targetPath, { withFileTypes: true }).slice(0, input.limit).map((entry) => {
|
|
29
|
+
const fullPath = resolve(targetPath, entry.name);
|
|
30
|
+
const stat = statSync(fullPath);
|
|
31
|
+
return {
|
|
32
|
+
name: entry.name,
|
|
33
|
+
relativePath: input.relativePath === "." ? entry.name : `${input.relativePath.replace(/\/+$/, "")}/${entry.name}`,
|
|
34
|
+
kind: entry.isDirectory() ? "directory" : "file",
|
|
35
|
+
sizeBytes: entry.isDirectory() ? undefined : stat.size,
|
|
36
|
+
modifiedAt: stat.mtime.toISOString()
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
entries,
|
|
41
|
+
truncated: readdirSync(targetPath).length > input.limit
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function readProjectFile(input) {
|
|
45
|
+
const targetPath = normalizeRelativePath(input.cwd, input.relativePath);
|
|
46
|
+
const buffer = readFileSync(targetPath);
|
|
47
|
+
const maxBytes = 512 * 1024;
|
|
48
|
+
return {
|
|
49
|
+
relativePath: input.relativePath,
|
|
50
|
+
contents: buffer.subarray(0, maxBytes).toString("utf8"),
|
|
51
|
+
truncated: buffer.length > maxBytes,
|
|
52
|
+
sizeBytes: buffer.length,
|
|
53
|
+
maxBytes
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function writeProjectFile(input) {
|
|
57
|
+
const targetPath = normalizeRelativePath(input.cwd, input.relativePath);
|
|
58
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
59
|
+
writeFileSync(targetPath, input.contents, "utf8");
|
|
60
|
+
return {
|
|
61
|
+
relativePath: input.relativePath
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function searchProjectEntries(input) {
|
|
65
|
+
const results = [];
|
|
66
|
+
const query = input.query.toLowerCase();
|
|
67
|
+
const visit = (currentPath, relativePath = ".") => {
|
|
68
|
+
if (results.length >= input.limit)
|
|
69
|
+
return;
|
|
70
|
+
for (const entry of readdirSync(currentPath, { withFileTypes: true })) {
|
|
71
|
+
if (results.length >= input.limit)
|
|
72
|
+
return;
|
|
73
|
+
const nextRelative = relativePath === "." ? entry.name : `${relativePath}/${entry.name}`;
|
|
74
|
+
if (entry.name.toLowerCase().includes(query)) {
|
|
75
|
+
results.push({
|
|
76
|
+
path: nextRelative,
|
|
77
|
+
kind: entry.isDirectory() ? "directory" : "file",
|
|
78
|
+
...relativePath === "." ? {} : { parentPath: relativePath }
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (entry.isDirectory() && !entry.name.startsWith(".git")) {
|
|
82
|
+
visit(resolve(currentPath, entry.name), nextRelative);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
visit(input.cwd);
|
|
87
|
+
return {
|
|
88
|
+
entries: results,
|
|
89
|
+
truncated: results.length >= input.limit
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function readGitStatus(cwd) {
|
|
93
|
+
const branch = gitResult(cwd, ["branch", "--show-current"]).stdout.trim() || null;
|
|
94
|
+
const shortStatus = gitResult(cwd, ["status", "--short"]).stdout.trim().split(/\r?\n/).filter(Boolean);
|
|
95
|
+
const diffStat = gitResult(cwd, ["diff", "--numstat"]).stdout.trim().split(/\r?\n/).filter(Boolean);
|
|
96
|
+
const files = diffStat.map((line) => {
|
|
97
|
+
const [insertionsRaw, deletionsRaw, ...pathParts] = line.split(/\t+/);
|
|
98
|
+
return {
|
|
99
|
+
path: pathParts.join("\t"),
|
|
100
|
+
insertions: Number.parseInt(insertionsRaw || "0", 10) || 0,
|
|
101
|
+
deletions: Number.parseInt(deletionsRaw || "0", 10) || 0
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
const aheadBehindRaw = branch ? gitResult(cwd, ["rev-list", "--left-right", "--count", `${branch}...@{upstream}`]) : { stdout: "", stderr: "", exitCode: 1 };
|
|
105
|
+
const [aheadRaw, behindRaw] = aheadBehindRaw.stdout.trim().split(/\s+/);
|
|
106
|
+
return {
|
|
107
|
+
branch,
|
|
108
|
+
hasWorkingTreeChanges: shortStatus.length > 0,
|
|
109
|
+
workingTree: {
|
|
110
|
+
files,
|
|
111
|
+
insertions: files.reduce((sum, file) => sum + file.insertions, 0),
|
|
112
|
+
deletions: files.reduce((sum, file) => sum + file.deletions, 0)
|
|
113
|
+
},
|
|
114
|
+
hasUpstream: aheadBehindRaw.exitCode === 0,
|
|
115
|
+
aheadCount: Number.parseInt(aheadRaw || "0", 10) || 0,
|
|
116
|
+
behindCount: Number.parseInt(behindRaw || "0", 10) || 0,
|
|
117
|
+
pr: null
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function listGitBranches(cwd) {
|
|
121
|
+
const branchOutput = gitResult(cwd, ["branch", "--format=%(refname:short)|%(HEAD)"]).stdout;
|
|
122
|
+
const currentBranch = gitResult(cwd, ["branch", "--show-current"]).stdout.trim();
|
|
123
|
+
const branches = branchOutput.split(/\r?\n/).filter(Boolean).map((line) => {
|
|
124
|
+
const [name, headMarker] = line.split("|");
|
|
125
|
+
return {
|
|
126
|
+
name: name.trim(),
|
|
127
|
+
current: headMarker?.trim() === "*",
|
|
128
|
+
isDefault: name?.trim() === currentBranch,
|
|
129
|
+
worktreePath: cwd
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
return { branches, isRepo: branches.length > 0 || !!currentBranch };
|
|
133
|
+
}
|
|
134
|
+
function createGitWorktree(input) {
|
|
135
|
+
const worktreePath = input.path ?? resolve(input.cwd, "..", input.newBranch);
|
|
136
|
+
mkdirSync(dirname(worktreePath), { recursive: true });
|
|
137
|
+
const result = gitResult(input.cwd, [
|
|
138
|
+
"worktree",
|
|
139
|
+
"add",
|
|
140
|
+
"-b",
|
|
141
|
+
input.newBranch,
|
|
142
|
+
worktreePath,
|
|
143
|
+
input.branch
|
|
144
|
+
]);
|
|
145
|
+
if (result.exitCode !== 0) {
|
|
146
|
+
throw new Error(result.stderr || "git worktree add failed");
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
worktree: {
|
|
150
|
+
path: worktreePath,
|
|
151
|
+
branch: input.newBranch
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function removeGitWorktree(input) {
|
|
156
|
+
const args = ["worktree", "remove"];
|
|
157
|
+
if (input.force) {
|
|
158
|
+
args.push("--force");
|
|
159
|
+
}
|
|
160
|
+
args.push(input.path);
|
|
161
|
+
const result = gitResult(input.cwd, args);
|
|
162
|
+
if (result.exitCode !== 0) {
|
|
163
|
+
throw new Error(result.stderr || "git worktree remove failed");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function readGitPatch(cwd, relativePath) {
|
|
167
|
+
const args = ["diff", "--", ...relativePath ? [relativePath] : []];
|
|
168
|
+
return {
|
|
169
|
+
diff: gitResult(cwd, args).stdout
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function openInEditor(cwd, editor) {
|
|
173
|
+
if (editor === "file-manager") {
|
|
174
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
175
|
+
spawn(command, [cwd], { detached: true, stdio: "ignore" }).unref();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const binary = editor === "vscode" ? "code" : editor === "cursor" ? "cursor" : editor;
|
|
179
|
+
spawn(binary, [cwd], { detached: true, stdio: "ignore" }).unref();
|
|
180
|
+
}
|
|
181
|
+
export {
|
|
182
|
+
writeProjectFile,
|
|
183
|
+
searchProjectEntries,
|
|
184
|
+
removeGitWorktree,
|
|
185
|
+
readProjectFile,
|
|
186
|
+
readGitStatus,
|
|
187
|
+
readGitPatch,
|
|
188
|
+
openInEditor,
|
|
189
|
+
normalizeRelativePath,
|
|
190
|
+
listGitBranches,
|
|
191
|
+
listDirectoryEntries,
|
|
192
|
+
gitResult,
|
|
193
|
+
createGitWorktree
|
|
194
|
+
};
|