@zhanguym/chatroom 1.0.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/LICENSE +202 -0
- package/README.md +52 -0
- package/assets/chatroom.svg +15 -0
- package/chatroom.config.example.json +24 -0
- package/dist/app/application.d.ts +20 -0
- package/dist/app/application.js +75 -0
- package/dist/app/event-bus.d.ts +25 -0
- package/dist/app/event-bus.js +11 -0
- package/dist/app/external-access-registry.d.ts +17 -0
- package/dist/app/external-access-registry.js +41 -0
- package/dist/app/lifecycle.d.ts +14 -0
- package/dist/app/lifecycle.js +130 -0
- package/dist/auth/auth-service.d.ts +88 -0
- package/dist/auth/auth-service.js +221 -0
- package/dist/auth/ingress-policy.d.ts +23 -0
- package/dist/auth/ingress-policy.js +61 -0
- package/dist/auth/passkey-service.d.ts +39 -0
- package/dist/auth/passkey-service.js +145 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +10 -0
- package/dist/cli/run-cli.d.ts +1 -0
- package/dist/cli/run-cli.js +41 -0
- package/dist/config/load-config.d.ts +7 -0
- package/dist/config/load-config.js +197 -0
- package/dist/config/platform-paths.d.ts +7 -0
- package/dist/config/platform-paths.js +34 -0
- package/dist/config/types.d.ts +24 -0
- package/dist/config/types.js +1 -0
- package/dist/core/auth/passkey-repository.d.ts +18 -0
- package/dist/core/auth/passkey-repository.js +1 -0
- package/dist/core/auth/repository.d.ts +36 -0
- package/dist/core/auth/repository.js +1 -0
- package/dist/core/auth/session-repository.d.ts +6 -0
- package/dist/core/auth/session-repository.js +1 -0
- package/dist/core/errors/chatroom-error.d.ts +9 -0
- package/dist/core/errors/chatroom-error.js +31 -0
- package/dist/core/logging/types.d.ts +31 -0
- package/dist/core/logging/types.js +1 -0
- package/dist/core/operations/bounded-value.d.ts +6 -0
- package/dist/core/operations/bounded-value.js +57 -0
- package/dist/core/operations/redactor.d.ts +4 -0
- package/dist/core/operations/redactor.js +69 -0
- package/dist/core/operations/repository.d.ts +18 -0
- package/dist/core/operations/repository.js +1 -0
- package/dist/core/operations/types.d.ts +28 -0
- package/dist/core/operations/types.js +8 -0
- package/dist/core/runtime/child-environment.d.ts +1 -0
- package/dist/core/runtime/child-environment.js +39 -0
- package/dist/core/runtime/command-runner.d.ts +16 -0
- package/dist/core/runtime/command-runner.js +48 -0
- package/dist/core/runtime/identity.d.ts +2 -0
- package/dist/core/runtime/identity.js +9 -0
- package/dist/infrastructure/database/app-database.d.ts +6 -0
- package/dist/infrastructure/database/app-database.js +22 -0
- package/dist/infrastructure/database/mcp-tool-settings-repository.d.ts +8 -0
- package/dist/infrastructure/database/mcp-tool-settings-repository.js +22 -0
- package/dist/infrastructure/database/oauth-repository.d.ts +20 -0
- package/dist/infrastructure/database/oauth-repository.js +140 -0
- package/dist/infrastructure/database/operation-repository.d.ts +16 -0
- package/dist/infrastructure/database/operation-repository.js +120 -0
- package/dist/infrastructure/database/passkey-repository.d.ts +11 -0
- package/dist/infrastructure/database/passkey-repository.js +57 -0
- package/dist/infrastructure/database/schema.d.ts +1 -0
- package/dist/infrastructure/database/schema.js +90 -0
- package/dist/infrastructure/database/web-session-repository.d.ts +10 -0
- package/dist/infrastructure/database/web-session-repository.js +26 -0
- package/dist/infrastructure/http/http-server.d.ts +28 -0
- package/dist/infrastructure/http/http-server.js +146 -0
- package/dist/infrastructure/http/ingress-middleware.d.ts +6 -0
- package/dist/infrastructure/http/ingress-middleware.js +58 -0
- package/dist/infrastructure/logging/file-log-store.d.ts +9 -0
- package/dist/infrastructure/logging/file-log-store.js +173 -0
- package/dist/infrastructure/logging/system-log.d.ts +22 -0
- package/dist/infrastructure/logging/system-log.js +117 -0
- package/dist/mcp/server/create-mcp-server.d.ts +3 -0
- package/dist/mcp/server/create-mcp-server.js +16 -0
- package/dist/mcp/server/plugin-mcp-registrar.d.ts +31 -0
- package/dist/mcp/server/plugin-mcp-registrar.js +70 -0
- package/dist/mcp/server/request-context.d.ts +3 -0
- package/dist/mcp/server/request-context.js +8 -0
- package/dist/mcp/server/tool-control.d.ts +31 -0
- package/dist/mcp/server/tool-control.js +76 -0
- package/dist/mcp/server/tool-support.d.ts +44 -0
- package/dist/mcp/server/tool-support.js +72 -0
- package/dist/native/linux/chatroom-computer-helper +0 -0
- package/dist/native/macos/ChatRoomComputerHelper.app/Contents/Info.plist +14 -0
- package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
- package/dist/native/macos/ChatRoomComputerHelper.app/Contents/_CodeSignature/CodeResources +115 -0
- package/dist/native/windows/chatroom-computer-helper.exe +0 -0
- package/dist/operations/operation-log.d.ts +24 -0
- package/dist/operations/operation-log.js +98 -0
- package/dist/plugins/cloud/api-client.d.ts +50 -0
- package/dist/plugins/cloud/api-client.js +112 -0
- package/dist/plugins/cloud/controller.d.ts +58 -0
- package/dist/plugins/cloud/controller.js +425 -0
- package/dist/plugins/cloud/device-proof.d.ts +9 -0
- package/dist/plugins/cloud/device-proof.js +17 -0
- package/dist/plugins/cloud/plugin.d.ts +4 -0
- package/dist/plugins/cloud/plugin.js +17 -0
- package/dist/plugins/cloud/state-store.d.ts +7 -0
- package/dist/plugins/cloud/state-store.js +79 -0
- package/dist/plugins/cloud/tunnel-client.d.ts +84 -0
- package/dist/plugins/cloud/tunnel-client.js +427 -0
- package/dist/plugins/cloud/types.d.ts +72 -0
- package/dist/plugins/cloud/types.js +60 -0
- package/dist/plugins/computer/audit.d.ts +34 -0
- package/dist/plugins/computer/audit.js +42 -0
- package/dist/plugins/computer/computer-native-backend.d.ts +11 -0
- package/dist/plugins/computer/computer-native-backend.js +58 -0
- package/dist/plugins/computer/computer-native-host.d.ts +30 -0
- package/dist/plugins/computer/computer-native-host.js +411 -0
- package/dist/plugins/computer/computer-protocol.d.ts +22 -0
- package/dist/plugins/computer/computer-protocol.js +74 -0
- package/dist/plugins/computer/computer-schemas.d.ts +285 -0
- package/dist/plugins/computer/computer-schemas.js +152 -0
- package/dist/plugins/computer/computer-service.d.ts +29 -0
- package/dist/plugins/computer/computer-service.js +117 -0
- package/dist/plugins/computer/computer-settings-repository.d.ts +8 -0
- package/dist/plugins/computer/computer-settings-repository.js +41 -0
- package/dist/plugins/computer/mcp.d.ts +3 -0
- package/dist/plugins/computer/mcp.js +126 -0
- package/dist/plugins/computer/plugin.d.ts +4 -0
- package/dist/plugins/computer/plugin.js +29 -0
- package/dist/plugins/computer/types.d.ts +174 -0
- package/dist/plugins/computer/types.js +1 -0
- package/dist/plugins/git/git-service.d.ts +30 -0
- package/dist/plugins/git/git-service.js +428 -0
- package/dist/plugins/git/plugin.d.ts +4 -0
- package/dist/plugins/git/plugin.js +12 -0
- package/dist/plugins/git/types.d.ts +32 -0
- package/dist/plugins/git/types.js +1 -0
- package/dist/plugins/plugin-manager.d.ts +11 -0
- package/dist/plugins/plugin-manager.js +63 -0
- package/dist/plugins/process/backend.d.ts +18 -0
- package/dist/plugins/process/backend.js +1 -0
- package/dist/plugins/process/head-tail-buffer.d.ts +17 -0
- package/dist/plugins/process/head-tail-buffer.js +49 -0
- package/dist/plugins/process/infrastructure/pipe-process-backend.d.ts +8 -0
- package/dist/plugins/process/infrastructure/pipe-process-backend.js +71 -0
- package/dist/plugins/process/infrastructure/pty-process-backend.d.ts +8 -0
- package/dist/plugins/process/infrastructure/pty-process-backend.js +51 -0
- package/dist/plugins/process/mcp.d.ts +3 -0
- package/dist/plugins/process/mcp.js +58 -0
- package/dist/plugins/process/plugin.d.ts +4 -0
- package/dist/plugins/process/plugin.js +25 -0
- package/dist/plugins/process/process-supervisor.d.ts +39 -0
- package/dist/plugins/process/process-supervisor.js +244 -0
- package/dist/plugins/process/types.d.ts +30 -0
- package/dist/plugins/process/types.js +1 -0
- package/dist/plugins/types.d.ts +35 -0
- package/dist/plugins/types.js +17 -0
- package/dist/plugins/web/api-types.d.ts +56 -0
- package/dist/plugins/web/api-types.js +1 -0
- package/dist/plugins/web/http/api-router.d.ts +13 -0
- package/dist/plugins/web/http/api-router.js +47 -0
- package/dist/plugins/web/http/auth-api-router.d.ts +8 -0
- package/dist/plugins/web/http/auth-api-router.js +123 -0
- package/dist/plugins/web/http/cloud-api-router.d.ts +4 -0
- package/dist/plugins/web/http/cloud-api-router.js +29 -0
- package/dist/plugins/web/http/computer-api-router.d.ts +5 -0
- package/dist/plugins/web/http/computer-api-router.js +69 -0
- package/dist/plugins/web/http/git-api-router.d.ts +3 -0
- package/dist/plugins/web/http/git-api-router.js +85 -0
- package/dist/plugins/web/http/logs-api-router.d.ts +3 -0
- package/dist/plugins/web/http/logs-api-router.js +49 -0
- package/dist/plugins/web/http/process-api-router.d.ts +3 -0
- package/dist/plugins/web/http/process-api-router.js +16 -0
- package/dist/plugins/web/http/runtime-api-router.d.ts +10 -0
- package/dist/plugins/web/http/runtime-api-router.js +34 -0
- package/dist/plugins/web/http/workspace-api-router.d.ts +3 -0
- package/dist/plugins/web/http/workspace-api-router.js +91 -0
- package/dist/plugins/web/plugin.d.ts +8 -0
- package/dist/plugins/web/plugin.js +17 -0
- package/dist/plugins/web/runtime.d.ts +18 -0
- package/dist/plugins/web/runtime.js +31 -0
- package/dist/plugins/workspace/mcp.d.ts +3 -0
- package/dist/plugins/workspace/mcp.js +24 -0
- package/dist/plugins/workspace/metadata.d.ts +6 -0
- package/dist/plugins/workspace/metadata.js +111 -0
- package/dist/plugins/workspace/plugin.d.ts +4 -0
- package/dist/plugins/workspace/plugin.js +22 -0
- package/dist/plugins/workspace/types.d.ts +23 -0
- package/dist/plugins/workspace/types.js +1 -0
- package/dist/plugins/workspace/workspace-fs.d.ts +28 -0
- package/dist/plugins/workspace/workspace-fs.js +200 -0
- package/dist/plugins/workspace/workspace-service.d.ts +15 -0
- package/dist/plugins/workspace/workspace-service.js +140 -0
- package/dist/presentation/http/http-utils.d.ts +9 -0
- package/dist/presentation/http/http-utils.js +89 -0
- package/dist/presentation/http/oauth-router.d.ts +5 -0
- package/dist/presentation/http/oauth-router.js +172 -0
- package/dist/web/assets/CloudView-CHjVbp-5.css +1 -0
- package/dist/web/assets/CloudView-CvX0dkgu.js +1 -0
- package/dist/web/assets/CodeViewer-CH1DEJge.css +1 -0
- package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-B5TxGkUW.js +3 -0
- package/dist/web/assets/ComputerView-91Z9Cefw.css +1 -0
- package/dist/web/assets/ComputerView-OOyEqWml.js +1 -0
- package/dist/web/assets/McpToolsView-BTnolyS6.css +1 -0
- package/dist/web/assets/McpToolsView-DjXWTKrZ.js +1 -0
- package/dist/web/assets/OperationsView-DqTY51Cr.js +4 -0
- package/dist/web/assets/ProcessesView-C13BykIa.js +4 -0
- package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-DyOhJmR6.js +1 -0
- package/dist/web/assets/SystemLogsView-Ci0u4BTb.js +1 -0
- package/dist/web/assets/VSelect-CAxEESw-.css +1 -0
- package/dist/web/assets/VSelect-DrRpKyYI.js +1 -0
- package/dist/web/assets/VSpacer-C3wAY3F0.js +1 -0
- package/dist/web/assets/VSpacer-DfbUir7X.css +1 -0
- package/dist/web/assets/VSwitch-Cos8PNSP.css +1 -0
- package/dist/web/assets/VSwitch-CxusR4iO.js +1 -0
- package/dist/web/assets/VTabs-DSWgbwhc.css +1 -0
- package/dist/web/assets/VTabs-qBcuuXh3.js +1 -0
- package/dist/web/assets/WorkspacesView-BzQ0aH74.css +1 -0
- package/dist/web/assets/WorkspacesView-CijIvfsp.js +2 -0
- package/dist/web/assets/_plugin-vue_export-helper-DlAUqK2U.js +1 -0
- package/dist/web/assets/chatroom-B5p-5n3y.svg +15 -0
- package/dist/web/assets/index-BDkarAkX.js +2 -0
- package/dist/web/assets/index-BegKQ3Yd.js +19 -0
- package/dist/web/assets/index-CkjqXRSj.css +1 -0
- package/dist/web/assets/syntax-highlight-B5ArnyrC.js +3 -0
- package/dist/web/assets/useOperationLabels-BDjzGDkT.js +1 -0
- package/dist/web/assets/utils-rtPwWNel.js +1 -0
- package/dist/web/index.html +19 -0
- package/package.json +125 -0
|
@@ -0,0 +1,428 @@
|
|
|
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";
|
|
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 result = await this.run(root, [
|
|
16
|
+
"status",
|
|
17
|
+
"--porcelain=v2",
|
|
18
|
+
"--branch",
|
|
19
|
+
"-z",
|
|
20
|
+
"--untracked-files=all",
|
|
21
|
+
]);
|
|
22
|
+
return parseStatus(result.stdout);
|
|
23
|
+
}
|
|
24
|
+
async diff(cwd, filePath, maxPreviewBytes = 2 * 1024 * 1024) {
|
|
25
|
+
const root = await this.requireRepository(cwd);
|
|
26
|
+
await this.requireHead(root);
|
|
27
|
+
const paths = filePath ? await this.relatedPaths(root, [filePath]) : [];
|
|
28
|
+
return this.withSnapshotIndex(root, async ({ env, tempDir }) => {
|
|
29
|
+
const patchPath = path.join(tempDir, "changes.patch");
|
|
30
|
+
await this.run(root, [
|
|
31
|
+
"diff",
|
|
32
|
+
"--cached",
|
|
33
|
+
"--binary",
|
|
34
|
+
"--no-ext-diff",
|
|
35
|
+
"--no-renames",
|
|
36
|
+
`--output=${patchPath}`,
|
|
37
|
+
"HEAD",
|
|
38
|
+
...(paths.length ? ["--", ...paths] : []),
|
|
39
|
+
], env);
|
|
40
|
+
return readPatch(patchPath, maxPreviewBytes);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async stage(cwd, paths) {
|
|
44
|
+
const root = await this.requireRepository(cwd);
|
|
45
|
+
await this.run(root, [
|
|
46
|
+
"add",
|
|
47
|
+
"-A",
|
|
48
|
+
"--",
|
|
49
|
+
...(await this.relatedPaths(root, paths)),
|
|
50
|
+
]);
|
|
51
|
+
return this.requireStatus(root);
|
|
52
|
+
}
|
|
53
|
+
async unstage(cwd, paths) {
|
|
54
|
+
const root = await this.requireRepository(cwd);
|
|
55
|
+
const expanded = await this.relatedPaths(root, paths);
|
|
56
|
+
if (await this.head(root))
|
|
57
|
+
await this.run(root, ["restore", "--staged", "--", ...expanded]);
|
|
58
|
+
else
|
|
59
|
+
await this.run(root, ["rm", "--cached", "-r", "--", ...expanded]);
|
|
60
|
+
return this.requireStatus(root);
|
|
61
|
+
}
|
|
62
|
+
async restore(cwd, filePath) {
|
|
63
|
+
const root = await this.requireRepository(cwd);
|
|
64
|
+
const requested = normalizePath(filePath);
|
|
65
|
+
const status = await this.requireStatus(root);
|
|
66
|
+
const change = status.changes.find((item) => item.path === requested || item.originalPath === requested);
|
|
67
|
+
if (!change)
|
|
68
|
+
throw new ChatRoomError("NOT_FOUND", `No Git change found for path: ${requested}`);
|
|
69
|
+
if (change.kind === "untracked") {
|
|
70
|
+
await this.run(root, ["clean", "-f", "--", change.path]);
|
|
71
|
+
return this.requireStatus(root);
|
|
72
|
+
}
|
|
73
|
+
await this.requireHead(root);
|
|
74
|
+
const paths = [
|
|
75
|
+
change.path,
|
|
76
|
+
...(change.originalPath ? [change.originalPath] : []),
|
|
77
|
+
];
|
|
78
|
+
const tracked = [];
|
|
79
|
+
const untracked = [];
|
|
80
|
+
for (const file of [...new Set(paths)]) {
|
|
81
|
+
if (await this.pathExistsInHead(root, file))
|
|
82
|
+
tracked.push(file);
|
|
83
|
+
else
|
|
84
|
+
untracked.push(file);
|
|
85
|
+
}
|
|
86
|
+
if (tracked.length)
|
|
87
|
+
await this.run(root, [
|
|
88
|
+
"restore",
|
|
89
|
+
"--source=HEAD",
|
|
90
|
+
"--staged",
|
|
91
|
+
"-W",
|
|
92
|
+
"--",
|
|
93
|
+
...tracked,
|
|
94
|
+
]);
|
|
95
|
+
if (untracked.length) {
|
|
96
|
+
await this.run(root, ["restore", "--staged", "--", ...untracked]).catch(() => undefined);
|
|
97
|
+
await this.run(root, ["clean", "-f", "--", ...untracked]);
|
|
98
|
+
}
|
|
99
|
+
return this.requireStatus(root);
|
|
100
|
+
}
|
|
101
|
+
async commit(cwd, message) {
|
|
102
|
+
const root = await this.requireRepository(cwd);
|
|
103
|
+
const normalized = message.trim();
|
|
104
|
+
if (!normalized)
|
|
105
|
+
throw new ChatRoomError("INVALID_INPUT", "Commit message is required");
|
|
106
|
+
if (normalized.length > 10_000)
|
|
107
|
+
throw new ChatRoomError("INVALID_INPUT", "Commit message is too long");
|
|
108
|
+
await this.run(root, ["commit", "-m", normalized]);
|
|
109
|
+
return this.requireStatus(root);
|
|
110
|
+
}
|
|
111
|
+
async branches(cwd) {
|
|
112
|
+
const root = await this.requireRepository(cwd);
|
|
113
|
+
const result = await this.run(root, [
|
|
114
|
+
"for-each-ref",
|
|
115
|
+
"--format=%(HEAD)%00%(refname:short)%00%(upstream:short)",
|
|
116
|
+
"refs/heads",
|
|
117
|
+
]);
|
|
118
|
+
return result.stdout
|
|
119
|
+
.split(/\r?\n/)
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.map((line) => {
|
|
122
|
+
const [head = "", name = "", upstream = ""] = line.split("\0");
|
|
123
|
+
return {
|
|
124
|
+
name,
|
|
125
|
+
current: head.trim() === "*",
|
|
126
|
+
upstream: upstream || null,
|
|
127
|
+
};
|
|
128
|
+
})
|
|
129
|
+
.filter((branch) => Boolean(branch.name));
|
|
130
|
+
}
|
|
131
|
+
async createBranch(cwd, name) {
|
|
132
|
+
const root = await this.requireRepository(cwd);
|
|
133
|
+
const branch = await this.validateBranchName(root, name);
|
|
134
|
+
await this.run(root, ["switch", "-c", branch]);
|
|
135
|
+
return this.requireStatus(root);
|
|
136
|
+
}
|
|
137
|
+
async switchBranch(cwd, name) {
|
|
138
|
+
const root = await this.requireRepository(cwd);
|
|
139
|
+
const branch = await this.validateBranchName(root, name);
|
|
140
|
+
await this.run(root, ["switch", branch]);
|
|
141
|
+
return this.requireStatus(root);
|
|
142
|
+
}
|
|
143
|
+
async deleteBranch(cwd, name) {
|
|
144
|
+
const root = await this.requireRepository(cwd);
|
|
145
|
+
const branch = await this.validateBranchName(root, name);
|
|
146
|
+
await this.run(root, ["branch", "-d", branch]);
|
|
147
|
+
return this.requireStatus(root);
|
|
148
|
+
}
|
|
149
|
+
async log(cwd, limit = 30) {
|
|
150
|
+
const root = await this.requireRepository(cwd);
|
|
151
|
+
if (!(await this.head(root)))
|
|
152
|
+
return [];
|
|
153
|
+
const count = Math.min(Math.max(Math.trunc(limit), 1), 100);
|
|
154
|
+
const result = await this.run(root, [
|
|
155
|
+
"log",
|
|
156
|
+
`-n${count}`,
|
|
157
|
+
"--pretty=format:%H%x1f%h%x1f%an%x1f%aI%x1f%s%x1e",
|
|
158
|
+
]);
|
|
159
|
+
return result.stdout
|
|
160
|
+
.split("\x1e")
|
|
161
|
+
.map((record) => record.trim())
|
|
162
|
+
.filter(Boolean)
|
|
163
|
+
.map((record) => {
|
|
164
|
+
const [hash = "", shortHash = "", author = "", date = "", subject = "",] = record.split("\x1f");
|
|
165
|
+
return { hash, shortHash, author, date, subject };
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async fetch(cwd) {
|
|
169
|
+
const root = await this.requireRepository(cwd);
|
|
170
|
+
await this.run(root, ["fetch", "--all", "--prune"]);
|
|
171
|
+
return this.requireStatus(root);
|
|
172
|
+
}
|
|
173
|
+
async pull(cwd) {
|
|
174
|
+
const root = await this.requireRepository(cwd);
|
|
175
|
+
await this.run(root, ["pull", "--ff-only"]);
|
|
176
|
+
return this.requireStatus(root);
|
|
177
|
+
}
|
|
178
|
+
async push(cwd) {
|
|
179
|
+
const root = await this.requireRepository(cwd);
|
|
180
|
+
await this.run(root, ["push"]);
|
|
181
|
+
return this.requireStatus(root);
|
|
182
|
+
}
|
|
183
|
+
run(cwd, args, env) {
|
|
184
|
+
return this.commands.run({
|
|
185
|
+
cwd,
|
|
186
|
+
command: "git",
|
|
187
|
+
args,
|
|
188
|
+
timeoutMs: 120_000,
|
|
189
|
+
env: { LC_ALL: "C", LANG: "C", ...(env ?? {}) },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
async repositoryRoot(cwd) {
|
|
193
|
+
try {
|
|
194
|
+
const root = path.resolve((await this.run(cwd, ["rev-parse", "--show-toplevel"])).stdout.trim());
|
|
195
|
+
return root === path.resolve(cwd) ? root : null;
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (isNotRepositoryError(error))
|
|
199
|
+
return null;
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async requireRepository(cwd) {
|
|
204
|
+
const root = await this.repositoryRoot(cwd);
|
|
205
|
+
if (!root)
|
|
206
|
+
throw new ChatRoomError("INVALID_INPUT", "Workspace is not a Git repository");
|
|
207
|
+
return root;
|
|
208
|
+
}
|
|
209
|
+
async requireStatus(cwd) {
|
|
210
|
+
const status = await this.status(cwd);
|
|
211
|
+
if (!status)
|
|
212
|
+
throw new ChatRoomError("INVALID_INPUT", "Workspace is not a Git repository");
|
|
213
|
+
return status;
|
|
214
|
+
}
|
|
215
|
+
async head(cwd) {
|
|
216
|
+
try {
|
|
217
|
+
return ((await this.run(cwd, ["rev-parse", "--verify", "HEAD"])).stdout.trim() || null);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (isMissingHeadError(error))
|
|
221
|
+
return null;
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async requireHead(cwd) {
|
|
226
|
+
const head = await this.head(cwd);
|
|
227
|
+
if (!head)
|
|
228
|
+
throw new ChatRoomError("CONFLICT", "Git operation requires at least one commit");
|
|
229
|
+
return head;
|
|
230
|
+
}
|
|
231
|
+
async relatedPaths(cwd, paths) {
|
|
232
|
+
const normalized = normalizePaths(paths);
|
|
233
|
+
const status = await this.requireStatus(cwd);
|
|
234
|
+
const output = new Set(normalized);
|
|
235
|
+
for (const requested of normalized) {
|
|
236
|
+
const change = status.changes.find((item) => item.path === requested || item.originalPath === requested);
|
|
237
|
+
if (change?.path)
|
|
238
|
+
output.add(change.path);
|
|
239
|
+
if (change?.originalPath)
|
|
240
|
+
output.add(change.originalPath);
|
|
241
|
+
}
|
|
242
|
+
return [...output];
|
|
243
|
+
}
|
|
244
|
+
async pathExistsInHead(cwd, filePath) {
|
|
245
|
+
try {
|
|
246
|
+
await this.run(cwd, ["cat-file", "-e", `HEAD:${filePath}`]);
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async validateBranchName(cwd, name) {
|
|
254
|
+
const normalized = name.trim();
|
|
255
|
+
if (!normalized)
|
|
256
|
+
throw new ChatRoomError("INVALID_INPUT", "Branch name is required");
|
|
257
|
+
await this.run(cwd, ["check-ref-format", "--branch", normalized]);
|
|
258
|
+
return normalized;
|
|
259
|
+
}
|
|
260
|
+
async withSnapshotIndex(cwd, useIndex) {
|
|
261
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "chatroom-git-"));
|
|
262
|
+
const env = { GIT_INDEX_FILE: path.join(tempDir, "index") };
|
|
263
|
+
try {
|
|
264
|
+
await this.run(cwd, ["read-tree", "HEAD"], env);
|
|
265
|
+
await this.run(cwd, ["add", "-A", "--", "."], env);
|
|
266
|
+
return await useIndex({ env, tempDir });
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function parseStatus(output) {
|
|
274
|
+
let branch = null;
|
|
275
|
+
let head = null;
|
|
276
|
+
let upstream = null;
|
|
277
|
+
let ahead = 0;
|
|
278
|
+
let behind = 0;
|
|
279
|
+
const changes = [];
|
|
280
|
+
const records = output.split("\0");
|
|
281
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
282
|
+
const record = records[index];
|
|
283
|
+
if (!record)
|
|
284
|
+
continue;
|
|
285
|
+
if (record.startsWith("# branch.oid ")) {
|
|
286
|
+
const value = record.slice("# branch.oid ".length);
|
|
287
|
+
head = value === "(initial)" ? null : value || null;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (record.startsWith("# branch.head ")) {
|
|
291
|
+
const value = record.slice("# branch.head ".length);
|
|
292
|
+
branch = value === "(detached)" ? null : value || null;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (record.startsWith("# branch.upstream ")) {
|
|
296
|
+
upstream = record.slice("# branch.upstream ".length) || null;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (record.startsWith("# branch.ab ")) {
|
|
300
|
+
const match = /^\+(\d+) -(\d+)$/.exec(record.slice("# branch.ab ".length));
|
|
301
|
+
if (match) {
|
|
302
|
+
ahead = Number(match[1]);
|
|
303
|
+
behind = Number(match[2]);
|
|
304
|
+
}
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const type = record[0];
|
|
308
|
+
if (type === "?") {
|
|
309
|
+
changes.push({
|
|
310
|
+
path: record.slice(2),
|
|
311
|
+
originalPath: null,
|
|
312
|
+
indexStatus: "?",
|
|
313
|
+
workingTreeStatus: "?",
|
|
314
|
+
kind: "untracked",
|
|
315
|
+
});
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (type === "!")
|
|
319
|
+
continue;
|
|
320
|
+
const fields = record.split(" ");
|
|
321
|
+
if (type === "1" && fields.length >= 9) {
|
|
322
|
+
pushTrackedChange(changes, fields[1], fields.slice(8).join(" "), null);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (type === "2" && fields.length >= 10) {
|
|
326
|
+
const originalPath = records[++index] || null;
|
|
327
|
+
pushTrackedChange(changes, fields[1], fields.slice(9).join(" "), originalPath);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (type === "u" && fields.length >= 11) {
|
|
331
|
+
pushTrackedChange(changes, fields[1], fields.slice(10).join(" "), null);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
branch,
|
|
336
|
+
head,
|
|
337
|
+
upstream,
|
|
338
|
+
ahead,
|
|
339
|
+
behind,
|
|
340
|
+
changes: changes.sort((a, b) => a.path.localeCompare(b.path)),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function pushTrackedChange(changes, xy, filePath, originalPath) {
|
|
344
|
+
const indexStatus = normalizePorcelainStatus(xy[0]);
|
|
345
|
+
const workingTreeStatus = normalizePorcelainStatus(xy[1]);
|
|
346
|
+
changes.push({
|
|
347
|
+
path: filePath,
|
|
348
|
+
originalPath,
|
|
349
|
+
indexStatus,
|
|
350
|
+
workingTreeStatus,
|
|
351
|
+
kind: changeKind(indexStatus, workingTreeStatus),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function normalizePorcelainStatus(value) {
|
|
355
|
+
return !value || value === "." ? " " : value;
|
|
356
|
+
}
|
|
357
|
+
function changeKind(indexStatus, workingTreeStatus) {
|
|
358
|
+
const pair = `${indexStatus}${workingTreeStatus}`;
|
|
359
|
+
if (pair === "??")
|
|
360
|
+
return "untracked";
|
|
361
|
+
if (["DD", "AU", "UD", "UA", "DU", "AA", "UU"].includes(pair) ||
|
|
362
|
+
indexStatus === "U" ||
|
|
363
|
+
workingTreeStatus === "U")
|
|
364
|
+
return "conflicted";
|
|
365
|
+
if (pair.includes("R"))
|
|
366
|
+
return "renamed";
|
|
367
|
+
if (pair.includes("C"))
|
|
368
|
+
return "copied";
|
|
369
|
+
if (pair.includes("A"))
|
|
370
|
+
return "added";
|
|
371
|
+
if (pair.includes("D"))
|
|
372
|
+
return "deleted";
|
|
373
|
+
return "modified";
|
|
374
|
+
}
|
|
375
|
+
function normalizePaths(paths) {
|
|
376
|
+
if (!Array.isArray(paths) || paths.length < 1)
|
|
377
|
+
throw new ChatRoomError("INVALID_INPUT", "At least one file path is required");
|
|
378
|
+
if (paths.length > MAX_PATHS)
|
|
379
|
+
throw new ChatRoomError("INVALID_INPUT", "Too many file paths");
|
|
380
|
+
return [...new Set(paths.map(normalizePath))];
|
|
381
|
+
}
|
|
382
|
+
function normalizePath(input) {
|
|
383
|
+
if (typeof input !== "string" || !input || input.includes("\0"))
|
|
384
|
+
throw new ChatRoomError("INVALID_INPUT", "File path is required");
|
|
385
|
+
if (path.isAbsolute(input) ||
|
|
386
|
+
path.win32.isAbsolute(input) ||
|
|
387
|
+
/^\\\\/.test(input))
|
|
388
|
+
throw new ChatRoomError("FORBIDDEN", "Absolute Git paths are not allowed");
|
|
389
|
+
const parts = input
|
|
390
|
+
.split(/[\\/]+/)
|
|
391
|
+
.filter((part) => part !== "" && part !== ".");
|
|
392
|
+
if (parts.length === 0 || parts.some((part) => part === ".."))
|
|
393
|
+
throw new ChatRoomError("FORBIDDEN", "Invalid Git path");
|
|
394
|
+
return parts.join("/");
|
|
395
|
+
}
|
|
396
|
+
async function readPatch(patchPath, maxBytes) {
|
|
397
|
+
const size = (await stat(patchPath)).size;
|
|
398
|
+
const previewBytes = Math.min(size, maxBytes);
|
|
399
|
+
const handle = await open(patchPath, "r");
|
|
400
|
+
try {
|
|
401
|
+
const buffer = Buffer.alloc(previewBytes);
|
|
402
|
+
const { bytesRead } = await handle.read(buffer, 0, previewBytes, 0);
|
|
403
|
+
return {
|
|
404
|
+
diff: buffer.subarray(0, bytesRead).toString("utf8"),
|
|
405
|
+
truncated: size > previewBytes,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
await handle.close();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function gitErrorText(error) {
|
|
413
|
+
if (!(error instanceof ChatRoomError))
|
|
414
|
+
return "";
|
|
415
|
+
const details = error.details;
|
|
416
|
+
return `${String(details?.stdout ?? "")}\n${String(details?.stderr ?? "")}`.toLowerCase();
|
|
417
|
+
}
|
|
418
|
+
function isNotRepositoryError(error) {
|
|
419
|
+
return gitErrorText(error).includes("not a git repository");
|
|
420
|
+
}
|
|
421
|
+
function isMissingHeadError(error) {
|
|
422
|
+
const text = gitErrorText(error);
|
|
423
|
+
return (text.includes("needed a single revision") ||
|
|
424
|
+
text.includes("unknown revision") ||
|
|
425
|
+
text.includes("bad revision") ||
|
|
426
|
+
text.includes("ambiguous argument 'head'") ||
|
|
427
|
+
text.includes("not a valid object name head"));
|
|
428
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CommandRunner } from "#core/runtime/command-runner";
|
|
2
|
+
import { createServiceToken } from "#plugins/types";
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/server";
|
|
2
|
+
import type { InternalPlugin, PluginContext } from "./types.js";
|
|
3
|
+
export declare class PluginManager {
|
|
4
|
+
private readonly context;
|
|
5
|
+
private readonly plugins;
|
|
6
|
+
private readonly active;
|
|
7
|
+
constructor(context: PluginContext, plugins: InternalPlugin[]);
|
|
8
|
+
start(): Promise<void>;
|
|
9
|
+
registerMcp(server: McpServer): void;
|
|
10
|
+
stop(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { PluginMcpRegistrar } from "#mcp/server/plugin-mcp-registrar";
|
|
2
|
+
export class PluginManager {
|
|
3
|
+
context;
|
|
4
|
+
plugins;
|
|
5
|
+
active = [];
|
|
6
|
+
constructor(context, plugins) {
|
|
7
|
+
this.context = context;
|
|
8
|
+
this.plugins = plugins;
|
|
9
|
+
}
|
|
10
|
+
async start() {
|
|
11
|
+
try {
|
|
12
|
+
for (const plugin of this.plugins) {
|
|
13
|
+
this.active.push(plugin);
|
|
14
|
+
try {
|
|
15
|
+
await plugin.activate(this.context);
|
|
16
|
+
this.context.logs.info("plugin", "plugin.activated", `Plugin activated: ${plugin.id}`, { pluginId: plugin.id });
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
this.context.logs.error("plugin", "plugin.activate_failed", `Plugin activation failed: ${plugin.id}`, { pluginId: plugin.id, error });
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
for (const plugin of this.active) {
|
|
24
|
+
if (!plugin.registerMcp)
|
|
25
|
+
continue;
|
|
26
|
+
plugin.registerMcp(new PluginMcpRegistrar(null, this.context.operations, plugin.id, this.context.mcpTools));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
try {
|
|
31
|
+
await this.stop();
|
|
32
|
+
}
|
|
33
|
+
catch (cleanupError) {
|
|
34
|
+
throw new AggregateError([error, cleanupError], "Plugin startup failed and rollback encountered errors");
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
registerMcp(server) {
|
|
40
|
+
for (const plugin of this.active) {
|
|
41
|
+
if (!plugin.registerMcp)
|
|
42
|
+
continue;
|
|
43
|
+
plugin.registerMcp(new PluginMcpRegistrar(server, this.context.operations, plugin.id, this.context.mcpTools));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async stop() {
|
|
47
|
+
const errors = [];
|
|
48
|
+
for (const plugin of [...this.active].reverse()) {
|
|
49
|
+
try {
|
|
50
|
+
await plugin.deactivate?.();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
this.context.logs.error("plugin", "plugin.deactivate_failed", `Plugin deactivation failed: ${plugin.id}`, { pluginId: plugin.id, error });
|
|
54
|
+
errors.push(error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
this.active.length = 0;
|
|
58
|
+
if (errors.length === 1)
|
|
59
|
+
throw errors[0];
|
|
60
|
+
if (errors.length > 1)
|
|
61
|
+
throw new AggregateError(errors, "Multiple plugins failed to stop");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface BackendProcess {
|
|
2
|
+
readonly pid: number | null;
|
|
3
|
+
write(data: string): void;
|
|
4
|
+
kill(signal?: NodeJS.Signals): void;
|
|
5
|
+
onStdout(listener: (chunk: Buffer) => void): void;
|
|
6
|
+
onStderr(listener: (chunk: Buffer) => void): void;
|
|
7
|
+
onExit(listener: (exit: {
|
|
8
|
+
exitCode: number | null;
|
|
9
|
+
signal: string | null;
|
|
10
|
+
}) => void): void;
|
|
11
|
+
}
|
|
12
|
+
export interface ProcessBackend {
|
|
13
|
+
readonly kind: "pipe" | "pty";
|
|
14
|
+
start(command: string, args: string[], options: {
|
|
15
|
+
cwd: string;
|
|
16
|
+
env: NodeJS.ProcessEnv;
|
|
17
|
+
}): Promise<BackendProcess>;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface BufferSnapshot {
|
|
2
|
+
text: string;
|
|
3
|
+
bytesSeen: number;
|
|
4
|
+
outputTruncated: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare class HeadTailBuffer {
|
|
7
|
+
private readonly headLimit;
|
|
8
|
+
private readonly tailLimit;
|
|
9
|
+
private head;
|
|
10
|
+
private tail;
|
|
11
|
+
private bytesSeen;
|
|
12
|
+
constructor(totalLimitBytes?: number);
|
|
13
|
+
append(chunk: Buffer | string): void;
|
|
14
|
+
snapshot(): BufferSnapshot;
|
|
15
|
+
private pushTail;
|
|
16
|
+
}
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export class HeadTailBuffer {
|
|
2
|
+
headLimit;
|
|
3
|
+
tailLimit;
|
|
4
|
+
head = Buffer.alloc(0);
|
|
5
|
+
tail = Buffer.alloc(0);
|
|
6
|
+
bytesSeen = 0;
|
|
7
|
+
constructor(totalLimitBytes = 256 * 1024) {
|
|
8
|
+
if (totalLimitBytes < 128)
|
|
9
|
+
throw new RangeError("Buffer limit must be at least 128 bytes");
|
|
10
|
+
this.headLimit = Math.floor(totalLimitBytes / 2);
|
|
11
|
+
this.tailLimit = totalLimitBytes - this.headLimit;
|
|
12
|
+
}
|
|
13
|
+
append(chunk) {
|
|
14
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
15
|
+
this.bytesSeen += data.length;
|
|
16
|
+
if (this.head.length < this.headLimit) {
|
|
17
|
+
const needed = this.headLimit - this.head.length;
|
|
18
|
+
this.head = Buffer.concat([this.head, data.subarray(0, needed)]);
|
|
19
|
+
if (data.length <= needed)
|
|
20
|
+
return;
|
|
21
|
+
this.pushTail(data.subarray(needed));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this.pushTail(data);
|
|
25
|
+
}
|
|
26
|
+
snapshot() {
|
|
27
|
+
const stored = this.head.length + this.tail.length;
|
|
28
|
+
const truncated = this.bytesSeen > stored;
|
|
29
|
+
if (!truncated) {
|
|
30
|
+
return {
|
|
31
|
+
text: Buffer.concat([this.head, this.tail]).toString("utf8"),
|
|
32
|
+
bytesSeen: this.bytesSeen,
|
|
33
|
+
outputTruncated: false,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
text: `${this.head.toString("utf8")}\n…[${this.bytesSeen - stored} bytes truncated]…\n${this.tail.toString("utf8")}`,
|
|
38
|
+
bytesSeen: this.bytesSeen,
|
|
39
|
+
outputTruncated: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
pushTail(data) {
|
|
43
|
+
const combined = Buffer.concat([this.tail, data]);
|
|
44
|
+
this.tail =
|
|
45
|
+
combined.length > this.tailLimit
|
|
46
|
+
? combined.subarray(combined.length - this.tailLimit)
|
|
47
|
+
: combined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BackendProcess, ProcessBackend } from "../backend.js";
|
|
2
|
+
export declare class PipeProcessBackend implements ProcessBackend {
|
|
3
|
+
readonly kind: "pipe";
|
|
4
|
+
start(command: string, args: string[], options: {
|
|
5
|
+
cwd: string;
|
|
6
|
+
env: NodeJS.ProcessEnv;
|
|
7
|
+
}): Promise<BackendProcess>;
|
|
8
|
+
}
|