toolcraft 0.0.767 → 0.0.769
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 +2 -2
- package/dist/composition.json +2 -2
- package/node_modules/@poe-code/safe-fs/README.md +10 -2
- package/node_modules/@poe-code/safe-fs/dist/contracts/filesystem.d.ts +7 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/capabilities.d.ts +1 -1
- package/node_modules/@poe-code/safe-fs/dist/fs/capabilities.js +4 -2
- package/node_modules/@poe-code/safe-fs/dist/fs/devices/index.js +4 -1
- package/node_modules/@poe-code/safe-fs/dist/fs/memory/index.js +41 -1
- package/node_modules/@poe-code/safe-fs/dist/fs/mount/index.js +4 -1
- package/node_modules/@poe-code/safe-fs/dist/fs/overlay/memory-publication.js +2 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/real/index.js +2 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/s3/filesystem.d.ts +1 -0
- package/node_modules/@poe-code/safe-fs/dist/fs/s3/filesystem.js +53 -14
- package/node_modules/@poe-code/safe-fs/dist/fs/scoped.js +3 -1
- package/node_modules/@poe-code/safe-fs/dist/storage.d.ts +9 -1
- package/node_modules/@poe-code/safe-fs/dist/storage.js +81 -0
- package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +2 -2
package/composition.json
CHANGED
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"name": "toolcraft",
|
|
136
|
-
"version": "0.0.
|
|
136
|
+
"version": "0.0.769",
|
|
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.
|
|
146
|
+
"version": "0.0.769",
|
|
147
147
|
"license": "MIT"
|
|
148
148
|
},
|
|
149
149
|
{
|
package/dist/composition.json
CHANGED
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"name": "toolcraft",
|
|
136
|
-
"version": "0.0.
|
|
136
|
+
"version": "0.0.769",
|
|
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.
|
|
146
|
+
"version": "0.0.769",
|
|
147
147
|
"license": "MIT"
|
|
148
148
|
},
|
|
149
149
|
{
|
|
@@ -10,6 +10,10 @@ working data. Supply `{fs, cwd, env, signal}` and an optional page-cache count
|
|
|
10
10
|
`env.TMPDIR` or `cwd`, with conditional unlink and cleanup through `close()`.
|
|
11
11
|
Providers must support those capabilities; there is no host fallback. Use an
|
|
12
12
|
external backend for large data because memory filesystems retain spilled bytes.
|
|
13
|
+
`IntegerTable.entries()` replays records in unsigned key order with a fixed
|
|
14
|
+
16-level node stack and batches of at most 128 records. It accepts zero-filled
|
|
15
|
+
caller storage with `allocate`, `read` and `write`; mutation during traversal
|
|
16
|
+
is rejected. Branch pointers use a separate fixed 128-entry cache.
|
|
13
17
|
|
|
14
18
|
## Quickstart
|
|
15
19
|
|
|
@@ -86,7 +90,7 @@ cause. Preserve cancellation/control exceptions separately. Cross-family
|
|
|
86
90
|
| Change the namespace | `mkdir`, `rm`, `rename`, `copyFile` |
|
|
87
91
|
| Remove an empty directory | Optional `rmdir`; never a recursive-delete fallback |
|
|
88
92
|
| Links and metadata | Optional `readlink`, `symlink`, `link`, `chmod`, `utimes`, `truncate` |
|
|
89
|
-
| Enumerate directories lazily | Optional `iterateDirectory`; memory and
|
|
93
|
+
| 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
94
|
| Stream bytes | Optional `readStream`, `writeStream`, using async iterables of byte chunks |
|
|
91
95
|
| 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
96
|
| Publish immutable objects atomically | Optional `publishFileConditional` with opaque identity/version stats and authoritative compare-and-publish |
|
|
@@ -133,7 +137,11 @@ For temporary staging that must be cleaned up after its parent moves, adapters
|
|
|
133
137
|
advertising `retainedStagingCleanup` accept `createStagedFile(..., { parent,
|
|
134
138
|
retainCleanup: true })`. Use the returned `staging.cleanup.remove()` and always
|
|
135
139
|
call `staging.cleanup.close()` in `finally`. Memory and its supported wrappers
|
|
136
|
-
retain only the owned staging entries; replacement entries remain protected.
|
|
140
|
+
retain only the owned staging entries; replacement entries remain protected. When
|
|
141
|
+
`atomicStagedFileMutation` is available, `publishStagedFile(..., { preserveIdentity: true,
|
|
142
|
+
parent, destination })` updates an existing file without breaking hardlink aliases.
|
|
143
|
+
Memory and its supported wrappers adopt staged bytes and timestamps while keeping
|
|
144
|
+
the destination identity and mode; unsupported adapters reject this option.
|
|
137
145
|
Overlays backed by stock Memory filesystems support conditional file mutations,
|
|
138
146
|
directory preparation, and confined staging. Publication may omit `ancestors`
|
|
139
147
|
when the destination shares the staging parent; the overlay checks the ancestry
|
|
@@ -76,6 +76,8 @@ export interface FileSystemCapabilities {
|
|
|
76
76
|
/** Owned staging serialized within a trusted host; requires external tree isolation. */
|
|
77
77
|
readonly trustedOwnedStaging?: boolean;
|
|
78
78
|
readonly atomicFileStaging?: boolean;
|
|
79
|
+
/** Staged payload publication can atomically preserve an existing inode. */
|
|
80
|
+
readonly atomicStagedFileMutation?: boolean;
|
|
79
81
|
/** Creates a cleanup handle bound to owned staging entries, independent of ancestor paths. */
|
|
80
82
|
readonly retainedStagingCleanup?: boolean;
|
|
81
83
|
readonly retainedStagingWrite?: boolean;
|
|
@@ -277,6 +279,11 @@ export interface CreateStagedFileOptions extends FsOptions {
|
|
|
277
279
|
readonly mtimeMs?: number;
|
|
278
280
|
}
|
|
279
281
|
export interface PublishStagedFileOptions extends FsOptions {
|
|
282
|
+
/** Requires atomicStagedFileMutation and an existing regular destination.
|
|
283
|
+
* Atomically adopts staged bytes and timestamps while preserving destination
|
|
284
|
+
* identity, links and mode. Consumes the staged pathname as ordinary publication.
|
|
285
|
+
* Unsupported hosts must reject before effects, never silently rename. */
|
|
286
|
+
readonly preserveIdentity?: boolean;
|
|
280
287
|
readonly ancestors?: readonly FileStagingEntry[];
|
|
281
288
|
/** Trusted, mutation-free validation. Must return literal true synchronously;
|
|
282
289
|
* a promise is not acceptance. Requires guardedStagingPublication. */
|
|
@@ -7,4 +7,4 @@ export declare function openRetainedResizeFile(filesystem: FileSystem, path: str
|
|
|
7
7
|
export declare function readOnlyCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
|
|
8
8
|
export declare function quotaCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
|
|
9
9
|
export declare function ownedMutationCapabilities(filesystem: FileSystem, capabilities?: FileSystemCapabilities): FileSystemCapabilities;
|
|
10
|
-
export declare function requireOwnedMutation(filesystem: FileSystem, path: string, capability: "retainedStagingCleanup" | "atomicFilePublication" | "atomicEntryRemoval" | "atomicEntryRemovalReceipt" | "atomicTreeRemoval" | "atomicFileMutation" | "atomicFileStaging" | "guardedStagingPublication" | "atomicStagingAncestry" | "atomicDirectoryMetadata", options: FsOptions, create?: boolean): Promise<void>;
|
|
10
|
+
export declare function requireOwnedMutation(filesystem: FileSystem, path: string, capability: "retainedStagingCleanup" | "atomicFilePublication" | "atomicEntryRemoval" | "atomicEntryRemovalReceipt" | "atomicTreeRemoval" | "atomicFileMutation" | "atomicFileStaging" | "atomicStagedFileMutation" | "guardedStagingPublication" | "atomicStagingAncestry" | "atomicDirectoryMetadata", options: FsOptions, create?: boolean): Promise<void>;
|
|
@@ -136,7 +136,7 @@ export function readOnlyCapabilities(capabilities) {
|
|
|
136
136
|
mkdir: false, recursiveMkdir: false, remove: false, removeDirectory: false, recursiveRemove: false,
|
|
137
137
|
rename: false, copy: false, exclusiveCopy: false, truncate: false, streamingAppend: false,
|
|
138
138
|
randomAccessWrite: false, hardlinks: false, permissions: false, conditionalChmod: false, timestamps: false,
|
|
139
|
-
descriptorWriteStream: false, atomicResize: false, retainedResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
139
|
+
descriptorWriteStream: false, atomicResize: false, retainedResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicTreeRemoval: false, atomicStagedFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
140
140
|
atomicFilePublication: false, atomicRename: false, atomicRenameNoReplace: false, streamingWrite: false,
|
|
141
141
|
});
|
|
142
142
|
}
|
|
@@ -144,7 +144,7 @@ export function quotaCapabilities(capabilities) {
|
|
|
144
144
|
const streamingWrite = requireCapabilities(capabilities.write, capabilities.append, capabilities.streamingAppend, !capabilities.readOnly);
|
|
145
145
|
const streamingAppend = requireCapabilities(capabilities.append, capabilities.streamingAppend, !capabilities.readOnly);
|
|
146
146
|
const { streamingWrite: ignoredWrite, streamingAppend: ignoredAppend, ...rest } = capabilities;
|
|
147
|
-
return Object.freeze({ ...rest, synchronousStagingResolution: false, retainedStagingCleanup: false, retainedStagingWrite: false, atomicStagingAncestry: false, synchronousDirectoryValidation: false, guardedStagingPublication: false, atomicFilePublication: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
147
|
+
return Object.freeze({ ...rest, synchronousStagingResolution: false, retainedStagingCleanup: false, retainedStagingWrite: false, atomicStagingAncestry: false, synchronousDirectoryValidation: false, guardedStagingPublication: false, atomicFilePublication: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicStagedFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
148
148
|
...(streamingWrite === undefined ? {} : { streamingWrite }),
|
|
149
149
|
...(streamingAppend === undefined ? {} : { streamingAppend }),
|
|
150
150
|
});
|
|
@@ -167,6 +167,8 @@ export function ownedMutationCapabilities(filesystem, capabilities = filesystem.
|
|
|
167
167
|
if (capabilities.atomicFileStaging === true && (capabilities.readOnly === true
|
|
168
168
|
|| typeof filesystem.createStagedFile !== "function" || typeof filesystem.publishStagedFile !== "function" || typeof filesystem.removeStagedFile !== "function"))
|
|
169
169
|
(unavailable ??= {}).atomicFileStaging = false;
|
|
170
|
+
if (capabilities.atomicStagedFileMutation === true && (capabilities.atomicFileStaging !== true || unavailable?.atomicFileStaging === false))
|
|
171
|
+
(unavailable ??= {}).atomicStagedFileMutation = false;
|
|
170
172
|
if (capabilities.retainedStagingCleanup === true && (capabilities.atomicFileStaging !== true || unavailable?.atomicFileStaging === false))
|
|
171
173
|
(unavailable ??= {}).retainedStagingCleanup = false;
|
|
172
174
|
if (capabilities.retainedStagingWrite === true && (capabilities.retainedStagingCleanup !== true || unavailable?.retainedStagingCleanup === false))
|
|
@@ -20,7 +20,7 @@ const deviceCapabilities = Object.freeze({
|
|
|
20
20
|
remove: false, removeDirectory: false, recursiveRemove: false, rename: false,
|
|
21
21
|
mkdir: false, recursiveMkdir: false, symlinks: false, hardlinks: false, readlink: false,
|
|
22
22
|
permissions: false, timestamps: false, truncate: false, randomAccessWrite: false,
|
|
23
|
-
open: true, retainedStagingCleanup: false, atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
23
|
+
open: true, retainedStagingCleanup: false, atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicTreeRemoval: false, atomicStagedFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
24
24
|
atomicRename: false, atomicRenameNoReplace: false, descriptorWriteStream: true, retainedResize: true, atomicResize: false,
|
|
25
25
|
});
|
|
26
26
|
function globalCapabilities(filesystem) {
|
|
@@ -31,6 +31,7 @@ function globalCapabilities(filesystem) {
|
|
|
31
31
|
synchronousDirectoryValidation: ["prepareDirectoryAncestry"], guardedStagingPublication: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
|
|
32
32
|
synchronousStagingResolution: ["prepareStagingResolution"],
|
|
33
33
|
trustedOwnedStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "prepareDirectory"],
|
|
34
|
+
atomicStagedFileMutation: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
|
|
34
35
|
atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicEntryRemovalReceipt: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
|
|
35
36
|
streamingRead: ["readStream"], streamingWrite: ["writeStream"], retainedRead: ["openReadFile"],
|
|
36
37
|
streamingAppend: ["writeStream"], descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
|
|
@@ -667,6 +668,8 @@ export class DeviceFileSystem {
|
|
|
667
668
|
await this.#mutable(path, options, false);
|
|
668
669
|
await requireOwnedMutation(this.#filesystem, staging.directory.path, "atomicFileStaging", options);
|
|
669
670
|
await requireOwnedMutation(this.#filesystem, destination, "atomicFileStaging", options, options.destination === null);
|
|
671
|
+
if (options.preserveIdentity)
|
|
672
|
+
await requireOwnedMutation(this.#filesystem, destination, "atomicStagedFileMutation", options);
|
|
670
673
|
if (!this.#filesystem.publishStagedFile)
|
|
671
674
|
throw new FsError("ENOTSUP", { path: destination });
|
|
672
675
|
if (options.commitGuard !== undefined)
|
|
@@ -584,7 +584,7 @@ const SHARED_STOCK_MEMORY_CAPABILITIES = Object.freeze({
|
|
|
584
584
|
permissions: true,
|
|
585
585
|
timestamps: true,
|
|
586
586
|
atomicRename: true,
|
|
587
|
-
atomicFileStaging: true, retainedStagingCleanup: true, retainedStagingWrite: true, atomicStagingAncestry: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicEntryRemovalReceipt: true, atomicTreeRemoval: true,
|
|
587
|
+
atomicStagedFileMutation: true, atomicFileStaging: true, retainedStagingCleanup: true, retainedStagingWrite: true, atomicStagingAncestry: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicEntryRemovalReceipt: true, atomicTreeRemoval: true,
|
|
588
588
|
synchronousDirectoryValidation: true, synchronousStagingResolution: true, guardedStagingPublication: true,
|
|
589
589
|
atomicDirectoryMetadata: true,
|
|
590
590
|
streamingRead: true,
|
|
@@ -2245,6 +2245,7 @@ export class MemoryFileSystem {
|
|
|
2245
2245
|
}
|
|
2246
2246
|
async publishStagedFile(receipt, destination, options) {
|
|
2247
2247
|
const signal = options.signal;
|
|
2248
|
+
const preserveIdentity = options.preserveIdentity === true;
|
|
2248
2249
|
const guard = options.commitGuard;
|
|
2249
2250
|
const ancestors = options.ancestors === undefined ? undefined : snapshotDirectoryAncestry(options.ancestors);
|
|
2250
2251
|
const snapshot = (entry) => ({ path: entry.path, stat: { ...entry.stat } });
|
|
@@ -2272,9 +2273,48 @@ export class MemoryFileSystem {
|
|
|
2272
2273
|
this.fail("EINVAL", "publishStagedFile", destination);
|
|
2273
2274
|
if (target.node && target.node.type !== "file")
|
|
2274
2275
|
this.fail("EAGAIN", "publishStagedFile", destination);
|
|
2276
|
+
if (preserveIdentity) {
|
|
2277
|
+
if (this.capabilities.atomicStagedFileMutation !== true)
|
|
2278
|
+
this.fail("ENOTSUP", "publishStagedFile", destination);
|
|
2279
|
+
if (file.node?.type !== "file" || target.node?.type !== "file" || expected === null)
|
|
2280
|
+
this.fail("EINVAL", "publishStagedFile", destination);
|
|
2281
|
+
this.permission(file.parent, 3, "publishStagedFile", staging.file.path);
|
|
2282
|
+
this.permission(target.parent, 3, "publishStagedFile", destination);
|
|
2283
|
+
this.permission(target.node, 2, "publishStagedFile", destination);
|
|
2284
|
+
// Provider-owned storage is copied directly, without assembling an archive
|
|
2285
|
+
// in caller memory. Preserve retained staging readers and independent writes.
|
|
2286
|
+
this.admitSize(target.node, file.node.byteLength, "publishStagedFile", destination);
|
|
2287
|
+
const allocation = this.allocate(file.node.byteLength, "publishStagedFile", destination);
|
|
2288
|
+
try {
|
|
2289
|
+
for (let offset = 0; offset < file.node.byteLength; offset += 65536) {
|
|
2290
|
+
allocation.data.set(file.node.data.subarray(offset, offset + 65536), offset);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
catch (error) {
|
|
2294
|
+
allocation.release();
|
|
2295
|
+
throw error;
|
|
2296
|
+
}
|
|
2297
|
+
// No awaits or overridable operations occur between admission and commit.
|
|
2298
|
+
const now = Date.now();
|
|
2299
|
+
this.replaceData(target.node, allocation);
|
|
2300
|
+
this.changed(target.node, now);
|
|
2301
|
+
target.node.atimeMs = file.node.atimeMs;
|
|
2302
|
+
target.node.mtimeMs = file.node.mtimeMs;
|
|
2303
|
+
file.parent.entries.delete(file.name);
|
|
2304
|
+
this.ledger.release(file.name.length * 2, 1);
|
|
2305
|
+
file.node.nlink--;
|
|
2306
|
+
file.node.ctimeMs = now;
|
|
2307
|
+
file.node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, file.node.revision + 1);
|
|
2308
|
+
this.releaseNode(file.node);
|
|
2309
|
+
this.changed(file.parent, now);
|
|
2310
|
+
memoryCaches.get(this.ledger).clearWrites();
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2275
2313
|
return MemoryFileSystem.prototype.rename.call(this, staging.file.path, destination, { ...controls, noReplace: expected === null });
|
|
2276
2314
|
}
|
|
2277
2315
|
async publishStagedFileSet(receipt, destination, options) {
|
|
2316
|
+
if (options.preserveIdentity)
|
|
2317
|
+
this.fail("ENOTSUP", "publishStagedFileSet", destination);
|
|
2278
2318
|
const signal = options.signal;
|
|
2279
2319
|
const guard = options.commitGuard;
|
|
2280
2320
|
const ancestors = options.ancestors === undefined ? undefined : snapshotDirectoryAncestry(options.ancestors);
|
|
@@ -109,6 +109,7 @@ export class MountFileSystem {
|
|
|
109
109
|
retainedStagingCleanup: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
|
|
110
110
|
synchronousDirectoryValidation: ["prepareDirectoryAncestry"], guardedStagingPublication: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
|
|
111
111
|
trustedOwnedStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "prepareDirectory"],
|
|
112
|
+
atomicStagedFileMutation: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
|
|
112
113
|
atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicEntryRemovalReceipt: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
|
|
113
114
|
symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"], timestamps: ["utimes"], readlink: ["readlink"],
|
|
114
115
|
descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
|
|
@@ -128,7 +129,7 @@ export class MountFileSystem {
|
|
|
128
129
|
return values.every(value => value === true) ? true : values.every(value => value === false) ? false : undefined;
|
|
129
130
|
};
|
|
130
131
|
const semantics = Object.fromEntries([
|
|
131
|
-
"guardedStagingPublication", "retainedStagingCleanup", "atomicEntryRemovalReceipt",
|
|
132
|
+
"guardedStagingPublication", "retainedStagingCleanup", "atomicEntryRemovalReceipt", "atomicStagedFileMutation",
|
|
132
133
|
"trustedOwnedStaging", "atomicFilePublication", "atomicEntryRemoval", "atomicTreeRemoval", "atomicFileMutation", "atomicFileStaging", "atomicDirectoryMetadata", "read", "stat", "readdir", "realpath", "access", "open", "versionedDescriptors",
|
|
133
134
|
"write", "append", "exclusiveCreate", "explicitDirectories", "implicitDirectories", "mkdir", "recursiveMkdir",
|
|
134
135
|
"remove", "removeDirectory", "recursiveRemove", "rename", "atomicRenameNoReplace", "copy", "exclusiveCopy", "readlink", "truncate",
|
|
@@ -1096,6 +1097,8 @@ export class MountFileSystem {
|
|
|
1096
1097
|
await requireOwnedMutation(local.mount.backend, target.local, "atomicFileStaging", options, options.destination === null);
|
|
1097
1098
|
if (!local.mount.backend.publishStagedFile)
|
|
1098
1099
|
fail("ENOTSUP");
|
|
1100
|
+
if (options.preserveIdentity)
|
|
1101
|
+
await requireOwnedMutation(local.mount.backend, target.local, "atomicStagedFileMutation", options);
|
|
1099
1102
|
if (ancestors || callerGuard !== undefined)
|
|
1100
1103
|
await requireOwnedMutation(local.mount.backend, target.local, "guardedStagingPublication", options, options.destination === null);
|
|
1101
1104
|
if (!ancestors) {
|
|
@@ -384,6 +384,8 @@ export class OverlayMemoryPublication {
|
|
|
384
384
|
return staging;
|
|
385
385
|
}
|
|
386
386
|
async publish(staging, path, options) {
|
|
387
|
+
if (options.preserveIdentity)
|
|
388
|
+
throw new FsError("ENOTSUP", { path });
|
|
387
389
|
options.signal?.throwIfAborted();
|
|
388
390
|
this.path(path);
|
|
389
391
|
const owned = this.stages.get(staging.directory);
|
|
@@ -413,6 +413,8 @@ export class RealFileSystem {
|
|
|
413
413
|
throw new FsError("EAGAIN");
|
|
414
414
|
}
|
|
415
415
|
async publishStagedFile(staging, destination, options) {
|
|
416
|
+
if (options.preserveIdentity)
|
|
417
|
+
throw new FsError("ENOTSUP", { path: destination });
|
|
416
418
|
return this.operation("publishStagedFile", staging.file.path, options, async () => {
|
|
417
419
|
options.signal?.throwIfAborted();
|
|
418
420
|
if (options.ancestors !== undefined || options.commitGuard !== undefined)
|
|
@@ -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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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 ||
|
|
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 (!
|
|
357
|
+
if (!truncated)
|
|
355
358
|
return;
|
|
356
|
-
token
|
|
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
|
-
|
|
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)) {
|
|
@@ -343,6 +343,8 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
|
|
|
343
343
|
if (property === "publishStagedFile") {
|
|
344
344
|
const publishOptions = (options ?? {});
|
|
345
345
|
await requireOwnedMutation(original, args[1], "atomicFileStaging", publishOptions, publishOptions.destination === null);
|
|
346
|
+
if (publishOptions.preserveIdentity)
|
|
347
|
+
await requireOwnedMutation(original, args[1], "atomicStagedFileMutation", publishOptions);
|
|
346
348
|
const query = options.destination === null ? { ...options, create: true } : options;
|
|
347
349
|
const declared = ownedMutationCapabilities(original, await original.capabilitiesFor?.(args[1], query) ?? original.capabilities);
|
|
348
350
|
assertReady();
|
|
@@ -444,7 +446,7 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
|
|
|
444
446
|
return executeDispatch(args);
|
|
445
447
|
if (operations.has(property)) {
|
|
446
448
|
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;
|
|
449
|
+
: ["writeFile", "appendFile", "writeStream", "rename", "copyFile", "link", "symlink", "access", "chmod", "truncate", "resizeFile", "publishFileConditional", "writeFileConditional", "publishStagedFile", "publishStagedFileSet"].includes(String(property)) ? 2 : 1;
|
|
448
450
|
args[optionIndex] = scopeTransportOptions((args[optionIndex] ?? {}), admit, credit);
|
|
449
451
|
}
|
|
450
452
|
return runScopedTransportBudget(admit, () => executeDispatch(args), credit);
|
|
@@ -39,11 +39,19 @@ export declare class IntegerTable {
|
|
|
39
39
|
private readonly storage;
|
|
40
40
|
private readonly maxEntries;
|
|
41
41
|
private root;
|
|
42
|
+
private revision;
|
|
43
|
+
private readonly branches;
|
|
42
44
|
private readonly cache;
|
|
43
|
-
constructor(storage: PagedStorage, maxEntries?: number);
|
|
45
|
+
constructor(storage: Pick<PagedStorage, "allocate" | "read" | "write">, maxEntries?: number);
|
|
44
46
|
private retain;
|
|
45
47
|
get(key: bigint): Promise<bigint | undefined>;
|
|
46
48
|
set(key: bigint, value: bigint): Promise<void>;
|
|
49
|
+
/** Ordered replay with a fixed 16-level radix stack (at most 2 KiB of
|
|
50
|
+
* owned node bytes) and 128 output records. Batches avoid alternating sparse
|
|
51
|
+
* index reads with consumer writes on every record. Mutation is not permitted. */
|
|
52
|
+
entries(): AsyncGenerator<readonly [bigint, bigint]>;
|
|
53
|
+
/** Hot radix paths must not evict dirty leaf pages in a one-page backend. */
|
|
54
|
+
private branch;
|
|
47
55
|
private lookup;
|
|
48
56
|
private persist;
|
|
49
57
|
}
|
|
@@ -192,6 +192,8 @@ export class IntegerTable {
|
|
|
192
192
|
storage;
|
|
193
193
|
maxEntries;
|
|
194
194
|
root = 0;
|
|
195
|
+
revision = 0;
|
|
196
|
+
branches = new Map();
|
|
195
197
|
cache = new Map();
|
|
196
198
|
constructor(storage, maxEntries = 4096) {
|
|
197
199
|
this.storage = storage;
|
|
@@ -224,19 +226,92 @@ export class IntegerTable {
|
|
|
224
226
|
async set(key, value) {
|
|
225
227
|
if (key < 0n || key > 0xffffffffffffffffn || value < 0n || value >= 0xffffffffffffffffn)
|
|
226
228
|
throw new RangeError("Invalid storage integer table value");
|
|
229
|
+
this.revision++;
|
|
227
230
|
await this.retain(key, value, true);
|
|
228
231
|
}
|
|
232
|
+
/** Ordered replay with a fixed 16-level radix stack (at most 2 KiB of
|
|
233
|
+
* owned node bytes) and 128 output records. Batches avoid alternating sparse
|
|
234
|
+
* index reads with consumer writes on every record. Mutation is not permitted. */
|
|
235
|
+
async *entries() {
|
|
236
|
+
const revision = this.revision;
|
|
237
|
+
const check = () => { if (revision !== this.revision)
|
|
238
|
+
throw new Error("Integer table changed during iteration"); };
|
|
239
|
+
for (const [key, entry] of this.cache)
|
|
240
|
+
if (entry.dirty) {
|
|
241
|
+
check();
|
|
242
|
+
await this.persist(key, entry.value);
|
|
243
|
+
check();
|
|
244
|
+
entry.dirty = false;
|
|
245
|
+
}
|
|
246
|
+
const storage = this.storage;
|
|
247
|
+
async function* visit(position, shift, prefix) {
|
|
248
|
+
check();
|
|
249
|
+
if (!position)
|
|
250
|
+
return;
|
|
251
|
+
const bytes = new Uint8Array(await storage.read(position, 128));
|
|
252
|
+
check();
|
|
253
|
+
if (bytes.length !== 128)
|
|
254
|
+
throw new RangeError("Truncated integer table node");
|
|
255
|
+
const node = new DataView(bytes.buffer);
|
|
256
|
+
for (let slot = 0; slot < 16; slot++) {
|
|
257
|
+
check();
|
|
258
|
+
const value = node.getBigUint64(slot * 8, true);
|
|
259
|
+
if (!value)
|
|
260
|
+
continue;
|
|
261
|
+
const key = prefix | BigInt(slot) << shift;
|
|
262
|
+
if (shift === 0n)
|
|
263
|
+
yield [key, value - 1n];
|
|
264
|
+
else
|
|
265
|
+
yield* visit(Number(value), shift - 4n, key);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const batch = [];
|
|
269
|
+
for await (const entry of visit(this.root, 60n, 0n)) {
|
|
270
|
+
batch.push(entry);
|
|
271
|
+
if (batch.length === 128) {
|
|
272
|
+
for (const entry of batch) {
|
|
273
|
+
check();
|
|
274
|
+
yield entry;
|
|
275
|
+
}
|
|
276
|
+
batch.length = 0;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const entry of batch) {
|
|
280
|
+
check();
|
|
281
|
+
yield entry;
|
|
282
|
+
}
|
|
283
|
+
check();
|
|
284
|
+
}
|
|
285
|
+
/** Hot radix paths must not evict dirty leaf pages in a one-page backend. */
|
|
286
|
+
branch(key, shift, position) {
|
|
287
|
+
const prefix = key >> shift | 1n << (64n - shift);
|
|
288
|
+
const value = position ?? this.branches.get(prefix);
|
|
289
|
+
if (value !== undefined) {
|
|
290
|
+
this.branches.delete(prefix);
|
|
291
|
+
this.branches.set(prefix, value);
|
|
292
|
+
if (this.branches.size > 128)
|
|
293
|
+
this.branches.delete(this.branches.keys().next().value);
|
|
294
|
+
}
|
|
295
|
+
return value;
|
|
296
|
+
}
|
|
229
297
|
async lookup(key) {
|
|
230
298
|
let node = this.root;
|
|
231
299
|
for (let shift = 60n; shift >= 0n; shift -= 4n) {
|
|
232
300
|
if (!node)
|
|
233
301
|
return undefined;
|
|
302
|
+
const cached = shift === 0n ? undefined : this.branch(key, shift);
|
|
303
|
+
if (cached !== undefined) {
|
|
304
|
+
node = cached;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
234
307
|
const slot = node + Number((key >> shift) & 15n) * 8;
|
|
235
308
|
const bytes = await this.storage.read(slot, 8);
|
|
236
309
|
const value = new DataView(bytes.buffer, bytes.byteOffset, 8).getBigUint64(0, true);
|
|
237
310
|
if (shift === 0n)
|
|
238
311
|
return value === 0n ? undefined : value - 1n;
|
|
239
312
|
node = Number(value);
|
|
313
|
+
if (node)
|
|
314
|
+
this.branch(key, shift, node);
|
|
240
315
|
}
|
|
241
316
|
return undefined;
|
|
242
317
|
}
|
|
@@ -245,6 +320,11 @@ export class IntegerTable {
|
|
|
245
320
|
this.root = this.storage.allocate(128);
|
|
246
321
|
let node = this.root;
|
|
247
322
|
for (let shift = 60n; shift >= 0n; shift -= 4n) {
|
|
323
|
+
const cached = shift === 0n ? undefined : this.branch(key, shift);
|
|
324
|
+
if (cached !== undefined) {
|
|
325
|
+
node = cached;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
248
328
|
const slot = node + Number((key >> shift) & 15n) * 8;
|
|
249
329
|
const bytes = await this.storage.read(slot, 8);
|
|
250
330
|
const view = new DataView(bytes.buffer, bytes.byteOffset, 8);
|
|
@@ -260,6 +340,7 @@ export class IntegerTable {
|
|
|
260
340
|
await this.storage.write(slot, bytes);
|
|
261
341
|
}
|
|
262
342
|
node = child;
|
|
343
|
+
this.branch(key, shift, child);
|
|
263
344
|
}
|
|
264
345
|
}
|
|
265
346
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.769",
|
|
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.
|
|
178
|
+
"toolcraft-schema": "0.0.769",
|
|
179
179
|
"toolcraft-design": "*",
|
|
180
180
|
"@poe-code/frontmatter": "*",
|
|
181
181
|
"@poe-code/agent-mcp-config": "*",
|