toolcraft 0.0.766 → 0.0.768

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.766",
136
+ "version": "0.0.768",
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.766",
146
+ "version": "0.0.768",
147
147
  "license": "MIT"
148
148
  },
149
149
  {
@@ -133,7 +133,7 @@
133
133
  },
134
134
  {
135
135
  "name": "toolcraft",
136
- "version": "0.0.766",
136
+ "version": "0.0.768",
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.766",
146
+ "version": "0.0.768",
147
147
  "license": "MIT"
148
148
  },
149
149
  {
@@ -86,7 +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 mount/overlay/readonly/quota/device/scoped delegation; backend order, no sorted-array fallback |
89
+ | Enumerate directories lazily | Optional `iterateDirectory`; memory, native and paged S3 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 |
@@ -109,6 +109,7 @@ export declare class S3FileSystem implements FileSystem {
109
109
  appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
110
110
  mkdir(input: string, options?: MkdirOptions): Promise<void>;
111
111
  readdir(input: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
112
+ iterateDirectory(input: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
112
113
  private tree;
113
114
  rmdir(input: string, options?: FsOptions): Promise<void>;
114
115
  rm(input: string, options?: RemoveOptions): Promise<void>;
@@ -341,25 +341,33 @@ export class S3FileSystem {
341
341
  if (key.endsWith("/") && object.Size !== 0)
342
342
  fail("ENOTSUP", "listObjectsV2", path, "nonempty slash-suffixed objects cannot be treated as directories");
343
343
  }
344
- async *pages(prefix, path, options, delimiter, maxKeys = this.pageSize) {
345
- const tokens = new Set();
346
- let token;
347
- let entries = 0;
344
+ async *pages(prefix, path, options, delimiter, maxKeys = this.pageSize, rememberTokens = true) {
345
+ // Buffered callers preserve exact repeated-token rejection. Lazy traversal
346
+ // detects continuation cycles without retaining the whole token history.
347
+ const tokens = rememberTokens ? new Set() : undefined;
348
+ let token, checkpoint;
349
+ let entries = 0, pageCount = 0, span = 0, power = 1;
348
350
  do {
349
351
  const page = await this.page(prefix, path, options, delimiter, token, maxKeys);
350
352
  entries += (page.Contents?.length ?? 0) + (page.CommonPrefixes?.length ?? 0);
351
- if (entries > this.maxListEntries || tokens.size >= this.maxListEntries)
353
+ if (entries > this.maxListEntries || ++pageCount > this.maxListEntries)
352
354
  fail("EFBIG", "listObjectsV2", path, "listing exceeds maxListEntries");
355
+ const truncated = page.IsTruncated, next = page.NextContinuationToken;
353
356
  yield page;
354
- if (!page.IsTruncated)
357
+ if (!truncated)
355
358
  return;
356
- token = page.NextContinuationToken;
357
- if (typeof token !== "string" || !token || tokens.has(token))
359
+ if (typeof next !== "string" || !next || next === token || next === checkpoint || tokens?.has(next))
358
360
  fail("EIO", "listObjectsV2", path, "missing or repeated continuation token");
359
- tokens.add(token);
361
+ if (++span === power) {
362
+ checkpoint = next;
363
+ span = 0;
364
+ power = Math.min(Number.MAX_SAFE_INTEGER, power * 2);
365
+ }
366
+ tokens?.add(next);
367
+ token = next;
360
368
  } while (true);
361
369
  }
362
- async inspect(path, options) {
370
+ async inspect(path, options, rememberTokens = true) {
363
371
  if (path === "/") {
364
372
  const listing = await this.page(this.prefix, path, options, undefined, undefined, 1);
365
373
  const marker = this.prefix && listing.Contents?.some(object => object.Key === this.prefix)
@@ -375,7 +383,7 @@ export class S3FileSystem {
375
383
  const directoryKey = this.directoryKey(path);
376
384
  let directory = false;
377
385
  let hasMarker = false;
378
- for await (const children of this.pages(directoryKey, path, options, undefined, 1)) {
386
+ for await (const children of this.pages(directoryKey, path, options, undefined, 1, rememberTokens)) {
379
387
  if ((children.Contents?.length ?? 0) > 0) {
380
388
  directory = true;
381
389
  hasMarker = children.Contents.some(object => object.Key === directoryKey);
@@ -393,7 +401,7 @@ export class S3FileSystem {
393
401
  return { stat: this.makeStat("directory", marker), ...(marker ? { metadata: marker } : {}) };
394
402
  return undefined;
395
403
  }
396
- async lookup(path, options) {
404
+ async lookup(path, options, rememberTokens = true) {
397
405
  const ancestors = [];
398
406
  for (let parent = posix.dirname(path); path !== "/"; parent = posix.dirname(parent)) {
399
407
  ancestors.unshift(parent);
@@ -403,13 +411,13 @@ export class S3FileSystem {
403
411
  for (const parent of ancestors) {
404
412
  if (parent === "/" && this.prefix === "")
405
413
  continue;
406
- const ancestor = await this.inspect(parent, options);
414
+ const ancestor = await this.inspect(parent, options, rememberTokens);
407
415
  if (!ancestor)
408
416
  fail("ENOENT", "stat", parent);
409
417
  if (ancestor.stat.type !== "directory")
410
418
  fail("ENOTDIR", "stat", parent);
411
419
  }
412
- return this.inspect(path, options);
420
+ return this.inspect(path, options, rememberTokens);
413
421
  }
414
422
  requireDirectorySuffix(input, info) {
415
423
  if (input.endsWith("/") && info?.stat.type === "file")
@@ -596,6 +604,37 @@ export class S3FileSystem {
596
604
  }
597
605
  return [...entries.values()].sort((left, right) => compareKeys(left.name, right.name));
598
606
  }
607
+ async *iterateDirectory(input, options = {}) {
608
+ const path = this.path(input);
609
+ const directory = await this.lookup(path, options, false);
610
+ if (!directory)
611
+ fail("ENOENT", "iterateDirectory", path);
612
+ if (directory.stat.type !== "directory")
613
+ fail("ENOTDIR", "iterateDirectory", path);
614
+ const prefix = this.directoryKey(path);
615
+ for await (const page of this.pages(prefix, path, options, "/", this.pageSize, false)) {
616
+ // Snapshot one bounded service page before yielding to caller code.
617
+ if ((page.Contents?.length ?? 0) + (page.CommonPrefixes?.length ?? 0) > this.pageSize)
618
+ fail("EIO", "iterateDirectory", path, "transport exceeded requested page size");
619
+ const entries = [];
620
+ for (const item of page.Contents ?? [])
621
+ if (item.Key !== prefix)
622
+ entries.push({ name: item.Key.slice(prefix.length), type: "file" });
623
+ for (const item of page.CommonPrefixes ?? [])
624
+ entries.push({ name: item.Prefix.slice(prefix.length, -1), type: "directory" });
625
+ for (const entry of entries) {
626
+ this.checkAbort(options, "iterateDirectory", path);
627
+ if (!entry.name || entry.name.includes("/"))
628
+ fail("EIO", "iterateDirectory", path, "invalid direct child in delimited listing");
629
+ // Revalidation preserves file/prefix collision detection across pages
630
+ // and follows the backend's live namespace without retaining all names.
631
+ const child = await this.lookup(path === "/" ? `/${entry.name}` : `${path}/${entry.name}`, options, false);
632
+ this.checkAbort(options, "iterateDirectory", path);
633
+ if (child)
634
+ yield { name: entry.name, type: child.stat.type };
635
+ }
636
+ }
637
+ }
599
638
  async tree(path, options) {
600
639
  const objects = new Map();
601
640
  for await (const page of this.pages(this.directoryKey(path), path, options)) {
@@ -444,7 +444,7 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
444
444
  return executeDispatch(args);
445
445
  if (operations.has(property)) {
446
446
  const optionIndex = ["compareEntry", "createStagedFile", "utimes"].includes(String(property)) ? 3
447
- : ["writeFile", "appendFile", "writeStream", "rename", "copyFile", "link", "symlink", "access", "chmod", "truncate", "resizeFile", "publishFileConditional", "writeFileConditional", "publishStagedFile"].includes(String(property)) ? 2 : 1;
447
+ : ["writeFile", "appendFile", "writeStream", "rename", "copyFile", "link", "symlink", "access", "chmod", "truncate", "resizeFile", "publishFileConditional", "writeFileConditional", "publishStagedFile", "publishStagedFileSet"].includes(String(property)) ? 2 : 1;
448
448
  args[optionIndex] = scopeTransportOptions((args[optionIndex] ?? {}), admit, credit);
449
449
  }
450
450
  return runScopedTransportBudget(admit, () => executeDispatch(args), credit);
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.766",
11
+ "version": "0.0.768",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.766",
3
+ "version": "0.0.768",
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.766",
3
+ "version": "0.0.768",
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.766",
178
+ "toolcraft-schema": "0.0.768",
179
179
  "toolcraft-design": "*",
180
180
  "@poe-code/frontmatter": "*",
181
181
  "@poe-code/agent-mcp-config": "*",