@galaxy-stack/ai-coder-core 0.3.0-alpha.1 → 0.3.0-alpha.7
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/CHANGELOG.d/2026-09-19-research-rejection-evidence.md +15 -0
- package/CHANGELOG.d/2026-09-21-evidence-missing-remediation.md +15 -0
- package/CHANGELOG.d/2026-09-21-observation-nudge-context.md +12 -0
- package/CHANGELOG.d/2026-09-22-model-retry-budget.md +18 -0
- package/CHANGELOG.d/2026-09-22-no-progress-recovery-turn.md +19 -0
- package/CHANGELOG.d/2026-09-23-agent-platform.md +5 -0
- package/CHANGELOG.d/2026-09-23-conservative-output-reserve.md +16 -0
- package/CHANGELOG.d/2026-09-24-approval-workspace-review.md +7 -0
- package/CHANGELOG.d/2026-09-24-incremental-ollama.md +5 -0
- package/CHANGELOG.md +71 -1
- package/README.md +78 -4
- package/dist/adapters/node/config/manual-provider-config.d.ts +19 -0
- package/dist/adapters/node/config/manual-provider-config.d.ts.map +1 -0
- package/dist/adapters/node/config/manual-provider-config.js +91 -0
- package/dist/adapters/node/config/manual-provider-config.js.map +1 -0
- package/dist/adapters/node/host/command-containment.d.ts +45 -0
- package/dist/adapters/node/host/command-containment.d.ts.map +1 -0
- package/dist/adapters/node/host/command-containment.js +580 -0
- package/dist/adapters/node/host/command-containment.js.map +1 -0
- package/dist/adapters/node/host/content-hash.d.ts +2 -0
- package/dist/adapters/node/host/content-hash.d.ts.map +1 -0
- package/dist/adapters/node/host/content-hash.js +5 -0
- package/dist/adapters/node/host/content-hash.js.map +1 -0
- package/dist/adapters/node/host/file-run-store.d.ts +29 -0
- package/dist/adapters/node/host/file-run-store.d.ts.map +1 -0
- package/dist/adapters/node/host/file-run-store.js +94 -0
- package/dist/adapters/node/host/file-run-store.js.map +1 -0
- package/dist/adapters/node/host/host-environment.d.ts +9 -0
- package/dist/adapters/node/host/host-environment.d.ts.map +1 -0
- package/dist/adapters/node/host/host-environment.js +29 -0
- package/dist/adapters/node/host/host-environment.js.map +1 -0
- package/dist/adapters/node/host/node-command-port.d.ts +21 -0
- package/dist/adapters/node/host/node-command-port.d.ts.map +1 -0
- package/dist/adapters/node/host/node-command-port.js +281 -0
- package/dist/adapters/node/host/node-command-port.js.map +1 -0
- package/dist/adapters/node/host/node-workspace-evidence-verifier.d.ts +22 -0
- package/dist/adapters/node/host/node-workspace-evidence-verifier.d.ts.map +1 -0
- package/dist/adapters/node/host/node-workspace-evidence-verifier.js +384 -0
- package/dist/adapters/node/host/node-workspace-evidence-verifier.js.map +1 -0
- package/dist/adapters/node/host/node-workspace-port.d.ts +87 -0
- package/dist/adapters/node/host/node-workspace-port.d.ts.map +1 -0
- package/dist/adapters/node/host/node-workspace-port.js +860 -0
- package/dist/adapters/node/host/node-workspace-port.js.map +1 -0
- package/dist/adapters/node/host/node-workspace-snapshot.d.ts +61 -0
- package/dist/adapters/node/host/node-workspace-snapshot.d.ts.map +1 -0
- package/dist/adapters/node/host/node-workspace-snapshot.js +281 -0
- package/dist/adapters/node/host/node-workspace-snapshot.js.map +1 -0
- package/dist/adapters/node/host/path-scope.d.ts +17 -0
- package/dist/adapters/node/host/path-scope.d.ts.map +1 -0
- package/dist/adapters/node/host/path-scope.js +89 -0
- package/dist/adapters/node/host/path-scope.js.map +1 -0
- package/dist/adapters/node/host/project-tools.d.ts +42 -0
- package/dist/adapters/node/host/project-tools.d.ts.map +1 -0
- package/dist/adapters/node/host/project-tools.js +362 -0
- package/dist/adapters/node/host/project-tools.js.map +1 -0
- package/dist/adapters/node/mcp/mcp-client.d.ts +206 -0
- package/dist/adapters/node/mcp/mcp-client.d.ts.map +1 -0
- package/dist/adapters/node/mcp/mcp-client.js +123 -0
- package/dist/adapters/node/mcp/mcp-client.js.map +1 -0
- package/dist/adapters/node/mcp/oauth-provider.d.ts +39 -0
- package/dist/adapters/node/mcp/oauth-provider.d.ts.map +1 -0
- package/dist/adapters/node/mcp/oauth-provider.js +168 -0
- package/dist/adapters/node/mcp/oauth-provider.js.map +1 -0
- package/dist/adapters/node/memory/sqlite-memory.d.ts +22 -0
- package/dist/adapters/node/memory/sqlite-memory.d.ts.map +1 -0
- package/dist/adapters/node/memory/sqlite-memory.js +83 -0
- package/dist/adapters/node/memory/sqlite-memory.js.map +1 -0
- package/dist/adapters/node/provider/ollama-chat-stream.d.ts +17 -0
- package/dist/adapters/node/provider/ollama-chat-stream.d.ts.map +1 -0
- package/dist/adapters/node/provider/ollama-chat-stream.js +176 -0
- package/dist/adapters/node/provider/ollama-chat-stream.js.map +1 -0
- package/dist/adapters/node/provider/ollama-coding-model.d.ts +89 -0
- package/dist/adapters/node/provider/ollama-coding-model.d.ts.map +1 -0
- package/dist/adapters/node/provider/ollama-coding-model.js +464 -0
- package/dist/adapters/node/provider/ollama-coding-model.js.map +1 -0
- package/dist/adapters/node/provider/ollama-research-port.d.ts +32 -0
- package/dist/adapters/node/provider/ollama-research-port.d.ts.map +1 -0
- package/dist/adapters/node/provider/ollama-research-port.js +346 -0
- package/dist/adapters/node/provider/ollama-research-port.js.map +1 -0
- package/dist/adapters/node/skills/directory-skills.d.ts +12 -0
- package/dist/adapters/node/skills/directory-skills.d.ts.map +1 -0
- package/dist/adapters/node/skills/directory-skills.js +90 -0
- package/dist/adapters/node/skills/directory-skills.js.map +1 -0
- package/dist/adapters/node/tools/tool-executor.d.ts +91 -0
- package/dist/adapters/node/tools/tool-executor.d.ts.map +1 -0
- package/dist/adapters/node/tools/tool-executor.js +948 -0
- package/dist/adapters/node/tools/tool-executor.js.map +1 -0
- package/dist/adapters/node/tools/workspace-review.d.ts +13 -0
- package/dist/adapters/node/tools/workspace-review.d.ts.map +1 -0
- package/dist/adapters/node/tools/workspace-review.js +81 -0
- package/dist/adapters/node/tools/workspace-review.js.map +1 -0
- package/dist/agent/index.d.ts +79 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +97 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/approval/approval-policy.d.ts.map +1 -1
- package/dist/approval/approval-policy.js +4 -1
- package/dist/approval/approval-policy.js.map +1 -1
- package/dist/context/context-profile.d.ts +1 -1
- package/dist/context/context-profile.js +2 -2
- package/dist/prompt/prompt-assembler.d.ts +1 -0
- package/dist/prompt/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt/prompt-assembler.js +16 -2
- package/dist/prompt/prompt-assembler.js.map +1 -1
- package/dist/runtime/run-controller.d.ts.map +1 -1
- package/dist/runtime/run-controller.js +126 -23
- package/dist/runtime/run-controller.js.map +1 -1
- package/dist/runtime/runtime-types.d.ts +19 -0
- package/dist/runtime/runtime-types.d.ts.map +1 -1
- package/docs/AGENT_PLATFORM.md +91 -0
- package/docs/GALAXY_AGENT_PLATFORM_PLAN.md +318 -0
- package/docs/GALAXY_AGENT_PLATFORM_TODO.md +158 -0
- package/package.json +19 -5
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants as fileSystemConstants } from "node:fs";
|
|
3
|
+
import { copyFile, lstat, link, mkdir, open, readFile, readdir, rename, rm, rmdir, unlink, } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
5
|
+
import { sha256Text } from "./content-hash.js";
|
|
6
|
+
import { WorkspaceScope } from "./path-scope.js";
|
|
7
|
+
const DEFAULT_LIMIT = 100;
|
|
8
|
+
const MAX_LIMIT = 1_000;
|
|
9
|
+
const DEFAULT_MAX_BYTES = 128 * 1024;
|
|
10
|
+
const MAX_READ_FILE_BYTES = 16 * 1024 * 1024;
|
|
11
|
+
const SEARCH_READ_CHUNK_BYTES = 64 * 1024;
|
|
12
|
+
const MAX_SEARCH_LINE_CHARACTERS = 1024 * 1024;
|
|
13
|
+
const MAX_SCANNED_ENTRIES = 100_000;
|
|
14
|
+
const IGNORED_DIRECTORY_NAMES = new Set([".git", "node_modules", "dist", "coverage"]);
|
|
15
|
+
function failure(code, message, retryable = false) {
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
ok: false,
|
|
18
|
+
error: Object.freeze({ code, message, retryable }),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function exceptionFailure(error) {
|
|
22
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
23
|
+
const nativeCode = typeof error === "object" && error !== null && "code" in error
|
|
24
|
+
? String(error.code ?? "WORKSPACE_ERROR")
|
|
25
|
+
: "WORKSPACE_ERROR";
|
|
26
|
+
const code = (() => {
|
|
27
|
+
switch (nativeCode) {
|
|
28
|
+
case "ABORT_ERR":
|
|
29
|
+
case "CANCELED": return "CANCELED";
|
|
30
|
+
case "DEADLINE_EXCEEDED": return "DEADLINE_EXCEEDED";
|
|
31
|
+
case "INVALID_INPUT":
|
|
32
|
+
case "ENOTDIR": return "INVALID_INPUT";
|
|
33
|
+
case "LIMIT_EXCEEDED": return "LIMIT_EXCEEDED";
|
|
34
|
+
case "CONFLICT": return "CONFLICT";
|
|
35
|
+
case "EACCES":
|
|
36
|
+
case "EPERM":
|
|
37
|
+
case "PERMISSION_DENIED": return "PERMISSION_DENIED";
|
|
38
|
+
case "ENOENT":
|
|
39
|
+
case "NOT_FOUND": return "NOT_FOUND";
|
|
40
|
+
case "EEXIST":
|
|
41
|
+
case "ALREADY_EXISTS": return "ALREADY_EXISTS";
|
|
42
|
+
case "EAGAIN":
|
|
43
|
+
case "EBUSY":
|
|
44
|
+
case "UNAVAILABLE": return "UNAVAILABLE";
|
|
45
|
+
case "PRECONDITION_FAILED": return "PRECONDITION_FAILED";
|
|
46
|
+
default: return "IO_ERROR";
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
return failure(code, message, code === "UNAVAILABLE");
|
|
50
|
+
}
|
|
51
|
+
function checkContext(context) {
|
|
52
|
+
if (context.signal.aborted)
|
|
53
|
+
return failure("CANCELED", "The run was canceled.");
|
|
54
|
+
if (Date.now() >= context.deadline)
|
|
55
|
+
return failure("DEADLINE_EXCEEDED", "The run deadline elapsed.");
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
function throwIfBlocked(context) {
|
|
59
|
+
const blocked = checkContext(context);
|
|
60
|
+
if (blocked !== undefined) {
|
|
61
|
+
throw Object.assign(new Error(blocked.error.message), { code: blocked.error.code });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function boundedLimit(limit) {
|
|
65
|
+
if (limit === undefined)
|
|
66
|
+
return DEFAULT_LIMIT;
|
|
67
|
+
if (!Number.isSafeInteger(limit) || limit < 1) {
|
|
68
|
+
throw Object.assign(new Error("Pagination limit must be a positive integer."), { code: "INVALID_INPUT" });
|
|
69
|
+
}
|
|
70
|
+
return Math.min(limit, MAX_LIMIT);
|
|
71
|
+
}
|
|
72
|
+
function decodeCursor(cursor) {
|
|
73
|
+
if (cursor === undefined)
|
|
74
|
+
return 0;
|
|
75
|
+
const match = /^offset:(\d+)$/.exec(cursor);
|
|
76
|
+
if (match?.[1] === undefined) {
|
|
77
|
+
throw Object.assign(new Error("Invalid pagination cursor."), { code: "INVALID_INPUT" });
|
|
78
|
+
}
|
|
79
|
+
const offset = Number(match[1]);
|
|
80
|
+
if (!Number.isSafeInteger(offset)) {
|
|
81
|
+
throw Object.assign(new Error("Invalid pagination cursor."), { code: "INVALID_INPUT" });
|
|
82
|
+
}
|
|
83
|
+
return offset;
|
|
84
|
+
}
|
|
85
|
+
function page(values, cursor, requestedLimit) {
|
|
86
|
+
const offset = decodeCursor(cursor);
|
|
87
|
+
const limit = boundedLimit(requestedLimit);
|
|
88
|
+
if (offset > values.length) {
|
|
89
|
+
throw Object.assign(new Error("Pagination cursor is outside the result set."), { code: "INVALID_INPUT" });
|
|
90
|
+
}
|
|
91
|
+
const result = values.slice(offset, offset + limit);
|
|
92
|
+
const nextOffset = offset + result.length;
|
|
93
|
+
const hasMore = nextOffset < values.length;
|
|
94
|
+
return {
|
|
95
|
+
values: Object.freeze(result),
|
|
96
|
+
pagination: Object.freeze({
|
|
97
|
+
hasMore,
|
|
98
|
+
...(hasMore ? { nextCursor: `offset:${nextOffset}` } : {}),
|
|
99
|
+
}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function relativeWorkspacePath(root, absolutePath) {
|
|
103
|
+
return relative(root, absolutePath).split("\\").join("/") || ".";
|
|
104
|
+
}
|
|
105
|
+
async function currentText(path, context) {
|
|
106
|
+
try {
|
|
107
|
+
return decodeUtf8(await readFile(path, { signal: context.signal }), path);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT")
|
|
111
|
+
return undefined;
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function decodeUtf8(bytes, path) {
|
|
116
|
+
try {
|
|
117
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw Object.assign(new Error(`Workspace text operation requires valid UTF-8: ${path}.`), { code: "INVALID_INPUT" });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function readUtf8(path, context) {
|
|
124
|
+
return decodeUtf8(await readFile(path, { signal: context.signal }), path);
|
|
125
|
+
}
|
|
126
|
+
async function pathExists(path) {
|
|
127
|
+
try {
|
|
128
|
+
await lstat(path);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT")
|
|
133
|
+
return false;
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function decodeReadCursor(cursor) {
|
|
138
|
+
const match = /^text:([a-f0-9]{64}):(\d+):(\d+):(\d+)$/.exec(cursor);
|
|
139
|
+
if (match?.[1] === undefined || match[2] === undefined || match[3] === undefined || match[4] === undefined) {
|
|
140
|
+
throw Object.assign(new Error("Invalid text pagination cursor."), { code: "INVALID_INPUT" });
|
|
141
|
+
}
|
|
142
|
+
const result = {
|
|
143
|
+
contentSha256: match[1],
|
|
144
|
+
startLine: Number(match[2]),
|
|
145
|
+
endLine: Number(match[3]),
|
|
146
|
+
offset: Number(match[4]),
|
|
147
|
+
};
|
|
148
|
+
if (!Number.isSafeInteger(result.startLine) || !Number.isSafeInteger(result.endLine) || !Number.isSafeInteger(result.offset)) {
|
|
149
|
+
throw Object.assign(new Error("Invalid text pagination cursor."), { code: "INVALID_INPUT" });
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
function utf8Chunk(bytes, offset, maxBytes) {
|
|
154
|
+
try {
|
|
155
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, offset));
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
throw Object.assign(new Error("Text cursor is not aligned to a UTF-8 character boundary."), { code: "INVALID_INPUT" });
|
|
159
|
+
}
|
|
160
|
+
let endOffset = Math.min(bytes.length, offset + maxBytes);
|
|
161
|
+
while (endOffset > offset) {
|
|
162
|
+
try {
|
|
163
|
+
const content = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(offset, endOffset));
|
|
164
|
+
return Object.freeze({ content, endOffset });
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
endOffset -= 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (offset >= bytes.length)
|
|
171
|
+
return Object.freeze({ content: "", endOffset: offset });
|
|
172
|
+
throw Object.assign(new Error("maxBytes is too small for the next UTF-8 character."), { code: "LIMIT_EXCEEDED" });
|
|
173
|
+
}
|
|
174
|
+
function countNewlines(value) {
|
|
175
|
+
let count = 0;
|
|
176
|
+
for (const character of value)
|
|
177
|
+
if (character === "\n")
|
|
178
|
+
count += 1;
|
|
179
|
+
return count;
|
|
180
|
+
}
|
|
181
|
+
async function scanUtf8Lines(absolutePath, context, onLine) {
|
|
182
|
+
const handle = await open(absolutePath, "r");
|
|
183
|
+
const hash = createHash("sha256");
|
|
184
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
185
|
+
const buffer = Buffer.allocUnsafe(SEARCH_READ_CHUNK_BYTES);
|
|
186
|
+
let carry = "";
|
|
187
|
+
let lineNumber = 1;
|
|
188
|
+
let position = 0;
|
|
189
|
+
const decode = (bytes, stream = false) => {
|
|
190
|
+
try {
|
|
191
|
+
return decoder.decode(bytes, { stream });
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
throw Object.assign(new Error(`Workspace text search requires valid UTF-8: ${absolutePath}.`), {
|
|
195
|
+
code: "INVALID_UTF8",
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const consume = (text) => {
|
|
200
|
+
const combined = carry + text;
|
|
201
|
+
let start = 0;
|
|
202
|
+
while (true) {
|
|
203
|
+
const newline = combined.indexOf("\n", start);
|
|
204
|
+
if (newline < 0)
|
|
205
|
+
break;
|
|
206
|
+
onLine(combined.slice(start, newline), lineNumber);
|
|
207
|
+
lineNumber += 1;
|
|
208
|
+
start = newline + 1;
|
|
209
|
+
}
|
|
210
|
+
carry = combined.slice(start);
|
|
211
|
+
if (carry.length > MAX_SEARCH_LINE_CHARACTERS) {
|
|
212
|
+
throw Object.assign(new Error(`Workspace text search line exceeds ${MAX_SEARCH_LINE_CHARACTERS} characters: ${absolutePath}.`), { code: "LIMIT_EXCEEDED" });
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
try {
|
|
216
|
+
while (true) {
|
|
217
|
+
throwIfBlocked(context);
|
|
218
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
|
|
219
|
+
if (bytesRead === 0)
|
|
220
|
+
break;
|
|
221
|
+
position += bytesRead;
|
|
222
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
223
|
+
hash.update(chunk);
|
|
224
|
+
consume(decode(chunk, true));
|
|
225
|
+
}
|
|
226
|
+
consume(decode());
|
|
227
|
+
onLine(carry, lineNumber);
|
|
228
|
+
return hash.digest("hex");
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
await handle.close();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function enforcePrecondition(current, precondition) {
|
|
235
|
+
if (precondition.kind === "must_not_exist") {
|
|
236
|
+
return current === undefined
|
|
237
|
+
? undefined
|
|
238
|
+
: failure("PRECONDITION_FAILED", "The target already exists. Re-read the file, then use a matches_sha256 precondition with the current content hash for edits; must_not_exist only fits the first creation.");
|
|
239
|
+
}
|
|
240
|
+
if (!/^[a-f0-9]{64}$/.test(precondition.contentSha256)) {
|
|
241
|
+
return failure("INVALID_INPUT", "Content hash preconditions must be lowercase SHA-256 values.");
|
|
242
|
+
}
|
|
243
|
+
if (current === undefined)
|
|
244
|
+
return failure("PRECONDITION_FAILED", "The target does not exist.");
|
|
245
|
+
const actual = sha256Text(current);
|
|
246
|
+
return actual === precondition.contentSha256
|
|
247
|
+
? undefined
|
|
248
|
+
: failure("PRECONDITION_FAILED", `Content hash mismatch; expected ${precondition.contentSha256}, but the current content hash is ${actual}. Re-read the file to see its current content, then use this current hash as contentSha256 in your next matches_sha256 precondition.`);
|
|
249
|
+
}
|
|
250
|
+
function globPattern(query) {
|
|
251
|
+
let source = "";
|
|
252
|
+
for (let index = 0; index < query.length; index += 1) {
|
|
253
|
+
const character = query[index];
|
|
254
|
+
if (character === "*") {
|
|
255
|
+
if (query[index + 1] === "*") {
|
|
256
|
+
index += 1;
|
|
257
|
+
if (query[index + 1] === "/") {
|
|
258
|
+
index += 1;
|
|
259
|
+
source += "(?:.*/)?";
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
source += ".*";
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
source += "[^/]*";
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else if (character === "?") {
|
|
270
|
+
source += "[^/]";
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
source += character === undefined ? "" : character.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return new RegExp(`^${source}$`, "i");
|
|
277
|
+
}
|
|
278
|
+
export class NodeWorkspacePort {
|
|
279
|
+
scope;
|
|
280
|
+
traversalExclusions = Object.freeze([...IGNORED_DIRECTORY_NAMES].sort());
|
|
281
|
+
constructor(scope) {
|
|
282
|
+
this.scope = scope;
|
|
283
|
+
}
|
|
284
|
+
static async create(workspaceRoot) {
|
|
285
|
+
return new NodeWorkspacePort(await WorkspaceScope.create(workspaceRoot));
|
|
286
|
+
}
|
|
287
|
+
async allEntries(path, depth, context) {
|
|
288
|
+
throwIfBlocked(context);
|
|
289
|
+
const root = await this.scope.resolveExisting(path);
|
|
290
|
+
const rootInfo = await lstat(root);
|
|
291
|
+
if (!rootInfo.isDirectory()) {
|
|
292
|
+
throw Object.assign(new Error("Workspace traversal requires a directory path."), { code: "INVALID_INPUT" });
|
|
293
|
+
}
|
|
294
|
+
const results = [];
|
|
295
|
+
const visit = async (absoluteDirectory, remainingDepth) => {
|
|
296
|
+
throwIfBlocked(context);
|
|
297
|
+
const directory = await this.scope.resolveExisting(relativeWorkspacePath(this.scope.root, absoluteDirectory));
|
|
298
|
+
const children = await readdir(directory, { withFileTypes: true });
|
|
299
|
+
children.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
300
|
+
for (const child of children) {
|
|
301
|
+
throwIfBlocked(context);
|
|
302
|
+
if (child.isDirectory() && IGNORED_DIRECTORY_NAMES.has(child.name))
|
|
303
|
+
continue;
|
|
304
|
+
if (child.name.endsWith(".galaxy-code.lock"))
|
|
305
|
+
continue;
|
|
306
|
+
const absoluteChild = join(directory, child.name);
|
|
307
|
+
const kind = child.isFile()
|
|
308
|
+
? "file"
|
|
309
|
+
: child.isDirectory()
|
|
310
|
+
? "directory"
|
|
311
|
+
: child.isSymbolicLink()
|
|
312
|
+
? "symlink"
|
|
313
|
+
: "other";
|
|
314
|
+
results.push(Object.freeze({
|
|
315
|
+
kind,
|
|
316
|
+
name: child.name,
|
|
317
|
+
path: relativeWorkspacePath(this.scope.root, absoluteChild),
|
|
318
|
+
}));
|
|
319
|
+
if (results.length > MAX_SCANNED_ENTRIES) {
|
|
320
|
+
throw Object.assign(new Error(`Workspace scan exceeded ${MAX_SCANNED_ENTRIES} entries.`), { code: "LIMIT_EXCEEDED" });
|
|
321
|
+
}
|
|
322
|
+
if (kind === "directory" && remainingDepth > 1)
|
|
323
|
+
await visit(absoluteChild, remainingDepth - 1);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
await visit(root, depth);
|
|
327
|
+
return Object.freeze(results);
|
|
328
|
+
}
|
|
329
|
+
async listDir(input, context) {
|
|
330
|
+
const canceled = checkContext(context);
|
|
331
|
+
if (canceled !== undefined)
|
|
332
|
+
return canceled;
|
|
333
|
+
try {
|
|
334
|
+
const depth = input.depth ?? 1;
|
|
335
|
+
if (!Number.isSafeInteger(depth) || depth < 1 || depth > 20)
|
|
336
|
+
return failure("INVALID_INPUT", "Depth must be between 1 and 20.");
|
|
337
|
+
const result = page(await this.allEntries(input.path, depth, context), input.cursor, input.limit);
|
|
338
|
+
return { ok: true, data: Object.freeze({ entries: result.values, pagination: result.pagination }) };
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
return exceptionFailure(error);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async stat(input, context) {
|
|
345
|
+
const canceled = checkContext(context);
|
|
346
|
+
if (canceled !== undefined)
|
|
347
|
+
return canceled;
|
|
348
|
+
try {
|
|
349
|
+
const lexical = this.scope.resolveLexical(input.path);
|
|
350
|
+
let info;
|
|
351
|
+
try {
|
|
352
|
+
info = await lstat(lexical);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
356
|
+
return { ok: true, data: Object.freeze({ kind: "missing" }) };
|
|
357
|
+
}
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
const kind = info.isFile()
|
|
361
|
+
? "file"
|
|
362
|
+
: info.isDirectory()
|
|
363
|
+
? "directory"
|
|
364
|
+
: info.isSymbolicLink()
|
|
365
|
+
? "symlink"
|
|
366
|
+
: "other";
|
|
367
|
+
const content = kind === "file" && info.size <= MAX_READ_FILE_BYTES
|
|
368
|
+
? await readFile(await this.scope.resolveExisting(input.path), { signal: context.signal })
|
|
369
|
+
: undefined;
|
|
370
|
+
return {
|
|
371
|
+
ok: true,
|
|
372
|
+
data: Object.freeze({
|
|
373
|
+
kind,
|
|
374
|
+
size: info.size,
|
|
375
|
+
mtimeMs: info.mtimeMs,
|
|
376
|
+
...(content === undefined ? {} : { contentSha256: createHash("sha256").update(content).digest("hex") }),
|
|
377
|
+
}),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
return exceptionFailure(error);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async readText(input, context) {
|
|
385
|
+
const canceled = checkContext(context);
|
|
386
|
+
if (canceled !== undefined)
|
|
387
|
+
return canceled;
|
|
388
|
+
try {
|
|
389
|
+
const resolvedPath = await this.scope.resolveExisting(input.path);
|
|
390
|
+
const info = await lstat(resolvedPath);
|
|
391
|
+
if (!info.isFile())
|
|
392
|
+
return failure("INVALID_INPUT", "readText requires a regular file.");
|
|
393
|
+
if (info.size > MAX_READ_FILE_BYTES) {
|
|
394
|
+
return failure("LIMIT_EXCEEDED", `Text file exceeds the ${MAX_READ_FILE_BYTES} byte host limit.`);
|
|
395
|
+
}
|
|
396
|
+
const fullContent = await readUtf8(resolvedPath, context);
|
|
397
|
+
const contentSha256 = sha256Text(fullContent);
|
|
398
|
+
const lines = fullContent.split("\n");
|
|
399
|
+
const decodedCursor = input.cursor === undefined ? undefined : decodeReadCursor(input.cursor);
|
|
400
|
+
if (decodedCursor !== undefined && decodedCursor.contentSha256 !== contentSha256) {
|
|
401
|
+
return failure("CONFLICT", "The file changed after the text cursor was issued.");
|
|
402
|
+
}
|
|
403
|
+
if (decodedCursor !== undefined &&
|
|
404
|
+
((input.startLine !== undefined && input.startLine !== decodedCursor.startLine) ||
|
|
405
|
+
(input.endLine !== undefined && input.endLine !== decodedCursor.endLine))) {
|
|
406
|
+
return failure("INVALID_INPUT", "Line range does not match the text cursor.");
|
|
407
|
+
}
|
|
408
|
+
const requestedStartLine = decodedCursor?.startLine ?? input.startLine ?? 1;
|
|
409
|
+
const requestedEndLine = decodedCursor?.endLine ?? Math.min(input.endLine ?? lines.length, lines.length);
|
|
410
|
+
if (!Number.isSafeInteger(requestedStartLine) ||
|
|
411
|
+
!Number.isSafeInteger(requestedEndLine) ||
|
|
412
|
+
requestedStartLine < 1 ||
|
|
413
|
+
requestedStartLine > lines.length ||
|
|
414
|
+
requestedEndLine < requestedStartLine ||
|
|
415
|
+
requestedEndLine > lines.length) {
|
|
416
|
+
return failure("INVALID_INPUT", "Invalid line range.");
|
|
417
|
+
}
|
|
418
|
+
const maxBytes = input.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
419
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 4 || maxBytes > DEFAULT_MAX_BYTES) {
|
|
420
|
+
return failure("INVALID_INPUT", `maxBytes must be between 4 and ${DEFAULT_MAX_BYTES}.`);
|
|
421
|
+
}
|
|
422
|
+
const selectedText = lines.slice(requestedStartLine - 1, requestedEndLine).join("\n");
|
|
423
|
+
const selectedBytes = Buffer.from(selectedText, "utf8");
|
|
424
|
+
const offset = decodedCursor?.offset ?? 0;
|
|
425
|
+
if (offset < 0 || offset > selectedBytes.length)
|
|
426
|
+
return failure("INVALID_INPUT", "Text cursor is outside the requested range.");
|
|
427
|
+
const chunk = utf8Chunk(selectedBytes, offset, maxBytes);
|
|
428
|
+
const prefix = selectedBytes.subarray(0, offset).toString("utf8");
|
|
429
|
+
const startLine = requestedStartLine + countNewlines(prefix);
|
|
430
|
+
const endLine = startLine + countNewlines(chunk.content);
|
|
431
|
+
const truncated = chunk.endOffset < selectedBytes.length;
|
|
432
|
+
return {
|
|
433
|
+
ok: true,
|
|
434
|
+
data: Object.freeze({
|
|
435
|
+
content: chunk.content,
|
|
436
|
+
contentSha256,
|
|
437
|
+
path: input.path,
|
|
438
|
+
resolvedPath,
|
|
439
|
+
startLine,
|
|
440
|
+
endLine,
|
|
441
|
+
truncated,
|
|
442
|
+
pagination: Object.freeze({
|
|
443
|
+
hasMore: truncated,
|
|
444
|
+
...(truncated
|
|
445
|
+
? { nextCursor: `text:${contentSha256}:${requestedStartLine}:${requestedEndLine}:${chunk.endOffset}` }
|
|
446
|
+
: {}),
|
|
447
|
+
}),
|
|
448
|
+
}),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
return exceptionFailure(error);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async searchPaths(input, context) {
|
|
456
|
+
const canceled = checkContext(context);
|
|
457
|
+
if (canceled !== undefined)
|
|
458
|
+
return canceled;
|
|
459
|
+
try {
|
|
460
|
+
if (input.query.length === 0 || input.query.length > 512) {
|
|
461
|
+
return failure("INVALID_INPUT", "Search query length must be between 1 and 512.");
|
|
462
|
+
}
|
|
463
|
+
const basePath = await this.scope.resolveExisting(input.path ?? ".");
|
|
464
|
+
const entries = await this.allEntries(input.path ?? ".", Number.POSITIVE_INFINITY, context);
|
|
465
|
+
const lowered = input.query.toLowerCase();
|
|
466
|
+
const pattern = input.mode === "glob" ? globPattern(input.query) : undefined;
|
|
467
|
+
const matches = entries
|
|
468
|
+
.filter((entry) => input.kind === undefined || entry.kind === input.kind)
|
|
469
|
+
.map((entry) => Object.freeze({
|
|
470
|
+
path: entry.path,
|
|
471
|
+
searchPath: relativeWorkspacePath(basePath, join(this.scope.root, entry.path)),
|
|
472
|
+
}))
|
|
473
|
+
.filter(({ searchPath }) => {
|
|
474
|
+
if (pattern !== undefined)
|
|
475
|
+
return pattern.test(searchPath);
|
|
476
|
+
if (input.mode === "fuzzy") {
|
|
477
|
+
let cursor = 0;
|
|
478
|
+
for (const character of searchPath.toLowerCase())
|
|
479
|
+
if (character === lowered[cursor])
|
|
480
|
+
cursor += 1;
|
|
481
|
+
return cursor === lowered.length;
|
|
482
|
+
}
|
|
483
|
+
return searchPath.toLowerCase().includes(lowered);
|
|
484
|
+
})
|
|
485
|
+
.map(({ path }) => path)
|
|
486
|
+
.sort();
|
|
487
|
+
const result = page(matches, input.cursor, input.limit);
|
|
488
|
+
return { ok: true, data: Object.freeze({ matches: result.values, pagination: result.pagination }) };
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
return exceptionFailure(error);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async searchText(input, context) {
|
|
495
|
+
const canceled = checkContext(context);
|
|
496
|
+
if (canceled !== undefined)
|
|
497
|
+
return canceled;
|
|
498
|
+
try {
|
|
499
|
+
if (input.query.length === 0 || input.query.length > 512)
|
|
500
|
+
return failure("INVALID_INPUT", "Search query length must be between 1 and 512.");
|
|
501
|
+
if (input.glob !== undefined && input.glob.length > 512) {
|
|
502
|
+
return failure("INVALID_INPUT", "Search glob must not exceed 512 characters.");
|
|
503
|
+
}
|
|
504
|
+
const caseSensitive = input.caseSensitive ?? true;
|
|
505
|
+
let matcher;
|
|
506
|
+
if (input.regex === true) {
|
|
507
|
+
let pattern;
|
|
508
|
+
try {
|
|
509
|
+
pattern = new RegExp(input.query, caseSensitive ? "u" : "iu");
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
return failure("INVALID_INPUT", "Search query is not a valid regular expression.");
|
|
513
|
+
}
|
|
514
|
+
matcher = (line) => pattern.exec(line)?.index ?? -1;
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
const query = caseSensitive ? input.query : input.query.toLocaleLowerCase("en-US");
|
|
518
|
+
matcher = caseSensitive
|
|
519
|
+
? (line) => line.indexOf(query)
|
|
520
|
+
: (line) => line.toLocaleLowerCase("en-US").indexOf(query);
|
|
521
|
+
}
|
|
522
|
+
const offset = decodeCursor(input.cursor);
|
|
523
|
+
const limit = boundedLimit(input.limit);
|
|
524
|
+
const basePath = await this.scope.resolveExisting(input.path ?? ".");
|
|
525
|
+
const entries = await this.allEntries(input.path ?? ".", Number.POSITIVE_INFINITY, context);
|
|
526
|
+
const pathPattern = input.glob === undefined ? undefined : globPattern(input.glob);
|
|
527
|
+
const matches = [];
|
|
528
|
+
let matchedCount = 0;
|
|
529
|
+
let hasMore = false;
|
|
530
|
+
search: for (const entry of entries) {
|
|
531
|
+
throwIfBlocked(context);
|
|
532
|
+
if (entry.kind !== "file")
|
|
533
|
+
continue;
|
|
534
|
+
const searchPath = relativeWorkspacePath(basePath, join(this.scope.root, entry.path));
|
|
535
|
+
if (pathPattern !== undefined && !pathPattern.test(searchPath))
|
|
536
|
+
continue;
|
|
537
|
+
const absolutePath = await this.scope.resolveExisting(entry.path);
|
|
538
|
+
const localMatches = [];
|
|
539
|
+
let contentHash;
|
|
540
|
+
try {
|
|
541
|
+
contentHash = await scanUtf8Lines(absolutePath, context, (line, lineNumber) => {
|
|
542
|
+
if (hasMore)
|
|
543
|
+
return;
|
|
544
|
+
const column = matcher(line);
|
|
545
|
+
if (column < 0)
|
|
546
|
+
return;
|
|
547
|
+
if (matchedCount < offset) {
|
|
548
|
+
matchedCount += 1;
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (matches.length + localMatches.length >= limit) {
|
|
552
|
+
hasMore = true;
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
localMatches.push(Object.freeze({
|
|
556
|
+
path: entry.path,
|
|
557
|
+
line: lineNumber,
|
|
558
|
+
column: column + 1,
|
|
559
|
+
preview: line.slice(0, 500),
|
|
560
|
+
}));
|
|
561
|
+
matchedCount += 1;
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
throwIfBlocked(context);
|
|
566
|
+
if (typeof error === "object" && error !== null && "name" in error && error.name === "AbortError")
|
|
567
|
+
throw error;
|
|
568
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code !== "INVALID_UTF8")
|
|
569
|
+
throw error;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
matches.push(...localMatches.map((match) => Object.freeze({ ...match, contentSha256: contentHash })));
|
|
573
|
+
if (hasMore)
|
|
574
|
+
break search;
|
|
575
|
+
}
|
|
576
|
+
if (!hasMore && matchedCount < offset) {
|
|
577
|
+
return failure("INVALID_INPUT", "Pagination cursor is outside the result set.");
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
ok: true,
|
|
581
|
+
data: Object.freeze({
|
|
582
|
+
matches: Object.freeze(matches),
|
|
583
|
+
pagination: Object.freeze({
|
|
584
|
+
hasMore,
|
|
585
|
+
...(hasMore ? { nextCursor: `offset:${offset + matches.length}` } : {}),
|
|
586
|
+
}),
|
|
587
|
+
}),
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
return exceptionFailure(error);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
async withTargetLock(target, context, operation) {
|
|
595
|
+
throwIfBlocked(context);
|
|
596
|
+
const lockPath = `${target}.galaxy-code.lock`;
|
|
597
|
+
const handle = await open(lockPath, "wx").catch((error) => {
|
|
598
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST") {
|
|
599
|
+
throw Object.assign(new Error("Another mutation is already in progress for this path."), { code: "UNAVAILABLE" });
|
|
600
|
+
}
|
|
601
|
+
throw error;
|
|
602
|
+
});
|
|
603
|
+
try {
|
|
604
|
+
throwIfBlocked(context);
|
|
605
|
+
return await operation();
|
|
606
|
+
}
|
|
607
|
+
finally {
|
|
608
|
+
await handle.close();
|
|
609
|
+
await unlink(lockPath).catch(() => undefined);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async atomicWrite(path, content, precondition, context) {
|
|
613
|
+
throwIfBlocked(context);
|
|
614
|
+
let resolvedPath;
|
|
615
|
+
let firstCreatedParent;
|
|
616
|
+
if (precondition.kind === "matches_sha256") {
|
|
617
|
+
// A hash-bound edit requires an existing target. Never create parent
|
|
618
|
+
// directories before proving that precondition.
|
|
619
|
+
try {
|
|
620
|
+
resolvedPath = await this.scope.resolveExisting(path);
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
624
|
+
throw Object.assign(new Error("The target does not exist."), { code: "PRECONDITION_FAILED" });
|
|
625
|
+
}
|
|
626
|
+
throw error;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
resolvedPath = await this.scope.resolveForWrite(path);
|
|
631
|
+
firstCreatedParent = await mkdir(dirname(resolvedPath), { recursive: true });
|
|
632
|
+
resolvedPath = await this.scope.resolveForWrite(path);
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
return await this.withTargetLock(resolvedPath, context, async () => {
|
|
636
|
+
const before = await currentText(resolvedPath, context);
|
|
637
|
+
const preconditionError = enforcePrecondition(before, precondition);
|
|
638
|
+
if (preconditionError !== undefined)
|
|
639
|
+
throw Object.assign(new Error(preconditionError.error.message), { code: preconditionError.error.code });
|
|
640
|
+
if (before === content) {
|
|
641
|
+
throw Object.assign(new Error("The requested write would not change the target content."), { code: "CONFLICT" });
|
|
642
|
+
}
|
|
643
|
+
const temporaryPath = join(dirname(resolvedPath), `.${basename(resolvedPath)}.${randomUUID()}.tmp`);
|
|
644
|
+
const existingInfo = await lstat(resolvedPath).catch((error) => {
|
|
645
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT")
|
|
646
|
+
return undefined;
|
|
647
|
+
throw error;
|
|
648
|
+
});
|
|
649
|
+
const mode = existingInfo === undefined ? 0o600 : existingInfo.mode & 0o777;
|
|
650
|
+
const handle = await open(temporaryPath, "wx", mode);
|
|
651
|
+
try {
|
|
652
|
+
await handle.writeFile(content, "utf8");
|
|
653
|
+
await handle.sync();
|
|
654
|
+
}
|
|
655
|
+
finally {
|
|
656
|
+
await handle.close();
|
|
657
|
+
}
|
|
658
|
+
try {
|
|
659
|
+
throwIfBlocked(context);
|
|
660
|
+
await this.scope.resolveForWrite(path);
|
|
661
|
+
if (precondition.kind === "matches_sha256") {
|
|
662
|
+
const latest = await currentText(resolvedPath, context);
|
|
663
|
+
if (latest === undefined || sha256Text(latest) !== precondition.contentSha256) {
|
|
664
|
+
throw Object.assign(new Error("The target changed while preparing the write."), { code: "PRECONDITION_FAILED" });
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (precondition.kind === "must_not_exist") {
|
|
668
|
+
await link(temporaryPath, resolvedPath);
|
|
669
|
+
await unlink(temporaryPath);
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
await rename(temporaryPath, resolvedPath);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
677
|
+
throw error;
|
|
678
|
+
}
|
|
679
|
+
return { ...(before === undefined ? {} : { before }), resolvedPath };
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
// Remove only directories created by this call, deepest first, and only
|
|
684
|
+
// while they remain empty. Never recursively remove concurrent content.
|
|
685
|
+
if (firstCreatedParent !== undefined) {
|
|
686
|
+
let candidate = dirname(resolvedPath);
|
|
687
|
+
while (true) {
|
|
688
|
+
const removed = await rmdir(candidate).then(() => true, () => false);
|
|
689
|
+
if (!removed || candidate === firstCreatedParent)
|
|
690
|
+
break;
|
|
691
|
+
candidate = dirname(candidate);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
throw error;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
async writeText(input, context) {
|
|
698
|
+
const canceled = checkContext(context);
|
|
699
|
+
if (canceled !== undefined)
|
|
700
|
+
return canceled;
|
|
701
|
+
try {
|
|
702
|
+
const result = await this.atomicWrite(input.path, input.content, input.precondition, context);
|
|
703
|
+
return {
|
|
704
|
+
ok: true,
|
|
705
|
+
data: Object.freeze({
|
|
706
|
+
path: input.path,
|
|
707
|
+
resolvedPath: result.resolvedPath,
|
|
708
|
+
...(result.before === undefined ? {} : { beforeContentSha256: sha256Text(result.before) }),
|
|
709
|
+
afterContentSha256: sha256Text(input.content),
|
|
710
|
+
length: input.content.length,
|
|
711
|
+
}),
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
return exceptionFailure(error);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async applyPatch(input, context) {
|
|
719
|
+
const canceled = checkContext(context);
|
|
720
|
+
if (canceled !== undefined)
|
|
721
|
+
return canceled;
|
|
722
|
+
if (input.oldText.length === 0)
|
|
723
|
+
return failure("INVALID_INPUT", "Patch oldText must not be empty.");
|
|
724
|
+
if (input.oldText === input.newText) {
|
|
725
|
+
return failure("INVALID_INPUT", "Patch newText must differ from oldText.");
|
|
726
|
+
}
|
|
727
|
+
try {
|
|
728
|
+
const absolutePath = await this.scope.resolveExisting(input.path);
|
|
729
|
+
const before = await readUtf8(absolutePath, context);
|
|
730
|
+
const preconditionError = enforcePrecondition(before, input.precondition);
|
|
731
|
+
if (preconditionError !== undefined)
|
|
732
|
+
return preconditionError;
|
|
733
|
+
const occurrences = before.split(input.oldText).length - 1;
|
|
734
|
+
if (occurrences === 0 || (occurrences > 1 && input.replaceAll !== true)) {
|
|
735
|
+
return failure("CONFLICT", occurrences > 1
|
|
736
|
+
? "Patch target is ambiguous. Extend oldText with more surrounding lines so it matches exactly one location."
|
|
737
|
+
: "Patch target was not found. Re-read the exact region and copy its bytes verbatim; common causes are CRLF line endings, invisible characters, Unicode normalization differences, or edited content.");
|
|
738
|
+
}
|
|
739
|
+
const after = input.replaceAll === true
|
|
740
|
+
? before.split(input.oldText).join(input.newText)
|
|
741
|
+
: before.replace(input.oldText, input.newText);
|
|
742
|
+
const writeResult = await this.atomicWrite(input.path, after, input.precondition, context);
|
|
743
|
+
return {
|
|
744
|
+
ok: true,
|
|
745
|
+
data: Object.freeze({
|
|
746
|
+
path: input.path,
|
|
747
|
+
resolvedPath: writeResult.resolvedPath,
|
|
748
|
+
beforeContentSha256: sha256Text(before),
|
|
749
|
+
afterContentSha256: sha256Text(after),
|
|
750
|
+
replacements: input.replaceAll === true ? occurrences : 1,
|
|
751
|
+
}),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
return exceptionFailure(error);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
async mkdir(input, context) {
|
|
759
|
+
const canceled = checkContext(context);
|
|
760
|
+
if (canceled !== undefined)
|
|
761
|
+
return canceled;
|
|
762
|
+
try {
|
|
763
|
+
const path = await this.scope.resolveForWrite(input.path);
|
|
764
|
+
throwIfBlocked(context);
|
|
765
|
+
let created;
|
|
766
|
+
if (input.recursive === true) {
|
|
767
|
+
created = await mkdir(path, { recursive: true }).then((createdPath) => createdPath !== undefined);
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
created = await mkdir(path).then(() => true, async (error) => {
|
|
771
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST") {
|
|
772
|
+
const existing = await lstat(path);
|
|
773
|
+
if (existing.isDirectory() && !existing.isSymbolicLink())
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
throw error;
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
await this.scope.resolveForWrite(input.path);
|
|
780
|
+
return { ok: true, data: Object.freeze({ created, path: input.path }) };
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
return exceptionFailure(error);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
async move(input, context) {
|
|
787
|
+
const canceled = checkContext(context);
|
|
788
|
+
if (canceled !== undefined)
|
|
789
|
+
return canceled;
|
|
790
|
+
try {
|
|
791
|
+
const source = await this.scope.resolveForDelete(input.sourcePath);
|
|
792
|
+
await lstat(source);
|
|
793
|
+
let destination = await this.scope.resolveForWrite(input.destinationPath);
|
|
794
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
795
|
+
destination = await this.scope.resolveForWrite(input.destinationPath);
|
|
796
|
+
if (input.overwrite !== true && await pathExists(destination))
|
|
797
|
+
return failure("ALREADY_EXISTS", "Destination exists.");
|
|
798
|
+
throwIfBlocked(context);
|
|
799
|
+
const verifiedSource = await this.scope.resolveForDelete(input.sourcePath);
|
|
800
|
+
const verifiedDestination = await this.scope.resolveForWrite(input.destinationPath);
|
|
801
|
+
await rename(verifiedSource, verifiedDestination);
|
|
802
|
+
return { ok: true, data: Object.freeze({ sourcePath: input.sourcePath, destinationPath: input.destinationPath }) };
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
return exceptionFailure(error);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
async copy(input, context) {
|
|
809
|
+
const canceled = checkContext(context);
|
|
810
|
+
if (canceled !== undefined)
|
|
811
|
+
return canceled;
|
|
812
|
+
try {
|
|
813
|
+
const source = await this.scope.resolveExisting(input.sourcePath);
|
|
814
|
+
const sourceInfo = await lstat(source);
|
|
815
|
+
if (!sourceInfo.isFile())
|
|
816
|
+
return failure("INVALID_INPUT", "copy requires a regular source file.");
|
|
817
|
+
let destination = await this.scope.resolveForWrite(input.destinationPath);
|
|
818
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
819
|
+
destination = await this.scope.resolveForWrite(input.destinationPath);
|
|
820
|
+
throwIfBlocked(context);
|
|
821
|
+
await copyFile(source, destination, input.overwrite === true ? 0 : fileSystemConstants.COPYFILE_EXCL);
|
|
822
|
+
return { ok: true, data: Object.freeze({ sourcePath: input.sourcePath, destinationPath: input.destinationPath }) };
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
return exceptionFailure(error);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
async delete(input, context) {
|
|
829
|
+
const canceled = checkContext(context);
|
|
830
|
+
if (canceled !== undefined)
|
|
831
|
+
return canceled;
|
|
832
|
+
try {
|
|
833
|
+
if (input.expectedContentSha256 !== undefined && !/^[a-f0-9]{64}$/.test(input.expectedContentSha256)) {
|
|
834
|
+
return failure("INVALID_INPUT", "Content hash preconditions must be lowercase SHA-256 values.");
|
|
835
|
+
}
|
|
836
|
+
const path = await this.scope.resolveForDelete(input.path);
|
|
837
|
+
const info = await lstat(path);
|
|
838
|
+
if (input.expectedContentSha256 !== undefined) {
|
|
839
|
+
if (!info.isFile() || info.isSymbolicLink()) {
|
|
840
|
+
return failure("INVALID_INPUT", "A content hash precondition requires a regular file.");
|
|
841
|
+
}
|
|
842
|
+
const content = await readFile(path, { signal: context.signal });
|
|
843
|
+
if (createHash("sha256").update(content).digest("hex") !== input.expectedContentSha256) {
|
|
844
|
+
return failure("PRECONDITION_FAILED", "Content hash mismatch.");
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
await this.scope.resolveForDelete(input.path);
|
|
848
|
+
throwIfBlocked(context);
|
|
849
|
+
await rm(path, { recursive: input.recursive ?? false, force: false });
|
|
850
|
+
return { ok: true, data: Object.freeze({ deleted: true, path: input.path }) };
|
|
851
|
+
}
|
|
852
|
+
catch (error) {
|
|
853
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
854
|
+
return { ok: true, data: Object.freeze({ deleted: false, path: input.path }) };
|
|
855
|
+
}
|
|
856
|
+
return exceptionFailure(error);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
//# sourceMappingURL=node-workspace-port.js.map
|