@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.
Files changed (60) hide show
  1. package/README.md +14 -0
  2. package/dist/src/bootstrap.js +161 -0
  3. package/dist/src/index.js +13153 -0
  4. package/dist/src/inspector/agent-runtime.js +1077 -0
  5. package/dist/src/inspector/analysis.js +41 -0
  6. package/dist/src/inspector/discovery.js +137 -0
  7. package/dist/src/inspector/journal.js +518 -0
  8. package/dist/src/inspector/mission.js +562 -0
  9. package/dist/src/inspector/prompt.js +97 -0
  10. package/dist/src/inspector/provider-session.js +65 -0
  11. package/dist/src/inspector/reconcile.js +118 -0
  12. package/dist/src/inspector/review.js +13 -0
  13. package/dist/src/inspector/service.js +1759 -0
  14. package/dist/src/inspector/skills.js +155 -0
  15. package/dist/src/inspector/tools.js +1592 -0
  16. package/dist/src/inspector/types.js +1 -0
  17. package/dist/src/inspector/upstream-sync.js +479 -0
  18. package/dist/src/orchestration.js +402 -0
  19. package/dist/src/remote.js +123 -0
  20. package/dist/src/scheduler.js +84 -0
  21. package/dist/src/server-helpers/broadcasters.js +161 -0
  22. package/dist/src/server-helpers/conversation-snapshot.js +382 -0
  23. package/dist/src/server-helpers/event-emitter.js +41 -0
  24. package/dist/src/server-helpers/github-auth-store.js +155 -0
  25. package/dist/src/server-helpers/github-credentials.js +38 -0
  26. package/dist/src/server-helpers/github-project-status-sync.js +196 -0
  27. package/dist/src/server-helpers/github-projects.js +147 -0
  28. package/dist/src/server-helpers/github-reconciler.js +89 -0
  29. package/dist/src/server-helpers/http-router.js +3781 -0
  30. package/dist/src/server-helpers/http-utils.js +135 -0
  31. package/dist/src/server-helpers/inspector-agent-lifecycle.js +104 -0
  32. package/dist/src/server-helpers/inspector-jobs.js +4145 -0
  33. package/dist/src/server-helpers/issue-analysis.js +362 -0
  34. package/dist/src/server-helpers/normalizers.js +31 -0
  35. package/dist/src/server-helpers/notifications.js +96 -0
  36. package/dist/src/server-helpers/orchestration-ops.js +287 -0
  37. package/dist/src/server-helpers/orchestration.js +39 -0
  38. package/dist/src/server-helpers/plugin-host-cache.js +86 -0
  39. package/dist/src/server-helpers/project-fs-ops.js +194 -0
  40. package/dist/src/server-helpers/project-registry.js +124 -0
  41. package/dist/src/server-helpers/queue-state.js +78 -0
  42. package/dist/src/server-helpers/remote-checkout.js +140 -0
  43. package/dist/src/server-helpers/remote-snapshots.js +119 -0
  44. package/dist/src/server-helpers/run-io.js +262 -0
  45. package/dist/src/server-helpers/run-mutations.js +1784 -0
  46. package/dist/src/server-helpers/run-steering.js +176 -0
  47. package/dist/src/server-helpers/run-writers.js +75 -0
  48. package/dist/src/server-helpers/server-paths.js +27 -0
  49. package/dist/src/server-helpers/snapshot-orchestrator.js +832 -0
  50. package/dist/src/server-helpers/snapshot-service.js +1143 -0
  51. package/dist/src/server-helpers/summaries.js +126 -0
  52. package/dist/src/server-helpers/task-config.js +50 -0
  53. package/dist/src/server-helpers/task-projection.js +98 -0
  54. package/dist/src/server-helpers/terminal-runtime.js +156 -0
  55. package/dist/src/server-helpers/terminal-sessions.js +22 -0
  56. package/dist/src/server-helpers/validation-failure.js +31 -0
  57. package/dist/src/server-helpers/ws-router.js +1308 -0
  58. package/dist/src/server.js +12628 -0
  59. package/dist/src/websocket.js +63 -0
  60. 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
+ };