@myagentroam/node 0.1.3 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/capabilities.d.ts +2 -0
- package/dist/capabilities.js +15 -4
- package/dist/claude-agent-sdk.d.ts +4 -16
- package/dist/claude-agent-sdk.js +18 -79
- package/dist/claude-channel.js +0 -1
- package/dist/codex-app-server.d.ts +5 -0
- package/dist/codex-app-server.js +26 -5
- package/dist/config.d.ts +3 -0
- package/dist/config.js +18 -2
- package/dist/connector/node-connector-options.d.ts +26 -0
- package/dist/connector/node-connector-options.js +1 -0
- package/dist/connector/node-operation-router.d.ts +9 -0
- package/dist/connector/node-operation-router.js +22 -0
- package/dist/connector.d.ts +62 -356
- package/dist/connector.js +714 -6176
- package/dist/control-outbound-scheduler.d.ts +26 -0
- package/dist/control-outbound-scheduler.js +121 -0
- package/dist/database.d.ts +4 -0
- package/dist/database.js +15 -10
- package/dist/event-buffer.js +0 -1
- package/dist/fake-runner.js +0 -1
- package/dist/health.js +0 -1
- package/dist/main.js +6 -1
- package/dist/migrations/v001.js +0 -1
- package/dist/migrations/v002.js +0 -1
- package/dist/migrations/v003.d.ts +4 -0
- package/dist/migrations/v003.js +44 -0
- package/dist/native-jsonl-reader.d.ts +14 -0
- package/dist/native-jsonl-reader.js +97 -0
- package/dist/native-session-history.d.ts +41 -4
- package/dist/native-session-history.js +340 -159
- package/dist/opencode-runtime.d.ts +33 -0
- package/dist/opencode-runtime.js +80 -0
- package/dist/opencode-server.d.ts +47 -0
- package/dist/opencode-server.js +250 -0
- package/dist/operational.js +10 -3
- package/dist/process-tree.js +0 -1
- package/dist/runner/abstract-runner.d.ts +153 -0
- package/dist/runner/abstract-runner.js +154 -0
- package/dist/runner/claude/managed-run-controller.d.ts +52 -0
- package/dist/runner/claude/managed-run-controller.js +326 -0
- package/dist/runner/claude-code-runner.d.ts +70 -0
- package/dist/runner/claude-code-runner.js +286 -0
- package/dist/runner/codex/conversation-parser.d.ts +59 -0
- package/dist/runner/codex/conversation-parser.js +816 -0
- package/dist/runner/codex/managed-run-controller.d.ts +80 -0
- package/dist/runner/codex/managed-run-controller.js +388 -0
- package/dist/runner/codex-runner.d.ts +85 -0
- package/dist/runner/codex-runner.js +365 -0
- package/dist/runner/fake-test-runner.d.ts +26 -0
- package/dist/runner/fake-test-runner.js +47 -0
- package/dist/runner/opencode/conversation-parser.d.ts +27 -0
- package/dist/runner/opencode/conversation-parser.js +217 -0
- package/dist/runner/opencode/managed-run-controller.d.ts +76 -0
- package/dist/runner/opencode/managed-run-controller.js +451 -0
- package/dist/runner/opencode-runner.d.ts +60 -0
- package/dist/runner/opencode-runner.js +330 -0
- package/dist/runner/runner-registry.d.ts +19 -0
- package/dist/runner/runner-registry.js +75 -0
- package/dist/runner-command-engine.js +0 -1
- package/dist/runner-profiles.d.ts +4 -0
- package/dist/runner-profiles.js +76 -3
- package/dist/runner-usage.d.ts +2 -2
- package/dist/runner-usage.js +3 -6
- package/dist/runtime-state.js +26 -14
- package/dist/service/attachment-domain-state.d.ts +5 -0
- package/dist/service/attachment-domain-state.js +5 -0
- package/dist/service/conversation-history-service.d.ts +34 -0
- package/dist/service/conversation-history-service.js +158 -0
- package/dist/service/external-session-resume-service.d.ts +32 -0
- package/dist/service/external-session-resume-service.js +98 -0
- package/dist/service/fake-managed-run-service.d.ts +29 -0
- package/dist/service/fake-managed-run-service.js +62 -0
- package/dist/service/managed-runner-session-service.d.ts +12 -0
- package/dist/service/managed-runner-session-service.js +35 -0
- package/dist/service/native-session-projection-service.d.ts +51 -0
- package/dist/service/native-session-projection-service.js +271 -0
- package/dist/service/native-session-watch-service.d.ts +37 -0
- package/dist/service/native-session-watch-service.js +150 -0
- package/dist/service/node-connection-lifecycle-service.d.ts +37 -0
- package/dist/service/node-connection-lifecycle-service.js +85 -0
- package/dist/service/node-control-channel.d.ts +32 -0
- package/dist/service/node-control-channel.js +85 -0
- package/dist/service/node-control-message-service.d.ts +28 -0
- package/dist/service/node-control-message-service.js +104 -0
- package/dist/service/node-request-service.d.ts +14 -0
- package/dist/service/node-request-service.js +50 -0
- package/dist/service/node-run-event-bridge.d.ts +40 -0
- package/dist/service/node-run-event-bridge.js +83 -0
- package/dist/service/node-terminal-channel.d.ts +29 -0
- package/dist/service/node-terminal-channel.js +182 -0
- package/dist/service/node-upgrade-coordinator.d.ts +29 -0
- package/dist/service/node-upgrade-coordinator.js +137 -0
- package/dist/service/node-workspace-coordinator.d.ts +32 -0
- package/dist/service/node-workspace-coordinator.js +80 -0
- package/dist/service/run-attachment-service.d.ts +37 -0
- package/dist/service/run-attachment-service.js +160 -0
- package/dist/service/run-domain-state.d.ts +4 -0
- package/dist/service/run-domain-state.js +4 -0
- package/dist/service/run-event-service.d.ts +31 -0
- package/dist/service/run-event-service.js +102 -0
- package/dist/service/run-workbench-service.d.ts +27 -0
- package/dist/service/run-workbench-service.js +113 -0
- package/dist/service/runner-channel-message-service.d.ts +13 -0
- package/dist/service/runner-channel-message-service.js +20 -0
- package/dist/service/runner-interaction-service.d.ts +40 -0
- package/dist/service/runner-interaction-service.js +246 -0
- package/dist/service/runner-service.d.ts +44 -0
- package/dist/service/runner-service.js +68 -0
- package/dist/service/session-catalog-service.d.ts +45 -0
- package/dist/service/session-catalog-service.js +224 -0
- package/dist/service/session-command-service.d.ts +23 -0
- package/dist/service/session-command-service.js +69 -0
- package/dist/service/session-context-service.d.ts +34 -0
- package/dist/service/session-context-service.js +87 -0
- package/dist/service/session-domain-state.d.ts +8 -0
- package/dist/service/session-domain-state.js +8 -0
- package/dist/service/session-identity-service.d.ts +24 -0
- package/dist/service/session-identity-service.js +86 -0
- package/dist/service/session-lifecycle-service.d.ts +31 -0
- package/dist/service/session-lifecycle-service.js +242 -0
- package/dist/service/session-message-service.d.ts +33 -0
- package/dist/service/session-message-service.js +157 -0
- package/dist/service/session-presentation-service.d.ts +33 -0
- package/dist/service/session-presentation-service.js +36 -0
- package/dist/service/session-query-service.d.ts +31 -0
- package/dist/service/session-query-service.js +77 -0
- package/dist/service/terminal-relay-state.d.ts +6 -0
- package/dist/service/terminal-relay-state.js +3 -0
- package/dist/service/terminal-service.d.ts +12 -0
- package/dist/service/terminal-service.js +52 -0
- package/dist/service/workbench-event-service.d.ts +21 -0
- package/dist/service/workbench-event-service.js +53 -0
- package/dist/service/workbench-manifest-service.d.ts +89 -0
- package/dist/service/workbench-manifest-service.js +70 -0
- package/dist/service/workspace-change-service.d.ts +9 -0
- package/dist/service/workspace-change-service.js +86 -0
- package/dist/service/workspace-content-search-service.d.ts +14 -0
- package/dist/service/workspace-content-search-service.js +477 -0
- package/dist/service/workspace-domain-state.d.ts +75 -0
- package/dist/service/workspace-domain-state.js +40 -0
- package/dist/service/workspace-file-service.d.ts +17 -0
- package/dist/service/workspace-file-service.js +109 -0
- package/dist/service/workspace-queue-workbench-service.d.ts +55 -0
- package/dist/service/workspace-queue-workbench-service.js +226 -0
- package/dist/service/workspace-service.d.ts +15 -0
- package/dist/service/workspace-service.js +67 -0
- package/dist/service/workspace-session-service.d.ts +23 -0
- package/dist/service/workspace-session-service.js +139 -0
- package/dist/service/workspace-upload-service.d.ts +37 -0
- package/dist/service/workspace-upload-service.js +240 -0
- package/dist/service/workspace-watch-service.d.ts +18 -0
- package/dist/service/workspace-watch-service.js +120 -0
- package/dist/service/workspace-workbench-service.d.ts +47 -0
- package/dist/service/workspace-workbench-service.js +223 -0
- package/dist/service.js +2 -3
- package/dist/storage.js +0 -1
- package/dist/supervisor.d.ts +1 -0
- package/dist/supervisor.js +154 -0
- package/dist/terminal.d.ts +1 -0
- package/dist/terminal.js +1 -2
- package/dist/util/node-operation-parsers.d.ts +92 -0
- package/dist/util/node-operation-parsers.js +382 -0
- package/dist/util/runner-native-session-parsers.d.ts +72 -0
- package/dist/util/runner-native-session-parsers.js +381 -0
- package/dist/util/secret-environment.d.ts +1 -0
- package/dist/util/secret-environment.js +25 -0
- package/dist/workspace.d.ts +62 -2
- package/dist/workspace.js +539 -57
- package/package.json +3 -2
- package/dist/capabilities.js.map +0 -1
- package/dist/claude-agent-sdk.js.map +0 -1
- package/dist/claude-channel.js.map +0 -1
- package/dist/codex-app-server.js.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/connector.js.map +0 -1
- package/dist/database.js.map +0 -1
- package/dist/event-buffer.js.map +0 -1
- package/dist/fake-runner.js.map +0 -1
- package/dist/health.js.map +0 -1
- package/dist/main.js.map +0 -1
- package/dist/migrations/v001.js.map +0 -1
- package/dist/migrations/v002.js.map +0 -1
- package/dist/native-session-history.js.map +0 -1
- package/dist/operational.js.map +0 -1
- package/dist/process-tree.js.map +0 -1
- package/dist/runner-command-engine.js.map +0 -1
- package/dist/runner-profiles.js.map +0 -1
- package/dist/runner-usage.js.map +0 -1
- package/dist/runtime-state.js.map +0 -1
- package/dist/service.js.map +0 -1
- package/dist/storage.js.map +0 -1
- package/dist/terminal.js.map +0 -1
- package/dist/workspace.js.map +0 -1
package/dist/workspace.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { watch } from 'node:fs';
|
|
3
|
-
import { lstat, readdir, readFile, realpath, stat, unlink } from 'node:fs/promises';
|
|
3
|
+
import { lstat, mkdir, opendir, readdir, readFile, realpath, rename, rm, stat, unlink, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { homedir, platform as hostPlatform } from 'node:os';
|
|
5
5
|
import { dirname, isAbsolute, posix, relative, resolve, sep, win32 } from 'node:path';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { minimatch } from 'minimatch';
|
|
8
8
|
const GIT_TIMEOUT_MS = 5_000;
|
|
9
9
|
const GIT_OUTPUT_LIMIT = 512 * 1024;
|
|
10
|
+
const GIT_BINARY_PREVIEW_LIMIT = 4 * 1024 * 1024;
|
|
10
11
|
const FILE_READ_LIMIT = 512 * 1024;
|
|
11
12
|
const DIRECTORY_PAGE_LIMIT = 200;
|
|
12
13
|
const DIRECTORY_PICKER_LIMIT = 200;
|
|
@@ -14,6 +15,7 @@ const SEARCH_RESULT_LIMIT = 200;
|
|
|
14
15
|
const FILE_INDEX_IDLE_TTL_MS = 60 * 60 * 1_000;
|
|
15
16
|
const FILE_INDEX_ENTRY_LIMIT = 100_000;
|
|
16
17
|
const FILE_INDEX_FALLBACK_POLL_MS = 30_000;
|
|
18
|
+
const FILE_INDEX_METADATA_CONCURRENCY = 32;
|
|
17
19
|
const DEFAULT_QUICK_OPEN_EXCLUDES = [
|
|
18
20
|
'**/.git/**',
|
|
19
21
|
'**/node_modules/**',
|
|
@@ -27,6 +29,7 @@ const DEFAULT_QUICK_OPEN_EXCLUDES = [
|
|
|
27
29
|
'**/build/**',
|
|
28
30
|
'**/out/**',
|
|
29
31
|
'**/coverage/**',
|
|
32
|
+
'**/test/runs/**',
|
|
30
33
|
'**/.next/**',
|
|
31
34
|
'**/.nuxt/**',
|
|
32
35
|
'**/.svelte-kit/**',
|
|
@@ -41,6 +44,36 @@ const DEFAULT_QUICK_OPEN_EXCLUDES = [
|
|
|
41
44
|
'**/venv/**'
|
|
42
45
|
];
|
|
43
46
|
const REF_PATTERN = /^(?!-)(?!.*\.\.)(?!.*@\{)[A-Za-z0-9][A-Za-z0-9._/-]{0,254}$/;
|
|
47
|
+
export async function createWorkspaceDirectory(parentPath, directoryName, allowedRoots) {
|
|
48
|
+
const name = directoryName.trim();
|
|
49
|
+
if (name.length === 0 ||
|
|
50
|
+
name.length > 255 ||
|
|
51
|
+
name === '.' ||
|
|
52
|
+
name === '..' ||
|
|
53
|
+
name.includes('/') ||
|
|
54
|
+
name.includes('\\') ||
|
|
55
|
+
name.includes('\0'))
|
|
56
|
+
throw new Error('WORKSPACE_DIRECTORY_NAME_INVALID');
|
|
57
|
+
const roots = await Promise.all(allowedRoots.map((root) => realpath(root)));
|
|
58
|
+
const parent = await realpath(parentPath).catch(() => {
|
|
59
|
+
throw new Error('WORKSPACE_DIRECTORY_NOT_ALLOWED');
|
|
60
|
+
});
|
|
61
|
+
const metadata = await stat(parent).catch(() => undefined);
|
|
62
|
+
if (metadata?.isDirectory() !== true || !isWithinAllowedRoot(parent, roots))
|
|
63
|
+
throw new Error('WORKSPACE_DIRECTORY_NOT_ALLOWED');
|
|
64
|
+
const target = resolve(parent, name);
|
|
65
|
+
if (dirname(target) !== parent || !isWithinAllowedRoot(target, roots))
|
|
66
|
+
throw new Error('WORKSPACE_DIRECTORY_NAME_INVALID');
|
|
67
|
+
try {
|
|
68
|
+
await mkdir(target);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error.code === 'EEXIST')
|
|
72
|
+
throw new Error('WORKSPACE_DIRECTORY_EXISTS');
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
return target;
|
|
76
|
+
}
|
|
44
77
|
/** A per-workspace, process-local index for the Quick Open file picker. */
|
|
45
78
|
export class WorkspaceFileIndex {
|
|
46
79
|
workspaceRoot;
|
|
@@ -63,6 +96,14 @@ export class WorkspaceFileIndex {
|
|
|
63
96
|
await this.ensureLoaded();
|
|
64
97
|
this.touch();
|
|
65
98
|
}
|
|
99
|
+
async contentCandidates() {
|
|
100
|
+
await this.ensureLoaded();
|
|
101
|
+
this.touch();
|
|
102
|
+
return {
|
|
103
|
+
entries: this.entries.filter((entry) => entry.kind === 'FILE'),
|
|
104
|
+
truncated: this.truncated
|
|
105
|
+
};
|
|
106
|
+
}
|
|
66
107
|
dispose() {
|
|
67
108
|
if (this.expiryTimer !== undefined)
|
|
68
109
|
clearTimeout(this.expiryTimer);
|
|
@@ -77,7 +118,7 @@ export class WorkspaceFileIndex {
|
|
|
77
118
|
this.stale = true;
|
|
78
119
|
this.signature = '';
|
|
79
120
|
}
|
|
80
|
-
async search(query, cursor, limit = SEARCH_RESULT_LIMIT) {
|
|
121
|
+
async search(query, cursor, limit = SEARCH_RESULT_LIMIT, includeDirectories = false) {
|
|
81
122
|
if (query.trim().length === 0 ||
|
|
82
123
|
query.length > 256 ||
|
|
83
124
|
!Number.isSafeInteger(limit) ||
|
|
@@ -87,15 +128,28 @@ export class WorkspaceFileIndex {
|
|
|
87
128
|
await this.ensureLoaded();
|
|
88
129
|
this.touch();
|
|
89
130
|
const after = decodeSearchCursor(cursor, query);
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
.
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
131
|
+
const afterEntry = after === undefined
|
|
132
|
+
? undefined
|
|
133
|
+
: this.entries.find((entry) => entry.path === after && (includeDirectories || entry.kind === 'FILE'));
|
|
134
|
+
const afterScore = afterEntry === undefined ? -1 : fuzzyPathScore(afterEntry.path, query);
|
|
135
|
+
const afterCandidate = afterEntry === undefined || afterScore < 0
|
|
136
|
+
? undefined
|
|
137
|
+
: { entry: afterEntry, score: afterScore };
|
|
138
|
+
const ranked = new RankedCandidateHeap(limit + 1);
|
|
139
|
+
for (const entry of this.entries) {
|
|
140
|
+
if (!includeDirectories && entry.kind !== 'FILE')
|
|
141
|
+
continue;
|
|
142
|
+
const score = fuzzyPathScore(entry.path, query);
|
|
143
|
+
if (score < 0)
|
|
144
|
+
continue;
|
|
145
|
+
const candidate = { entry, score };
|
|
146
|
+
if (afterCandidate !== undefined && compareRankedCandidate(candidate, afterCandidate) <= 0)
|
|
147
|
+
continue;
|
|
148
|
+
ranked.add(candidate);
|
|
149
|
+
}
|
|
150
|
+
const candidates = ranked.sorted();
|
|
151
|
+
const page = candidates.slice(0, limit).map((candidate) => candidate.entry);
|
|
152
|
+
const next = candidates[limit]?.entry.path;
|
|
99
153
|
return {
|
|
100
154
|
entries: page,
|
|
101
155
|
nextCursor: next === undefined ? null : encodeSearchCursor(query, page.at(-1)?.path ?? after ?? ''),
|
|
@@ -125,43 +179,52 @@ export class WorkspaceFileIndex {
|
|
|
125
179
|
const entryLimit = this.limits.entryLimit ?? FILE_INDEX_ENTRY_LIMIT;
|
|
126
180
|
let truncated = false;
|
|
127
181
|
const directories = ['.'];
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
182
|
+
let directoryCursor = 0;
|
|
183
|
+
while (directoryCursor < directories.length && !truncated) {
|
|
184
|
+
const relativeDirectory = directories[directoryCursor];
|
|
185
|
+
directoryCursor += 1;
|
|
186
|
+
const directory = await resolveWorkspaceEntryFromCanonicalRoot(root, relativeDirectory, true).catch(() => undefined);
|
|
131
187
|
if (directory === undefined)
|
|
132
188
|
continue;
|
|
133
|
-
const
|
|
134
|
-
if (
|
|
189
|
+
const stream = await opendir(directory).catch(() => undefined);
|
|
190
|
+
if (stream === undefined)
|
|
135
191
|
continue;
|
|
136
|
-
|
|
137
|
-
|
|
192
|
+
let pending = [];
|
|
193
|
+
const appendPending = async () => {
|
|
194
|
+
const resolvedEntries = await Promise.all(pending);
|
|
195
|
+
pending = [];
|
|
196
|
+
for (const entry of resolvedEntries) {
|
|
197
|
+
if (entry === undefined)
|
|
198
|
+
continue;
|
|
199
|
+
if (entry.path === '.git' ||
|
|
200
|
+
entry.path.startsWith('.git/') ||
|
|
201
|
+
isExcluded(entry.path, excludes))
|
|
202
|
+
continue;
|
|
203
|
+
if (entry.kind === 'DIRECTORY')
|
|
204
|
+
directories.push(entry.path);
|
|
205
|
+
indexed.push(entry);
|
|
206
|
+
if (indexed.length >= entryLimit) {
|
|
207
|
+
truncated = true;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
for await (const child of stream) {
|
|
213
|
+
const candidatePath = relativeDirectory === '.'
|
|
214
|
+
? child.name
|
|
215
|
+
: posix.join(relativeDirectory.replaceAll('\\', '/'), child.name);
|
|
138
216
|
if (candidatePath === '.git' ||
|
|
139
217
|
candidatePath.startsWith('.git/') ||
|
|
140
218
|
isExcluded(candidatePath, excludes))
|
|
141
219
|
continue;
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (metadata === undefined || (!metadata.isFile() && !metadata.isDirectory()))
|
|
147
|
-
continue;
|
|
148
|
-
const safePath = relative(root, resolved).split(sep).join('/');
|
|
149
|
-
if (metadata.isDirectory()) {
|
|
150
|
-
directories.push(safePath);
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
indexed.push({
|
|
154
|
-
path: safePath,
|
|
155
|
-
name: child.name,
|
|
156
|
-
kind: 'FILE',
|
|
157
|
-
size: metadata.size,
|
|
158
|
-
modifiedAt: metadata.mtimeMs
|
|
159
|
-
});
|
|
160
|
-
if (indexed.length >= entryLimit) {
|
|
161
|
-
truncated = true;
|
|
220
|
+
pending.push(inspectWorkspaceIndexEntry(root, candidatePath, child.name));
|
|
221
|
+
if (pending.length >= FILE_INDEX_METADATA_CONCURRENCY)
|
|
222
|
+
await appendPending();
|
|
223
|
+
if (truncated)
|
|
162
224
|
break;
|
|
163
|
-
}
|
|
164
225
|
}
|
|
226
|
+
if (!truncated && pending.length > 0)
|
|
227
|
+
await appendPending();
|
|
165
228
|
}
|
|
166
229
|
this.root = root;
|
|
167
230
|
this.entries = indexed;
|
|
@@ -172,7 +235,12 @@ export class WorkspaceFileIndex {
|
|
|
172
235
|
// fs.watch is advisory. Any change safely invalidates the in-memory index;
|
|
173
236
|
// the next lookup rebuilds it and never trusts stale paths for file reads.
|
|
174
237
|
try {
|
|
175
|
-
this.watcher = watch(root, { persistent: false, recursive: true }, () => {
|
|
238
|
+
this.watcher = watch(root, { persistent: false, recursive: true }, (_event, filename) => {
|
|
239
|
+
if (filename !== null) {
|
|
240
|
+
const path = filename.toString().replaceAll('\\', '/');
|
|
241
|
+
if (path === '.git' || path.startsWith('.git/') || isExcluded(path, excludes))
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
176
244
|
this.invalidate();
|
|
177
245
|
});
|
|
178
246
|
this.watcher.on('error', () => {
|
|
@@ -219,10 +287,11 @@ export class WorkspaceFileIndex {
|
|
|
219
287
|
}
|
|
220
288
|
}
|
|
221
289
|
function fileIndexSignature(entries, truncated) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
290
|
+
const hash = createHash('sha256');
|
|
291
|
+
hash.update(truncated ? '1\0' : '0\0');
|
|
292
|
+
for (const entry of entries)
|
|
293
|
+
hash.update(`${entry.path}\0${String(entry.size)}\0${String(entry.modifiedAt)}\0`);
|
|
294
|
+
return hash.digest('hex');
|
|
226
295
|
}
|
|
227
296
|
async function workspaceExcludePatterns(root) {
|
|
228
297
|
const settingsPath = resolve(root, '.vscode/settings.json');
|
|
@@ -242,6 +311,64 @@ function isExcluded(path, patterns) {
|
|
|
242
311
|
return patterns.some((pattern) => minimatch(path, pattern, { dot: true, nocase: hostPlatform() !== 'linux' }) ||
|
|
243
312
|
minimatch(`${path}/`, pattern, { dot: true, nocase: hostPlatform() !== 'linux' }));
|
|
244
313
|
}
|
|
314
|
+
function compareRankedCandidate(left, right) {
|
|
315
|
+
return right.score - left.score || left.entry.path.localeCompare(right.entry.path);
|
|
316
|
+
}
|
|
317
|
+
/** Keeps only the best page-sized candidate set, with the current worst item at the root. */
|
|
318
|
+
class RankedCandidateHeap {
|
|
319
|
+
capacity;
|
|
320
|
+
values = [];
|
|
321
|
+
constructor(capacity) {
|
|
322
|
+
this.capacity = capacity;
|
|
323
|
+
}
|
|
324
|
+
add(candidate) {
|
|
325
|
+
if (this.values.length < this.capacity) {
|
|
326
|
+
this.values.push(candidate);
|
|
327
|
+
this.bubbleUp(this.values.length - 1);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (compareRankedCandidate(candidate, this.values[0]) >= 0)
|
|
331
|
+
return;
|
|
332
|
+
this.values[0] = candidate;
|
|
333
|
+
this.sinkDown(0);
|
|
334
|
+
}
|
|
335
|
+
sorted() {
|
|
336
|
+
return [...this.values].sort(compareRankedCandidate);
|
|
337
|
+
}
|
|
338
|
+
bubbleUp(start) {
|
|
339
|
+
let index = start;
|
|
340
|
+
while (index > 0) {
|
|
341
|
+
const parent = Math.floor((index - 1) / 2);
|
|
342
|
+
if (compareRankedCandidate(this.values[index], this.values[parent]) <= 0)
|
|
343
|
+
break;
|
|
344
|
+
[this.values[index], this.values[parent]] = [
|
|
345
|
+
this.values[parent],
|
|
346
|
+
this.values[index]
|
|
347
|
+
];
|
|
348
|
+
index = parent;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
sinkDown(start) {
|
|
352
|
+
let index = start;
|
|
353
|
+
while (true) {
|
|
354
|
+
const left = index * 2 + 1;
|
|
355
|
+
if (left >= this.values.length)
|
|
356
|
+
return;
|
|
357
|
+
const right = left + 1;
|
|
358
|
+
const worse = right < this.values.length &&
|
|
359
|
+
compareRankedCandidate(this.values[right], this.values[left]) > 0
|
|
360
|
+
? right
|
|
361
|
+
: left;
|
|
362
|
+
if (compareRankedCandidate(this.values[worse], this.values[index]) <= 0)
|
|
363
|
+
return;
|
|
364
|
+
[this.values[index], this.values[worse]] = [
|
|
365
|
+
this.values[worse],
|
|
366
|
+
this.values[index]
|
|
367
|
+
];
|
|
368
|
+
index = worse;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
245
372
|
function fuzzyPathScore(path, query) {
|
|
246
373
|
const source = path.toLocaleLowerCase();
|
|
247
374
|
const needle = query.trim().toLocaleLowerCase();
|
|
@@ -352,6 +479,98 @@ export async function listWorkspaceFiles(workspaceRoot, requestedPath = '.', cur
|
|
|
352
479
|
truncated: nextName !== undefined
|
|
353
480
|
};
|
|
354
481
|
}
|
|
482
|
+
export async function mutateWorkspaceFile(workspaceRoot, mutation) {
|
|
483
|
+
const root = await realpath(workspaceRoot);
|
|
484
|
+
if (mutation.action === 'CREATE_FILE' || mutation.action === 'CREATE_DIRECTORY') {
|
|
485
|
+
const target = await resolveNewWorkspaceEntry(root, mutation.path);
|
|
486
|
+
if (mutation.action === 'CREATE_FILE')
|
|
487
|
+
await writeFile(target, '', { flag: 'wx' });
|
|
488
|
+
else
|
|
489
|
+
await mkdir(target);
|
|
490
|
+
return { path: workspaceRelativePath(root, target) };
|
|
491
|
+
}
|
|
492
|
+
await assertWorkspaceMutationPathHasNoSymlinks(root, mutation.path);
|
|
493
|
+
const source = await resolveWorkspaceEntryMetadata(root, mutation.path);
|
|
494
|
+
if (workspaceRelativePath(root, source.path) === '.')
|
|
495
|
+
throw new Error('FILE_ROOT_MUTATION_INVALID');
|
|
496
|
+
if (mutation.action === 'DELETE') {
|
|
497
|
+
await rm(source.path, { recursive: source.metadata.isDirectory(), force: false });
|
|
498
|
+
return { deleted: true };
|
|
499
|
+
}
|
|
500
|
+
const destination = await resolveNewWorkspaceEntry(root, mutation.destination);
|
|
501
|
+
if (source.metadata.isDirectory()) {
|
|
502
|
+
const destinationFromSource = relative(source.path, destination);
|
|
503
|
+
if (destinationFromSource === '' ||
|
|
504
|
+
(!isAbsolute(destinationFromSource) &&
|
|
505
|
+
destinationFromSource !== '..' &&
|
|
506
|
+
!destinationFromSource.startsWith(`..${sep}`)))
|
|
507
|
+
throw new Error('FILE_MOVE_DESCENDANT_INVALID');
|
|
508
|
+
}
|
|
509
|
+
await rename(source.path, destination);
|
|
510
|
+
return { path: workspaceRelativePath(root, destination) };
|
|
511
|
+
}
|
|
512
|
+
async function resolveNewWorkspaceEntry(root, requestedPath) {
|
|
513
|
+
const normalized = requestedPath.replaceAll('\\', '/');
|
|
514
|
+
const segments = normalized.split('/');
|
|
515
|
+
const name = segments.at(-1);
|
|
516
|
+
if (requestedPath.includes('\0') ||
|
|
517
|
+
normalized.startsWith('/') ||
|
|
518
|
+
isAbsolute(requestedPath) ||
|
|
519
|
+
win32.isAbsolute(normalized) ||
|
|
520
|
+
name === undefined ||
|
|
521
|
+
name.length === 0 ||
|
|
522
|
+
segments.some(invalidWorkspaceMutationSegment))
|
|
523
|
+
throw new Error('FILE_PATH_ESCAPE');
|
|
524
|
+
const parentPath = segments.length === 1 ? '.' : segments.slice(0, -1).join('/');
|
|
525
|
+
await assertWorkspaceMutationPathHasNoSymlinks(root, parentPath);
|
|
526
|
+
const parent = await resolveWorkspaceEntryFromCanonicalRoot(root, parentPath, true);
|
|
527
|
+
const target = resolve(parent, name);
|
|
528
|
+
try {
|
|
529
|
+
await lstat(target);
|
|
530
|
+
throw new Error('FILE_ALREADY_EXISTS');
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
if (error.code !== 'ENOENT')
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
return target;
|
|
537
|
+
}
|
|
538
|
+
async function assertWorkspaceMutationPathHasNoSymlinks(root, requestedPath) {
|
|
539
|
+
const normalized = requestedPath.replaceAll('\\', '/');
|
|
540
|
+
if (normalized === '.')
|
|
541
|
+
return;
|
|
542
|
+
const segments = normalized.split('/');
|
|
543
|
+
if (requestedPath.includes('\0') ||
|
|
544
|
+
normalized.startsWith('/') ||
|
|
545
|
+
isAbsolute(requestedPath) ||
|
|
546
|
+
win32.isAbsolute(normalized) ||
|
|
547
|
+
segments.some(invalidWorkspaceMutationSegment))
|
|
548
|
+
throw new Error('FILE_PATH_ESCAPE');
|
|
549
|
+
let candidate = root;
|
|
550
|
+
for (const segment of segments) {
|
|
551
|
+
candidate = resolve(candidate, segment);
|
|
552
|
+
const metadata = await lstat(candidate).catch((error) => {
|
|
553
|
+
if (error.code === 'ENOENT')
|
|
554
|
+
throw new Error('FILE_NOT_FOUND');
|
|
555
|
+
throw error;
|
|
556
|
+
});
|
|
557
|
+
if (metadata.isSymbolicLink())
|
|
558
|
+
throw new Error('FILE_PATH_ESCAPE');
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function invalidWorkspaceMutationSegment(segment) {
|
|
562
|
+
if (segment.length === 0 || segment === '.' || segment === '..' || segment === '.git')
|
|
563
|
+
return true;
|
|
564
|
+
if (hostPlatform() !== 'win32')
|
|
565
|
+
return false;
|
|
566
|
+
const normalized = segment.replace(/[. ]+$/u, '').toUpperCase();
|
|
567
|
+
return (/[<>:"|?*]/u.test(segment) ||
|
|
568
|
+
/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\..*)?$/u.test(normalized));
|
|
569
|
+
}
|
|
570
|
+
function workspaceRelativePath(root, path) {
|
|
571
|
+
const value = relative(root, path).split(sep).join('/');
|
|
572
|
+
return value.length === 0 ? '.' : value;
|
|
573
|
+
}
|
|
355
574
|
function isSkippableWorkspaceEntryError(error) {
|
|
356
575
|
return (error instanceof Error &&
|
|
357
576
|
(error.message === 'FILE_PATH_ESCAPE' ||
|
|
@@ -518,16 +737,28 @@ function isUtf8ContinuationByte(value) {
|
|
|
518
737
|
return (value & 0b1100_0000) === 0b1000_0000;
|
|
519
738
|
}
|
|
520
739
|
/** Searches file names only, so an exploratory request cannot exfiltrate file contents. */
|
|
521
|
-
export async function searchWorkspaceFiles(workspaceRoot, query, cursor, limit = SEARCH_RESULT_LIMIT) {
|
|
740
|
+
export async function searchWorkspaceFiles(workspaceRoot, query, cursor, limit = SEARCH_RESULT_LIMIT, includeDirectories = false) {
|
|
522
741
|
const index = new WorkspaceFileIndex(workspaceRoot);
|
|
523
742
|
try {
|
|
524
|
-
return await index.search(query, cursor, limit);
|
|
743
|
+
return await index.search(query, cursor, limit, includeDirectories);
|
|
525
744
|
}
|
|
526
745
|
finally {
|
|
527
746
|
index.dispose();
|
|
528
747
|
}
|
|
529
748
|
}
|
|
530
749
|
async function resolveWorkspaceEntry(workspaceRoot, requestedPath, directory) {
|
|
750
|
+
const root = await realpath(workspaceRoot);
|
|
751
|
+
return resolveWorkspaceEntryFromCanonicalRoot(root, requestedPath, directory);
|
|
752
|
+
}
|
|
753
|
+
async function resolveWorkspaceEntryFromCanonicalRoot(root, requestedPath, directory) {
|
|
754
|
+
const entry = await resolveWorkspaceEntryMetadata(root, requestedPath);
|
|
755
|
+
if (directory
|
|
756
|
+
? !entry.metadata.isDirectory()
|
|
757
|
+
: !entry.metadata.isFile() && !entry.metadata.isDirectory())
|
|
758
|
+
throw new Error(directory ? 'FILE_NOT_DIRECTORY' : 'FILE_NOT_REGULAR');
|
|
759
|
+
return entry.path;
|
|
760
|
+
}
|
|
761
|
+
async function resolveWorkspaceEntryMetadata(root, requestedPath) {
|
|
531
762
|
if (requestedPath.includes('\0') ||
|
|
532
763
|
requestedPath.length === 0 ||
|
|
533
764
|
requestedPath.startsWith('/') ||
|
|
@@ -536,7 +767,6 @@ async function resolveWorkspaceEntry(workspaceRoot, requestedPath, directory) {
|
|
|
536
767
|
const normalized = requestedPath.replaceAll('\\', '/');
|
|
537
768
|
if (normalized.split('/').some((segment) => segment === '..' || segment === '.git'))
|
|
538
769
|
throw new Error('FILE_PATH_ESCAPE');
|
|
539
|
-
const root = await realpath(workspaceRoot);
|
|
540
770
|
let candidate;
|
|
541
771
|
try {
|
|
542
772
|
candidate = await realpath(resolve(root, normalized));
|
|
@@ -547,14 +777,39 @@ async function resolveWorkspaceEntry(workspaceRoot, requestedPath, directory) {
|
|
|
547
777
|
throw error;
|
|
548
778
|
}
|
|
549
779
|
const pathToCandidate = relative(root, candidate);
|
|
550
|
-
if (pathToCandidate
|
|
780
|
+
if (isAbsolute(pathToCandidate) ||
|
|
781
|
+
pathToCandidate.startsWith('..') ||
|
|
551
782
|
pathToCandidate === '..' ||
|
|
552
783
|
pathToCandidate.startsWith(`..${sep}`))
|
|
553
784
|
throw new Error('FILE_PATH_ESCAPE');
|
|
554
785
|
const metadata = await lstat(candidate);
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
786
|
+
return { path: candidate, metadata };
|
|
787
|
+
}
|
|
788
|
+
async function inspectWorkspaceIndexEntry(root, requestedPath, name) {
|
|
789
|
+
try {
|
|
790
|
+
const entry = await resolveWorkspaceEntryMetadata(root, requestedPath);
|
|
791
|
+
if (!entry.metadata.isFile() && !entry.metadata.isDirectory())
|
|
792
|
+
return undefined;
|
|
793
|
+
const safePath = relative(root, entry.path).split(sep).join('/');
|
|
794
|
+
return entry.metadata.isDirectory()
|
|
795
|
+
? {
|
|
796
|
+
path: safePath,
|
|
797
|
+
name,
|
|
798
|
+
kind: 'DIRECTORY',
|
|
799
|
+
size: null,
|
|
800
|
+
modifiedAt: entry.metadata.mtimeMs
|
|
801
|
+
}
|
|
802
|
+
: {
|
|
803
|
+
path: safePath,
|
|
804
|
+
name,
|
|
805
|
+
kind: 'FILE',
|
|
806
|
+
size: entry.metadata.size,
|
|
807
|
+
modifiedAt: entry.metadata.mtimeMs
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
return undefined;
|
|
812
|
+
}
|
|
558
813
|
}
|
|
559
814
|
function encodeDirectoryCursor(path, name) {
|
|
560
815
|
return Buffer.from(JSON.stringify({ path, name })).toString('base64url');
|
|
@@ -776,6 +1031,201 @@ export async function resolveGitRepository(workspacePath, repositoryPath) {
|
|
|
776
1031
|
}
|
|
777
1032
|
return repository;
|
|
778
1033
|
}
|
|
1034
|
+
export async function listGitHistoryRefs(repository) {
|
|
1035
|
+
const result = await runGit(repository, [
|
|
1036
|
+
'for-each-ref',
|
|
1037
|
+
'--format=%(refname)%09%(refname:short)%09%(objectname)%09%(HEAD)%09%(symref)',
|
|
1038
|
+
'refs/heads',
|
|
1039
|
+
'refs/remotes'
|
|
1040
|
+
]);
|
|
1041
|
+
return result.text
|
|
1042
|
+
.split(/\r?\n/)
|
|
1043
|
+
.flatMap((line) => {
|
|
1044
|
+
const [name, displayName, hash, head, symref] = line.split('\t');
|
|
1045
|
+
if (name === undefined ||
|
|
1046
|
+
displayName === undefined ||
|
|
1047
|
+
hash === undefined ||
|
|
1048
|
+
symref === undefined ||
|
|
1049
|
+
symref.length > 0 ||
|
|
1050
|
+
!/^[0-9a-f]{40,64}$/.test(hash))
|
|
1051
|
+
return [];
|
|
1052
|
+
const kind = name.startsWith('refs/heads/')
|
|
1053
|
+
? 'LOCAL'
|
|
1054
|
+
: name.startsWith('refs/remotes/')
|
|
1055
|
+
? 'REMOTE'
|
|
1056
|
+
: undefined;
|
|
1057
|
+
return kind === undefined ? [] : [{ name, displayName, kind, hash, current: head === '*' }];
|
|
1058
|
+
})
|
|
1059
|
+
.sort((left, right) => left.kind.localeCompare(right.kind) || left.displayName.localeCompare(right.displayName));
|
|
1060
|
+
}
|
|
1061
|
+
function selectedHistoryRefs(available, requested) {
|
|
1062
|
+
if (requested === undefined || requested.length === 0)
|
|
1063
|
+
return available;
|
|
1064
|
+
const unique = [...new Set(requested)];
|
|
1065
|
+
const selected = unique.map((name) => available.find((entry) => entry.name === name));
|
|
1066
|
+
if (selected.some((entry) => entry === undefined))
|
|
1067
|
+
throw new Error('GIT_HISTORY_REF_INVALID');
|
|
1068
|
+
return selected;
|
|
1069
|
+
}
|
|
1070
|
+
function historyCursor(offset, fingerprint) {
|
|
1071
|
+
return Buffer.from(JSON.stringify({ offset, fingerprint })).toString('base64url');
|
|
1072
|
+
}
|
|
1073
|
+
function parseHistoryCursor(value, fingerprint) {
|
|
1074
|
+
if (value === undefined)
|
|
1075
|
+
return 0;
|
|
1076
|
+
try {
|
|
1077
|
+
const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
|
|
1078
|
+
if (!Number.isInteger(parsed.offset) ||
|
|
1079
|
+
parsed.offset < 0 ||
|
|
1080
|
+
parsed.fingerprint !== fingerprint)
|
|
1081
|
+
throw new Error('invalid');
|
|
1082
|
+
return parsed.offset;
|
|
1083
|
+
}
|
|
1084
|
+
catch {
|
|
1085
|
+
throw new Error('GIT_HISTORY_CURSOR_INVALID');
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
export async function readGitHistory(repository, input) {
|
|
1089
|
+
const available = await listGitHistoryRefs(repository);
|
|
1090
|
+
const selected = selectedHistoryRefs(available, input.refs);
|
|
1091
|
+
if (selected.length === 0)
|
|
1092
|
+
return { commits: [], nextCursor: null };
|
|
1093
|
+
const limit = input.limit ?? 50;
|
|
1094
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
1095
|
+
throw new Error('GIT_HISTORY_LIMIT_INVALID');
|
|
1096
|
+
const names = selected.map((entry) => entry.name).sort();
|
|
1097
|
+
// Bind the offset cursor to the exact branch tips. Advancing a branch invalidates
|
|
1098
|
+
// the cursor instead of silently skipping or duplicating commits between pages.
|
|
1099
|
+
const fingerprint = createHash('sha256')
|
|
1100
|
+
.update(selected
|
|
1101
|
+
.map((entry) => `${entry.name}\0${entry.hash}`)
|
|
1102
|
+
.sort()
|
|
1103
|
+
.join('\0'))
|
|
1104
|
+
.digest('hex');
|
|
1105
|
+
const offset = parseHistoryCursor(input.cursor, fingerprint);
|
|
1106
|
+
const result = await runGit(repository, [
|
|
1107
|
+
'log',
|
|
1108
|
+
'--topo-order',
|
|
1109
|
+
'--date-order',
|
|
1110
|
+
`--skip=${offset}`,
|
|
1111
|
+
`--max-count=${limit + 1}`,
|
|
1112
|
+
'-z',
|
|
1113
|
+
'--format=%H%x00%P%x00%an%x00%ae%x00%ct%x00%s',
|
|
1114
|
+
...names,
|
|
1115
|
+
'--'
|
|
1116
|
+
]);
|
|
1117
|
+
const refsByHash = new Map();
|
|
1118
|
+
for (const entry of available) {
|
|
1119
|
+
const refs = refsByHash.get(entry.hash) ?? [];
|
|
1120
|
+
refs.push(entry.displayName);
|
|
1121
|
+
refsByHash.set(entry.hash, refs);
|
|
1122
|
+
}
|
|
1123
|
+
const fields = result.text.split('\0');
|
|
1124
|
+
const commits = [];
|
|
1125
|
+
for (let index = 0; index + 5 < fields.length; index += 6) {
|
|
1126
|
+
const [hash, parents = '', authorName = '', authorEmail = '', committedAt = '0', subject = ''] = fields.slice(index, index + 6);
|
|
1127
|
+
if (hash === undefined || !/^[0-9a-f]{40,64}$/.test(hash))
|
|
1128
|
+
continue;
|
|
1129
|
+
commits.push({
|
|
1130
|
+
hash,
|
|
1131
|
+
parents: parents.length === 0 ? [] : parents.split(' '),
|
|
1132
|
+
authorName,
|
|
1133
|
+
authorEmail,
|
|
1134
|
+
committedAt: Number(committedAt) * 1000,
|
|
1135
|
+
subject,
|
|
1136
|
+
refs: refsByHash.get(hash) ?? []
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
const hasMore = commits.length > limit;
|
|
1140
|
+
return {
|
|
1141
|
+
commits: commits.slice(0, limit),
|
|
1142
|
+
nextCursor: hasMore ? historyCursor(offset + limit, fingerprint) : null
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
async function assertHistoryCommitReachable(repository, commit, refs) {
|
|
1146
|
+
if (!/^[0-9a-f]{40,64}$/.test(commit))
|
|
1147
|
+
throw new Error('GIT_HISTORY_COMMIT_INVALID');
|
|
1148
|
+
const selected = selectedHistoryRefs(await listGitHistoryRefs(repository), refs);
|
|
1149
|
+
for (const ref of selected) {
|
|
1150
|
+
const result = await runGitAllowFailure(repository, [
|
|
1151
|
+
'merge-base',
|
|
1152
|
+
'--is-ancestor',
|
|
1153
|
+
commit,
|
|
1154
|
+
ref.name
|
|
1155
|
+
]);
|
|
1156
|
+
if (result.code === 0)
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
throw new Error('GIT_HISTORY_COMMIT_INVALID');
|
|
1160
|
+
}
|
|
1161
|
+
export async function readGitHistoryFiles(repository, commit, refs) {
|
|
1162
|
+
await assertHistoryCommitReachable(repository, commit, refs);
|
|
1163
|
+
const result = await runGit(repository, [
|
|
1164
|
+
'diff-tree',
|
|
1165
|
+
'--root',
|
|
1166
|
+
'--first-parent',
|
|
1167
|
+
'--no-commit-id',
|
|
1168
|
+
'-r',
|
|
1169
|
+
'-z',
|
|
1170
|
+
'-M',
|
|
1171
|
+
'-C',
|
|
1172
|
+
'--name-status',
|
|
1173
|
+
commit
|
|
1174
|
+
]);
|
|
1175
|
+
const records = result.text.split('\0');
|
|
1176
|
+
const files = [];
|
|
1177
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
1178
|
+
const status = records[index];
|
|
1179
|
+
if (status === undefined || status.length === 0)
|
|
1180
|
+
continue;
|
|
1181
|
+
const code = status[0] ?? 'M';
|
|
1182
|
+
if (code === 'R' || code === 'C') {
|
|
1183
|
+
const previousPath = records[index + 1];
|
|
1184
|
+
const path = records[index + 2];
|
|
1185
|
+
if (previousPath !== undefined && path !== undefined)
|
|
1186
|
+
files.push({ path, previousPath, change: gitChange(code) });
|
|
1187
|
+
index += 2;
|
|
1188
|
+
}
|
|
1189
|
+
else {
|
|
1190
|
+
const path = records[index + 1];
|
|
1191
|
+
if (path !== undefined)
|
|
1192
|
+
files.push({ path, change: gitChange(code) });
|
|
1193
|
+
index += 1;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return files;
|
|
1197
|
+
}
|
|
1198
|
+
export async function readGitHistoryFileDiff(repository, commit, requestedPath, refs, requestedPreviousPath) {
|
|
1199
|
+
await assertHistoryCommitReachable(repository, commit, refs);
|
|
1200
|
+
const path = safeGitWorkspacePath(requestedPath);
|
|
1201
|
+
const previousPath = requestedPreviousPath === undefined ? undefined : safeGitWorkspacePath(requestedPreviousPath);
|
|
1202
|
+
const result = await runGit(repository, [
|
|
1203
|
+
'show',
|
|
1204
|
+
'--format=',
|
|
1205
|
+
'--first-parent',
|
|
1206
|
+
'--no-ext-diff',
|
|
1207
|
+
'--no-textconv',
|
|
1208
|
+
commit,
|
|
1209
|
+
'--',
|
|
1210
|
+
`:(literal)${path}`,
|
|
1211
|
+
...(previousPath === undefined || previousPath === path ? [] : [`:(literal)${previousPath}`])
|
|
1212
|
+
]);
|
|
1213
|
+
const binary = result.output.includes(0) || result.text.includes('Binary files ');
|
|
1214
|
+
if (binary) {
|
|
1215
|
+
const [beforePreview, afterPreview] = await Promise.all([
|
|
1216
|
+
readGitBinaryPreview(repository, `${commit}^`, previousPath ?? path),
|
|
1217
|
+
readGitBinaryPreview(repository, commit, path)
|
|
1218
|
+
]);
|
|
1219
|
+
return {
|
|
1220
|
+
binary: true,
|
|
1221
|
+
summary: `binary diff sha256=${createHash('sha256').update(result.output).digest('hex')} bytes=${result.output.length}`,
|
|
1222
|
+
truncated: result.truncated,
|
|
1223
|
+
...(beforePreview === undefined ? {} : { beforePreview }),
|
|
1224
|
+
...(afterPreview === undefined ? {} : { afterPreview })
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
return { binary: false, text: result.text, truncated: result.truncated };
|
|
1228
|
+
}
|
|
779
1229
|
export async function readCurrentChanges(workspacePath) {
|
|
780
1230
|
return (await readCurrentChangesSummary(workspacePath)).changes;
|
|
781
1231
|
}
|
|
@@ -793,10 +1243,16 @@ export async function readCurrentChangeDiff(workspacePath, requestedPath) {
|
|
|
793
1243
|
const after = await readWorkingTreeText(workspacePath, path);
|
|
794
1244
|
const binary = result.output.includes(0) || result.text.includes('Binary files ') || after?.binary === true;
|
|
795
1245
|
if (binary) {
|
|
1246
|
+
const [beforePreview, afterPreview] = await Promise.all([
|
|
1247
|
+
readGitBinaryPreview(workspacePath, 'HEAD', path),
|
|
1248
|
+
readWorkingTreeBinaryPreview(workspacePath, path)
|
|
1249
|
+
]);
|
|
796
1250
|
return {
|
|
797
1251
|
binary: true,
|
|
798
1252
|
summary: `binary diff sha256=${createHash('sha256').update(result.output).digest('hex')} bytes=${result.output.length}`,
|
|
799
|
-
truncated: result.truncated || after?.truncated === true
|
|
1253
|
+
truncated: result.truncated || after?.truncated === true,
|
|
1254
|
+
...(beforePreview === undefined ? {} : { beforePreview }),
|
|
1255
|
+
...(afterPreview === undefined ? {} : { afterPreview })
|
|
800
1256
|
};
|
|
801
1257
|
}
|
|
802
1258
|
return {
|
|
@@ -834,6 +1290,32 @@ async function assertCurrentChangePathAccessible(workspacePath, path) {
|
|
|
834
1290
|
return;
|
|
835
1291
|
await resolveWorkspaceEntry(root, path, false);
|
|
836
1292
|
}
|
|
1293
|
+
async function readGitBinaryPreview(repository, ref, path) {
|
|
1294
|
+
const result = await runGitAllowFailure(repository, ['show', `${ref}:${path}`], GIT_BINARY_PREVIEW_LIMIT);
|
|
1295
|
+
if (result.code !== 0)
|
|
1296
|
+
return undefined;
|
|
1297
|
+
return result.truncated
|
|
1298
|
+
? { truncated: true }
|
|
1299
|
+
: { contentBase64: result.output.toString('base64'), truncated: false };
|
|
1300
|
+
}
|
|
1301
|
+
async function readWorkingTreeBinaryPreview(workspacePath, path) {
|
|
1302
|
+
const root = await realpath(workspacePath);
|
|
1303
|
+
let file;
|
|
1304
|
+
try {
|
|
1305
|
+
file = await resolveWorkspaceEntry(root, path, false);
|
|
1306
|
+
}
|
|
1307
|
+
catch (error) {
|
|
1308
|
+
if (error instanceof Error && error.message === 'FILE_NOT_FOUND')
|
|
1309
|
+
return undefined;
|
|
1310
|
+
throw error;
|
|
1311
|
+
}
|
|
1312
|
+
const metadata = await lstat(file);
|
|
1313
|
+
if (!metadata.isFile())
|
|
1314
|
+
return undefined;
|
|
1315
|
+
if (metadata.size > GIT_BINARY_PREVIEW_LIMIT)
|
|
1316
|
+
return { truncated: true };
|
|
1317
|
+
return { contentBase64: (await readFile(file)).toString('base64'), truncated: false };
|
|
1318
|
+
}
|
|
837
1319
|
async function readWorkingTreeText(workspacePath, path) {
|
|
838
1320
|
const root = await realpath(workspacePath);
|
|
839
1321
|
let file;
|
|
@@ -938,7 +1420,7 @@ async function runGit(workspacePath, args) {
|
|
|
938
1420
|
}
|
|
939
1421
|
return result;
|
|
940
1422
|
}
|
|
941
|
-
async function runGitAllowFailure(workspacePath, args) {
|
|
1423
|
+
async function runGitAllowFailure(workspacePath, args, outputLimit = GIT_OUTPUT_LIMIT) {
|
|
942
1424
|
return new Promise((resolve, reject) => {
|
|
943
1425
|
const child = spawn('git', [...args], {
|
|
944
1426
|
cwd: workspacePath,
|
|
@@ -952,12 +1434,13 @@ async function runGitAllowFailure(workspacePath, args) {
|
|
|
952
1434
|
let truncated = false;
|
|
953
1435
|
const timer = setTimeout(() => child.kill('SIGKILL'), GIT_TIMEOUT_MS);
|
|
954
1436
|
child.stdout.on('data', (chunk) => {
|
|
955
|
-
|
|
956
|
-
|
|
1437
|
+
const remaining = Math.max(0, outputLimit - size);
|
|
1438
|
+
if (remaining > 0) {
|
|
1439
|
+
const retained = chunk.subarray(0, remaining);
|
|
957
1440
|
chunks.push(retained);
|
|
958
1441
|
size += retained.length;
|
|
959
1442
|
}
|
|
960
|
-
truncated ||=
|
|
1443
|
+
truncated ||= chunk.length > remaining;
|
|
961
1444
|
});
|
|
962
1445
|
child.stderr.on('data', (chunk) => errors.push(chunk.subarray(0, 4096)));
|
|
963
1446
|
child.once('error', (error) => {
|
|
@@ -986,4 +1469,3 @@ function summarizeText(value) {
|
|
|
986
1469
|
bytes: Buffer.byteLength(value)
|
|
987
1470
|
});
|
|
988
1471
|
}
|
|
989
|
-
//# sourceMappingURL=workspace.js.map
|