@chatroomcp/chatroom 0.1.3
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 +50 -0
- package/assets/chatroom.svg +15 -0
- package/chatroom.config.example.json +24 -0
- package/dist/app/application.d.ts +18 -0
- package/dist/app/application.js +63 -0
- package/dist/app/event-bus.d.ts +23 -0
- package/dist/app/event-bus.js +12 -0
- package/dist/app/external-access-registry.d.ts +18 -0
- package/dist/app/external-access-registry.js +48 -0
- package/dist/app/lifecycle.d.ts +12 -0
- package/dist/app/lifecycle.js +41 -0
- package/dist/auth/auth-service.d.ts +88 -0
- package/dist/auth/auth-service.js +222 -0
- package/dist/auth/ingress-policy.d.ts +21 -0
- package/dist/auth/ingress-policy.js +57 -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 +42 -0
- package/dist/config/load-config.d.ts +7 -0
- package/dist/config/load-config.js +182 -0
- package/dist/config/platform-paths.d.ts +6 -0
- package/dist/config/platform-paths.js +34 -0
- package/dist/config/types.d.ts +25 -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 +59 -0
- package/dist/core/auth/repository.js +7 -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 +23 -0
- package/dist/core/errors/chatroom-error.js +46 -0
- package/dist/core/operations/bounded-value.d.ts +6 -0
- package/dist/core/operations/bounded-value.js +41 -0
- package/dist/core/operations/redactor.d.ts +4 -0
- package/dist/core/operations/redactor.js +65 -0
- package/dist/core/operations/repository.d.ts +19 -0
- package/dist/core/operations/repository.js +1 -0
- package/dist/core/operations/types.d.ts +31 -0
- package/dist/core/operations/types.js +9 -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 +15 -0
- package/dist/core/runtime/command-runner.js +39 -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 +104 -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 +129 -0
- package/dist/infrastructure/database/passkey-repository.d.ts +11 -0
- package/dist/infrastructure/database/passkey-repository.js +57 -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 +26 -0
- package/dist/infrastructure/http/http-server.js +166 -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/tool-support.d.ts +105 -0
- package/dist/mcp/server/tool-support.js +124 -0
- package/dist/operations/operation-log.d.ts +23 -0
- package/dist/operations/operation-log.js +103 -0
- package/dist/plugins/cloud/api-client.d.ts +56 -0
- package/dist/plugins/cloud/api-client.js +122 -0
- package/dist/plugins/cloud/controller.d.ts +48 -0
- package/dist/plugins/cloud/controller.js +282 -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 +74 -0
- package/dist/plugins/cloud/tunnel-client.d.ts +36 -0
- package/dist/plugins/cloud/tunnel-client.js +293 -0
- package/dist/plugins/cloud/types.d.ts +70 -0
- package/dist/plugins/cloud/types.js +32 -0
- package/dist/plugins/plugin-manager.d.ts +11 -0
- package/dist/plugins/plugin-manager.js +24 -0
- package/dist/plugins/process/backend.d.ts +19 -0
- package/dist/plugins/process/backend.js +1 -0
- package/dist/plugins/process/head-tail-buffer.d.ts +18 -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 +61 -0
- package/dist/plugins/process/infrastructure/pty-process-backend.d.ts +8 -0
- package/dist/plugins/process/infrastructure/pty-process-backend.js +50 -0
- package/dist/plugins/process/mcp.d.ts +4 -0
- package/dist/plugins/process/mcp.js +69 -0
- package/dist/plugins/process/plugin.d.ts +4 -0
- package/dist/plugins/process/plugin.js +28 -0
- package/dist/plugins/process/process-supervisor.d.ts +36 -0
- package/dist/plugins/process/process-supervisor.js +232 -0
- package/dist/plugins/process/types.d.ts +34 -0
- package/dist/plugins/process/types.js +1 -0
- package/dist/plugins/types.d.ts +32 -0
- package/dist/plugins/types.js +17 -0
- package/dist/plugins/web/api-types.d.ts +10 -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 +146 -0
- package/dist/plugins/web/http/cloud-api-router.d.ts +4 -0
- package/dist/plugins/web/http/cloud-api-router.js +41 -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/workspace-api-router.d.ts +3 -0
- package/dist/plugins/web/http/workspace-api-router.js +82 -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 +36 -0
- package/dist/plugins/web/runtime.js +66 -0
- package/dist/plugins/workspace/domain/filesystem.d.ts +25 -0
- package/dist/plugins/workspace/domain/filesystem.js +1 -0
- package/dist/plugins/workspace/domain/git.d.ts +7 -0
- package/dist/plugins/workspace/domain/git.js +1 -0
- package/dist/plugins/workspace/domain/repository.d.ts +16 -0
- package/dist/plugins/workspace/domain/repository.js +1 -0
- package/dist/plugins/workspace/domain/review.d.ts +28 -0
- package/dist/plugins/workspace/domain/review.js +1 -0
- package/dist/plugins/workspace/domain/workspace.d.ts +23 -0
- package/dist/plugins/workspace/domain/workspace.js +1 -0
- package/dist/plugins/workspace/git/git-command-runner.d.ts +6 -0
- package/dist/plugins/workspace/git/git-command-runner.js +15 -0
- package/dist/plugins/workspace/git/git-service.d.ts +14 -0
- package/dist/plugins/workspace/git/git-service.js +107 -0
- package/dist/plugins/workspace/git/git-snapshot-service.d.ts +33 -0
- package/dist/plugins/workspace/git/git-snapshot-service.js +352 -0
- package/dist/plugins/workspace/infrastructure/workspace-fs.d.ts +38 -0
- package/dist/plugins/workspace/infrastructure/workspace-fs.js +340 -0
- package/dist/plugins/workspace/infrastructure/workspace-repository.d.ts +12 -0
- package/dist/plugins/workspace/infrastructure/workspace-repository.js +51 -0
- package/dist/plugins/workspace/mcp.d.ts +4 -0
- package/dist/plugins/workspace/mcp.js +168 -0
- package/dist/plugins/workspace/plugin.d.ts +10 -0
- package/dist/plugins/workspace/plugin.js +37 -0
- package/dist/plugins/workspace/workspace-helpers.d.ts +9 -0
- package/dist/plugins/workspace/workspace-helpers.js +67 -0
- package/dist/plugins/workspace/workspace-service.d.ts +46 -0
- package/dist/plugins/workspace/workspace-service.js +339 -0
- package/dist/plugins/workspace/worktree-review-service.d.ts +16 -0
- package/dist/plugins/workspace/worktree-review-service.js +51 -0
- package/dist/presentation/http/http-utils.d.ts +7 -0
- package/dist/presentation/http/http-utils.js +47 -0
- package/dist/presentation/http/oauth-router.d.ts +5 -0
- package/dist/presentation/http/oauth-router.js +163 -0
- package/dist/web/assets/chatroom-B5p-5n3y.svg +15 -0
- package/dist/web/assets/index-BF_mWl1D.js +26 -0
- package/dist/web/assets/index-JPZut9tc.css +1 -0
- package/dist/web/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
- package/dist/web/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
- package/dist/web/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
- package/dist/web/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
- package/dist/web/index.html +19 -0
- package/package.json +77 -0
|
@@ -0,0 +1,352 @@
|
|
|
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
|
+
export class GitSnapshotService {
|
|
6
|
+
runner;
|
|
7
|
+
git;
|
|
8
|
+
constructor(runner, git) {
|
|
9
|
+
this.runner = runner;
|
|
10
|
+
this.git = git;
|
|
11
|
+
}
|
|
12
|
+
async fileDiff(cwd, filePath, maxPreviewBytes = 2 * 1024 * 1024) {
|
|
13
|
+
return this.withIndex(cwd, async ({ env, tempDir }) => {
|
|
14
|
+
await this.stageAll(cwd, env);
|
|
15
|
+
const state = await this.stateFromIndex(cwd, env);
|
|
16
|
+
if (!state.byPath.has(filePath))
|
|
17
|
+
throw new ChatRoomError("NOT_FOUND", "Worktree file change not found");
|
|
18
|
+
const patchPath = path.join(tempDir, "file.patch");
|
|
19
|
+
await this.writeHeadPatch(cwd, env, patchPath, [filePath]);
|
|
20
|
+
return {
|
|
21
|
+
path: filePath,
|
|
22
|
+
...(await readPatchPreview(patchPath, maxPreviewBytes)),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async apply(worktreeRoot, sourceRoot, requestedPaths) {
|
|
27
|
+
const [worktreeHead, sourceHead, worktree, source] = await Promise.all([
|
|
28
|
+
this.git.head(worktreeRoot),
|
|
29
|
+
this.git.head(sourceRoot),
|
|
30
|
+
this.snapshot(worktreeRoot),
|
|
31
|
+
this.snapshot(sourceRoot),
|
|
32
|
+
]);
|
|
33
|
+
if (!worktreeHead || !sourceHead || worktreeHead !== sourceHead)
|
|
34
|
+
throw new ChatRoomError("CONFLICT", "Source checkout HEAD no longer matches the worktree HEAD");
|
|
35
|
+
if (!worktree.state.files.length)
|
|
36
|
+
throw new ChatRoomError("CONFLICT", "Worktree has no changes to apply");
|
|
37
|
+
const baseTree = await this.baseTree(worktreeRoot, worktreeHead);
|
|
38
|
+
const fullAnalysis = await this.analyze(worktreeRoot, baseTree, source.tree, worktree.tree, worktree.state.files.map((file) => file.path));
|
|
39
|
+
const reviewed = reviewFiles(worktree.state, source.state, fullAnalysis);
|
|
40
|
+
const pending = reviewed.filter((file) => !file.applied);
|
|
41
|
+
const selected = resolveRequestedPaths(pending, requestedPaths);
|
|
42
|
+
const selectedConflicts = selected
|
|
43
|
+
.filter((file) => file.conflict)
|
|
44
|
+
.map((file) => file.path);
|
|
45
|
+
if (requestedPaths !== undefined && selectedConflicts.length)
|
|
46
|
+
throw new ChatRoomError("CONFLICT", "Selected worktree files have merge conflicts", {
|
|
47
|
+
paths: selectedConflicts,
|
|
48
|
+
});
|
|
49
|
+
const paths = selected
|
|
50
|
+
.filter((file) => !file.conflict)
|
|
51
|
+
.map((file) => file.path);
|
|
52
|
+
if (!paths.length)
|
|
53
|
+
throw new ChatRoomError("CONFLICT", "No selected worktree files can be merged automatically", {
|
|
54
|
+
paths: selectedConflicts,
|
|
55
|
+
});
|
|
56
|
+
const selectedTree = await this.treeForPaths(worktreeRoot, paths);
|
|
57
|
+
const merge = await this.analyze(worktreeRoot, baseTree, source.tree, selectedTree, paths);
|
|
58
|
+
if (merge.conflicts.size)
|
|
59
|
+
throw new ChatRoomError("CONFLICT", "Selected worktree files have merge conflicts", {
|
|
60
|
+
paths: [...merge.conflicts],
|
|
61
|
+
});
|
|
62
|
+
return this.withIndex(worktreeRoot, async ({ tempDir }) => {
|
|
63
|
+
const patchPath = path.join(tempDir, "merge.patch");
|
|
64
|
+
await this.writeTreePatch(sourceRoot, patchPath, source.tree, merge.mergedTree, paths);
|
|
65
|
+
const bytes = (await stat(patchPath)).size;
|
|
66
|
+
if (bytes === 0)
|
|
67
|
+
throw new ChatRoomError("CONFLICT", "Selected worktree files are already integrated");
|
|
68
|
+
const [currentWorktreeHead, currentSourceHead, currentSource, currentSelectedTree,] = await Promise.all([
|
|
69
|
+
this.git.head(worktreeRoot),
|
|
70
|
+
this.git.head(sourceRoot),
|
|
71
|
+
this.snapshot(sourceRoot),
|
|
72
|
+
this.treeForPaths(worktreeRoot, paths),
|
|
73
|
+
]);
|
|
74
|
+
if (currentWorktreeHead !== worktreeHead ||
|
|
75
|
+
currentSourceHead !== sourceHead ||
|
|
76
|
+
currentSource.tree !== source.tree ||
|
|
77
|
+
currentSelectedTree !== selectedTree)
|
|
78
|
+
throw new ChatRoomError("CONFLICT", "Workspace changed while preparing worktree merge");
|
|
79
|
+
await this.runner.run(sourceRoot, [
|
|
80
|
+
"apply",
|
|
81
|
+
"--check",
|
|
82
|
+
"--binary",
|
|
83
|
+
patchPath,
|
|
84
|
+
]);
|
|
85
|
+
await this.runner.run(sourceRoot, ["apply", "--binary", patchPath]);
|
|
86
|
+
return { bytes, paths, conflicts: selectedConflicts };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async applyState(worktreeRoot, sourceRoot) {
|
|
90
|
+
const [worktreeHead, sourceHead, worktree, source] = await Promise.all([
|
|
91
|
+
this.git.head(worktreeRoot),
|
|
92
|
+
this.git.head(sourceRoot),
|
|
93
|
+
this.snapshot(worktreeRoot),
|
|
94
|
+
this.snapshot(sourceRoot),
|
|
95
|
+
]);
|
|
96
|
+
if (!worktreeHead || !sourceHead || worktreeHead !== sourceHead) {
|
|
97
|
+
return {
|
|
98
|
+
files: worktree.state.files.map(({ target, ...file }) => ({
|
|
99
|
+
...file,
|
|
100
|
+
applied: source.state.byPath.get(file.path)?.target === target,
|
|
101
|
+
conflict: false,
|
|
102
|
+
})),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const baseTree = await this.baseTree(worktreeRoot, worktreeHead);
|
|
106
|
+
const analysis = await this.analyze(worktreeRoot, baseTree, source.tree, worktree.tree, worktree.state.files.map((file) => file.path));
|
|
107
|
+
return { files: reviewFiles(worktree.state, source.state, analysis) };
|
|
108
|
+
}
|
|
109
|
+
async analyze(cwd, baseTree, oursTree, theirsTree, paths) {
|
|
110
|
+
const merge = await this.mergeTrees(cwd, baseTree, oursTree, theirsTree);
|
|
111
|
+
const delta = await this.runner.run(cwd, [
|
|
112
|
+
"diff",
|
|
113
|
+
"--name-only",
|
|
114
|
+
"-z",
|
|
115
|
+
"--no-renames",
|
|
116
|
+
oursTree,
|
|
117
|
+
merge.mergedTree,
|
|
118
|
+
"--",
|
|
119
|
+
...paths,
|
|
120
|
+
]);
|
|
121
|
+
return {
|
|
122
|
+
...merge,
|
|
123
|
+
deltaPaths: new Set(delta.stdout.split("\0").filter(Boolean)),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async mergeTrees(cwd, baseTree, oursTree, theirsTree) {
|
|
127
|
+
const args = [
|
|
128
|
+
"merge-tree",
|
|
129
|
+
"--write-tree",
|
|
130
|
+
`--merge-base=${baseTree}`,
|
|
131
|
+
"--messages",
|
|
132
|
+
"--name-only",
|
|
133
|
+
"-z",
|
|
134
|
+
oursTree,
|
|
135
|
+
theirsTree,
|
|
136
|
+
];
|
|
137
|
+
let stdout;
|
|
138
|
+
try {
|
|
139
|
+
stdout = (await this.runner.run(cwd, args)).stdout;
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
const details = error instanceof ChatRoomError
|
|
143
|
+
? error.details
|
|
144
|
+
: undefined;
|
|
145
|
+
if (error instanceof ChatRoomError &&
|
|
146
|
+
error.code === "PROCESS_FAILED" &&
|
|
147
|
+
details?.exitCode === 1)
|
|
148
|
+
stdout = String(details.stdout ?? "");
|
|
149
|
+
else
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
const tokens = stdout.split("\0");
|
|
153
|
+
const mergedTree = tokens[0]?.trim() ?? "";
|
|
154
|
+
if (!/^[0-9a-f]{40,64}$/.test(mergedTree))
|
|
155
|
+
throw new ChatRoomError("INTERNAL", "Git merge-tree did not return a merged tree");
|
|
156
|
+
const conflicts = new Set();
|
|
157
|
+
for (let index = 1; index < tokens.length && tokens[index] !== ""; index++) {
|
|
158
|
+
if (tokens[index])
|
|
159
|
+
conflicts.add(tokens[index]);
|
|
160
|
+
}
|
|
161
|
+
return { mergedTree, conflicts };
|
|
162
|
+
}
|
|
163
|
+
async snapshot(cwd) {
|
|
164
|
+
return this.withIndex(cwd, async ({ env }) => {
|
|
165
|
+
await this.stageAll(cwd, env);
|
|
166
|
+
const [state, tree] = await Promise.all([
|
|
167
|
+
this.stateFromIndex(cwd, env),
|
|
168
|
+
this.writeTree(cwd, env),
|
|
169
|
+
]);
|
|
170
|
+
return { state, tree };
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async treeForPaths(cwd, paths) {
|
|
174
|
+
return this.withIndex(cwd, async ({ env }) => {
|
|
175
|
+
await this.runner.run(cwd, ["add", "-A", "--", ...paths], env);
|
|
176
|
+
return this.writeTree(cwd, env);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async baseTree(cwd, head) {
|
|
180
|
+
return (await this.runner.run(cwd, ["rev-parse", `${head}^{tree}`])).stdout.trim();
|
|
181
|
+
}
|
|
182
|
+
async writeTree(cwd, env) {
|
|
183
|
+
return (await this.runner.run(cwd, ["write-tree"], env)).stdout.trim();
|
|
184
|
+
}
|
|
185
|
+
async stageAll(cwd, env) {
|
|
186
|
+
await this.runner.run(cwd, ["add", "-A", "--", "."], env);
|
|
187
|
+
}
|
|
188
|
+
async stateFromIndex(cwd, env) {
|
|
189
|
+
const [raw, numstat] = await Promise.all([
|
|
190
|
+
this.runner.run(cwd, [
|
|
191
|
+
"diff",
|
|
192
|
+
"--cached",
|
|
193
|
+
"--raw",
|
|
194
|
+
"-z",
|
|
195
|
+
"--no-renames",
|
|
196
|
+
"--abbrev=40",
|
|
197
|
+
"HEAD",
|
|
198
|
+
], env),
|
|
199
|
+
this.runner.run(cwd, ["diff", "--cached", "--numstat", "-z", "--no-renames", "HEAD"], env),
|
|
200
|
+
]);
|
|
201
|
+
const stats = parseNumstat(numstat.stdout);
|
|
202
|
+
const tokens = raw.stdout.split("\0");
|
|
203
|
+
const files = [];
|
|
204
|
+
for (let index = 0; index + 1 < tokens.length; index += 2) {
|
|
205
|
+
const header = tokens[index];
|
|
206
|
+
const filePath = tokens[index + 1];
|
|
207
|
+
if (!header || !filePath)
|
|
208
|
+
continue;
|
|
209
|
+
const match = /^:(\d{6}) (\d{6}) ([0-9a-f]{40}) ([0-9a-f]{40}) ([AMDT])$/.exec(header);
|
|
210
|
+
if (!match)
|
|
211
|
+
throw new ChatRoomError("INTERNAL", `Unexpected Git raw diff entry: ${header}`);
|
|
212
|
+
const statEntry = stats.get(filePath) ?? {
|
|
213
|
+
additions: null,
|
|
214
|
+
deletions: null,
|
|
215
|
+
binary: true,
|
|
216
|
+
};
|
|
217
|
+
files.push({
|
|
218
|
+
path: filePath,
|
|
219
|
+
status: rawStatus(match[5]),
|
|
220
|
+
additions: statEntry.additions,
|
|
221
|
+
deletions: statEntry.deletions,
|
|
222
|
+
binary: statEntry.binary,
|
|
223
|
+
target: `${match[2]}:${match[4]}`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return { files, byPath: new Map(files.map((file) => [file.path, file])) };
|
|
227
|
+
}
|
|
228
|
+
async writeHeadPatch(cwd, env, patchPath, paths) {
|
|
229
|
+
await this.runner.run(cwd, [
|
|
230
|
+
"diff",
|
|
231
|
+
"--cached",
|
|
232
|
+
"--binary",
|
|
233
|
+
"--no-ext-diff",
|
|
234
|
+
"--no-renames",
|
|
235
|
+
`--output=${patchPath}`,
|
|
236
|
+
"HEAD",
|
|
237
|
+
"--",
|
|
238
|
+
...paths,
|
|
239
|
+
], env);
|
|
240
|
+
}
|
|
241
|
+
async writeTreePatch(cwd, patchPath, fromTree, toTree, paths) {
|
|
242
|
+
await this.runner.run(cwd, [
|
|
243
|
+
"diff",
|
|
244
|
+
"--binary",
|
|
245
|
+
"--no-ext-diff",
|
|
246
|
+
"--no-renames",
|
|
247
|
+
`--output=${patchPath}`,
|
|
248
|
+
fromTree,
|
|
249
|
+
toTree,
|
|
250
|
+
"--",
|
|
251
|
+
...paths,
|
|
252
|
+
]);
|
|
253
|
+
}
|
|
254
|
+
async withIndex(cwd, useIndex) {
|
|
255
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "chatroom-worktree-"));
|
|
256
|
+
const env = { GIT_INDEX_FILE: path.join(tempDir, "index") };
|
|
257
|
+
try {
|
|
258
|
+
await this.runner.run(cwd, ["read-tree", "HEAD"], env);
|
|
259
|
+
return await useIndex({ env, tempDir });
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function reviewFiles(worktree, source, analysis) {
|
|
267
|
+
const mappedConflicts = mapConflictPaths(worktree.files, analysis.conflicts);
|
|
268
|
+
return worktree.files.map(({ target, ...file }) => {
|
|
269
|
+
const conflict = mappedConflicts.has(file.path);
|
|
270
|
+
const exact = source.byPath.get(file.path)?.target === target;
|
|
271
|
+
const integrated = !conflict && !analysis.deltaPaths.has(file.path);
|
|
272
|
+
return { ...file, applied: exact || integrated, conflict };
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
function mapConflictPaths(files, conflicts) {
|
|
276
|
+
const mapped = new Set();
|
|
277
|
+
for (const file of files) {
|
|
278
|
+
for (const conflict of conflicts) {
|
|
279
|
+
if (conflict === file.path ||
|
|
280
|
+
conflict.startsWith(`${file.path}/`) ||
|
|
281
|
+
file.path.startsWith(`${conflict}/`)) {
|
|
282
|
+
mapped.add(file.path);
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (conflicts.size && !mapped.size)
|
|
288
|
+
for (const file of files)
|
|
289
|
+
mapped.add(file.path);
|
|
290
|
+
return mapped;
|
|
291
|
+
}
|
|
292
|
+
function resolveRequestedPaths(pending, requestedPaths) {
|
|
293
|
+
if (requestedPaths === undefined)
|
|
294
|
+
return pending;
|
|
295
|
+
const requested = [...new Set(requestedPaths)];
|
|
296
|
+
if (!requested.length)
|
|
297
|
+
throw new ChatRoomError("INVALID_INPUT", "At least one file path is required");
|
|
298
|
+
const byPath = new Map(pending.map((file) => [file.path, file]));
|
|
299
|
+
return requested.map((filePath) => {
|
|
300
|
+
const file = byPath.get(filePath);
|
|
301
|
+
if (!file)
|
|
302
|
+
throw new ChatRoomError("CONFLICT", `Worktree file is already applied or unavailable: ${filePath}`);
|
|
303
|
+
return file;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
function rawStatus(value) {
|
|
307
|
+
if (value === "A")
|
|
308
|
+
return "added";
|
|
309
|
+
if (value === "D")
|
|
310
|
+
return "deleted";
|
|
311
|
+
if (value === "T")
|
|
312
|
+
return "type-changed";
|
|
313
|
+
return "modified";
|
|
314
|
+
}
|
|
315
|
+
function parseNumstat(value) {
|
|
316
|
+
const result = new Map();
|
|
317
|
+
for (const entry of value.split("\0")) {
|
|
318
|
+
if (!entry)
|
|
319
|
+
continue;
|
|
320
|
+
const first = entry.indexOf("\t");
|
|
321
|
+
const second = first < 0 ? -1 : entry.indexOf("\t", first + 1);
|
|
322
|
+
if (first < 0 || second < 0)
|
|
323
|
+
continue;
|
|
324
|
+
const additionsText = entry.slice(0, first);
|
|
325
|
+
const deletionsText = entry.slice(first + 1, second);
|
|
326
|
+
const filePath = entry.slice(second + 1);
|
|
327
|
+
const binary = additionsText === "-" || deletionsText === "-";
|
|
328
|
+
result.set(filePath, {
|
|
329
|
+
additions: binary ? null : Number(additionsText),
|
|
330
|
+
deletions: binary ? null : Number(deletionsText),
|
|
331
|
+
binary,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
async function readPatchPreview(patchPath, maxPreviewBytes) {
|
|
337
|
+
const size = (await stat(patchPath)).size;
|
|
338
|
+
const previewBytes = Math.min(size, maxPreviewBytes);
|
|
339
|
+
const handle = await open(patchPath, "r");
|
|
340
|
+
try {
|
|
341
|
+
const buffer = Buffer.alloc(previewBytes);
|
|
342
|
+
const { bytesRead } = await handle.read(buffer, 0, previewBytes, 0);
|
|
343
|
+
return {
|
|
344
|
+
diff: buffer.subarray(0, bytesRead).toString("utf8"),
|
|
345
|
+
bytes: size,
|
|
346
|
+
truncated: size > previewBytes,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
await handle.close();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ChangeSet, FileInfo, PatchReplacement, SearchMatch } from "../domain/filesystem.js";
|
|
2
|
+
export declare class WorkspaceFs {
|
|
3
|
+
readonly root: string;
|
|
4
|
+
private readonly realRoot;
|
|
5
|
+
private constructor();
|
|
6
|
+
static create(root: string): Promise<WorkspaceFs>;
|
|
7
|
+
read(relativePath: string, options?: {
|
|
8
|
+
maxBytes?: number;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
content: string;
|
|
11
|
+
bytes: number;
|
|
12
|
+
truncated: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
readBytes(relativePath: string, options?: {
|
|
15
|
+
maxBytes?: number;
|
|
16
|
+
}): Promise<{
|
|
17
|
+
data: Buffer;
|
|
18
|
+
bytes: number;
|
|
19
|
+
truncated: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
write(relativePath: string, content: string): Promise<{
|
|
22
|
+
path: string;
|
|
23
|
+
bytes: number;
|
|
24
|
+
}>;
|
|
25
|
+
list(relativePath?: string, options?: {
|
|
26
|
+
recursive?: boolean;
|
|
27
|
+
maxEntries?: number;
|
|
28
|
+
}): Promise<FileInfo[]>;
|
|
29
|
+
search(query: string, options?: {
|
|
30
|
+
path?: string;
|
|
31
|
+
maxResults?: number;
|
|
32
|
+
}): Promise<SearchMatch[]>;
|
|
33
|
+
patch(replacements: PatchReplacement[]): Promise<ChangeSet>;
|
|
34
|
+
private normalizeRelative;
|
|
35
|
+
private lexical;
|
|
36
|
+
private resolveReadable;
|
|
37
|
+
private resolveWritable;
|
|
38
|
+
}
|