@chatroomcp/chatroom 0.2.0 → 0.2.2

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 (112) hide show
  1. package/README.md +1 -1
  2. package/dist/app/application.d.ts +0 -1
  3. package/dist/app/application.js +2 -0
  4. package/dist/app/event-bus.d.ts +0 -2
  5. package/dist/app/event-bus.js +0 -1
  6. package/dist/app/external-access-registry.d.ts +0 -1
  7. package/dist/app/external-access-registry.js +0 -1
  8. package/dist/app/lifecycle.d.ts +0 -1
  9. package/dist/auth/auth-service.js +0 -4
  10. package/dist/auth/ingress-policy.d.ts +0 -1
  11. package/dist/auth/ingress-policy.js +0 -1
  12. package/dist/cli/run-cli.js +0 -1
  13. package/dist/config/types.d.ts +0 -1
  14. package/dist/core/auth/repository.d.ts +0 -23
  15. package/dist/core/auth/repository.js +0 -6
  16. package/dist/core/errors/chatroom-error.d.ts +0 -15
  17. package/dist/core/errors/chatroom-error.js +0 -15
  18. package/dist/core/operations/bounded-value.d.ts +0 -1
  19. package/dist/core/operations/bounded-value.js +26 -10
  20. package/dist/core/operations/redactor.js +0 -1
  21. package/dist/core/operations/repository.d.ts +0 -1
  22. package/dist/core/operations/types.d.ts +0 -4
  23. package/dist/core/operations/types.js +0 -2
  24. package/dist/infrastructure/database/app-database.js +4 -97
  25. package/dist/infrastructure/database/migrations.d.ts +2 -0
  26. package/dist/infrastructure/database/migrations.js +44 -0
  27. package/dist/infrastructure/database/operation-repository.js +3 -9
  28. package/dist/infrastructure/database/schema.d.ts +2 -0
  29. package/dist/infrastructure/database/schema.js +85 -0
  30. package/dist/infrastructure/http/http-server.js +0 -1
  31. package/dist/mcp/server/create-mcp-server.js +3 -3
  32. package/dist/mcp/server/plugin-mcp-registrar.d.ts +0 -2
  33. package/dist/mcp/server/plugin-mcp-registrar.js +0 -4
  34. package/dist/mcp/server/tool-support.d.ts +0 -61
  35. package/dist/mcp/server/tool-support.js +0 -54
  36. package/dist/native/linux/chatroom-computer-helper +0 -0
  37. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/Info.plist +1 -1
  38. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
  39. package/dist/native/windows/chatroom-computer-helper.exe +0 -0
  40. package/dist/operations/operation-log.d.ts +0 -3
  41. package/dist/operations/operation-log.js +0 -15
  42. package/dist/plugins/cloud/api-client.js +1 -0
  43. package/dist/plugins/cloud/controller.d.ts +2 -0
  44. package/dist/plugins/cloud/controller.js +28 -5
  45. package/dist/plugins/cloud/tunnel-client.js +0 -4
  46. package/dist/plugins/computer/computer-native-host.d.ts +2 -1
  47. package/dist/plugins/computer/computer-native-host.js +107 -17
  48. package/dist/plugins/computer/computer-schemas.d.ts +1 -0
  49. package/dist/plugins/computer/computer-schemas.js +1 -1
  50. package/dist/plugins/computer/types.d.ts +1 -1
  51. package/dist/plugins/git/git-service.d.ts +33 -0
  52. package/dist/plugins/git/git-service.js +414 -0
  53. package/dist/plugins/git/plugin.d.ts +4 -0
  54. package/dist/plugins/git/plugin.js +12 -0
  55. package/dist/plugins/git/types.d.ts +32 -0
  56. package/dist/plugins/process/backend.d.ts +0 -1
  57. package/dist/plugins/process/head-tail-buffer.d.ts +0 -1
  58. package/dist/plugins/process/infrastructure/pipe-process-backend.js +0 -1
  59. package/dist/plugins/process/infrastructure/pty-process-backend.js +1 -4
  60. package/dist/plugins/process/process-supervisor.js +10 -23
  61. package/dist/plugins/process/types.d.ts +0 -5
  62. package/dist/plugins/types.d.ts +0 -1
  63. package/dist/plugins/web/api-types.d.ts +3 -7
  64. package/dist/plugins/web/http/api-router.d.ts +0 -1
  65. package/dist/plugins/web/http/api-router.js +2 -4
  66. package/dist/plugins/web/http/git-api-router.d.ts +3 -0
  67. package/dist/plugins/web/http/git-api-router.js +98 -0
  68. package/dist/plugins/web/http/workspace-api-router.js +15 -37
  69. package/dist/plugins/web/plugin.js +4 -6
  70. package/dist/plugins/web/runtime.d.ts +5 -27
  71. package/dist/plugins/web/runtime.js +3 -42
  72. package/dist/plugins/workspace/mcp.js +17 -135
  73. package/dist/plugins/workspace/metadata.d.ts +4 -0
  74. package/dist/plugins/workspace/metadata.js +105 -0
  75. package/dist/plugins/workspace/plugin.d.ts +2 -8
  76. package/dist/plugins/workspace/plugin.js +8 -20
  77. package/dist/plugins/workspace/types.d.ts +18 -0
  78. package/dist/plugins/workspace/{infrastructure/workspace-fs.d.ts → workspace-fs.d.ts} +2 -15
  79. package/dist/plugins/workspace/workspace-fs.js +111 -0
  80. package/dist/plugins/workspace/workspace-service.d.ts +7 -42
  81. package/dist/plugins/workspace/workspace-service.js +61 -315
  82. package/dist/presentation/http/http-utils.d.ts +0 -1
  83. package/dist/presentation/http/oauth-router.d.ts +0 -1
  84. package/dist/presentation/http/oauth-router.js +0 -1
  85. package/dist/web/assets/{index-BXlxEk8f.css → index-DFKEvyzR.css} +1 -1
  86. package/dist/web/assets/index-vzPPspuL.js +26 -0
  87. package/dist/web/index.html +2 -2
  88. package/package.json +4 -3
  89. package/dist/plugins/workspace/domain/filesystem.d.ts +0 -25
  90. package/dist/plugins/workspace/domain/git.d.ts +0 -7
  91. package/dist/plugins/workspace/domain/repository.d.ts +0 -16
  92. package/dist/plugins/workspace/domain/repository.js +0 -1
  93. package/dist/plugins/workspace/domain/review.d.ts +0 -28
  94. package/dist/plugins/workspace/domain/review.js +0 -1
  95. package/dist/plugins/workspace/domain/workspace.d.ts +0 -23
  96. package/dist/plugins/workspace/domain/workspace.js +0 -1
  97. package/dist/plugins/workspace/git/git-command-runner.d.ts +0 -6
  98. package/dist/plugins/workspace/git/git-command-runner.js +0 -15
  99. package/dist/plugins/workspace/git/git-service.d.ts +0 -14
  100. package/dist/plugins/workspace/git/git-service.js +0 -107
  101. package/dist/plugins/workspace/git/git-snapshot-service.d.ts +0 -33
  102. package/dist/plugins/workspace/git/git-snapshot-service.js +0 -352
  103. package/dist/plugins/workspace/infrastructure/workspace-fs.js +0 -340
  104. package/dist/plugins/workspace/infrastructure/workspace-repository.d.ts +0 -12
  105. package/dist/plugins/workspace/infrastructure/workspace-repository.js +0 -51
  106. package/dist/plugins/workspace/workspace-helpers.d.ts +0 -9
  107. package/dist/plugins/workspace/workspace-helpers.js +0 -67
  108. package/dist/plugins/workspace/worktree-review-service.d.ts +0 -16
  109. package/dist/plugins/workspace/worktree-review-service.js +0 -51
  110. package/dist/web/assets/index-DUM6zVHS.js +0 -26
  111. /package/dist/plugins/{workspace/domain/filesystem.js → git/types.js} +0 -0
  112. /package/dist/plugins/workspace/{domain/git.js → types.js} +0 -0
@@ -0,0 +1,414 @@
1
+ import { mkdtemp, open, rm, stat } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { ChatRoomError } from "../../core/errors/chatroom-error.js";
5
+ const MAX_PATHS = 512;
6
+ export class GitService {
7
+ commands;
8
+ constructor(commands) {
9
+ this.commands = commands;
10
+ }
11
+ async status(cwd) {
12
+ const root = await this.repositoryRoot(cwd);
13
+ if (!root)
14
+ return null;
15
+ const [branch, head, porcelain] = await Promise.all([
16
+ this.branch(root),
17
+ this.head(root),
18
+ this.run(root, [
19
+ "status",
20
+ "--porcelain=v1",
21
+ "-z",
22
+ "--untracked-files=all",
23
+ ]),
24
+ ]);
25
+ const upstream = head ? await this.upstream(root) : null;
26
+ const { ahead, behind } = upstream
27
+ ? await this.aheadBehind(root)
28
+ : { ahead: 0, behind: 0 };
29
+ return {
30
+ branch,
31
+ head,
32
+ upstream,
33
+ ahead,
34
+ behind,
35
+ changes: parseStatus(porcelain.stdout),
36
+ };
37
+ }
38
+ async diff(cwd, filePath, maxPreviewBytes = 2 * 1024 * 1024) {
39
+ const root = await this.requireRepository(cwd);
40
+ await this.requireHead(root);
41
+ const paths = filePath ? await this.relatedPaths(root, [filePath]) : [];
42
+ return this.withSnapshotIndex(root, async ({ env, tempDir }) => {
43
+ const patchPath = path.join(tempDir, "changes.patch");
44
+ await this.run(root, [
45
+ "diff",
46
+ "--cached",
47
+ "--binary",
48
+ "--no-ext-diff",
49
+ "--no-renames",
50
+ `--output=${patchPath}`,
51
+ "HEAD",
52
+ ...(paths.length ? ["--", ...paths] : []),
53
+ ], env);
54
+ return readPatch(patchPath, maxPreviewBytes);
55
+ });
56
+ }
57
+ async stage(cwd, paths) {
58
+ const root = await this.requireRepository(cwd);
59
+ await this.run(root, [
60
+ "add",
61
+ "-A",
62
+ "--",
63
+ ...(await this.relatedPaths(root, paths)),
64
+ ]);
65
+ return this.requireStatus(root);
66
+ }
67
+ async unstage(cwd, paths) {
68
+ const root = await this.requireRepository(cwd);
69
+ const expanded = await this.relatedPaths(root, paths);
70
+ if (await this.head(root))
71
+ await this.run(root, ["restore", "--staged", "--", ...expanded]);
72
+ else
73
+ await this.run(root, ["rm", "--cached", "-r", "--", ...expanded]);
74
+ return this.requireStatus(root);
75
+ }
76
+ async restore(cwd, filePath) {
77
+ const root = await this.requireRepository(cwd);
78
+ const requested = normalizePath(filePath);
79
+ const status = await this.requireStatus(root);
80
+ const change = status.changes.find((item) => item.path === requested || item.originalPath === requested);
81
+ if (!change)
82
+ throw new ChatRoomError("NOT_FOUND", `No Git change found for path: ${requested}`);
83
+ if (change.kind === "untracked") {
84
+ await this.run(root, ["clean", "-f", "--", change.path]);
85
+ return this.requireStatus(root);
86
+ }
87
+ await this.requireHead(root);
88
+ const paths = [
89
+ change.path,
90
+ ...(change.originalPath ? [change.originalPath] : []),
91
+ ];
92
+ const tracked = [];
93
+ const untracked = [];
94
+ for (const file of [...new Set(paths)]) {
95
+ if (await this.pathExistsInHead(root, file))
96
+ tracked.push(file);
97
+ else
98
+ untracked.push(file);
99
+ }
100
+ if (tracked.length)
101
+ await this.run(root, [
102
+ "restore",
103
+ "--source=HEAD",
104
+ "--staged",
105
+ "-W",
106
+ "--",
107
+ ...tracked,
108
+ ]);
109
+ if (untracked.length) {
110
+ await this.run(root, ["restore", "--staged", "--", ...untracked]).catch(() => undefined);
111
+ await this.run(root, ["clean", "-f", "--", ...untracked]);
112
+ }
113
+ return this.requireStatus(root);
114
+ }
115
+ async commit(cwd, message) {
116
+ const root = await this.requireRepository(cwd);
117
+ const normalized = message.trim();
118
+ if (!normalized)
119
+ throw new ChatRoomError("INVALID_INPUT", "Commit message is required");
120
+ if (normalized.length > 10_000)
121
+ throw new ChatRoomError("INVALID_INPUT", "Commit message is too long");
122
+ await this.run(root, ["commit", "-m", normalized]);
123
+ return this.requireStatus(root);
124
+ }
125
+ async branches(cwd) {
126
+ const root = await this.requireRepository(cwd);
127
+ const result = await this.run(root, [
128
+ "for-each-ref",
129
+ "--format=%(HEAD)%00%(refname:short)%00%(upstream:short)",
130
+ "refs/heads",
131
+ ]);
132
+ return result.stdout
133
+ .split(/\r?\n/)
134
+ .filter(Boolean)
135
+ .map((line) => {
136
+ const [head = "", name = "", upstream = ""] = line.split("\0");
137
+ return {
138
+ name,
139
+ current: head.trim() === "*",
140
+ upstream: upstream || null,
141
+ };
142
+ })
143
+ .filter((branch) => Boolean(branch.name));
144
+ }
145
+ async createBranch(cwd, name) {
146
+ const root = await this.requireRepository(cwd);
147
+ const branch = await this.validateBranchName(root, name);
148
+ await this.run(root, ["switch", "-c", branch]);
149
+ return this.requireStatus(root);
150
+ }
151
+ async switchBranch(cwd, name) {
152
+ const root = await this.requireRepository(cwd);
153
+ const branch = await this.validateBranchName(root, name);
154
+ await this.run(root, ["switch", branch]);
155
+ return this.requireStatus(root);
156
+ }
157
+ async deleteBranch(cwd, name) {
158
+ const root = await this.requireRepository(cwd);
159
+ const branch = await this.validateBranchName(root, name);
160
+ await this.run(root, ["branch", "-d", branch]);
161
+ return this.requireStatus(root);
162
+ }
163
+ async log(cwd, limit = 30) {
164
+ const root = await this.requireRepository(cwd);
165
+ if (!(await this.head(root)))
166
+ return [];
167
+ const count = Math.min(Math.max(Math.trunc(limit), 1), 100);
168
+ const result = await this.run(root, [
169
+ "log",
170
+ `-n${count}`,
171
+ "--pretty=format:%H%x1f%h%x1f%an%x1f%aI%x1f%s%x1e",
172
+ ]);
173
+ return result.stdout
174
+ .split("\x1e")
175
+ .map((record) => record.trim())
176
+ .filter(Boolean)
177
+ .map((record) => {
178
+ const [hash = "", shortHash = "", author = "", date = "", subject = "",] = record.split("\x1f");
179
+ return { hash, shortHash, author, date, subject };
180
+ });
181
+ }
182
+ async fetch(cwd) {
183
+ const root = await this.requireRepository(cwd);
184
+ await this.run(root, ["fetch", "--all", "--prune"]);
185
+ return this.requireStatus(root);
186
+ }
187
+ async pull(cwd) {
188
+ const root = await this.requireRepository(cwd);
189
+ await this.run(root, ["pull", "--ff-only"]);
190
+ return this.requireStatus(root);
191
+ }
192
+ async push(cwd) {
193
+ const root = await this.requireRepository(cwd);
194
+ await this.run(root, ["push"]);
195
+ return this.requireStatus(root);
196
+ }
197
+ run(cwd, args, env) {
198
+ return this.commands.run({
199
+ cwd,
200
+ command: "git",
201
+ args,
202
+ timeoutMs: 120_000,
203
+ env: { LC_ALL: "C", LANG: "C", ...(env ?? {}) },
204
+ });
205
+ }
206
+ async repositoryRoot(cwd) {
207
+ try {
208
+ const root = path.resolve((await this.run(cwd, ["rev-parse", "--show-toplevel"])).stdout.trim());
209
+ return root === path.resolve(cwd) ? root : null;
210
+ }
211
+ catch (error) {
212
+ if (isNotRepositoryError(error))
213
+ return null;
214
+ throw error;
215
+ }
216
+ }
217
+ async requireRepository(cwd) {
218
+ const root = await this.repositoryRoot(cwd);
219
+ if (!root)
220
+ throw new ChatRoomError("INVALID_INPUT", "Workspace is not a Git repository");
221
+ return root;
222
+ }
223
+ async requireStatus(cwd) {
224
+ const status = await this.status(cwd);
225
+ if (!status)
226
+ throw new ChatRoomError("INVALID_INPUT", "Workspace is not a Git repository");
227
+ return status;
228
+ }
229
+ async head(cwd) {
230
+ try {
231
+ return ((await this.run(cwd, ["rev-parse", "--verify", "HEAD"])).stdout.trim() || null);
232
+ }
233
+ catch (error) {
234
+ if (isMissingHeadError(error))
235
+ return null;
236
+ throw error;
237
+ }
238
+ }
239
+ async requireHead(cwd) {
240
+ const head = await this.head(cwd);
241
+ if (!head)
242
+ throw new ChatRoomError("CONFLICT", "Git operation requires at least one commit");
243
+ return head;
244
+ }
245
+ async branch(cwd) {
246
+ return ((await this.run(cwd, ["branch", "--show-current"])).stdout.trim() || null);
247
+ }
248
+ async upstream(cwd) {
249
+ try {
250
+ return ((await this.run(cwd, [
251
+ "rev-parse",
252
+ "--abbrev-ref",
253
+ "--symbolic-full-name",
254
+ "@{upstream}",
255
+ ])).stdout.trim() || null);
256
+ }
257
+ catch {
258
+ return null;
259
+ }
260
+ }
261
+ async aheadBehind(cwd) {
262
+ try {
263
+ const [ahead = "0", behind = "0"] = (await this.run(cwd, [
264
+ "rev-list",
265
+ "--left-right",
266
+ "--count",
267
+ "HEAD...@{upstream}",
268
+ ])).stdout
269
+ .trim()
270
+ .split(/\s+/);
271
+ return { ahead: Number(ahead) || 0, behind: Number(behind) || 0 };
272
+ }
273
+ catch {
274
+ return { ahead: 0, behind: 0 };
275
+ }
276
+ }
277
+ async relatedPaths(cwd, paths) {
278
+ const normalized = normalizePaths(paths);
279
+ const status = await this.requireStatus(cwd);
280
+ const output = new Set(normalized);
281
+ for (const requested of normalized) {
282
+ const change = status.changes.find((item) => item.path === requested || item.originalPath === requested);
283
+ if (change?.path)
284
+ output.add(change.path);
285
+ if (change?.originalPath)
286
+ output.add(change.originalPath);
287
+ }
288
+ return [...output];
289
+ }
290
+ async pathExistsInHead(cwd, filePath) {
291
+ try {
292
+ await this.run(cwd, ["cat-file", "-e", `HEAD:${filePath}`]);
293
+ return true;
294
+ }
295
+ catch {
296
+ return false;
297
+ }
298
+ }
299
+ async validateBranchName(cwd, name) {
300
+ const normalized = name.trim();
301
+ if (!normalized)
302
+ throw new ChatRoomError("INVALID_INPUT", "Branch name is required");
303
+ await this.run(cwd, ["check-ref-format", "--branch", normalized]);
304
+ return normalized;
305
+ }
306
+ async withSnapshotIndex(cwd, useIndex) {
307
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "chatroom-git-"));
308
+ const env = { GIT_INDEX_FILE: path.join(tempDir, "index") };
309
+ try {
310
+ await this.run(cwd, ["read-tree", "HEAD"], env);
311
+ await this.run(cwd, ["add", "-A", "--", "."], env);
312
+ return await useIndex({ env, tempDir });
313
+ }
314
+ finally {
315
+ await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
316
+ }
317
+ }
318
+ }
319
+ function parseStatus(output) {
320
+ const fields = output.split("\0");
321
+ const changes = [];
322
+ for (let index = 0; index < fields.length; index++) {
323
+ const record = fields[index];
324
+ if (!record || record.length < 3)
325
+ continue;
326
+ const indexStatus = record[0] ?? " ";
327
+ const workingTreeStatus = record[1] ?? " ";
328
+ const currentPath = record.slice(3);
329
+ let originalPath = null;
330
+ if (["R", "C"].includes(indexStatus) ||
331
+ ["R", "C"].includes(workingTreeStatus))
332
+ originalPath = fields[++index] || null;
333
+ changes.push({
334
+ path: currentPath,
335
+ originalPath,
336
+ indexStatus,
337
+ workingTreeStatus,
338
+ kind: changeKind(indexStatus, workingTreeStatus),
339
+ });
340
+ }
341
+ return changes.sort((a, b) => a.path.localeCompare(b.path));
342
+ }
343
+ function changeKind(indexStatus, workingTreeStatus) {
344
+ const pair = `${indexStatus}${workingTreeStatus}`;
345
+ if (pair === "??")
346
+ return "untracked";
347
+ if (["DD", "AU", "UD", "UA", "DU", "AA", "UU"].includes(pair) ||
348
+ indexStatus === "U" ||
349
+ workingTreeStatus === "U")
350
+ return "conflicted";
351
+ if (pair.includes("R"))
352
+ return "renamed";
353
+ if (pair.includes("C"))
354
+ return "copied";
355
+ if (pair.includes("A"))
356
+ return "added";
357
+ if (pair.includes("D"))
358
+ return "deleted";
359
+ return "modified";
360
+ }
361
+ function normalizePaths(paths) {
362
+ if (!Array.isArray(paths) || paths.length < 1)
363
+ throw new ChatRoomError("INVALID_INPUT", "At least one file path is required");
364
+ if (paths.length > MAX_PATHS)
365
+ throw new ChatRoomError("INVALID_INPUT", "Too many file paths");
366
+ return [...new Set(paths.map(normalizePath))];
367
+ }
368
+ function normalizePath(input) {
369
+ if (typeof input !== "string" || !input || input.includes("\0"))
370
+ throw new ChatRoomError("INVALID_INPUT", "File path is required");
371
+ if (path.isAbsolute(input) ||
372
+ path.win32.isAbsolute(input) ||
373
+ /^\\\\/.test(input))
374
+ throw new ChatRoomError("FORBIDDEN", "Absolute Git paths are not allowed");
375
+ const parts = input
376
+ .split(/[\\/]+/)
377
+ .filter((part) => part !== "" && part !== ".");
378
+ if (parts.length === 0 || parts.some((part) => part === ".."))
379
+ throw new ChatRoomError("FORBIDDEN", "Invalid Git path");
380
+ return parts.join("/");
381
+ }
382
+ async function readPatch(patchPath, maxBytes) {
383
+ const size = (await stat(patchPath)).size;
384
+ const previewBytes = Math.min(size, maxBytes);
385
+ const handle = await open(patchPath, "r");
386
+ try {
387
+ const buffer = Buffer.alloc(previewBytes);
388
+ const { bytesRead } = await handle.read(buffer, 0, previewBytes, 0);
389
+ return {
390
+ diff: buffer.subarray(0, bytesRead).toString("utf8"),
391
+ truncated: size > previewBytes,
392
+ };
393
+ }
394
+ finally {
395
+ await handle.close();
396
+ }
397
+ }
398
+ function gitErrorText(error) {
399
+ if (!(error instanceof ChatRoomError))
400
+ return "";
401
+ const details = error.details;
402
+ return `${String(details?.stdout ?? "")}\n${String(details?.stderr ?? "")}`.toLowerCase();
403
+ }
404
+ function isNotRepositoryError(error) {
405
+ return gitErrorText(error).includes("not a git repository");
406
+ }
407
+ function isMissingHeadError(error) {
408
+ const text = gitErrorText(error);
409
+ return (text.includes("needed a single revision") ||
410
+ text.includes("unknown revision") ||
411
+ text.includes("bad revision") ||
412
+ text.includes("ambiguous argument 'head'") ||
413
+ text.includes("not a valid object name head"));
414
+ }
@@ -0,0 +1,4 @@
1
+ import type { InternalPlugin } from "../types.js";
2
+ import { GitService } from "./git-service.js";
3
+ export declare const GitServiceToken: import("../types.js").ServiceToken<GitService>;
4
+ export declare function createGitPlugin(): InternalPlugin;
@@ -0,0 +1,12 @@
1
+ import { CommandRunner } from "../../core/runtime/command-runner.js";
2
+ import { createServiceToken } from "../types.js";
3
+ import { GitService } from "./git-service.js";
4
+ export const GitServiceToken = createServiceToken("git");
5
+ export function createGitPlugin() {
6
+ return {
7
+ id: "git",
8
+ activate(context) {
9
+ context.services.provide(GitServiceToken, new GitService(new CommandRunner()));
10
+ },
11
+ };
12
+ }
@@ -0,0 +1,32 @@
1
+ export type GitChangeKind = "modified" | "added" | "deleted" | "renamed" | "copied" | "untracked" | "conflicted";
2
+ export interface GitChange {
3
+ path: string;
4
+ originalPath: string | null;
5
+ indexStatus: string;
6
+ workingTreeStatus: string;
7
+ kind: GitChangeKind;
8
+ }
9
+ export interface GitStatus {
10
+ branch: string | null;
11
+ head: string | null;
12
+ upstream: string | null;
13
+ ahead: number;
14
+ behind: number;
15
+ changes: GitChange[];
16
+ }
17
+ export interface GitDiff {
18
+ diff: string;
19
+ truncated: boolean;
20
+ }
21
+ export interface GitBranch {
22
+ name: string;
23
+ current: boolean;
24
+ upstream: string | null;
25
+ }
26
+ export interface GitCommit {
27
+ hash: string;
28
+ shortHash: string;
29
+ author: string;
30
+ date: string;
31
+ subject: string;
32
+ }
@@ -1,4 +1,3 @@
1
- /** OS/process-backend port consumed by ProcessSupervisor. */
2
1
  export interface BackendProcess {
3
2
  readonly pid: number | null;
4
3
  write(data: string): void;
@@ -1,4 +1,3 @@
1
- /** Bounded process-output buffer that preserves useful context from both ends of a stream. */
2
1
  interface BufferSnapshot {
3
2
  text: string;
4
3
  bytesSeen: number;
@@ -1,4 +1,3 @@
1
- /** Pipe-based ProcessBackend implemented with Node child_process.spawn. */
2
1
  import { spawn } from "node:child_process";
3
2
  export class PipeProcessBackend {
4
3
  kind = "pipe";
@@ -1,4 +1,3 @@
1
- /** Optional PTY ProcessBackend backed by node-pty when native PTY support is installed. */
2
1
  import { ChatRoomError } from "../../../core/errors/chatroom-error.js";
3
2
  export class PtyProcessBackend {
4
3
  kind = "pty";
@@ -38,9 +37,7 @@ class PtyBackendProcess {
38
37
  onStdout(listener) {
39
38
  this.processHandle.onData((data) => listener(Buffer.from(data)));
40
39
  }
41
- onStderr(_listener) {
42
- /* PTYs expose a merged stream. */
43
- }
40
+ onStderr(_listener) { }
44
41
  onExit(listener) {
45
42
  this.processHandle.onExit((event) => listener({
46
43
  exitCode: event.exitCode,
@@ -1,7 +1,3 @@
1
- /**
2
- * Owns all ChatRoom-managed process lifecycle, timeout handling, output buffering, stdin, and process operation events.
3
- * Platform-specific creation details are delegated to pipe/PTY backends.
4
- */
5
1
  import { randomUUID } from "node:crypto";
6
2
  import { ChatRoomError } from "../../core/errors/chatroom-error.js";
7
3
  import { childEnvironment } from "../../core/runtime/child-environment.js";
@@ -48,7 +44,6 @@ export class ProcessSupervisor {
48
44
  this.operations.associate(operationId, { processId });
49
45
  let backend;
50
46
  try {
51
- // Child environments inherit only the runtime allowlist, then apply explicit request overrides.
52
47
  backend = await (request.pty ? this.backends.pty : this.backends.pipe).start(request.command, args, {
53
48
  cwd: request.cwd,
54
49
  env: childEnvironment(request.env),
@@ -75,6 +70,7 @@ export class ProcessSupervisor {
75
70
  signal: null,
76
71
  timeout: null,
77
72
  forceTimeout: null,
73
+ outputEventTimer: null,
78
74
  timedOut: false,
79
75
  operationId,
80
76
  settled,
@@ -90,7 +86,6 @@ export class ProcessSupervisor {
90
86
  if (managed.state !== "running")
91
87
  return;
92
88
  managed.timedOut = true;
93
- // Timeouts first request graceful termination and escalate to SIGKILL only if the backend remains alive.
94
89
  managed.backend.kill("SIGTERM");
95
90
  managed.forceTimeout = setTimeout(() => {
96
91
  if (managed.state === "running")
@@ -137,7 +132,6 @@ export class ProcessSupervisor {
137
132
  Promise.allSettled(survivors.map(([, item]) => item.settled)),
138
133
  new Promise((resolve) => setTimeout(resolve, 1500)),
139
134
  ]);
140
- // A backend that does not report close after SIGKILL must still be settled before SQLite closes.
141
135
  for (const [processId, item] of survivors)
142
136
  if (item.state === "running")
143
137
  this.onExit(processId, null, "SIGKILL");
@@ -147,22 +141,14 @@ export class ProcessSupervisor {
147
141
  const item = this.processes.get(processId);
148
142
  if (!item || item.state !== "running")
149
143
  return;
150
- // Store only bounded head/tail output while forwarding live chunks to SSE.
151
144
  (stream === "stdout" ? item.stdout : item.stderr).append(chunk);
152
- const text = chunk.toString("utf8");
153
- this.eventBus.emit({
154
- type: "process-output",
155
- processId,
156
- stream,
157
- chunk: text,
158
- });
159
- const snapshot = this.snapshotOf(processId, item);
160
- this.operations.updateRunning(item.operationId, {
161
- stdout: snapshot.stdout,
162
- stderr: snapshot.stderr,
163
- outputTruncated: snapshot.outputTruncated,
164
- pid: snapshot.pid,
165
- });
145
+ if (!item.outputEventTimer) {
146
+ item.outputEventTimer = setTimeout(() => {
147
+ item.outputEventTimer = null;
148
+ if (item.state === "running")
149
+ this.eventBus.emit({ type: "process-output", processId });
150
+ }, 500).unref();
151
+ }
166
152
  }
167
153
  onExit(processId, exitCode, signal) {
168
154
  const item = this.processes.get(processId);
@@ -172,7 +158,8 @@ export class ProcessSupervisor {
172
158
  clearTimeout(item.timeout);
173
159
  if (item.forceTimeout)
174
160
  clearTimeout(item.forceTimeout);
175
- // A signal is represented as killed/cancelled; otherwise zero means exited and non-zero means failed.
161
+ if (item.outputEventTimer)
162
+ clearTimeout(item.outputEventTimer);
176
163
  item.finishedAt = new Date();
177
164
  item.exitCode = exitCode;
178
165
  item.signal = signal;
@@ -1,7 +1,3 @@
1
- /**
2
- * Process lifecycle and request types shared above the platform-specific backend layer.
3
- * ProcessId is independent from the OS PID and transport/session identity.
4
- */
5
1
  type ProcessState = "running" | "exited" | "killed" | "failed";
6
2
  export type ProcessId = string;
7
3
  export interface ProcessStartRequest {
@@ -24,7 +20,6 @@ export interface ProcessSnapshot {
24
20
  durationMs: number;
25
21
  exitCode: number | null;
26
22
  signal: string | null;
27
- /** Bounded retained output rather than an unbounded transcript. */
28
23
  stdout: string;
29
24
  stderr: string;
30
25
  outputTruncated: boolean;
@@ -7,7 +7,6 @@ import type { ExternalAccessRegistry } from "../app/external-access-registry.js"
7
7
  export interface ServiceToken<T> {
8
8
  readonly key: symbol;
9
9
  readonly name: string;
10
- /** Compile-time marker only; it is never set at runtime. */
11
10
  readonly __type?: T;
12
11
  }
13
12
  export declare function createServiceToken<T>(name: string): ServiceToken<T>;
@@ -1,10 +1,9 @@
1
1
  import type { Operation as DomainOperation } from "../../core/operations/types.js";
2
- import type { GitInfo } from "../workspace/domain/git.js";
2
+ import type { GitBranch, GitChange, GitCommit, GitDiff, GitStatus } from "../git/types.js";
3
3
  import type { ProcessSnapshot } from "../process/types.js";
4
4
  import type { ComputerDisplay, ComputerPermission, ComputerSnapshot, ComputerStatus } from "../computer/types.js";
5
- import type { Workspace } from "../workspace/domain/workspace.js";
6
- import type { WorktreeApplyPreview, WorktreeFileDiff } from "../workspace/domain/review.js";
7
- export type { ComputerDisplay, ComputerPermission, ComputerStatus, ProcessSnapshot, WorktreeApplyPreview, WorktreeFileDiff, };
5
+ import type { WorkspaceEntry, WorkspaceFile, WorkspaceInfo, WorkspaceSkill } from "../workspace/types.js";
6
+ export type { ComputerDisplay, ComputerPermission, ComputerStatus, GitBranch, GitChange, GitCommit, GitDiff, GitStatus, ProcessSnapshot, WorkspaceEntry, WorkspaceFile, WorkspaceInfo, WorkspaceSkill, };
8
7
  export interface ComputerPreviewView {
9
8
  snapshotId: string;
10
9
  revision: number;
@@ -19,6 +18,3 @@ export interface ComputerPreviewView {
19
18
  screenshot: ComputerSnapshot["screenshot"] | null;
20
19
  }
21
20
  export type Operation = DomainOperation;
22
- export interface WorkspaceView extends Workspace {
23
- git?: GitInfo | null;
24
- }
@@ -1,4 +1,3 @@
1
- /** HTTP/JSON presentation adapter for runtime status, workspaces, operations, processes, and management actions. */
2
1
  import { Router } from "express";
3
2
  import type { WebRuntime } from "../runtime.js";
4
3
  import type { RuntimeEventBus } from "../../../app/event-bus.js";
@@ -1,9 +1,9 @@
1
- /** HTTP/JSON presentation adapter for runtime status, workspaces, operations, processes, and management actions. */
2
1
  import { Router } from "express";
3
2
  import { ChatRoomError } from "../../../core/errors/chatroom-error.js";
4
3
  import { asyncRoute, parseCookie, requireString, } from "../../../presentation/http/http-utils.js";
5
4
  import { createProcessApiRouter } from "./process-api-router.js";
6
5
  import { createWorkspaceApiRouter } from "./workspace-api-router.js";
6
+ import { createGitApiRouter } from "./git-api-router.js";
7
7
  import { createCloudApiRouter } from "./cloud-api-router.js";
8
8
  import { createComputerApiRouter } from "./computer-api-router.js";
9
9
  const SESSION_COOKIE = "chatroom_session";
@@ -81,9 +81,6 @@ export function createApiRouter(application, eventBus, auth, passkeys, ingress,
81
81
  ...(typeof req.query.status === "string"
82
82
  ? { status: req.query.status }
83
83
  : {}),
84
- ...(typeof req.query.workspaceId === "string"
85
- ? { workspaceId: req.query.workspaceId }
86
- : {}),
87
84
  }));
88
85
  });
89
86
  router.delete("/operations", (_req, res) => {
@@ -97,6 +94,7 @@ export function createApiRouter(application, eventBus, auth, passkeys, ingress,
97
94
  res.json(event);
98
95
  });
99
96
  router.use(createWorkspaceApiRouter(application));
97
+ router.use(createGitApiRouter(application));
100
98
  router.use(createProcessApiRouter(application));
101
99
  router.use(createComputerApiRouter(application.computer, application.operations, ingress));
102
100
  router.use(createCloudApiRouter(cloud, application.operations));
@@ -0,0 +1,3 @@
1
+ import { Router } from "express";
2
+ import type { WebRuntime } from "../runtime.js";
3
+ export declare function createGitApiRouter(application: WebRuntime): Router;