@codeworksh/plugin 0.0.1-dev.20260925150557 → 0.1.0-canary.1.1.gc0ff5572
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 +13 -5
- package/{event-BnLveFBB.d.mts → event-6sTQ_4bq.d.mts} +53 -49
- package/event.d.mts +1 -1
- package/filesystem-CuGE9poU.d.mts +268 -0
- package/filesystem-omtKo08x.mjs +381 -0
- package/filesystem-omtKo08x.mjs.map +1 -0
- package/ids.mjs +1 -1
- package/ids.mjs.map +1 -1
- package/index.d.mts +4 -3
- package/index.mjs +3 -2
- package/io-Dgh8U41k.mjs +188 -0
- package/io-Dgh8U41k.mjs.map +1 -0
- package/{io-BYs7hbiY.d.mts → io-nhvNKC33.d.mts} +51 -8
- package/{location-D4hAfuXF.d.mts → location-Q39els63.d.mts} +9 -14
- package/location.d.mts +2 -2
- package/location.mjs +2 -5
- package/location.mjs.map +1 -1
- package/package.json +8 -3
- package/plugin/prompt.d.mts +1 -1
- package/plugin/prompt.mjs.map +1 -1
- package/plugin/section.d.mts +27 -0
- package/plugin/section.mjs +65 -0
- package/plugin/section.mjs.map +1 -0
- package/plugin/tool.d.mts +1 -1
- package/{plugin-B1M3SjEb.d.mts → plugin-Deb2QWIP.d.mts} +27 -9
- package/{plugin-lorpq7Oi.mjs → plugin-XaYYRsX-.mjs} +3 -1
- package/{plugin-lorpq7Oi.mjs.map → plugin-XaYYRsX-.mjs.map} +1 -1
- package/plugin.d.mts +5 -4
- package/plugin.mjs +3 -2
- package/prompt-DWaA5ZXj.d.mts +21 -0
- package/sandbox/driver.d.mts +1 -1
- package/sandbox/driver.mjs +7 -3
- package/sandbox/driver.mjs.map +1 -1
- package/sandbox/errors.d.mts +1 -1
- package/sandbox/filesystem.d.mts +2 -2
- package/sandbox/filesystem.mjs +2 -142
- package/sandbox/instance.d.mts +1 -1
- package/sandbox/io.d.mts +2 -2
- package/sandbox/io.mjs +3 -127
- package/sandbox.d.mts +3 -3
- package/sandbox.mjs +8 -5
- package/sandbox.mjs.map +1 -1
- package/schema.d.mts +3 -1
- package/schema.mjs +4 -2
- package/schema.mjs.map +1 -1
- package/settings-47Mzxhl0.d.mts +587 -0
- package/settings.d.mts +2 -2
- package/settings.mjs +56 -5
- package/settings.mjs.map +1 -1
- package/tool/progress.d.mts +1 -1
- package/{tool-BoVqkYvL.d.mts → tool-D4_I_rzy.d.mts} +2 -2
- package/{tool-DuYBgiJf.d.mts → tool-_ajVYIY5.d.mts} +20 -1
- package/tool.d.mts +1 -1
- package/tool.mjs +2 -1
- package/tool.mjs.map +1 -1
- package/filesystem-Cp-KapCE.d.mts +0 -142
- package/prompt-BcpagjGh.d.mts +0 -10
- package/sandbox/filesystem.mjs.map +0 -1
- package/sandbox/io.mjs.map +0 -1
- package/settings-bbJi7FiQ.d.mts +0 -207
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { posix } from "./posix.mjs";
|
|
3
|
+
import { Context, Effect, Schema } from "effect";
|
|
4
|
+
//#region src/sandbox/scan.ts
|
|
5
|
+
var scan_exports = /* @__PURE__ */ __exportAll({
|
|
6
|
+
LineScanner: () => LineScanner,
|
|
7
|
+
parse: () => parse,
|
|
8
|
+
script: () => script,
|
|
9
|
+
scriptArgs: () => scriptArgs,
|
|
10
|
+
validate: () => validate,
|
|
11
|
+
viaShell: () => viaShell
|
|
12
|
+
});
|
|
13
|
+
/** A defect, not a file error: callers derive these from already-validated input. */
|
|
14
|
+
const validate = (options) => {
|
|
15
|
+
const { startLine, endLine, maxBytes } = options;
|
|
16
|
+
if (!Number.isSafeInteger(startLine) || startLine < 0) throw new RangeError(`Invalid startLine: ${startLine}`);
|
|
17
|
+
if (endLine !== void 0 && (!Number.isSafeInteger(endLine) || endLine <= startLine)) throw new RangeError(`Invalid endLine: ${endLine}`);
|
|
18
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError(`Invalid maxBytes: ${maxBytes}`);
|
|
19
|
+
};
|
|
20
|
+
const NEWLINE = 10;
|
|
21
|
+
const BOM = [
|
|
22
|
+
239,
|
|
23
|
+
187,
|
|
24
|
+
191
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Computes a {@link LineScan} from a file's bytes fed in order, in memory
|
|
28
|
+
* bounded by `maxBytes`. Feed at most the file size observed when the read
|
|
29
|
+
* started, so a file that keeps growing cannot keep the scan going.
|
|
30
|
+
*/
|
|
31
|
+
var LineScanner = class {
|
|
32
|
+
start;
|
|
33
|
+
end;
|
|
34
|
+
maxBytes;
|
|
35
|
+
decoder = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
36
|
+
/** The first bytes, held until it is known whether they are a byte-order mark. */
|
|
37
|
+
head = [];
|
|
38
|
+
totalBytes = 0;
|
|
39
|
+
line = 0;
|
|
40
|
+
lineLength = 0;
|
|
41
|
+
lineOpen = false;
|
|
42
|
+
/** The current line's bytes, kept only while it is selected and could still fit. */
|
|
43
|
+
pieces = [];
|
|
44
|
+
selected = [];
|
|
45
|
+
selectedBytes = 0;
|
|
46
|
+
firstLineBytes = 0;
|
|
47
|
+
full = false;
|
|
48
|
+
constructor(options) {
|
|
49
|
+
validate(options);
|
|
50
|
+
this.start = options.startLine;
|
|
51
|
+
this.end = options.endLine ?? Number.POSITIVE_INFINITY;
|
|
52
|
+
this.maxBytes = options.maxBytes;
|
|
53
|
+
}
|
|
54
|
+
push(chunk) {
|
|
55
|
+
this.totalBytes += chunk.length;
|
|
56
|
+
if (this.head !== void 0) {
|
|
57
|
+
const take = Math.min(BOM.length - this.head.length, chunk.length);
|
|
58
|
+
this.head.push(...chunk.subarray(0, take));
|
|
59
|
+
if (this.head.length < BOM.length) return;
|
|
60
|
+
this.releaseHead();
|
|
61
|
+
chunk = chunk.subarray(take);
|
|
62
|
+
}
|
|
63
|
+
this.scan(chunk);
|
|
64
|
+
}
|
|
65
|
+
finish() {
|
|
66
|
+
if (this.head !== void 0) this.releaseHead();
|
|
67
|
+
if (this.lineOpen) this.endLine();
|
|
68
|
+
return {
|
|
69
|
+
text: this.selected.map((line) => this.decoder.decode(line)).join("\n"),
|
|
70
|
+
lines: this.selected.length,
|
|
71
|
+
totalLines: this.line,
|
|
72
|
+
totalBytes: this.totalBytes,
|
|
73
|
+
firstLineBytes: this.firstLineBytes
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
releaseHead() {
|
|
77
|
+
const head = Uint8Array.from(this.head ?? []);
|
|
78
|
+
this.head = void 0;
|
|
79
|
+
if (!(head.length === BOM.length && BOM.every((byte, index) => head[index] === byte))) this.scan(head);
|
|
80
|
+
}
|
|
81
|
+
scan(chunk) {
|
|
82
|
+
let from = 0;
|
|
83
|
+
while (from < chunk.length) {
|
|
84
|
+
const newline = chunk.indexOf(NEWLINE, from);
|
|
85
|
+
const to = newline === -1 ? chunk.length : newline;
|
|
86
|
+
this.append(chunk.subarray(from, to));
|
|
87
|
+
if (newline === -1) return;
|
|
88
|
+
this.endLine();
|
|
89
|
+
from = newline + 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
isSelected() {
|
|
93
|
+
return !this.full && this.line >= this.start && this.line < this.end;
|
|
94
|
+
}
|
|
95
|
+
append(bytes) {
|
|
96
|
+
if (bytes.length === 0) {
|
|
97
|
+
this.lineOpen = true;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.lineOpen = true;
|
|
101
|
+
if (this.isSelected() && this.lineLength + bytes.length <= this.maxBytes) this.pieces.push(new Uint8Array(bytes));
|
|
102
|
+
this.lineLength += bytes.length;
|
|
103
|
+
}
|
|
104
|
+
endLine() {
|
|
105
|
+
if (this.isSelected()) {
|
|
106
|
+
const first = this.selected.length === 0;
|
|
107
|
+
if (first) this.firstLineBytes = this.lineLength;
|
|
108
|
+
const added = first ? this.lineLength : this.lineLength + 1;
|
|
109
|
+
if (this.selectedBytes + added > this.maxBytes) this.full = true;
|
|
110
|
+
else {
|
|
111
|
+
this.selected.push(concat(this.pieces, this.lineLength));
|
|
112
|
+
this.selectedBytes += added;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
this.pieces = [];
|
|
116
|
+
this.lineLength = 0;
|
|
117
|
+
this.lineOpen = false;
|
|
118
|
+
this.line++;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const concat = (pieces, length) => {
|
|
122
|
+
if (pieces.length === 1) return pieces[0];
|
|
123
|
+
const out = new Uint8Array(length);
|
|
124
|
+
let offset = 0;
|
|
125
|
+
for (const piece of pieces) {
|
|
126
|
+
out.set(piece, offset);
|
|
127
|
+
offset += piece.length;
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* {@link LineScanner} as a POSIX shell script, for backends that only expose a
|
|
133
|
+
* process API. Run as `sh -c <script> _ <path> <startLine> <endLine|-1> <maxBytes>`.
|
|
134
|
+
* The whole file is read inside the sandbox; only the window is printed, after a
|
|
135
|
+
* header line `<lines> <totalLines> <totalBytes> <firstLineBytes>` and before a
|
|
136
|
+
* closing `|` — see {@link parse}.
|
|
137
|
+
* Exits 2 when the path does not exist.
|
|
138
|
+
*/
|
|
139
|
+
const script = [
|
|
140
|
+
"[ -e \"$1\" ] || exit 2",
|
|
141
|
+
"size=$(($(wc -c < \"$1\"))) || exit 1",
|
|
142
|
+
"head -c \"$size\" -- \"$1\" | LC_ALL=C awk -v s=\"$2\" -v e=\"$3\" -v m=\"$4\" -v size=\"$size\" '",
|
|
143
|
+
"NR == 1 && $0 == \"\\357\\273\\277\" && size == 3 { exit }",
|
|
144
|
+
"NR == 1 && substr($0, 1, 3) == \"\\357\\273\\277\" { $0 = substr($0, 4) }",
|
|
145
|
+
"{ total = NR }",
|
|
146
|
+
"NR > s && (e < 0 || NR <= e) && !full {",
|
|
147
|
+
" len = length($0)",
|
|
148
|
+
" added = lines == 0 ? len : len + 1",
|
|
149
|
+
" if (lines == 0 && !seen) { seen = 1; first = len }",
|
|
150
|
+
" if (bytes + added > m) { full = 1; next }",
|
|
151
|
+
" out = lines == 0 ? $0 : out \"\\n\" $0",
|
|
152
|
+
" bytes += added; lines++",
|
|
153
|
+
"}",
|
|
154
|
+
"END { printf \"%.0f %.0f %.0f %.0f\\n%s|\", lines, total, size, first, out }",
|
|
155
|
+
"'"
|
|
156
|
+
].join("\n");
|
|
157
|
+
/** Arguments for {@link script} after the path. */
|
|
158
|
+
const scriptArgs = (options) => {
|
|
159
|
+
validate(options);
|
|
160
|
+
return [
|
|
161
|
+
String(options.startLine),
|
|
162
|
+
String(options.endLine ?? -1),
|
|
163
|
+
String(options.maxBytes)
|
|
164
|
+
];
|
|
165
|
+
};
|
|
166
|
+
/** Read {@link script}'s output. */
|
|
167
|
+
const parse = (stdout) => {
|
|
168
|
+
const body = stdout.trimEnd();
|
|
169
|
+
const newline = body.indexOf("\n");
|
|
170
|
+
const header = (newline === -1 ? "" : body.slice(0, newline)).trim().split(/\s+/).map(Number);
|
|
171
|
+
const [lines, totalLines, totalBytes, firstLineBytes] = header;
|
|
172
|
+
if (!body.endsWith("|") || header.length !== 4 || header.some((value) => !Number.isSafeInteger(value))) throw new Error(`Unexpected scanLines output: ${stdout.slice(0, 200)}`);
|
|
173
|
+
return {
|
|
174
|
+
text: body.slice(newline + 1, -1),
|
|
175
|
+
lines,
|
|
176
|
+
totalLines,
|
|
177
|
+
totalBytes,
|
|
178
|
+
firstLineBytes
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* `Provider.scanLines` for a backend that runs an argument vector and collects
|
|
183
|
+
* its output: runs {@link script} in the sandbox and reads the result. A missing
|
|
184
|
+
* path rejects with `ENOENT`, the shape `isNotFoundError` recognises.
|
|
185
|
+
*/
|
|
186
|
+
const viaShell = (run) => async (path, options) => {
|
|
187
|
+
const result = await run([
|
|
188
|
+
"sh",
|
|
189
|
+
"-c",
|
|
190
|
+
script,
|
|
191
|
+
"_",
|
|
192
|
+
path,
|
|
193
|
+
...scriptArgs(options)
|
|
194
|
+
]);
|
|
195
|
+
if (result.exitCode === 2) throw Object.assign(/* @__PURE__ */ new Error(`ENOENT: no such file or directory, scanLines '${path}'`), { code: "ENOENT" });
|
|
196
|
+
if (result.exitCode !== 0) throw new Error(`scanLines failed (exit ${result.exitCode}): ${(result.stderr ?? result.stdout).trim()}`);
|
|
197
|
+
return parse(result.stdout);
|
|
198
|
+
};
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/sandbox/filesystem.ts
|
|
201
|
+
var filesystem_exports = /* @__PURE__ */ __exportAll({
|
|
202
|
+
FileSystemError: () => FileSystemError,
|
|
203
|
+
OperationUnsupportedError: () => OperationUnsupportedError,
|
|
204
|
+
SandboxFileSystem: () => filesystem_exports,
|
|
205
|
+
Scan: () => scan_exports,
|
|
206
|
+
Service: () => Service,
|
|
207
|
+
fromProvider: () => fromProvider,
|
|
208
|
+
isNotFoundError: () => isNotFoundError,
|
|
209
|
+
readBytesScript: () => readBytesScript,
|
|
210
|
+
readBytesViaShell: () => readBytesViaShell,
|
|
211
|
+
realpathScripts: () => realpathScripts,
|
|
212
|
+
validateRmOptions: () => validateRmOptions,
|
|
213
|
+
withCwd: () => withCwd
|
|
214
|
+
});
|
|
215
|
+
/**
|
|
216
|
+
* The runtime filesystem contract, independent of any backend.
|
|
217
|
+
*
|
|
218
|
+
* Two surfaces, deliberately:
|
|
219
|
+
* - {@link Provider} is what a backend implements — plain promises that reject,
|
|
220
|
+
* matching every SDK we wrap.
|
|
221
|
+
* - {@link Interface} is what the harness consumes — Effect with a typed error
|
|
222
|
+
* channel and tracing spans. {@link fromProvider} bridges the two exactly
|
|
223
|
+
* once, so no consumer ever writes `Effect.tryPromise` against a filesystem.
|
|
224
|
+
*
|
|
225
|
+
* Implementations:
|
|
226
|
+
* - Local:
|
|
227
|
+
* implement it over a VFS (`./local`);
|
|
228
|
+
* local filesystem use OS primitives; hence have broader filesytem capabilities.
|
|
229
|
+
*
|
|
230
|
+
* - Remote:
|
|
231
|
+
* implement it over a provider (`./remote`).
|
|
232
|
+
* remote filesytems depends on the interface provided by the remote provider; hence can have limited filesytem capabilities.
|
|
233
|
+
*
|
|
234
|
+
* `isFile`/`isDirectory` are required booleans; size, mtime, and isSymbolicLink are omitted when the
|
|
235
|
+
* backend cannot report them — never fabricated.
|
|
236
|
+
*
|
|
237
|
+
* **Paths are POSIX, and the harness is Unix-only (for now!).** Every path uses `/`
|
|
238
|
+
* separators on both the host and inside a sandbox — Windows is not supported,
|
|
239
|
+
* so no translation layer exists. Consumers must use the shared Effect POSIX
|
|
240
|
+
* path implementation, never the platform default, or a host running the harness would
|
|
241
|
+
* impose its own flavour on a remote sandbox's paths. Relative paths resolve
|
|
242
|
+
* against the backend's configured `cwd`.
|
|
243
|
+
*/
|
|
244
|
+
var OperationUnsupportedError = class extends Schema.TaggedError()("OperationUnsupportedError", {
|
|
245
|
+
operation: Schema.String,
|
|
246
|
+
message: Schema.String
|
|
247
|
+
}) {};
|
|
248
|
+
/** A backend operation failed. `cause` carries the provider's own rejection. */
|
|
249
|
+
var FileSystemError = class extends Schema.TaggedError()("SandboxFileSystemError", {
|
|
250
|
+
method: Schema.String,
|
|
251
|
+
path: Schema.String,
|
|
252
|
+
cause: Schema.optional(Schema.Defect())
|
|
253
|
+
}) {};
|
|
254
|
+
/** Whether a provider failure means the path itself is definitively absent. */
|
|
255
|
+
const isNotFoundError = (cause) => {
|
|
256
|
+
const code = cause?.code;
|
|
257
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* Shell forms of `realpath` for backends that only expose a process API. Run as
|
|
261
|
+
* `sh -c <script> _ <path>`, trying each in turn. `realpath -e` (coreutils)
|
|
262
|
+
* resolves every component, a symlinked file included, and fails on a missing
|
|
263
|
+
* path. Where it is absent, `pwd -P` is the portable primitive: directories
|
|
264
|
+
* resolve directly, anything else resolves its parent and keeps the final name
|
|
265
|
+
* as given — so a symlinked *file* stays unresolved there.
|
|
266
|
+
*/
|
|
267
|
+
const realpathScripts = [
|
|
268
|
+
"realpath -e -- \"$1\"",
|
|
269
|
+
"cd -- \"$1\" && pwd -P",
|
|
270
|
+
"cd -- \"$(dirname -- \"$1\")\" && printf '%s/%s' \"$(pwd -P)\" \"$(basename -- \"$1\")\""
|
|
271
|
+
];
|
|
272
|
+
/**
|
|
273
|
+
* Shell form of `readBytes` for backends that only expose a process API. Run as
|
|
274
|
+
* `sh -c <script> _ <path> <offset + 1> <length>`; prints the bytes as base64,
|
|
275
|
+
* so binary content survives a transport that decodes stdout as text. Exits 2
|
|
276
|
+
* when the path does not exist, 3 when it cannot be opened.
|
|
277
|
+
*/
|
|
278
|
+
const readBytesScript = [
|
|
279
|
+
"[ -e \"$1\" ] || exit 2",
|
|
280
|
+
"{ : < \"$1\"; } 2>/dev/null || exit 3",
|
|
281
|
+
"tail -c +\"$2\" -- \"$1\" | head -c \"$3\" | base64"
|
|
282
|
+
].join("\n");
|
|
283
|
+
/**
|
|
284
|
+
* `Provider.readBytes` for a backend that runs an argument vector and collects
|
|
285
|
+
* its output. A missing path rejects with `ENOENT`, the shape `isNotFoundError` recognises.
|
|
286
|
+
*/
|
|
287
|
+
const readBytesViaShell = (run) => async (path, offset, length) => {
|
|
288
|
+
const result = await run([
|
|
289
|
+
"sh",
|
|
290
|
+
"-c",
|
|
291
|
+
readBytesScript,
|
|
292
|
+
"_",
|
|
293
|
+
path,
|
|
294
|
+
String(offset + 1),
|
|
295
|
+
String(length)
|
|
296
|
+
]);
|
|
297
|
+
if (result.exitCode === 2) throw Object.assign(/* @__PURE__ */ new Error(`ENOENT: no such file or directory, readBytes '${path}'`), { code: "ENOENT" });
|
|
298
|
+
if (result.exitCode === 3) throw new Error(`readBytes: cannot open '${path}'`);
|
|
299
|
+
if (result.exitCode !== 0) throw new Error(`readBytes failed (exit ${result.exitCode}): ${(result.stderr ?? result.stdout).trim()}`);
|
|
300
|
+
return new Uint8Array(Buffer.from(result.stdout.replace(/\s/g, ""), "base64"));
|
|
301
|
+
};
|
|
302
|
+
/** The runtime filesystem service — the live {@link Interface} for the active sandbox. */
|
|
303
|
+
var Service = class extends Context.Service()("@codeworksh/plugin/sandbox/filesystem/Service") {};
|
|
304
|
+
/**
|
|
305
|
+
* Reject `rm` options a provider does not implement, before any mutation. Only
|
|
306
|
+
* `recursive` and `force` are part of the contract; anything else is refused
|
|
307
|
+
* loudly rather than silently ignored.
|
|
308
|
+
*/
|
|
309
|
+
const validateRmOptions = (options, operation = "rm") => Effect.suspend(() => {
|
|
310
|
+
for (const option of Object.keys(options ?? {})) {
|
|
311
|
+
if (option === "recursive" || option === "force") continue;
|
|
312
|
+
return Effect.fail(new OperationUnsupportedError({
|
|
313
|
+
operation,
|
|
314
|
+
message: `Unsupported rm option: ${option}`
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
return Effect.void;
|
|
318
|
+
});
|
|
319
|
+
/**
|
|
320
|
+
* Lift a {@link Provider} into the runtime {@link Interface}: one place that
|
|
321
|
+
* converts rejections into {@link FileSystemError}, validates `rm` options,
|
|
322
|
+
* creates missing parents on write, and names a tracing span per operation.
|
|
323
|
+
*/
|
|
324
|
+
const fromProvider = (provider) => {
|
|
325
|
+
const attempt = (method, path, run) => Effect.tryPromise({
|
|
326
|
+
try: run,
|
|
327
|
+
catch: (cause) => new FileSystemError({
|
|
328
|
+
method,
|
|
329
|
+
path,
|
|
330
|
+
cause
|
|
331
|
+
})
|
|
332
|
+
});
|
|
333
|
+
const writeCreatingParents = (path, content) => {
|
|
334
|
+
const write = attempt("writeFile", path, () => provider.writeFile(path, content));
|
|
335
|
+
const parent = posix.dirname(path);
|
|
336
|
+
return write.pipe(Effect.catch(() => attempt("mkdir", parent, () => provider.mkdir(parent, { recursive: true })).pipe(Effect.ignore, Effect.andThen(write))));
|
|
337
|
+
};
|
|
338
|
+
return {
|
|
339
|
+
readFile: Effect.fn("SandboxFileSystem.readFile")((path) => attempt("readFile", path, () => provider.readFile(path))),
|
|
340
|
+
readFileBuffer: Effect.fn("SandboxFileSystem.readFileBuffer")((path) => attempt("readFileBuffer", path, () => provider.readFileBuffer(path))),
|
|
341
|
+
readBytes: Effect.fn("SandboxFileSystem.readBytes")((path, offset, length) => attempt("readBytes", path, () => provider.readBytes(path, offset, length))),
|
|
342
|
+
writeFile: Effect.fn("SandboxFileSystem.writeFile")((path, content) => Effect.uninterruptible(writeCreatingParents(path, content))),
|
|
343
|
+
stat: Effect.fn("SandboxFileSystem.stat")((path) => attempt("stat", path, () => provider.stat(path))),
|
|
344
|
+
...provider.lstat === void 0 ? {} : { lstat: Effect.fn("SandboxFileSystem.lstat")((path) => attempt("lstat", path, () => provider.lstat(path))) },
|
|
345
|
+
readdir: Effect.fn("SandboxFileSystem.readdir")((path) => attempt("readdir", path, () => provider.readdir(path))),
|
|
346
|
+
exists: Effect.fn("SandboxFileSystem.exists")((path) => attempt("exists", path, () => provider.exists(path))),
|
|
347
|
+
mkdir: Effect.fn("SandboxFileSystem.mkdir")((path, options) => attempt("mkdir", path, () => provider.mkdir(path, options))),
|
|
348
|
+
rm: Effect.fn("SandboxFileSystem.rm")((path, options) => validateRmOptions(options).pipe(Effect.andThen(attempt("rm", path, () => provider.rm(path, options))))),
|
|
349
|
+
realpath: Effect.fn("SandboxFileSystem.realpath")((path) => attempt("realpath", path, () => provider.realpath(path))),
|
|
350
|
+
scanLines: Effect.fn("SandboxFileSystem.scanLines")((path, options) => Effect.sync(() => validate(options)).pipe(Effect.andThen(attempt("scanLines", path, () => provider.scanLines(path, options)))))
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
/**
|
|
354
|
+
* Bind a cwd-neutral filesystem to one mount's working directory.
|
|
355
|
+
*
|
|
356
|
+
* The counterpart of `Shell.withCwd`, and the reason relative paths mean the
|
|
357
|
+
* same thing to both: a shared transport stays rooted at the namespace root, so
|
|
358
|
+
* resolution happens here, per mount, rather than inside a VFS whose `chdir` is
|
|
359
|
+
* global state two mounts would fight over.
|
|
360
|
+
*/
|
|
361
|
+
const withCwd = (fs, cwd) => {
|
|
362
|
+
const at = (path) => posix.resolve(cwd, path);
|
|
363
|
+
return {
|
|
364
|
+
readFile: (path) => fs.readFile(at(path)),
|
|
365
|
+
readFileBuffer: (path) => fs.readFileBuffer(at(path)),
|
|
366
|
+
readBytes: (path, offset, length) => fs.readBytes(at(path), offset, length),
|
|
367
|
+
writeFile: (path, content) => fs.writeFile(at(path), content),
|
|
368
|
+
stat: (path) => fs.stat(at(path)),
|
|
369
|
+
readdir: (path) => fs.readdir(at(path)),
|
|
370
|
+
exists: (path) => fs.exists(at(path)),
|
|
371
|
+
mkdir: (path, options) => fs.mkdir(at(path), options),
|
|
372
|
+
rm: (path, options) => fs.rm(at(path), options),
|
|
373
|
+
realpath: (path) => fs.realpath(at(path)),
|
|
374
|
+
scanLines: (path, options) => fs.scanLines(at(path), options),
|
|
375
|
+
...fs.lstat === void 0 ? {} : { lstat: (path) => fs.lstat(at(path)) }
|
|
376
|
+
};
|
|
377
|
+
};
|
|
378
|
+
//#endregion
|
|
379
|
+
export { fromProvider as a, readBytesViaShell as c, withCwd as d, scan_exports as f, filesystem_exports as i, realpathScripts as l, OperationUnsupportedError as n, isNotFoundError as o, Service as r, readBytesScript as s, FileSystemError as t, validateRmOptions as u };
|
|
380
|
+
|
|
381
|
+
//# sourceMappingURL=filesystem-omtKo08x.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filesystem-omtKo08x.mjs","names":["validateScan"],"sources":["../../src/sandbox/scan.ts","../../src/sandbox/filesystem.ts"],"sourcesContent":["/**\n * Bounded line reads: one pass over a file that returns a window of its lines\n * without ever holding more than that window.\n *\n * Lines are the file split on `\\n`, where a trailing newline ends the last line\n * rather than starting an empty one: `\"\"` has 0 lines, `\"a\"` and `\"a\\n\"` have 1,\n * `\"a\\n\\n\"` has 2. A UTF-8 byte-order mark at the start of the file is not part\n * of the first line. Line lengths are in bytes.\n *\n * Two implementations share these rules, and a backend uses whichever fits:\n * {@link LineScanner} over a byte stream, or {@link script} run inside the\n * sandbox so only the window crosses the network.\n */\n\nexport interface LineScanOptions {\n\t/** First selected line, 0-based. */\n\treadonly startLine: number;\n\t/** End of the selection, exclusive; absent selects to the end of the file. */\n\treadonly endLine?: number;\n\t/** Most UTF-8 bytes of selected text to return; selection stops at the last whole line that fits. */\n\treadonly maxBytes: number;\n}\n\nexport interface LineScan {\n\t/**\n\t * The selected whole lines that fit `maxBytes`, joined with `\\n`. Invalid\n\t * UTF-8 becomes U+FFFD; how many per bad sequence depends on the decoder of\n\t * the backend's transport.\n\t */\n\treadonly text: string;\n\t/** Lines in {@link text}. */\n\treadonly lines: number;\n\t/** Lines in the whole file. */\n\treadonly totalLines: number;\n\t/** Bytes in the whole file, byte-order mark included. */\n\treadonly totalBytes: number;\n\t/**\n\t * Bytes of the first selected line, 0 when nothing is selected. Larger than\n\t * `maxBytes` exactly when that line alone does not fit, and `text` is empty.\n\t */\n\treadonly firstLineBytes: number;\n}\n\n/** A defect, not a file error: callers derive these from already-validated input. */\nexport const validate = (options: LineScanOptions): void => {\n\tconst { startLine, endLine, maxBytes } = options;\n\tif (!Number.isSafeInteger(startLine) || startLine < 0) throw new RangeError(`Invalid startLine: ${startLine}`);\n\tif (endLine !== undefined && (!Number.isSafeInteger(endLine) || endLine <= startLine))\n\t\tthrow new RangeError(`Invalid endLine: ${endLine}`);\n\tif (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError(`Invalid maxBytes: ${maxBytes}`);\n};\n\nconst NEWLINE = 0x0a;\nconst BOM = [0xef, 0xbb, 0xbf] as const;\n\n/**\n * Computes a {@link LineScan} from a file's bytes fed in order, in memory\n * bounded by `maxBytes`. Feed at most the file size observed when the read\n * started, so a file that keeps growing cannot keep the scan going.\n */\nexport class LineScanner {\n\tprivate readonly start: number;\n\tprivate readonly end: number;\n\tprivate readonly maxBytes: number;\n\t// Only a mark at the very start of the file is dropped, and that happens on\n\t// the bytes; one starting a later line is text.\n\tprivate readonly decoder = new TextDecoder(\"utf-8\", { ignoreBOM: true });\n\n\t/** The first bytes, held until it is known whether they are a byte-order mark. */\n\tprivate head: Array<number> | undefined = [];\n\tprivate totalBytes = 0;\n\tprivate line = 0;\n\tprivate lineLength = 0;\n\tprivate lineOpen = false;\n\t/** The current line's bytes, kept only while it is selected and could still fit. */\n\tprivate pieces: Array<Uint8Array> = [];\n\tprivate selected: Array<Uint8Array> = [];\n\tprivate selectedBytes = 0;\n\tprivate firstLineBytes = 0;\n\tprivate full = false;\n\n\tconstructor(options: LineScanOptions) {\n\t\tvalidate(options);\n\t\tthis.start = options.startLine;\n\t\tthis.end = options.endLine ?? Number.POSITIVE_INFINITY;\n\t\tthis.maxBytes = options.maxBytes;\n\t}\n\n\tpush(chunk: Uint8Array): void {\n\t\tthis.totalBytes += chunk.length;\n\t\tif (this.head !== undefined) {\n\t\t\tconst take = Math.min(BOM.length - this.head.length, chunk.length);\n\t\t\tthis.head.push(...chunk.subarray(0, take));\n\t\t\tif (this.head.length < BOM.length) return;\n\t\t\tthis.releaseHead();\n\t\t\tchunk = chunk.subarray(take);\n\t\t}\n\t\tthis.scan(chunk);\n\t}\n\n\tfinish(): LineScan {\n\t\tif (this.head !== undefined) this.releaseHead();\n\t\tif (this.lineOpen) this.endLine();\n\t\tconst text = this.selected.map((line) => this.decoder.decode(line)).join(\"\\n\");\n\t\treturn {\n\t\t\ttext,\n\t\t\tlines: this.selected.length,\n\t\t\ttotalLines: this.line,\n\t\t\ttotalBytes: this.totalBytes,\n\t\t\tfirstLineBytes: this.firstLineBytes,\n\t\t};\n\t}\n\n\tprivate releaseHead(): void {\n\t\tconst head = Uint8Array.from(this.head ?? []);\n\t\tthis.head = undefined;\n\t\tconst bom = head.length === BOM.length && BOM.every((byte, index) => head[index] === byte);\n\t\tif (!bom) this.scan(head);\n\t}\n\n\tprivate scan(chunk: Uint8Array): void {\n\t\tlet from = 0;\n\t\twhile (from < chunk.length) {\n\t\t\tconst newline = chunk.indexOf(NEWLINE, from);\n\t\t\tconst to = newline === -1 ? chunk.length : newline;\n\t\t\tthis.append(chunk.subarray(from, to));\n\t\t\tif (newline === -1) return;\n\t\t\tthis.endLine();\n\t\t\tfrom = newline + 1;\n\t\t}\n\t}\n\n\tprivate isSelected(): boolean {\n\t\treturn !this.full && this.line >= this.start && this.line < this.end;\n\t}\n\n\tprivate append(bytes: Uint8Array): void {\n\t\tif (bytes.length === 0) {\n\t\t\tthis.lineOpen = true;\n\t\t\treturn;\n\t\t}\n\t\tthis.lineOpen = true;\n\t\t// Past `maxBytes` the line cannot fit, so its bytes are only counted. Copied,\n\t\t// not sliced: a Node `Buffer` slice is a view the reader may overwrite.\n\t\tif (this.isSelected() && this.lineLength + bytes.length <= this.maxBytes) this.pieces.push(new Uint8Array(bytes));\n\t\tthis.lineLength += bytes.length;\n\t}\n\n\tprivate endLine(): void {\n\t\tif (this.isSelected()) {\n\t\t\tconst first = this.selected.length === 0;\n\t\t\tif (first) this.firstLineBytes = this.lineLength;\n\t\t\tconst added = first ? this.lineLength : this.lineLength + 1;\n\t\t\tif (this.selectedBytes + added > this.maxBytes) this.full = true;\n\t\t\telse {\n\t\t\t\tthis.selected.push(concat(this.pieces, this.lineLength));\n\t\t\t\tthis.selectedBytes += added;\n\t\t\t}\n\t\t}\n\t\tthis.pieces = [];\n\t\tthis.lineLength = 0;\n\t\tthis.lineOpen = false;\n\t\tthis.line++;\n\t}\n}\n\nconst concat = (pieces: ReadonlyArray<Uint8Array>, length: number): Uint8Array => {\n\tif (pieces.length === 1) return pieces[0]!;\n\tconst out = new Uint8Array(length);\n\tlet offset = 0;\n\tfor (const piece of pieces) {\n\t\tout.set(piece, offset);\n\t\toffset += piece.length;\n\t}\n\treturn out;\n};\n\n/**\n * {@link LineScanner} as a POSIX shell script, for backends that only expose a\n * process API. Run as `sh -c <script> _ <path> <startLine> <endLine|-1> <maxBytes>`.\n * The whole file is read inside the sandbox; only the window is printed, after a\n * header line `<lines> <totalLines> <totalBytes> <firstLineBytes>` and before a\n * closing `|` — see {@link parse}.\n * Exits 2 when the path does not exist.\n */\nexport const script = [\n\t'[ -e \"$1\" ] || exit 2',\n\t// Arithmetic expansion drops the padding some `wc` builds print.\n\t'size=$(($(wc -c < \"$1\"))) || exit 1',\n\t// `C` makes awk's `length` count bytes; `head -c` bounds a file that is still growing.\n\t'head -c \"$size\" -- \"$1\" | LC_ALL=C awk -v s=\"$2\" -v e=\"$3\" -v m=\"$4\" -v size=\"$size\" \\'',\n\t// A file that is only a byte-order mark has no lines; anything after one keeps its own.\n\t'NR == 1 && $0 == \"\\\\357\\\\273\\\\277\" && size == 3 { exit }',\n\t'NR == 1 && substr($0, 1, 3) == \"\\\\357\\\\273\\\\277\" { $0 = substr($0, 4) }',\n\t\"{ total = NR }\",\n\t\"NR > s && (e < 0 || NR <= e) && !full {\",\n\t\" len = length($0)\",\n\t\" added = lines == 0 ? len : len + 1\",\n\t\" if (lines == 0 && !seen) { seen = 1; first = len }\",\n\t\" if (bytes + added > m) { full = 1; next }\",\n\t' out = lines == 0 ? $0 : out \"\\\\n\" $0',\n\t\" bytes += added; lines++\",\n\t\"}\",\n\t// The trailing `|` survives a transport that trims trailing whitespace off stdout.\n\t// `%.0f`, not `%d`: some mawk builds clamp `%d` to a 32-bit integer.\n\t'END { printf \"%.0f %.0f %.0f %.0f\\\\n%s|\", lines, total, size, first, out }',\n\t\"'\",\n].join(\"\\n\");\n\n/** Arguments for {@link script} after the path. */\nexport const scriptArgs = (options: LineScanOptions): Array<string> => {\n\tvalidate(options);\n\treturn [String(options.startLine), String(options.endLine ?? -1), String(options.maxBytes)];\n};\n\n/** Read {@link script}'s output. */\nexport const parse = (stdout: string): LineScan => {\n\tconst body = stdout.trimEnd();\n\tconst newline = body.indexOf(\"\\n\");\n\tconst header = (newline === -1 ? \"\" : body.slice(0, newline)).trim().split(/\\s+/).map(Number);\n\tconst [lines, totalLines, totalBytes, firstLineBytes] = header;\n\tif (!body.endsWith(\"|\") || header.length !== 4 || header.some((value) => !Number.isSafeInteger(value)))\n\t\tthrow new Error(`Unexpected scanLines output: ${stdout.slice(0, 200)}`);\n\treturn {\n\t\ttext: body.slice(newline + 1, -1),\n\t\tlines: lines!,\n\t\ttotalLines: totalLines!,\n\t\ttotalBytes: totalBytes!,\n\t\tfirstLineBytes: firstLineBytes!,\n\t};\n};\n\nexport interface ShellRun {\n\treadonly exitCode: number;\n\treadonly stdout: string;\n\treadonly stderr?: string;\n}\n\n/**\n * `Provider.scanLines` for a backend that runs an argument vector and collects\n * its output: runs {@link script} in the sandbox and reads the result. A missing\n * path rejects with `ENOENT`, the shape `isNotFoundError` recognises.\n */\nexport const viaShell =\n\t(run: (argv: ReadonlyArray<string>) => Promise<ShellRun>) =>\n\t// oxlint-disable-next-line effecttsgo/async-function -- the Provider contract is Promise-based\n\tasync (path: string, options: LineScanOptions): Promise<LineScan> => {\n\t\tconst result = await run([\"sh\", \"-c\", script, \"_\", path, ...scriptArgs(options)]);\n\t\tif (result.exitCode === 2)\n\t\t\tthrow Object.assign(new Error(`ENOENT: no such file or directory, scanLines '${path}'`), { code: \"ENOENT\" });\n\t\tif (result.exitCode !== 0)\n\t\t\tthrow new Error(`scanLines failed (exit ${result.exitCode}): ${(result.stderr ?? result.stdout).trim()}`);\n\t\treturn parse(result.stdout);\n\t};\n","import { Context, Effect, Schema } from \"effect\";\nimport { posix } from \"../posix.ts\";\nimport { type LineScan, type LineScanOptions, type ShellRun, validate as validateScan } from \"./scan.ts\";\n\nexport type { LineScan, LineScanOptions } from \"./scan.ts\";\n/** The shared line-scan rules: `LineScanner` for byte streams, `script` for shell-only backends. */\nexport * as Scan from \"./scan.ts\";\n\n/**\n * The runtime filesystem contract, independent of any backend.\n *\n * Two surfaces, deliberately:\n * - {@link Provider} is what a backend implements — plain promises that reject,\n * matching every SDK we wrap.\n * - {@link Interface} is what the harness consumes — Effect with a typed error\n * channel and tracing spans. {@link fromProvider} bridges the two exactly\n * once, so no consumer ever writes `Effect.tryPromise` against a filesystem.\n *\n * Implementations:\n * - Local:\n * implement it over a VFS (`./local`);\n * local filesystem use OS primitives; hence have broader filesytem capabilities.\n *\n * - Remote:\n * implement it over a provider (`./remote`).\n * remote filesytems depends on the interface provided by the remote provider; hence can have limited filesytem capabilities.\n *\n * `isFile`/`isDirectory` are required booleans; size, mtime, and isSymbolicLink are omitted when the\n * backend cannot report them — never fabricated.\n *\n * **Paths are POSIX, and the harness is Unix-only (for now!).** Every path uses `/`\n * separators on both the host and inside a sandbox — Windows is not supported,\n * so no translation layer exists. Consumers must use the shared Effect POSIX\n * path implementation, never the platform default, or a host running the harness would\n * impose its own flavour on a remote sandbox's paths. Relative paths resolve\n * against the backend's configured `cwd`.\n */\n\nexport class OperationUnsupportedError extends Schema.TaggedError<OperationUnsupportedError>()(\n\t\"OperationUnsupportedError\",\n\t{\n\t\toperation: Schema.String,\n\t\tmessage: Schema.String,\n\t},\n) {}\n\n/** A backend operation failed. `cause` carries the provider's own rejection. */\nexport class FileSystemError extends Schema.TaggedError<FileSystemError>()(\"SandboxFileSystemError\", {\n\tmethod: Schema.String,\n\tpath: Schema.String,\n\tcause: Schema.optional(Schema.Defect()),\n}) {}\n\nexport interface FileStat {\n\treadonly isFile: boolean;\n\treadonly isDirectory: boolean;\n\treadonly isSymbolicLink?: boolean;\n\treadonly size?: number;\n\treadonly mtime?: Date;\n}\n\nexport interface RmOptions {\n\treadonly recursive?: boolean;\n\treadonly force?: boolean;\n}\n\n/** Whether a provider failure means the path itself is definitively absent. */\nexport const isNotFoundError = (cause: unknown) => {\n\tconst code = (cause as NodeJS.ErrnoException | undefined)?.code;\n\treturn code === \"ENOENT\" || code === \"ENOTDIR\";\n};\n\n/**\n * Shell forms of `realpath` for backends that only expose a process API. Run as\n * `sh -c <script> _ <path>`, trying each in turn. `realpath -e` (coreutils)\n * resolves every component, a symlinked file included, and fails on a missing\n * path. Where it is absent, `pwd -P` is the portable primitive: directories\n * resolve directly, anything else resolves its parent and keeps the final name\n * as given — so a symlinked *file* stays unresolved there.\n */\nexport const realpathScripts = [\n\t'realpath -e -- \"$1\"',\n\t'cd -- \"$1\" && pwd -P',\n\t'cd -- \"$(dirname -- \"$1\")\" && printf \\'%s/%s\\' \"$(pwd -P)\" \"$(basename -- \"$1\")\"',\n] as const;\n\n/**\n * Shell form of `readBytes` for backends that only expose a process API. Run as\n * `sh -c <script> _ <path> <offset + 1> <length>`; prints the bytes as base64,\n * so binary content survives a transport that decodes stdout as text. Exits 2\n * when the path does not exist, 3 when it cannot be opened.\n */\nexport const readBytesScript = [\n\t'[ -e \"$1\" ] || exit 2',\n\t// A pipeline exits with its last command's status, and `head` closing early is\n\t// normal, so `tail` cannot report a file it cannot open; opening it first does.\n\t'{ : < \"$1\"; } 2>/dev/null || exit 3',\n\t'tail -c +\"$2\" -- \"$1\" | head -c \"$3\" | base64',\n].join(\"\\n\");\n\n/**\n * `Provider.readBytes` for a backend that runs an argument vector and collects\n * its output. A missing path rejects with `ENOENT`, the shape `isNotFoundError` recognises.\n */\nexport const readBytesViaShell =\n\t(run: (argv: ReadonlyArray<string>) => Promise<ShellRun>) =>\n\t// oxlint-disable-next-line effecttsgo/async-function -- the Provider contract is Promise-based\n\tasync (path: string, offset: number, length: number): Promise<Uint8Array> => {\n\t\tconst result = await run([\"sh\", \"-c\", readBytesScript, \"_\", path, String(offset + 1), String(length)]);\n\t\tif (result.exitCode === 2)\n\t\t\tthrow Object.assign(new Error(`ENOENT: no such file or directory, readBytes '${path}'`), { code: \"ENOENT\" });\n\t\tif (result.exitCode === 3) throw new Error(`readBytes: cannot open '${path}'`);\n\t\tif (result.exitCode !== 0)\n\t\t\tthrow new Error(`readBytes failed (exit ${result.exitCode}): ${(result.stderr ?? result.stdout).trim()}`);\n\t\treturn new Uint8Array(Buffer.from(result.stdout.replace(/\\s/g, \"\"), \"base64\"));\n\t};\n\n/**\n * The backend-facing contract. Backends author plain promises and let them\n * reject; {@link fromProvider} turns rejections into typed failures.\n */\nexport interface Provider {\n\treadonly readFile: (path: string) => Promise<string>;\n\treadonly readFileBuffer: (path: string) => Promise<Uint8Array>;\n\t/** Up to `length` bytes from `offset`; fewer at the end of the file. */\n\treadonly readBytes: (path: string, offset: number, length: number) => Promise<Uint8Array>;\n\t/**\n\t * Write the file. Parents may be missing — do not create them here;\n\t * {@link fromProvider} owns that guarantee for every backend.\n\t */\n\treadonly writeFile: (path: string, content: string | Uint8Array) => Promise<void>;\n\treadonly stat: (path: string) => Promise<FileStat>;\n\treadonly readdir: (path: string) => Promise<string[]>;\n\treadonly exists: (path: string) => Promise<boolean>;\n\treadonly mkdir: (path: string, options?: { recursive?: boolean }) => Promise<void>;\n\treadonly rm: (path: string, options?: RmOptions) => Promise<void>;\n\t/** Canonical absolute path with symlinks resolved; rejects when the path does not exist. */\n\treadonly realpath: (path: string) => Promise<string>;\n\t/**\n\t * A window of the file's lines, by the rules in `Scan`. Bounded: never\n\t * returns, or holds, more than the window. A remote backend runs the scan\n\t * inside the sandbox (`Scan.script`) so only the window crosses the network.\n\t */\n\treadonly scanLines: (path: string, options: LineScanOptions) => Promise<LineScan>;\n\t/**\n\t * Metadata for the directory entry itself rather than a symlink's target.\n\t * Optional: a backend whose `stat` has mixed symlink semantics implements it\n\t * so symlink identity is asked for explicitly, never inferred.\n\t */\n\treadonly lstat?: (path: string) => Promise<FileStat>;\n}\n\n/**\n * The consumer-facing contract. `exists` distinguishes \"absent\" from \"could not\n * tell\": a backend failure is a typed failure, not `false`, so a caller acting\n * on absence never acts on a network blip. Use `SandboxFs.existsOrFalse` where\n * a best-effort answer really is wanted.\n */\nexport interface Interface {\n\treadonly readFile: (path: string) => Effect.Effect<string, FileSystemError>;\n\treadonly readFileBuffer: (path: string) => Effect.Effect<Uint8Array, FileSystemError>;\n\t/** Up to `length` bytes from `offset`; fewer at the end of the file. */\n\treadonly readBytes: (path: string, offset: number, length: number) => Effect.Effect<Uint8Array, FileSystemError>;\n\t/** Creates missing parent directories, on every backend. */\n\treadonly writeFile: (path: string, content: string | Uint8Array) => Effect.Effect<void, FileSystemError>;\n\treadonly stat: (path: string) => Effect.Effect<FileStat, FileSystemError>;\n\treadonly readdir: (path: string) => Effect.Effect<string[], FileSystemError>;\n\treadonly exists: (path: string) => Effect.Effect<boolean, FileSystemError>;\n\treadonly mkdir: (path: string, options?: { recursive?: boolean }) => Effect.Effect<void, FileSystemError>;\n\treadonly rm: (path: string, options?: RmOptions) => Effect.Effect<void, FileSystemError | OperationUnsupportedError>;\n\t/** Canonical absolute path with symlinks resolved; fails when the path does not exist. */\n\treadonly realpath: (path: string) => Effect.Effect<string, FileSystemError>;\n\t/** A bounded window of the file's lines; see `Scan`. Invalid options are a defect. */\n\treadonly scanLines: (path: string, options: LineScanOptions) => Effect.Effect<LineScan, FileSystemError>;\n\t// `lstat` is present only when the backend supports it; check before calling.\n\treadonly lstat?: (path: string) => Effect.Effect<FileStat, FileSystemError>;\n}\n\n/** The runtime filesystem service — the live {@link Interface} for the active sandbox. */\nexport class Service extends Context.Service<Service, Interface>()(\"@codeworksh/plugin/sandbox/filesystem/Service\") {}\n\n/**\n * Reject `rm` options a provider does not implement, before any mutation. Only\n * `recursive` and `force` are part of the contract; anything else is refused\n * loudly rather than silently ignored.\n */\nexport const validateRmOptions = (\n\toptions: RmOptions | undefined,\n\toperation = \"rm\",\n): Effect.Effect<void, OperationUnsupportedError> =>\n\tEffect.suspend(() => {\n\t\tfor (const option of Object.keys((options ?? {}) as Record<string, unknown>)) {\n\t\t\tif (option === \"recursive\" || option === \"force\") continue;\n\t\t\treturn Effect.fail(new OperationUnsupportedError({ operation, message: `Unsupported rm option: ${option}` }));\n\t\t}\n\t\treturn Effect.void;\n\t});\n\n/**\n * Lift a {@link Provider} into the runtime {@link Interface}: one place that\n * converts rejections into {@link FileSystemError}, validates `rm` options,\n * creates missing parents on write, and names a tracing span per operation.\n */\nexport const fromProvider = (provider: Provider): Interface => {\n\tconst attempt = <A>(method: string, path: string, run: () => Promise<A>) =>\n\t\tEffect.tryPromise({ try: run, catch: (cause) => new FileSystemError({ method, path, cause }) });\n\n\t// The parent-creation guarantee, installed once for every backend instead of\n\t// re-implemented per provider: anything that reaches `Interface` reaches it\n\t// through here, so a backend cannot forget it.\n\t//\n\t// Lazy by design — try the write first, so the happy path stays a single call\n\t// and no remote backend pays a round-trip per write. A failure is usually a\n\t// missing parent, so create it and retry once. The mkdir's own error is\n\t// dropped on purpose: when the write failed for some other reason (EACCES,\n\t// EROFS, a dropped connection), the retry reproduces it, and *that* is the\n\t// failure the caller must see rather than a misleading mkdir error standing\n\t// in for it.\n\tconst writeCreatingParents = (path: string, content: string | Uint8Array) => {\n\t\tconst write = attempt(\"writeFile\", path, () => provider.writeFile(path, content));\n\t\tconst parent = posix.dirname(path);\n\t\treturn write.pipe(\n\t\t\tEffect.catch(() =>\n\t\t\t\tattempt(\"mkdir\", parent, () => provider.mkdir(parent, { recursive: true })).pipe(\n\t\t\t\t\tEffect.ignore,\n\t\t\t\t\tEffect.andThen(write),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\t};\n\n\treturn {\n\t\treadFile: Effect.fn(\"SandboxFileSystem.readFile\")((path: string) =>\n\t\t\tattempt(\"readFile\", path, () => provider.readFile(path)),\n\t\t),\n\t\treadFileBuffer: Effect.fn(\"SandboxFileSystem.readFileBuffer\")((path: string) =>\n\t\t\tattempt(\"readFileBuffer\", path, () => provider.readFileBuffer(path)),\n\t\t),\n\t\treadBytes: Effect.fn(\"SandboxFileSystem.readBytes\")((path: string, offset: number, length: number) =>\n\t\t\tattempt(\"readBytes\", path, () => provider.readBytes(path, offset, length)),\n\t\t),\n\t\t// A started write cannot be called back; interrupting would only abandon it while\n\t\t// its bytes still land, after a file's mutation lock has passed to the next writer.\n\t\twriteFile: Effect.fn(\"SandboxFileSystem.writeFile\")((path: string, content: string | Uint8Array) =>\n\t\t\tEffect.uninterruptible(writeCreatingParents(path, content)),\n\t\t),\n\t\tstat: Effect.fn(\"SandboxFileSystem.stat\")((path: string) => attempt(\"stat\", path, () => provider.stat(path))),\n\t\t// carried through only when the backend actually implements it\n\t\t...(provider.lstat === undefined\n\t\t\t? {}\n\t\t\t: {\n\t\t\t\t\tlstat: Effect.fn(\"SandboxFileSystem.lstat\")((path: string) =>\n\t\t\t\t\t\tattempt(\"lstat\", path, () => provider.lstat!(path)),\n\t\t\t\t\t),\n\t\t\t\t}),\n\t\treaddir: Effect.fn(\"SandboxFileSystem.readdir\")((path: string) =>\n\t\t\tattempt(\"readdir\", path, () => provider.readdir(path)),\n\t\t),\n\t\t// A backend that cannot answer fails; it does not report \"absent\". A\n\t\t// caller acting on absence — deleting a record, say — must not be told\n\t\t// the path is gone because a token expired or a request timed out.\n\t\texists: Effect.fn(\"SandboxFileSystem.exists\")((path: string) =>\n\t\t\tattempt(\"exists\", path, () => provider.exists(path)),\n\t\t),\n\t\tmkdir: Effect.fn(\"SandboxFileSystem.mkdir\")((path: string, options?: { recursive?: boolean }) =>\n\t\t\tattempt(\"mkdir\", path, () => provider.mkdir(path, options)),\n\t\t),\n\t\trm: Effect.fn(\"SandboxFileSystem.rm\")((path: string, options?: RmOptions) =>\n\t\t\tvalidateRmOptions(options).pipe(Effect.andThen(attempt(\"rm\", path, () => provider.rm(path, options)))),\n\t\t),\n\t\trealpath: Effect.fn(\"SandboxFileSystem.realpath\")((path: string) =>\n\t\t\tattempt(\"realpath\", path, () => provider.realpath(path)),\n\t\t),\n\t\tscanLines: Effect.fn(\"SandboxFileSystem.scanLines\")((path: string, options: LineScanOptions) =>\n\t\t\tEffect.sync(() => validateScan(options)).pipe(\n\t\t\t\tEffect.andThen(attempt(\"scanLines\", path, () => provider.scanLines(path, options))),\n\t\t\t),\n\t\t),\n\t};\n};\n\n/**\n * Bind a cwd-neutral filesystem to one mount's working directory.\n *\n * The counterpart of `Shell.withCwd`, and the reason relative paths mean the\n * same thing to both: a shared transport stays rooted at the namespace root, so\n * resolution happens here, per mount, rather than inside a VFS whose `chdir` is\n * global state two mounts would fight over.\n */\nexport const withCwd = (fs: Interface, cwd: string): Interface => {\n\tconst at = (path: string) => posix.resolve(cwd, path);\n\treturn {\n\t\treadFile: (path) => fs.readFile(at(path)),\n\t\treadFileBuffer: (path) => fs.readFileBuffer(at(path)),\n\t\treadBytes: (path, offset, length) => fs.readBytes(at(path), offset, length),\n\t\twriteFile: (path, content) => fs.writeFile(at(path), content),\n\t\tstat: (path) => fs.stat(at(path)),\n\t\treaddir: (path) => fs.readdir(at(path)),\n\t\texists: (path) => fs.exists(at(path)),\n\t\tmkdir: (path, options) => fs.mkdir(at(path), options),\n\t\trm: (path, options) => fs.rm(at(path), options),\n\t\trealpath: (path) => fs.realpath(at(path)),\n\t\tscanLines: (path, options) => fs.scanLines(at(path), options),\n\t\t// carried through only when the backend implements it, so a caller can\n\t\t// still detect absence by checking the property\n\t\t...(fs.lstat === undefined ? {} : { lstat: (path: string) => fs.lstat!(at(path)) }),\n\t};\n};\n\nexport * as SandboxFileSystem from \"./filesystem.ts\";\n"],"mappings":"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"}
|
package/ids.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { uuidv7 } from "uuidv7";
|
|
|
10
10
|
* twice is two types, and a plugin would have to cast to hand either back. They live here, in the
|
|
11
11
|
* package both sides depend on, rather than being re-declared on each side.
|
|
12
12
|
*/
|
|
13
|
-
const SessionID = Schema.String.check(Schema.
|
|
13
|
+
const SessionID = Schema.String.check(Schema.isStartingWith("ses")).pipe(Schema.brand("Session.ID"), withStatics((schema) => ({
|
|
14
14
|
ascending: (id) => {
|
|
15
15
|
if (!id) return schema.make("ses_" + uuidv7());
|
|
16
16
|
if (!id.startsWith("ses")) throw new Error(`ID ${id} does not start with ses`);
|
package/ids.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ids.mjs","names":[],"sources":["../../src/ids.ts"],"sourcesContent":["import { Schema } from \"effect\";\nimport { uuidv7 } from \"uuidv7\";\nimport { withStatics } from \"./schema.ts\";\n\n/**\n * The identifiers that cross the plugin boundary.\n *\n * A plugin reads a session id off its context and a message id off every tool call, so both\n * brands have to be the ones the harness itself mints -- a structurally identical brand declared\n * twice is two types, and a plugin would have to cast to hand either back. They live here, in the\n * package both sides depend on, rather than being re-declared on each side.\n */\n\n// Session identity. Branded so a session ID is not interchangeable with an\n// entry, part, or workspace ID at the type level — service signatures take\n// `SessionID`, never a bare `string`.\n//\n// `create` mints a new one; `ascending` adopts an ID that came from outside\n// (a persisted row, a request payload) and rejects a foreign prefix rather\n// than branding it silently. uuidv7 keeps IDs lexicographically sortable by\n// creation time, so `ORDER BY id` is `ORDER BY created`.\nexport const SessionID = Schema.String.check(Schema.
|
|
1
|
+
{"version":3,"file":"ids.mjs","names":[],"sources":["../../src/ids.ts"],"sourcesContent":["import { Schema } from \"effect\";\nimport { uuidv7 } from \"uuidv7\";\nimport { withStatics } from \"./schema.ts\";\n\n/**\n * The identifiers that cross the plugin boundary.\n *\n * A plugin reads a session id off its context and a message id off every tool call, so both\n * brands have to be the ones the harness itself mints -- a structurally identical brand declared\n * twice is two types, and a plugin would have to cast to hand either back. They live here, in the\n * package both sides depend on, rather than being re-declared on each side.\n */\n\n// Session identity. Branded so a session ID is not interchangeable with an\n// entry, part, or workspace ID at the type level — service signatures take\n// `SessionID`, never a bare `string`.\n//\n// `create` mints a new one; `ascending` adopts an ID that came from outside\n// (a persisted row, a request payload) and rejects a foreign prefix rather\n// than branding it silently. uuidv7 keeps IDs lexicographically sortable by\n// creation time, so `ORDER BY id` is `ORDER BY created`.\nexport const SessionID = Schema.String.check(Schema.isStartingWith(\"ses\")).pipe(\n\tSchema.brand(\"Session.ID\"),\n\twithStatics((schema) => ({\n\t\tascending: (id?: string) => {\n\t\t\tif (!id) return schema.make(\"ses_\" + uuidv7());\n\t\t\tif (!id.startsWith(\"ses\")) throw new Error(`ID ${id} does not start with ses`);\n\t\t\treturn schema.make(id);\n\t\t},\n\t\tcreate: () => schema.make(\"ses_\" + uuidv7()),\n\t})),\n);\nexport type SessionID = typeof SessionID.Type;\n\n/**\n * A conversation message id.\n *\n * Deliberately unprefixed. aikit mints assistant message ids itself, as bare\n * uuidv7, and `stream()` accepts no id -- so any naming rule of ours would have\n * to be imposed on messages aikit hands us, by rewriting a field of the value we\n * were given. Sharing aikit's format instead means an id means the same thing on\n * both sides of the boundary and never needs translating.\n *\n * The brand carries the meaning; uuidv7 keeps ids time-ordered, so they sort by\n * creation.\n */\nexport const MessageID = Schema.String.pipe(\n\tSchema.brand(\"Message.ID\"),\n\twithStatics((schema) => ({\n\t\tcreate: () => schema.make(uuidv7()),\n\t\t/**\n\t\t * Adopt an id minted elsewhere -- in practice aikit's, which arrives as a\n\t\t * plain string on every stream event and inside every assistant message.\n\t\t *\n\t\t * `make` does the same thing. This exists so the boundary where a foreign\n\t\t * id enters the domain is named and greppable, and so the pairing with\n\t\t * `create` says which side chose the id.\n\t\t */\n\t\tfrom: (id: string) => schema.make(id),\n\t})),\n);\nexport type MessageID = typeof MessageID.Type;\n"],"mappings":";;;;;;;;;;;;AAqBA,MAAa,YAAY,OAAO,OAAO,MAAM,OAAO,eAAe,KAAK,CAAC,CAAC,CAAC,KAC1E,OAAO,MAAM,YAAY,GACzB,aAAa,YAAY;CACxB,YAAY,OAAgB;EAC3B,IAAI,CAAC,IAAI,OAAO,OAAO,KAAK,SAAS,OAAO,CAAC;EAC7C,IAAI,CAAC,GAAG,WAAW,KAAK,GAAG,MAAM,IAAI,MAAM,MAAM,GAAG,yBAAyB;EAC7E,OAAO,OAAO,KAAK,EAAE;CACtB;CACA,cAAc,OAAO,KAAK,SAAS,OAAO,CAAC;AAC5C,EAAE,CACH;;;;;;;;;;;;;AAeA,MAAa,YAAY,OAAO,OAAO,KACtC,OAAO,MAAM,YAAY,GACzB,aAAa,YAAY;CACxB,cAAc,OAAO,KAAK,OAAO,CAAC;;;;;;;;;CASlC,OAAO,OAAe,OAAO,KAAK,EAAE;AACrC,EAAE,CACH"}
|
package/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { t as section_d_exports } from "./plugin/section.mjs";
|
|
2
|
+
import { g as tool_d_exports } from "./tool-_ajVYIY5.mjs";
|
|
3
|
+
import { t as plugin_d_exports } from "./plugin-Deb2QWIP.mjs";
|
|
4
|
+
export { plugin_d_exports as Plugin, section_d_exports as Section, tool_d_exports as Tool };
|
package/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import { t as plugin_exports } from "./plugin-
|
|
1
|
+
import { t as plugin_exports } from "./plugin-XaYYRsX-.mjs";
|
|
2
|
+
import { t as section_exports } from "./plugin/section.mjs";
|
|
2
3
|
import { t as tool_exports } from "./tool.mjs";
|
|
3
|
-
export { plugin_exports as Plugin, tool_exports as Tool };
|
|
4
|
+
export { plugin_exports as Plugin, section_exports as Section, tool_exports as Tool };
|