toolcraft 0.0.762 → 0.0.763

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/composition.json CHANGED
@@ -133,7 +133,7 @@
133
133
  },
134
134
  {
135
135
  "name": "toolcraft",
136
- "version": "0.0.762",
136
+ "version": "0.0.763",
137
137
  "license": "MIT"
138
138
  },
139
139
  {
@@ -143,7 +143,7 @@
143
143
  },
144
144
  {
145
145
  "name": "toolcraft-schema",
146
- "version": "0.0.762",
146
+ "version": "0.0.763",
147
147
  "license": "MIT"
148
148
  },
149
149
  {
@@ -133,7 +133,7 @@
133
133
  },
134
134
  {
135
135
  "name": "toolcraft",
136
- "version": "0.0.762",
136
+ "version": "0.0.763",
137
137
  "license": "MIT"
138
138
  },
139
139
  {
@@ -143,7 +143,7 @@
143
143
  },
144
144
  {
145
145
  "name": "toolcraft-schema",
146
- "version": "0.0.762",
146
+ "version": "0.0.763",
147
147
  "license": "MIT"
148
148
  },
149
149
  {
@@ -86,6 +86,7 @@ cause. Preserve cancellation/control exceptions separately. Cross-family
86
86
  | Change the namespace | `mkdir`, `rm`, `rename`, `copyFile` |
87
87
  | Remove an empty directory | Optional `rmdir`; never a recursive-delete fallback |
88
88
  | Links and metadata | Optional `readlink`, `symlink`, `link`, `chmod`, `utimes`, `truncate` |
89
+ | Enumerate directories lazily | Optional `iterateDirectory`; memory and native backends, with readonly/quota/device/scoped delegation; backend order, no sorted-array fallback |
89
90
  | Stream bytes | Optional `readStream`, `writeStream`, using async iterables of byte chunks |
90
91
  | Retain an open file | Optional `open` with positioned I/O and synchronization; memory and real backends support `noFollow: true` to atomically refuse a final symlink |
91
92
  | Publish immutable objects atomically | Optional `publishFileConditional` with opaque identity/version stats and authoritative compare-and-publish |
@@ -352,6 +352,11 @@ export interface FileSystem {
352
352
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
353
353
  compareEntry?(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<EntryComparison>;
354
354
  readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
355
+ /** Optional bounded, lazy directory enumeration in backend-defined order.
356
+ * No complete listing/sort fallback. Consumers must retire the iterator on
357
+ * early exit. Namespace changes may reject or follow the backend's live view;
358
+ * this is not a tree snapshot or a retained path-authority grant. */
359
+ iterateDirectory?(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
355
360
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
356
361
  rm(path: string, options?: RemoveOptions): Promise<void>;
357
362
  /** Atomic final-entry removal; every directory is refused, including raced replacements. */
@@ -25,6 +25,7 @@ export declare class DeviceFileSystem implements FileSystem {
25
25
  writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
26
26
  appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
27
27
  writeStream(path: string, source: ByteSource, options?: WriteFileOptions): Promise<void>;
28
+ iterateDirectory(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
28
29
  readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
29
30
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
30
31
  rm(path: string, options?: RemoveOptions): Promise<void>;
@@ -490,6 +490,40 @@ export class DeviceFileSystem {
490
490
  throw new FsError("ENOTSUP", { path });
491
491
  await this.#filesystem.writeStream(path, source, options);
492
492
  }
493
+ async *iterateDirectory(path, options = {}) {
494
+ const resolved = await this.#resolve(path, options);
495
+ if (resolved === nullPath)
496
+ throw new FsError("ENOTDIR", { syscall: "iterateDirectory", path });
497
+ const virtual = resolved === "/" ? "dev" : resolved === deviceDirectory ? "null" : undefined;
498
+ if (!this.#filesystem.iterateDirectory)
499
+ throw new FsError("ENOTSUP", { syscall: "iterateDirectory", path });
500
+ let seen = false;
501
+ try {
502
+ for await (const entry of this.#filesystem.iterateDirectory(virtual ? resolved : path, options)) {
503
+ options.signal?.throwIfAborted();
504
+ if (entry.name === "." || entry.name === "..")
505
+ continue;
506
+ if (!entry.name || entry.name.includes("/") || entry.name.includes("\0"))
507
+ throw new FsError("EIO", { path, message: "invalid directory entry" });
508
+ if (entry.name === virtual) {
509
+ if (seen)
510
+ continue;
511
+ seen = true;
512
+ yield { name: virtual, type: virtual === "dev" ? "directory" : "character" };
513
+ }
514
+ else
515
+ yield { name: entry.name, type: entry.type };
516
+ }
517
+ }
518
+ catch (error) {
519
+ options.signal?.throwIfAborted();
520
+ if (!virtual || !isFsError(error, "ENOENT") && !isFsError(error, "ENOTDIR"))
521
+ throw error;
522
+ }
523
+ options.signal?.throwIfAborted();
524
+ if (virtual && !seen)
525
+ yield { name: virtual, type: virtual === "dev" ? "directory" : "character" };
526
+ }
493
527
  readdir(path, options = {}) {
494
528
  const fast = this.#tryResolveSync(path, options);
495
529
  if (fast !== undefined && fast !== "/") {
@@ -134,6 +134,7 @@ export declare class MemoryFileSystem implements FileSystem {
134
134
  appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
135
135
  stat(path: string, options?: FsOptions): Promise<FileStat>;
136
136
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
137
+ iterateDirectory(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
137
138
  readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
138
139
  tryMkdirFastSync(path: string, recursive: boolean, mode: number | undefined): boolean;
139
140
  tryRmFastSync(path: string, recursive: boolean, force: boolean): boolean;
@@ -2508,6 +2508,26 @@ export class MemoryFileSystem {
2508
2508
  ownedStats.set(stat, { filesystem: this, path, root: this.root });
2509
2509
  return stat;
2510
2510
  }
2511
+ async *iterateDirectory(path, options = {}) {
2512
+ options.signal?.throwIfAborted();
2513
+ const root = this.root, node = this.resolveNode(path, "iterateDirectory", true);
2514
+ if (node.type !== "directory")
2515
+ this.fail("ENOTDIR", "iterateDirectory", path);
2516
+ this.permission(node, 4, "iterateDirectory", path);
2517
+ const revision = node.revision, ino = node.ino;
2518
+ const check = () => {
2519
+ options.signal?.throwIfAborted();
2520
+ if (this.root !== root || node.ino !== ino || node.revision !== revision || this.resolveNode(path, "iterateDirectory", true) !== node)
2521
+ this.fail("EBUSY", "iterateDirectory", path);
2522
+ this.permission(node, 4, "iterateDirectory", path);
2523
+ };
2524
+ node.atimeMs = Date.now();
2525
+ for (const [name, entry] of node.entries) {
2526
+ check();
2527
+ yield { name, type: entry.type };
2528
+ }
2529
+ check();
2530
+ }
2511
2531
  async readdir(path, options = {}) {
2512
2532
  const limit = directoryEntryLimit(options, path);
2513
2533
  const node = this.resolveNode(path, "readdir", true);
@@ -13,6 +13,7 @@ export declare class ReadOnlyFileSystem implements FileSystem {
13
13
  stat(path: string, options?: FsOptions): Promise<FileStat>;
14
14
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
15
15
  compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").EntryComparison>;
16
+ iterateDirectory(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
16
17
  readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
17
18
  realpath(path: string, options?: FsOptions): Promise<string>;
18
19
  access(path: string, mode?: number, options?: FsOptions): Promise<void>;
@@ -122,6 +122,16 @@ export class ReadOnlyFileSystem {
122
122
  compareEntry(path, peer, peerPath, options = {}) {
123
123
  return compareEntries(this, path, peer, peerPath, options);
124
124
  }
125
+ async *iterateDirectory(path, options = {}) {
126
+ options.signal?.throwIfAborted();
127
+ if (!this.#filesystem.iterateDirectory)
128
+ throw new FsError("ENOTSUP", { syscall: "iterateDirectory", path });
129
+ for await (const entry of this.#filesystem.iterateDirectory(path, options)) {
130
+ options.signal?.throwIfAborted();
131
+ yield { name: entry.name, type: entry.type };
132
+ }
133
+ options.signal?.throwIfAborted();
134
+ }
125
135
  async readdir(path, options) {
126
136
  const limit = options?.maxEntries === undefined ? undefined : directoryEntryLimit(options, path);
127
137
  const entries = await this.#filesystem.readdir(path, options);
@@ -81,6 +81,7 @@ export declare class RealFileSystem implements FileSystem {
81
81
  appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
82
82
  stat(path: string, options?: FsOptions): Promise<FileStat>;
83
83
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
84
+ iterateDirectory(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
84
85
  readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
85
86
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
86
87
  rmdir(path: string, options?: FsOptions): Promise<void>;
@@ -638,6 +638,33 @@ export class RealFileSystem {
638
638
  return fileStat(await native.lstat(target, { bigint: true }));
639
639
  });
640
640
  }
641
+ async *iterateDirectory(path, options = {}) {
642
+ let handle, failed = false;
643
+ try {
644
+ // Preserve the acquired handle even if cancellation arrives during open:
645
+ // the following read admission will observe it and finally will close it.
646
+ handle = await this.operation("iterateDirectory", path, options, async () => {
647
+ const target = await this.path(path, options);
648
+ options.signal?.throwIfAborted();
649
+ return native.opendir(target, { bufferSize: 1 });
650
+ }, undefined, true);
651
+ while (true) {
652
+ const entry = await this.operation("iterateDirectory", path, options, () => handle.read());
653
+ if (entry === null)
654
+ break;
655
+ yield { name: entry.name, type: fileType(entry) };
656
+ }
657
+ }
658
+ catch (error) {
659
+ failed = true;
660
+ options.signal?.throwIfAborted();
661
+ throw error;
662
+ }
663
+ finally {
664
+ if (handle)
665
+ await finishCleanup(() => handle.close(), failed);
666
+ }
667
+ }
641
668
  async readdir(path, options = {}) {
642
669
  const limit = directoryEntryLimit(options, path);
643
670
  return this.operation("readdir", path, options, async () => {
@@ -21,7 +21,7 @@ const operations = new Set([
21
21
  "prepareDirectoryAncestry", "prepareStagingResolution", "publishStagedFileSet",
22
22
  "publishFileConditional", "removeEntryConditional", "removeTreeConditional", "writeFileConditional", "removeFileConditional", "createStagedFile", "publishStagedFile", "removeStagedFile", "prepareDirectory",
23
23
  "confineExtraction", "access", "appendFile", "canonicalizeMissingTarget", "capabilitiesFor", "chmod", "compareEntry",
24
- "copyFile", "link", "lstat", "mkdir", "openReadFile", "openResizeFile", "readFile", "readStream", "readdir",
24
+ "copyFile", "link", "lstat", "mkdir", "openReadFile", "openResizeFile", "readFile", "readStream", "readdir", "iterateDirectory",
25
25
  "readlink", "realpath", "rename", "resizeFile", "rm", "rmdir", "unlink", "stat", "symlink", "truncate", "utimes",
26
26
  "writeFile", "writeStream",
27
27
  ]);
@@ -188,7 +188,7 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
188
188
  close: (options = {}) => closing ??= Promise.resolve().then(() => descriptor.close(options)),
189
189
  };
190
190
  };
191
- const wrapStream = (source, options) => {
191
+ const wrapStream = (source, options, chargeNext = false) => {
192
192
  const budgetFrames = getScopedTransportBudget();
193
193
  return {
194
194
  [Symbol.asyncIterator]() {
@@ -203,6 +203,8 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
203
203
  await closing;
204
204
  return { done: true, value: undefined };
205
205
  }
206
+ if (chargeNext)
207
+ admit(options);
206
208
  const result = await withScopedTransportBudget(budgetFrames, operation);
207
209
  assertOpen(options);
208
210
  if (closing) {
@@ -430,7 +432,11 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
430
432
  });
431
433
  return Reflect.apply(method, original, args);
432
434
  })();
435
+ if (property === "iterateDirectory")
436
+ args[1] = { ...args[1], signal: combineSignal(args[1]?.signal) };
433
437
  const result = Reflect.apply(method, original, args);
438
+ if (property === "iterateDirectory")
439
+ return wrapStream(result, args[1], true);
434
440
  return property === "readStream" ? wrapStream(result, args[1]) : result;
435
441
  };
436
442
  const dispatch = (...args) => {
@@ -528,7 +534,7 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
528
534
  ? async (...args) => ownedMutationCapabilities(original, retainedResizeCapabilities(original, await dispatch(...args)))
529
535
  : property === "openReadFile"
530
536
  ? async (...args) => wrapHandle(await dispatch(...args))
531
- : operations.has(property) && property !== "canonicalizeMissingTarget" && property !== "readStream"
537
+ : operations.has(property) && property !== "canonicalizeMissingTarget" && property !== "readStream" && property !== "iterateDirectory"
532
538
  ? (...args) => {
533
539
  try {
534
540
  const res = dispatch(...args);
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.762",
11
+ "version": "0.0.763",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.762",
3
+ "version": "0.0.763",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft",
3
- "version": "0.0.762",
3
+ "version": "0.0.763",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -175,7 +175,7 @@
175
175
  "@poe-code/terminal-text"
176
176
  ],
177
177
  "optionalDependencies": {
178
- "toolcraft-schema": "0.0.762",
178
+ "toolcraft-schema": "0.0.763",
179
179
  "toolcraft-design": "*",
180
180
  "@poe-code/frontmatter": "*",
181
181
  "@poe-code/agent-mcp-config": "*",