@struktoai/mirage-dsh 0.0.1-alpha.1

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.
@@ -0,0 +1,54 @@
1
+ # The mirage bundle patch. Applied over dsh-base by `dsh --profile <name>`
2
+ # after `dsh plugin --profile <name> add @struktoai/mirage-dsh`: it swaps
3
+ # the filesystem and shell providers for mirage-backed ones, so dsh's file
4
+ # tools and bash tool operate on a workspace of mounted resources instead
5
+ # of the host disk.
6
+ #
7
+ # Override the `mirage` row's config in the profile's own cordis.patch.yml
8
+ # to mount real resources (the block form is `resource` registry name,
9
+ # `mode`, and the resource's `config`; `!!js` expressions resolve env vars
10
+ # at mount time):
11
+ #
12
+ # - id: mirage
13
+ # config:
14
+ # mounts:
15
+ # /tmp: { resource: ram, mode: exec }
16
+ # /slack:
17
+ # resource: slack
18
+ # mode: read
19
+ # config: { token: !!js process.env.SLACK_BOT_TOKEN }
20
+ # runtimes:
21
+ # - { name: monty, captures: [python, python3] }
22
+
23
+ # The host filesystem and bash providers step aside; mirage provides the
24
+ # same ctx.fs and ctx.shell seams over the workspace.
25
+ - id: fs-sandbox
26
+ disabled: true
27
+
28
+ - id: bash-sandbox
29
+ disabled: true
30
+
31
+ # PowerShell and the ripgrep-based search tool run host subprocesses,
32
+ # which the mirage world does not contain; the bash tool's grep covers
33
+ # search inside the workspace.
34
+ - id: pwsh-sandbox
35
+ disabled: true
36
+
37
+ - id: tool-pwsh
38
+ disabled: true
39
+
40
+ - id: tool-fs-search
41
+ disabled: true
42
+
43
+ - insert:
44
+ - id: mirage
45
+ name: '@struktoai/mirage-dsh/service'
46
+ config:
47
+ mounts:
48
+ /tmp: { resource: ram, mode: exec }
49
+
50
+ - id: mirage-fs
51
+ name: '@struktoai/mirage-dsh/fs'
52
+
53
+ - id: mirage-shell
54
+ name: '@struktoai/mirage-dsh/shell'
@@ -0,0 +1,392 @@
1
+ import {
2
+ applyLiteralEdit,
3
+ decodeStrictText,
4
+ detectsCrlf,
5
+ normalizeLineEndings,
6
+ restoreLineEndings
7
+ } from "./chunk-CATVBGGR.js";
8
+
9
+ // src/fs.ts
10
+ import { Buffer } from "buffer";
11
+ import { posix } from "path";
12
+ import { FileSystem, FsError as FsError2, FsTargetKey, FsVersion } from "@deepseek-ai/dsh-fs";
13
+ import { compareCodePoints, FileType, isMissingPath as isMissingPath2 } from "@struktoai/mirage-core";
14
+
15
+ // src/errors.ts
16
+ import { FsError } from "@deepseek-ai/dsh-fs";
17
+ import { isMissingPath } from "@struktoai/mirage-core";
18
+ function assertNotAborted(signal, operation) {
19
+ if (signal?.aborted === true) {
20
+ throw new FsError(`${operation} aborted`, "FS_ABORTED");
21
+ }
22
+ }
23
+ function codeOf(err) {
24
+ if (err === null || typeof err !== "object") return null;
25
+ const stamped = err;
26
+ return typeof stamped.code === "string" ? stamped.code : null;
27
+ }
28
+ function codeFor(err) {
29
+ if (isMissingPath(err)) return "FS_NOT_FOUND";
30
+ switch (codeOf(err)) {
31
+ case "EISDIR":
32
+ return "FS_NOT_REGULAR_FILE";
33
+ case "ENOTDIR":
34
+ return "FS_NOT_DIRECTORY";
35
+ case "EACCES":
36
+ case "EPERM":
37
+ return "FS_PERMISSION_DENIED";
38
+ default:
39
+ return "FS_IO_ERROR";
40
+ }
41
+ }
42
+ function mapMirageError(err, operation, displayPath) {
43
+ if (err instanceof FsError) return err;
44
+ if (err instanceof DOMException && err.name === "AbortError") {
45
+ return new FsError(`${operation} aborted`, "FS_ABORTED", { cause: err });
46
+ }
47
+ const message = err instanceof Error ? err.message : String(err);
48
+ return new FsError(`cannot ${operation} "${displayPath}": ${message}`, codeFor(err), {
49
+ cause: err
50
+ });
51
+ }
52
+
53
+ // src/fs.ts
54
+ var DEFAULT_DIFF_BASIS_MAX_BYTES = 10 * 1024 * 1024;
55
+ function versionOf(stat) {
56
+ if (stat.fingerprint !== null) return FsVersion(`fp:${stat.fingerprint}`);
57
+ if (stat.revision !== null) return FsVersion(`rev:${stat.revision}`);
58
+ if (stat.modified !== null || stat.size !== null) {
59
+ return FsVersion(`meta:${stat.modified ?? ""}:${String(stat.size ?? "")}`);
60
+ }
61
+ return FsVersion("unversioned");
62
+ }
63
+ function typeOf(stat) {
64
+ switch (stat.type) {
65
+ case FileType.DIRECTORY:
66
+ return "directory";
67
+ case FileType.SYMLINK:
68
+ return "other";
69
+ default:
70
+ return "file";
71
+ }
72
+ }
73
+ async function* singleChunk(text) {
74
+ yield await Promise.resolve(text);
75
+ }
76
+ var MirageFileSystem = class extends FileSystem {
77
+ static inject = ["mirage"];
78
+ fsOps = null;
79
+ cwd;
80
+ diffBasisMaxBytes;
81
+ // Per-targetKey tail promise: serializes mutating ops so the
82
+ // read -> guard -> write window cannot interleave (one concurrent writer
83
+ // wins, the rest observe the new version and reject as stale).
84
+ locks = /* @__PURE__ */ new Map();
85
+ constructor(ctx, config = {}) {
86
+ super(ctx);
87
+ this.cwd = config.cwd ?? "/";
88
+ this.diffBasisMaxBytes = config.diffBasisMaxBytes ?? DEFAULT_DIFF_BASIS_MAX_BYTES;
89
+ }
90
+ // The workspace may still be building (declarative mounts resolve
91
+ // asynchronously), so every entry point awaits the service's `ready`
92
+ // once and caches the op door. The caller's signal can fire during
93
+ // that wait, after its entry assertion passed, so it is asserted
94
+ // again here, before the op it guards dispatches.
95
+ async ops(signal, operation = "ready") {
96
+ this.fsOps ??= (await this.ctx.mirage.ready).fs;
97
+ assertNotAborted(signal, operation);
98
+ return this.fsOps;
99
+ }
100
+ get links() {
101
+ if (this.fsOps === null) {
102
+ throw new Error("mirage: filesystem used before the workspace is ready");
103
+ }
104
+ return this.fsOps.links;
105
+ }
106
+ normalize(path, cwd) {
107
+ return posix.resolve(cwd ?? this.cwd, path);
108
+ }
109
+ follow(path) {
110
+ const links = this.links;
111
+ if (links === null) return path;
112
+ try {
113
+ return links.follow(path);
114
+ } catch (err) {
115
+ throw mapMirageError(err, "resolve", path);
116
+ }
117
+ }
118
+ withLock(key, body) {
119
+ const mapKey = String(key);
120
+ const previous = this.locks.get(mapKey) ?? Promise.resolve();
121
+ const next = previous.then(body, body);
122
+ const tail = next.then(
123
+ () => void 0,
124
+ () => void 0
125
+ );
126
+ this.locks.set(mapKey, tail);
127
+ void tail.then(() => {
128
+ if (this.locks.get(mapKey) === tail) this.locks.delete(mapKey);
129
+ });
130
+ return next;
131
+ }
132
+ async resolve(path, opts) {
133
+ assertNotAborted(opts?.signal, "resolve");
134
+ await this.ops(opts?.signal, "resolve");
135
+ const followed = this.follow(this.normalize(path, opts?.cwd));
136
+ return { targetKey: FsTargetKey(followed), displayPath: followed };
137
+ }
138
+ processPath(target) {
139
+ return String(target.targetKey);
140
+ }
141
+ fileUrl(target) {
142
+ const encoded = String(target.targetKey).split("/").map(encodeURIComponent).join("/");
143
+ return `file://${encoded}`;
144
+ }
145
+ contains(parent, child) {
146
+ const parentKey = String(parent.targetKey);
147
+ const childKey = String(child.targetKey);
148
+ if (parentKey === childKey) return true;
149
+ const prefix = parentKey === "/" ? "/" : `${parentKey}/`;
150
+ return childKey.startsWith(prefix);
151
+ }
152
+ async stat(target, signal) {
153
+ assertNotAborted(signal, "stat");
154
+ const key = String(target.targetKey);
155
+ let stat;
156
+ try {
157
+ stat = await (await this.ops(signal, "stat")).stat(key);
158
+ } catch (err) {
159
+ if (isMissingPath2(err)) return void 0;
160
+ throw mapMirageError(err, "stat", target.displayPath);
161
+ }
162
+ return {
163
+ version: versionOf(stat),
164
+ type: typeOf(stat),
165
+ ...stat.size !== null ? { size: stat.size } : {}
166
+ };
167
+ }
168
+ async lstat(path, opts, signal) {
169
+ assertNotAborted(signal, "lstat");
170
+ await this.ops(signal, "lstat");
171
+ const normalized = this.normalize(path, opts?.cwd);
172
+ const parentFollowed = normalized === "/" ? "/" : posix.join(this.follow(posix.dirname(normalized)), posix.basename(normalized));
173
+ const links = this.links;
174
+ if (links?.isLink(parentFollowed) === true) {
175
+ const linkTarget = links.readlink(parentFollowed) ?? "";
176
+ return {
177
+ version: FsVersion(`link:${linkTarget}`),
178
+ type: "symlink",
179
+ size: new TextEncoder().encode(linkTarget).byteLength
180
+ };
181
+ }
182
+ let stat;
183
+ try {
184
+ stat = await (await this.ops(signal, "lstat")).stat(parentFollowed);
185
+ } catch (err) {
186
+ if (isMissingPath2(err)) return void 0;
187
+ throw mapMirageError(err, "lstat", normalized);
188
+ }
189
+ return {
190
+ version: versionOf(stat),
191
+ type: typeOf(stat) === "directory" ? "directory" : typeOf(stat),
192
+ ...stat.size !== null ? { size: stat.size } : {}
193
+ };
194
+ }
195
+ async readText(target, signal) {
196
+ assertNotAborted(signal, "read");
197
+ const key = String(target.targetKey);
198
+ let bytes;
199
+ try {
200
+ bytes = await (await this.ops(signal, "read")).readFile(key);
201
+ } catch (err) {
202
+ throw mapMirageError(err, "read", target.displayPath);
203
+ }
204
+ return decodeStrictText(bytes, target.displayPath);
205
+ }
206
+ async streamText(target, signal) {
207
+ const text = await this.readText(target, signal);
208
+ return singleChunk(text);
209
+ }
210
+ async listDir(target, signal) {
211
+ assertNotAborted(signal, "list");
212
+ const key = String(target.targetKey);
213
+ const info = await this.stat(target, signal);
214
+ if (info === void 0) {
215
+ throw new FsError2(
216
+ `cannot list "${target.displayPath}": no such file or directory`,
217
+ "FS_NOT_FOUND"
218
+ );
219
+ }
220
+ if (info.type !== "directory") {
221
+ throw new FsError2(`cannot list "${target.displayPath}": not a directory`, "FS_NOT_DIRECTORY");
222
+ }
223
+ let children;
224
+ try {
225
+ children = await (await this.ops(signal, "list")).readdir(key);
226
+ } catch (err) {
227
+ throw mapMirageError(err, "list", target.displayPath);
228
+ }
229
+ const names = /* @__PURE__ */ new Set();
230
+ for (const childPath of children) {
231
+ names.add(posix.basename(childPath));
232
+ }
233
+ const links = this.links;
234
+ if (links !== null) {
235
+ const base = key === "/" ? "/" : `${key}/`;
236
+ for (const linkPath of links.symlinkTargets().keys()) {
237
+ if (linkPath.startsWith(base) && !linkPath.slice(base.length).includes("/")) {
238
+ names.add(linkPath.slice(base.length));
239
+ }
240
+ }
241
+ }
242
+ const entries = [];
243
+ for (const name of [...names].sort(compareCodePoints)) {
244
+ entries.push(await this.dirEntry(key, name));
245
+ }
246
+ return entries;
247
+ }
248
+ async dirEntry(parentKey, name) {
249
+ const childPath = parentKey === "/" ? `/${name}` : `${parentKey}/${name}`;
250
+ let followed;
251
+ try {
252
+ followed = this.follow(childPath);
253
+ } catch {
254
+ return {
255
+ name,
256
+ type: "other",
257
+ target: { targetKey: FsTargetKey(childPath), displayPath: childPath }
258
+ };
259
+ }
260
+ const target = { targetKey: FsTargetKey(followed), displayPath: childPath };
261
+ let stat;
262
+ try {
263
+ stat = await (await this.ops()).stat(followed);
264
+ } catch {
265
+ return { name, type: "other", target };
266
+ }
267
+ return {
268
+ name,
269
+ type: typeOf(stat),
270
+ target,
271
+ version: versionOf(stat),
272
+ ...stat.size !== null ? { size: stat.size } : {}
273
+ };
274
+ }
275
+ async writeText(target, content, expected, signal) {
276
+ return this.withLock(target.targetKey, async () => {
277
+ assertNotAborted(signal, "write");
278
+ const key = String(target.targetKey);
279
+ const existing = await this.stat(target, signal);
280
+ if (existing !== void 0 && existing.type !== "file") {
281
+ throw new FsError2(
282
+ `cannot write "${target.displayPath}": not a regular file`,
283
+ "FS_NOT_REGULAR_FILE"
284
+ );
285
+ }
286
+ if (expected?.kind === "createIfAbsent" && existing !== void 0) {
287
+ throw new FsError2(
288
+ `cannot write "${target.displayPath}": file already exists`,
289
+ "FS_NOT_OBSERVED"
290
+ );
291
+ }
292
+ if (expected?.kind === "replaceIfVersion") {
293
+ if (existing?.version !== expected.version) {
294
+ throw new FsError2(
295
+ `cannot write "${target.displayPath}": file changed since it was read`,
296
+ "FS_STALE_VERSION"
297
+ );
298
+ }
299
+ }
300
+ const before = existing === void 0 ? null : await this.diffBasis(target, content);
301
+ const crlf = before !== null && detectsCrlf(before);
302
+ try {
303
+ await (await this.ops(signal, "write")).writeFile(key, restoreLineEndings(normalizeLineEndings(content), crlf));
304
+ } catch (err) {
305
+ throw mapMirageError(err, "write", target.displayPath);
306
+ }
307
+ return {
308
+ operation: existing === void 0 ? "create" : "update",
309
+ version: await this.versionAfterWrite(target),
310
+ before: before === null ? null : normalizeLineEndings(before),
311
+ after: normalizeLineEndings(content)
312
+ };
313
+ });
314
+ }
315
+ // The overwrite-diff basis: the prior content when it is text and both
316
+ // sides fit the exclusive limit, else null (dsh renders a whole-file
317
+ // diff then). Never fails the write itself.
318
+ async diffBasis(target, content) {
319
+ if (Buffer.byteLength(content, "utf8") >= this.diffBasisMaxBytes) return null;
320
+ let bytes;
321
+ try {
322
+ bytes = await (await this.ops()).readFile(String(target.targetKey));
323
+ } catch {
324
+ return null;
325
+ }
326
+ if (bytes.byteLength >= this.diffBasisMaxBytes) return null;
327
+ try {
328
+ return decodeStrictText(bytes, target.displayPath);
329
+ } catch {
330
+ return null;
331
+ }
332
+ }
333
+ async versionAfterWrite(target) {
334
+ const after = await this.stat(target);
335
+ return after?.version ?? FsVersion(`missing:${String(target.targetKey)}`);
336
+ }
337
+ async editText(target, edit, expected, signal) {
338
+ return this.withLock(target.targetKey, async () => {
339
+ assertNotAborted(signal, "edit");
340
+ const key = String(target.targetKey);
341
+ const existing = await this.stat(target, signal);
342
+ if (existing === void 0) {
343
+ throw new FsError2(
344
+ `cannot edit "${target.displayPath}": file changed since it was read`,
345
+ "FS_STALE_VERSION"
346
+ );
347
+ }
348
+ if (existing.type !== "file") {
349
+ throw new FsError2(
350
+ `cannot edit "${target.displayPath}": not a regular file`,
351
+ "FS_NOT_REGULAR_FILE"
352
+ );
353
+ }
354
+ if (expected !== void 0 && existing.version !== expected.version) {
355
+ throw new FsError2(
356
+ `cannot edit "${target.displayPath}": file changed since it was read`,
357
+ "FS_STALE_VERSION"
358
+ );
359
+ }
360
+ let bytes;
361
+ try {
362
+ bytes = await (await this.ops(signal, "edit")).readFile(key);
363
+ } catch (err) {
364
+ throw mapMirageError(err, "edit", target.displayPath);
365
+ }
366
+ const raw = decodeStrictText(bytes, target.displayPath);
367
+ const original = normalizeLineEndings(raw);
368
+ const edited = applyLiteralEdit(
369
+ original,
370
+ normalizeLineEndings(edit.oldString),
371
+ normalizeLineEndings(edit.newString),
372
+ edit.replaceAll,
373
+ target.displayPath
374
+ );
375
+ try {
376
+ await (await this.ops(signal, "edit")).writeFile(key, restoreLineEndings(edited, detectsCrlf(raw)));
377
+ } catch (err) {
378
+ throw mapMirageError(err, "edit", target.displayPath);
379
+ }
380
+ return {
381
+ version: await this.versionAfterWrite(target),
382
+ before: original,
383
+ after: edited
384
+ };
385
+ });
386
+ }
387
+ };
388
+
389
+ export {
390
+ MirageFileSystem
391
+ };
392
+ //# sourceMappingURL=chunk-C67TSSYB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/fs.ts","../src/errors.ts"],"sourcesContent":["// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { Buffer } from 'node:buffer'\nimport { posix } from 'node:path'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { FileSystem, FsError, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'\nimport type {\n FsDirEntry,\n FsEditOutcome,\n FsEditRequest,\n FsInfo,\n FsPathInfo,\n FsTarget,\n FsWriteIntent,\n FsWriteOutcome,\n} from '@deepseek-ai/dsh-fs'\nimport { compareCodePoints, FileType, isMissingPath } from '@struktoai/mirage-core'\nimport type { FileStat, Ops } from '@struktoai/mirage-core'\nimport { assertNotAborted, mapMirageError } from './errors.ts'\nimport {\n applyLiteralEdit,\n decodeStrictText,\n detectsCrlf,\n normalizeLineEndings,\n restoreLineEndings,\n} from './text.ts'\nimport type {} from './service.ts'\n\ntype LinksSeam = NonNullable<Ops['links']>\n\nconst DEFAULT_DIFF_BASIS_MAX_BYTES = 10 * 1024 * 1024\n\n/** Configuration for the mirage filesystem backend. */\nexport interface MirageFsConfig {\n /** Virtual base directory for relative paths. Defaults to `/`. */\n cwd?: string\n /** Exclusive byte limit on each overwrite-diff side. Defaults to 10 MiB. */\n diffBasisMaxBytes?: number\n}\n\n// The freshness token behind FsVersion. Backends that report a fingerprint\n// or revision get a content-derived token; the rest fall back to metadata\n// (modified + size), and a backend reporting neither yields a constant, so\n// stale guards degrade to always-fresh there instead of always-stale.\nfunction versionOf(stat: FileStat): FsVersion {\n if (stat.fingerprint !== null) return FsVersion(`fp:${stat.fingerprint}`)\n if (stat.revision !== null) return FsVersion(`rev:${stat.revision}`)\n if (stat.modified !== null || stat.size !== null) {\n return FsVersion(`meta:${stat.modified ?? ''}:${String(stat.size ?? '')}`)\n }\n return FsVersion('unversioned')\n}\n\nfunction typeOf(stat: FileStat): FsInfo['type'] {\n switch (stat.type) {\n case FileType.DIRECTORY:\n return 'directory'\n case FileType.SYMLINK:\n return 'other'\n default:\n return 'file'\n }\n}\n\nasync function* singleChunk(text: string): AsyncGenerator<string, void, void> {\n yield await Promise.resolve(text)\n}\n\n/**\n * Mirage-backed implementation of `ctx.fs`. Targets are canonical virtual\n * paths (namespace symlinks followed), every operation walks the workspace\n * op door — session grants, admission policies, cache read-through and\n * post-write invalidation all fire exactly as they do for a shell command —\n * and `processPath` answers in the same virtual path space the mirage shell\n * executes in, so the two providers share one execution world.\n */\nexport class MirageFileSystem extends FileSystem {\n static readonly inject = ['mirage']\n\n private fsOps: Ops | null = null\n private readonly cwd: string\n private readonly diffBasisMaxBytes: number\n // Per-targetKey tail promise: serializes mutating ops so the\n // read -> guard -> write window cannot interleave (one concurrent writer\n // wins, the rest observe the new version and reject as stale).\n private readonly locks = new Map<string, Promise<void>>()\n\n constructor(ctx: Context, config: MirageFsConfig = {}) {\n super(ctx)\n this.cwd = config.cwd ?? '/'\n this.diffBasisMaxBytes = config.diffBasisMaxBytes ?? DEFAULT_DIFF_BASIS_MAX_BYTES\n }\n\n // The workspace may still be building (declarative mounts resolve\n // asynchronously), so every entry point awaits the service's `ready`\n // once and caches the op door. The caller's signal can fire during\n // that wait, after its entry assertion passed, so it is asserted\n // again here, before the op it guards dispatches.\n private async ops(signal?: AbortSignal, operation = 'ready'): Promise<Ops> {\n this.fsOps ??= (await this.ctx.mirage.ready).fs\n assertNotAborted(signal, operation)\n return this.fsOps\n }\n\n private get links(): LinksSeam | null {\n if (this.fsOps === null) {\n throw new Error('mirage: filesystem used before the workspace is ready')\n }\n return this.fsOps.links\n }\n\n private normalize(path: string, cwd?: string): string {\n return posix.resolve(cwd ?? this.cwd, path)\n }\n\n private follow(path: string): string {\n const links = this.links\n if (links === null) return path\n try {\n return links.follow(path)\n } catch (err) {\n throw mapMirageError(err, 'resolve', path)\n }\n }\n\n private withLock<T>(key: FsTargetKey, body: () => Promise<T>): Promise<T> {\n const mapKey = String(key)\n const previous = this.locks.get(mapKey) ?? Promise.resolve()\n const next = previous.then(body, body)\n // The stored tail is void (an outcome would pin whole file contents in\n // the map) and drops on settlement unless a newer op queued behind it.\n const tail = next.then(\n () => undefined,\n () => undefined,\n )\n this.locks.set(mapKey, tail)\n void tail.then(() => {\n if (this.locks.get(mapKey) === tail) this.locks.delete(mapKey)\n })\n return next\n }\n\n async resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget> {\n assertNotAborted(opts?.signal, 'resolve')\n await this.ops(opts?.signal, 'resolve')\n const followed = this.follow(this.normalize(path, opts?.cwd))\n return { targetKey: FsTargetKey(followed), displayPath: followed }\n }\n\n processPath(target: FsTarget): string {\n return String(target.targetKey)\n }\n\n fileUrl(target: FsTarget): string {\n const encoded = String(target.targetKey).split('/').map(encodeURIComponent).join('/')\n return `file://${encoded}`\n }\n\n contains(parent: FsTarget, child: FsTarget): boolean {\n const parentKey = String(parent.targetKey)\n const childKey = String(child.targetKey)\n if (parentKey === childKey) return true\n const prefix = parentKey === '/' ? '/' : `${parentKey}/`\n return childKey.startsWith(prefix)\n }\n\n async stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined> {\n assertNotAborted(signal, 'stat')\n const key = String(target.targetKey)\n let stat: FileStat\n try {\n stat = await (await this.ops(signal, 'stat')).stat(key)\n } catch (err) {\n if (isMissingPath(err)) return undefined\n throw mapMirageError(err, 'stat', target.displayPath)\n }\n return {\n version: versionOf(stat),\n type: typeOf(stat),\n ...(stat.size !== null ? { size: stat.size } : {}),\n }\n }\n\n async lstat(\n path: string,\n opts?: { cwd?: string },\n signal?: AbortSignal,\n ): Promise<FsPathInfo | undefined> {\n assertNotAborted(signal, 'lstat')\n await this.ops(signal, 'lstat')\n const normalized = this.normalize(path, opts?.cwd)\n // Follow every component except the last: the probe is about the path\n // entry itself, so a link at the leaf must report as one.\n const parentFollowed =\n normalized === '/'\n ? '/'\n : posix.join(this.follow(posix.dirname(normalized)), posix.basename(normalized))\n const links = this.links\n if (links?.isLink(parentFollowed) === true) {\n const linkTarget = links.readlink(parentFollowed) ?? ''\n return {\n version: FsVersion(`link:${linkTarget}`),\n type: 'symlink',\n size: new TextEncoder().encode(linkTarget).byteLength,\n }\n }\n let stat: FileStat\n try {\n stat = await (await this.ops(signal, 'lstat')).stat(parentFollowed)\n } catch (err) {\n if (isMissingPath(err)) return undefined\n throw mapMirageError(err, 'lstat', normalized)\n }\n return {\n version: versionOf(stat),\n type: typeOf(stat) === 'directory' ? 'directory' : typeOf(stat),\n ...(stat.size !== null ? { size: stat.size } : {}),\n }\n }\n\n async readText(target: FsTarget, signal?: AbortSignal): Promise<string> {\n assertNotAborted(signal, 'read')\n const key = String(target.targetKey)\n let bytes: Uint8Array\n try {\n bytes = await (await this.ops(signal, 'read')).readFile(key)\n } catch (err) {\n throw mapMirageError(err, 'read', target.displayPath)\n }\n return decodeStrictText(bytes, target.displayPath)\n }\n\n async streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>> {\n // mirage reads land as one rendered buffer, so streaming is a single\n // chunk with readText's exact text semantics.\n const text = await this.readText(target, signal)\n return singleChunk(text)\n }\n\n async listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]> {\n assertNotAborted(signal, 'list')\n const key = String(target.targetKey)\n const info = await this.stat(target, signal)\n if (info === undefined) {\n throw new FsError(\n `cannot list \"${target.displayPath}\": no such file or directory`,\n 'FS_NOT_FOUND',\n )\n }\n if (info.type !== 'directory') {\n throw new FsError(`cannot list \"${target.displayPath}\": not a directory`, 'FS_NOT_DIRECTORY')\n }\n let children: string[]\n try {\n children = await (await this.ops(signal, 'list')).readdir(key)\n } catch (err) {\n throw mapMirageError(err, 'list', target.displayPath)\n }\n const names = new Set<string>()\n for (const childPath of children) {\n names.add(posix.basename(childPath))\n }\n // Namespace symlinks live above every backend, so a listing merges\n // them in here exactly as mirage's own ls does.\n const links = this.links\n if (links !== null) {\n const base = key === '/' ? '/' : `${key}/`\n for (const linkPath of links.symlinkTargets().keys()) {\n if (linkPath.startsWith(base) && !linkPath.slice(base.length).includes('/')) {\n names.add(linkPath.slice(base.length))\n }\n }\n }\n const entries: FsDirEntry[] = []\n for (const name of [...names].sort(compareCodePoints)) {\n entries.push(await this.dirEntry(key, name))\n }\n return entries\n }\n\n private async dirEntry(parentKey: string, name: string): Promise<FsDirEntry> {\n const childPath = parentKey === '/' ? `/${name}` : `${parentKey}/${name}`\n let followed: string\n try {\n followed = this.follow(childPath)\n } catch {\n // A cyclic link must not reject the whole listing; it lists as an\n // entry of unknown kind keyed on its own path.\n return {\n name,\n type: 'other',\n target: { targetKey: FsTargetKey(childPath), displayPath: childPath },\n }\n }\n const target: FsTarget = { targetKey: FsTargetKey(followed), displayPath: childPath }\n let stat: FileStat\n try {\n stat = await (await this.ops()).stat(followed)\n } catch {\n // A child the listing named but stat cannot classify (a broken link,\n // a race with a delete) still lists, as an entry of unknown kind.\n return { name, type: 'other', target }\n }\n return {\n name,\n type: typeOf(stat),\n target,\n version: versionOf(stat),\n ...(stat.size !== null ? { size: stat.size } : {}),\n }\n }\n\n async writeText(\n target: FsTarget,\n content: string,\n expected?: FsWriteIntent,\n signal?: AbortSignal,\n ): Promise<FsWriteOutcome> {\n return this.withLock(target.targetKey, async () => {\n assertNotAborted(signal, 'write')\n const key = String(target.targetKey)\n const existing = await this.stat(target, signal)\n if (existing !== undefined && existing.type !== 'file') {\n throw new FsError(\n `cannot write \"${target.displayPath}\": not a regular file`,\n 'FS_NOT_REGULAR_FILE',\n )\n }\n if (expected?.kind === 'createIfAbsent' && existing !== undefined) {\n throw new FsError(\n `cannot write \"${target.displayPath}\": file already exists`,\n 'FS_NOT_OBSERVED',\n )\n }\n if (expected?.kind === 'replaceIfVersion') {\n if (existing?.version !== expected.version) {\n throw new FsError(\n `cannot write \"${target.displayPath}\": file changed since it was read`,\n 'FS_STALE_VERSION',\n )\n }\n }\n const before = existing === undefined ? null : await this.diffBasis(target, content)\n const crlf = before !== null && detectsCrlf(before)\n try {\n await (\n await this.ops(signal, 'write')\n ).writeFile(key, restoreLineEndings(normalizeLineEndings(content), crlf))\n } catch (err) {\n throw mapMirageError(err, 'write', target.displayPath)\n }\n return {\n operation: existing === undefined ? ('create' as const) : ('update' as const),\n version: await this.versionAfterWrite(target),\n before: before === null ? null : normalizeLineEndings(before),\n after: normalizeLineEndings(content),\n }\n })\n }\n\n // The overwrite-diff basis: the prior content when it is text and both\n // sides fit the exclusive limit, else null (dsh renders a whole-file\n // diff then). Never fails the write itself.\n private async diffBasis(target: FsTarget, content: string): Promise<string | null> {\n if (Buffer.byteLength(content, 'utf8') >= this.diffBasisMaxBytes) return null\n let bytes: Uint8Array\n try {\n bytes = await (await this.ops()).readFile(String(target.targetKey))\n } catch {\n return null\n }\n if (bytes.byteLength >= this.diffBasisMaxBytes) return null\n try {\n return decodeStrictText(bytes, target.displayPath)\n } catch {\n return null\n }\n }\n\n private async versionAfterWrite(target: FsTarget): Promise<FsVersion> {\n const after = await this.stat(target)\n return after?.version ?? FsVersion(`missing:${String(target.targetKey)}`)\n }\n\n async editText(\n target: FsTarget,\n edit: FsEditRequest,\n expected?: { version: FsVersion },\n signal?: AbortSignal,\n ): Promise<FsEditOutcome> {\n return this.withLock(target.targetKey, async () => {\n assertNotAborted(signal, 'edit')\n const key = String(target.targetKey)\n const existing = await this.stat(target, signal)\n // Stale guard before literal matching: an edit based on an old read\n // reports FS_STALE_VERSION, not a match failure against newer content.\n if (existing === undefined) {\n throw new FsError(\n `cannot edit \"${target.displayPath}\": file changed since it was read`,\n 'FS_STALE_VERSION',\n )\n }\n if (existing.type !== 'file') {\n throw new FsError(\n `cannot edit \"${target.displayPath}\": not a regular file`,\n 'FS_NOT_REGULAR_FILE',\n )\n }\n if (expected !== undefined && existing.version !== expected.version) {\n throw new FsError(\n `cannot edit \"${target.displayPath}\": file changed since it was read`,\n 'FS_STALE_VERSION',\n )\n }\n let bytes: Uint8Array\n try {\n bytes = await (await this.ops(signal, 'edit')).readFile(key)\n } catch (err) {\n throw mapMirageError(err, 'edit', target.displayPath)\n }\n const raw = decodeStrictText(bytes, target.displayPath)\n const original = normalizeLineEndings(raw)\n const edited = applyLiteralEdit(\n original,\n normalizeLineEndings(edit.oldString),\n normalizeLineEndings(edit.newString),\n edit.replaceAll,\n target.displayPath,\n )\n try {\n await (\n await this.ops(signal, 'edit')\n ).writeFile(key, restoreLineEndings(edited, detectsCrlf(raw)))\n } catch (err) {\n throw mapMirageError(err, 'edit', target.displayPath)\n }\n return {\n version: await this.versionAfterWrite(target),\n before: original,\n after: edited,\n }\n })\n }\n}\n","// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { FsError } from '@deepseek-ai/dsh-fs'\nimport type { FsErrorCode } from '@deepseek-ai/dsh-fs'\nimport { isMissingPath } from '@struktoai/mirage-core'\n\nexport function assertNotAborted(signal: AbortSignal | undefined, operation: string): void {\n if (signal?.aborted === true) {\n throw new FsError(`${operation} aborted`, 'FS_ABORTED')\n }\n}\n\nfunction codeOf(err: unknown): string | null {\n if (err === null || typeof err !== 'object') return null\n const stamped = err as { code?: unknown }\n return typeof stamped.code === 'string' ? stamped.code : null\n}\n\n// mirage stamps POSIX codes on its errors (ENOENT, EISDIR, ENOTDIR, EACCES,\n// EXDEV; PolicyDenied carries EACCES too), so the dsh taxonomy is derived\n// from the stamp rather than from message sniffing or class checks that\n// would couple this adapter to mirage internals.\nfunction codeFor(err: unknown): FsErrorCode {\n if (isMissingPath(err)) return 'FS_NOT_FOUND'\n switch (codeOf(err)) {\n case 'EISDIR':\n return 'FS_NOT_REGULAR_FILE'\n case 'ENOTDIR':\n return 'FS_NOT_DIRECTORY'\n case 'EACCES':\n case 'EPERM':\n return 'FS_PERMISSION_DENIED'\n default:\n return 'FS_IO_ERROR'\n }\n}\n\nexport function mapMirageError(err: unknown, operation: string, displayPath: string): FsError {\n if (err instanceof FsError) return err\n if (err instanceof DOMException && err.name === 'AbortError') {\n return new FsError(`${operation} aborted`, 'FS_ABORTED', { cause: err })\n }\n const message = err instanceof Error ? err.message : String(err)\n return new FsError(`cannot ${operation} \"${displayPath}\": ${message}`, codeFor(err), {\n cause: err,\n })\n}\n"],"mappings":"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","names":["FsError","isMissingPath","isMissingPath","FsError"]}
@@ -0,0 +1,70 @@
1
+ // src/text.ts
2
+ import { FsError } from "@deepseek-ai/dsh-fs";
3
+ var BINARY_SAMPLE_BYTES = 8192;
4
+ function decodeStrictText(bytes, displayPath) {
5
+ if (bytes.subarray(0, BINARY_SAMPLE_BYTES).includes(0)) {
6
+ throw new FsError(`cannot read "${displayPath}": binary file`, "FS_NOT_TEXT");
7
+ }
8
+ try {
9
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
10
+ } catch (error) {
11
+ throw new FsError(`cannot read "${displayPath}": invalid UTF-8 text`, "FS_NOT_TEXT", {
12
+ cause: error
13
+ });
14
+ }
15
+ }
16
+ function normalizeLineEndings(value) {
17
+ return value.replaceAll("\r\n", "\n");
18
+ }
19
+ function detectsCrlf(value) {
20
+ const sample = value.slice(0, 4096);
21
+ const crlf = sample.split("\r\n").length - 1;
22
+ const lf = sample.split("\n").length - 1 - crlf;
23
+ return crlf > lf;
24
+ }
25
+ function restoreLineEndings(value, crlf) {
26
+ return crlf ? normalizeLineEndings(value).replaceAll("\n", "\r\n") : value;
27
+ }
28
+ function countOccurrences(haystack, needle) {
29
+ if (needle.length === 0) return 0;
30
+ let count = 0;
31
+ let index = haystack.indexOf(needle);
32
+ while (index !== -1) {
33
+ count++;
34
+ index = haystack.indexOf(needle, index + needle.length);
35
+ }
36
+ return count;
37
+ }
38
+ function applyLiteralEdit(content, oldString, newString, replaceAll, displayPath) {
39
+ const matches = countOccurrences(content, oldString);
40
+ if (matches === 0) {
41
+ throw new FsError(`cannot edit "${displayPath}": oldString not found`, "FS_EDIT_NOT_FOUND");
42
+ }
43
+ if (matches > 1 && !replaceAll) {
44
+ throw new FsError(
45
+ `cannot edit "${displayPath}": oldString matches ${String(matches)} locations; pass replaceAll or a longer unique string`,
46
+ "FS_AMBIGUOUS_EDIT"
47
+ );
48
+ }
49
+ return replaceAll ? content.replaceAll(oldString, newString) : content.replace(oldString, newString);
50
+ }
51
+ function tailCap(text, maxBytes) {
52
+ const bytes = new TextEncoder().encode(text);
53
+ if (bytes.byteLength <= maxBytes) return { text, truncated: false };
54
+ let start = bytes.byteLength - maxBytes;
55
+ while (start < bytes.byteLength && ((bytes[start] ?? 0) & 192) === 128) start++;
56
+ return {
57
+ text: new TextDecoder("utf-8", { fatal: false }).decode(bytes.subarray(start)),
58
+ truncated: true
59
+ };
60
+ }
61
+
62
+ export {
63
+ decodeStrictText,
64
+ normalizeLineEndings,
65
+ detectsCrlf,
66
+ restoreLineEndings,
67
+ applyLiteralEdit,
68
+ tailCap
69
+ };
70
+ //# sourceMappingURL=chunk-CATVBGGR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/text.ts"],"sourcesContent":["// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { FsError } from '@deepseek-ai/dsh-fs'\n\nconst BINARY_SAMPLE_BYTES = 8192\n\n// dsh text semantics: a NUL in the leading sample or invalid UTF-8 is a\n// binary file, refused as FS_NOT_TEXT rather than decoded lossily (the\n// mirage facade's readFileText decodes with fatal: false, which the dsh\n// seam contract forbids).\nexport function decodeStrictText(bytes: Uint8Array, displayPath: string): string {\n if (bytes.subarray(0, BINARY_SAMPLE_BYTES).includes(0)) {\n throw new FsError(`cannot read \"${displayPath}\": binary file`, 'FS_NOT_TEXT')\n }\n try {\n return new TextDecoder('utf-8', { fatal: true }).decode(bytes)\n } catch (error: unknown) {\n throw new FsError(`cannot read \"${displayPath}\": invalid UTF-8 text`, 'FS_NOT_TEXT', {\n cause: error,\n })\n }\n}\n\nexport function normalizeLineEndings(value: string): string {\n return value.replaceAll('\\r\\n', '\\n')\n}\n\n// CRLF-majority sniff over a bounded sample, the same heuristic dsh's own\n// backends use to keep a rewritten file in its original line-ending style.\nexport function detectsCrlf(value: string): boolean {\n const sample = value.slice(0, 4096)\n const crlf = sample.split('\\r\\n').length - 1\n const lf = sample.split('\\n').length - 1 - crlf\n return crlf > lf\n}\n\nexport function restoreLineEndings(value: string, crlf: boolean): string {\n return crlf ? normalizeLineEndings(value).replaceAll('\\n', '\\r\\n') : value\n}\n\nfunction countOccurrences(haystack: string, needle: string): number {\n if (needle.length === 0) return 0\n let count = 0\n let index = haystack.indexOf(needle)\n while (index !== -1) {\n count++\n index = haystack.indexOf(needle, index + needle.length)\n }\n return count\n}\n\n// Literal replacement with dsh's edit taxonomy: zero matches and an empty\n// oldString are FS_EDIT_NOT_FOUND, several matches without replaceAll is\n// FS_AMBIGUOUS_EDIT.\nexport function applyLiteralEdit(\n content: string,\n oldString: string,\n newString: string,\n replaceAll: boolean,\n displayPath: string,\n): string {\n const matches = countOccurrences(content, oldString)\n if (matches === 0) {\n throw new FsError(`cannot edit \"${displayPath}\": oldString not found`, 'FS_EDIT_NOT_FOUND')\n }\n if (matches > 1 && !replaceAll) {\n throw new FsError(\n `cannot edit \"${displayPath}\": oldString matches ${String(matches)} locations; pass replaceAll or a longer unique string`,\n 'FS_AMBIGUOUS_EDIT',\n )\n }\n return replaceAll\n ? content.replaceAll(oldString, newString)\n : content.replace(oldString, newString)\n}\n\n// Byte-accurate tail cap: keep the LAST maxBytes bytes of the encoded text,\n// re-aligned to a UTF-8 sequence boundary so the kept tail still decodes.\nexport function tailCap(text: string, maxBytes: number): { text: string; truncated: boolean } {\n const bytes = new TextEncoder().encode(text)\n if (bytes.byteLength <= maxBytes) return { text, truncated: false }\n let start = bytes.byteLength - maxBytes\n while (start < bytes.byteLength && ((bytes[start] ?? 0) & 0xc0) === 0x80) start++\n return {\n text: new TextDecoder('utf-8', { fatal: false }).decode(bytes.subarray(start)),\n truncated: true,\n }\n}\n"],"mappings":";AAcA,SAAS,eAAe;AAExB,IAAM,sBAAsB;AAMrB,SAAS,iBAAiB,OAAmB,aAA6B;AAC/E,MAAI,MAAM,SAAS,GAAG,mBAAmB,EAAE,SAAS,CAAC,GAAG;AACtD,UAAM,IAAI,QAAQ,gBAAgB,WAAW,kBAAkB,aAAa;AAAA,EAC9E;AACA,MAAI;AACF,WAAO,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,OAAO,KAAK;AAAA,EAC/D,SAAS,OAAgB;AACvB,UAAM,IAAI,QAAQ,gBAAgB,WAAW,yBAAyB,eAAe;AAAA,MACnF,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AACF;AAEO,SAAS,qBAAqB,OAAuB;AAC1D,SAAO,MAAM,WAAW,QAAQ,IAAI;AACtC;AAIO,SAAS,YAAY,OAAwB;AAClD,QAAM,SAAS,MAAM,MAAM,GAAG,IAAI;AAClC,QAAM,OAAO,OAAO,MAAM,MAAM,EAAE,SAAS;AAC3C,QAAM,KAAK,OAAO,MAAM,IAAI,EAAE,SAAS,IAAI;AAC3C,SAAO,OAAO;AAChB;AAEO,SAAS,mBAAmB,OAAe,MAAuB;AACvE,SAAO,OAAO,qBAAqB,KAAK,EAAE,WAAW,MAAM,MAAM,IAAI;AACvE;AAEA,SAAS,iBAAiB,UAAkB,QAAwB;AAClE,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,MAAI,QAAQ;AACZ,MAAI,QAAQ,SAAS,QAAQ,MAAM;AACnC,SAAO,UAAU,IAAI;AACnB;AACA,YAAQ,SAAS,QAAQ,QAAQ,QAAQ,OAAO,MAAM;AAAA,EACxD;AACA,SAAO;AACT;AAKO,SAAS,iBACd,SACA,WACA,WACA,YACA,aACQ;AACR,QAAM,UAAU,iBAAiB,SAAS,SAAS;AACnD,MAAI,YAAY,GAAG;AACjB,UAAM,IAAI,QAAQ,gBAAgB,WAAW,0BAA0B,mBAAmB;AAAA,EAC5F;AACA,MAAI,UAAU,KAAK,CAAC,YAAY;AAC9B,UAAM,IAAI;AAAA,MACR,gBAAgB,WAAW,wBAAwB,OAAO,OAAO,CAAC;AAAA,MAClE;AAAA,IACF;AAAA,EACF;AACA,SAAO,aACH,QAAQ,WAAW,WAAW,SAAS,IACvC,QAAQ,QAAQ,WAAW,SAAS;AAC1C;AAIO,SAAS,QAAQ,MAAc,UAAwD;AAC5F,QAAM,QAAQ,IAAI,YAAY,EAAE,OAAO,IAAI;AAC3C,MAAI,MAAM,cAAc,SAAU,QAAO,EAAE,MAAM,WAAW,MAAM;AAClE,MAAI,QAAQ,MAAM,aAAa;AAC/B,SAAO,QAAQ,MAAM,gBAAgB,MAAM,KAAK,KAAK,KAAK,SAAU,IAAM;AAC1E,SAAO;AAAA,IACL,MAAM,IAAI,YAAY,SAAS,EAAE,OAAO,MAAM,CAAC,EAAE,OAAO,MAAM,SAAS,KAAK,CAAC;AAAA,IAC7E,WAAW;AAAA,EACb;AACF;","names":[]}