toolcraft 0.0.765 → 0.0.767

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.765",
136
+ "version": "0.0.767",
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.765",
146
+ "version": "0.0.767",
147
147
  "license": "MIT"
148
148
  },
149
149
  {
@@ -133,7 +133,7 @@
133
133
  },
134
134
  {
135
135
  "name": "toolcraft",
136
- "version": "0.0.765",
136
+ "version": "0.0.767",
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.765",
146
+ "version": "0.0.767",
147
147
  "license": "MIT"
148
148
  },
149
149
  {
@@ -86,10 +86,11 @@ 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
+ | Enumerate directories lazily | Optional `iterateDirectory`; memory and native backends, with mount/overlay/readonly/quota/device/scoped delegation; backend order, no sorted-array fallback |
90
90
  | Stream bytes | Optional `readStream`, `writeStream`, using async iterables of byte chunks |
91
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 |
92
92
  | Publish immutable objects atomically | Optional `publishFileConditional` with opaque identity/version stats and authoritative compare-and-publish |
93
+ | Publish a checked source set | Optional `publishStagedFileSet` publishes a staged file and retires sibling entries in one backend commit; mount views require all entries on the same supporting backend |
93
94
  | Compare backing entries | Optional `compareEntry`, returning `same`, `distinct`, or `unknown` |
94
95
 
95
96
  All required methods must exist, but a backend may reject an operation with `FsError`, such as `EROFS` for a write or `ENOTSUP` for unsupported semantics. Check optional methods and `capabilities` rather than assuming every backend behaves like a local disk. Errors expose `code` and may include `syscall`, `path`, `dest`, and `cause`.
@@ -1,4 +1,4 @@
1
- import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
1
+ import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
3
  import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
4
4
  export interface MountFileSystemOptions {
@@ -40,6 +40,7 @@ export declare class MountFileSystem implements FileSystem {
40
40
  stat(path: string, options?: FsOptions): Promise<FileStat>;
41
41
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
42
42
  readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
43
+ iterateDirectory(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
43
44
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
44
45
  rmdir(path: string, options?: FsOptions): Promise<void>;
45
46
  unlink(path: string, options?: FsOptions): Promise<void>;
@@ -55,6 +56,7 @@ export declare class MountFileSystem implements FileSystem {
55
56
  removeTreeConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
56
57
  createStagedFile(path: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
57
58
  publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
59
+ publishStagedFileSet(staging: FileStaging, destination: string, supplied: PublishStagedFileSetOptions): Promise<FileStat>;
58
60
  removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
59
61
  prepareDirectory(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
60
62
  rename(source: string, destination: string, options?: RenameOptions): Promise<void>;
@@ -483,7 +483,7 @@ export class MountFileSystem {
483
483
  release = await this.acquireNamespace(options, [
484
484
  "rename", "symlink", "link", "unlink", "rm", "rmdir",
485
485
  "removeEntryConditional", "removeFileConditional", "removeTreeConditional",
486
- "publishStagedFile", "removeStagedFile",
486
+ "publishStagedFile", "publishStagedFileSet", "removeStagedFile",
487
487
  ].includes(syscall));
488
488
  const result = await action();
489
489
  if (!preserveReceipt)
@@ -802,6 +802,76 @@ export class MountFileSystem {
802
802
  return [...entries.values()].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
803
803
  });
804
804
  }
805
+ async *iterateDirectory(path, options = {}) {
806
+ try {
807
+ const location = await this.operation("iterateDirectory", path, options, async () => {
808
+ const resolved = await this.resolve(path, options);
809
+ if (resolved.stat?.type !== "directory")
810
+ fail("ENOTDIR");
811
+ return resolved;
812
+ });
813
+ // Only configured mount children are retained, never backend entries.
814
+ const mounted = new Set();
815
+ for (const mount of this.mounts) {
816
+ if (mount.path !== location.path && within(location.path, mount.path)) {
817
+ const suffix = mount.path.slice(location.path === "/" ? 1 : location.path.length + 1);
818
+ mounted.add(suffix.split("/")[0]);
819
+ }
820
+ }
821
+ const admit = async () => {
822
+ const current = await this.resolve(path, options);
823
+ if (current.path !== location.path || current.mount !== location.mount ||
824
+ current.local !== location.local || current.synthetic !== location.synthetic ||
825
+ current.stat?.type !== "directory")
826
+ fail("EBUSY");
827
+ };
828
+ if (!location.synthetic) {
829
+ const iterator = await this.operation("iterateDirectory", path, options, async () => {
830
+ await admit();
831
+ const backend = location.mount.backend;
832
+ if (!backend.iterateDirectory)
833
+ fail("ENOTSUP");
834
+ return backend.iterateDirectory(location.local, options)[Symbol.asyncIterator]();
835
+ }, undefined, true); // Transfer ownership before checking cancellation on the first pull.
836
+ // Admission covers each backend pull, but never a consumer pause. In
837
+ // particular, caller-backed traversal may publish scratch files between pulls.
838
+ let complete = false, failed = false;
839
+ try {
840
+ while (true) {
841
+ const result = await this.operation("iterateDirectory", path, options, async () => {
842
+ await admit();
843
+ return iterator.next();
844
+ });
845
+ if (result.done) {
846
+ complete = true;
847
+ break;
848
+ }
849
+ const { name, type } = result.value;
850
+ if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\0"))
851
+ fail("EIO");
852
+ if (!mounted.has(name))
853
+ yield { name, type };
854
+ }
855
+ }
856
+ catch (error) {
857
+ failed = true;
858
+ throw error;
859
+ }
860
+ finally {
861
+ if (!complete)
862
+ await finishCleanup(() => iterator.return?.(), failed);
863
+ }
864
+ }
865
+ for (const name of mounted) {
866
+ await this.operation("iterateDirectory", path, options, admit);
867
+ yield { name, type: "directory" };
868
+ }
869
+ options.signal?.throwIfAborted();
870
+ }
871
+ catch (error) {
872
+ throw this.error(error, "iterateDirectory", path, options);
873
+ }
874
+ }
805
875
  mkdir(path, options = {}) {
806
876
  return this.operation("mkdir", path, options, async () => {
807
877
  const directories = new Map();
@@ -1069,6 +1139,83 @@ export class MountFileSystem {
1069
1139
  });
1070
1140
  }, destination, true);
1071
1141
  }
1142
+ async publishStagedFileSet(staging, destination, supplied) {
1143
+ const entry = (value) => ({ path: value.path, stat: snapshotStat(value.stat) });
1144
+ staging = { parent: entry(staging.parent), directory: entry(staging.directory), file: entry(staging.file) };
1145
+ const options = {
1146
+ ...supplied, parent: snapshotStat(supplied.parent),
1147
+ destination: supplied.destination === null ? null : snapshotStat(supplied.destination),
1148
+ companions: supplied.companions.map(value => ({ ...value, expected: value.expected === null ? null : snapshotStat(value.expected) })),
1149
+ ...(supplied.ancestors === undefined ? {} : { ancestors: snapshotDirectoryAncestry(supplied.ancestors) }),
1150
+ };
1151
+ return this.operation("publishStagedFileSet", staging.file.path, options, async () => {
1152
+ validatePath(destination);
1153
+ if (normalizePath(globalPath(destination)) !== destination || destination === "/")
1154
+ fail("EINVAL");
1155
+ const parent = destination.slice(0, destination.lastIndexOf("/")) || "/";
1156
+ const paths = new Set([destination]);
1157
+ for (const companion of options.companions) {
1158
+ validatePath(companion.path);
1159
+ if (normalizePath(globalPath(companion.path)) !== companion.path || paths.has(companion.path) ||
1160
+ (companion.path.slice(0, companion.path.lastIndexOf("/")) || "/") !== parent)
1161
+ fail("EINVAL");
1162
+ paths.add(companion.path);
1163
+ }
1164
+ const ancestors = options.ancestors;
1165
+ if (ancestors && ancestors.at(-1)?.path !== parent)
1166
+ fail("EINVAL");
1167
+ const guard = ancestors ? await this.prepareAncestry(ancestors, options) : undefined;
1168
+ const local = await this.localStaging(staging, options);
1169
+ const target = await this.resolve(destination, options, { followFinal: false, entry: true, allowMissing: true });
1170
+ if (this.protected(target.path))
1171
+ fail("EBUSY");
1172
+ if (local.mount !== target.mount)
1173
+ fail("EXDEV");
1174
+ if (ancestors && target.path !== destination)
1175
+ fail("EAGAIN");
1176
+ this.mutable(target);
1177
+ const backend = local.mount.backend;
1178
+ if (!backend.publishStagedFileSet)
1179
+ fail("ENOTSUP");
1180
+ await requireOwnedMutation(backend, local.staging.directory.path, "atomicFileStaging", options);
1181
+ await requireOwnedMutation(backend, target.local, "atomicFileStaging", options, options.destination === null);
1182
+ const companions = [];
1183
+ for (const companion of options.companions) {
1184
+ const resolved = await this.resolve(companion.path, options, { followFinal: false, entry: true, allowMissing: true });
1185
+ if (this.protected(resolved.path))
1186
+ fail("EBUSY");
1187
+ if (resolved.mount !== target.mount)
1188
+ fail("EXDEV");
1189
+ this.mutable(resolved);
1190
+ companions.push({ ...companion, path: resolved.local });
1191
+ }
1192
+ let localAncestors;
1193
+ if (ancestors) {
1194
+ const first = ancestors.findIndex(value => value.path === target.mount.path);
1195
+ if (first < 0)
1196
+ fail("EINVAL");
1197
+ localAncestors = ancestors.slice(first).map(value => ({
1198
+ path: target.mount.path === "/" ? value.path : value.path.slice(target.mount.path.length) || "/", stat: value.stat,
1199
+ }));
1200
+ }
1201
+ const callerGuard = options.commitGuard;
1202
+ if (guard || callerGuard !== undefined)
1203
+ await requireOwnedMutation(backend, target.local, "guardedStagingPublication", options, options.destination === null);
1204
+ // One backend performs all version checks and the indivisible commit.
1205
+ // The namespace lock covers admission through its committed receipt.
1206
+ return snapshotStat(await backend.publishStagedFileSet(local.staging, target.local, {
1207
+ ...options, companions,
1208
+ ...(localAncestors === undefined ? {} : { ancestors: localAncestors }),
1209
+ ...(guard || callerGuard !== undefined ? { commitGuard: () => {
1210
+ if (callerGuard !== undefined)
1211
+ runStagingGuard(callerGuard);
1212
+ if (guard)
1213
+ runStagingGuard(guard);
1214
+ return true;
1215
+ } } : {}),
1216
+ }));
1217
+ }, destination, true);
1218
+ }
1072
1219
  removeStagedFile(staging, options = {}) {
1073
1220
  return this.operation("removeStagedFile", staging.directory.path, options, async () => {
1074
1221
  const local = await this.localStaging(staging, options);
@@ -64,6 +64,7 @@ export declare class OverlayFileSystem implements FileSystem {
64
64
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
65
65
  compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").EntryComparison>;
66
66
  readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
67
+ iterateDirectory(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
67
68
  realpath(path: string, options?: FsOptions): Promise<string>;
68
69
  access(path: string, requestedMode?: number, options?: FsOptions): Promise<void>;
69
70
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
@@ -760,6 +760,72 @@ export class OverlayFileSystem {
760
760
  const limit = directoryEntryLimit(options, path);
761
761
  return this.run(options, async () => this.listing(await this.required(path, options), options, limit), false);
762
762
  }
763
+ async *iterateDirectory(path, options = {}) {
764
+ const directory = await this.run(options, async () => {
765
+ const entry = await this.required(path, options);
766
+ if (entry.stat.type !== "directory")
767
+ fail("ENOTDIR", path);
768
+ this.permission(entry, 4);
769
+ return entry.path;
770
+ }, false);
771
+ const admit = async () => {
772
+ const current = await this.required(path, options);
773
+ if (current.path !== directory || current.stat.type !== "directory")
774
+ fail("EBUSY", path);
775
+ this.permission(current, 4);
776
+ };
777
+ for (const backend of [this.#upper, this.#lower]) {
778
+ const iterator = await this.run(options, async () => {
779
+ await admit();
780
+ if (backend === this.#lower && (!await this.lowerVisible(directory, options) || this.opaque.has(directory)))
781
+ return undefined;
782
+ if ((await this.maybeStat(backend, directory, options))?.type !== "directory")
783
+ return undefined;
784
+ if (!backend.iterateDirectory)
785
+ fail("ENOTSUP", directory);
786
+ return backend.iterateDirectory(directory, options)[Symbol.asyncIterator]();
787
+ }, false);
788
+ if (!iterator)
789
+ continue;
790
+ let complete = false, failed = false;
791
+ try {
792
+ while (true) {
793
+ const result = await this.run(options, async () => {
794
+ await admit();
795
+ if (backend === this.#lower && (!await this.lowerVisible(directory, options) || this.opaque.has(directory)))
796
+ fail("EBUSY", path);
797
+ const next = await iterator.next();
798
+ options.signal?.throwIfAborted();
799
+ if (next.done)
800
+ return { done: true };
801
+ const name = next.value.name;
802
+ if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\0"))
803
+ fail("EIO", directory, "invalid backend directory entry");
804
+ const child = await this.lookup(directory === "/" ? `/${name}` : `${directory}/${name}`, options);
805
+ options.signal?.throwIfAborted();
806
+ // A lower name shadowed by upper storage is skipped by lookup,
807
+ // without retaining a directory-sized set of previously seen names.
808
+ return { done: false, entry: child?.backend === backend ? { name, type: child.stat.type } : undefined };
809
+ }, false);
810
+ if (result.done) {
811
+ complete = true;
812
+ break;
813
+ }
814
+ if (result.entry)
815
+ yield result.entry;
816
+ }
817
+ }
818
+ catch (error) {
819
+ failed = true;
820
+ throw error;
821
+ }
822
+ finally {
823
+ if (!complete)
824
+ await finishCleanup(() => iterator.return?.(), failed);
825
+ }
826
+ }
827
+ options.signal?.throwIfAborted();
828
+ }
763
829
  async realpath(path, options = {}) {
764
830
  return this.run(options, async () => (await this.required(path, options)).path, false);
765
831
  }
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.765",
11
+ "version": "0.0.767",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.765",
3
+ "version": "0.0.767",
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.765",
3
+ "version": "0.0.767",
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.765",
178
+ "toolcraft-schema": "0.0.767",
179
179
  "toolcraft-design": "*",
180
180
  "@poe-code/frontmatter": "*",
181
181
  "@poe-code/agent-mcp-config": "*",