@vibe-agent-toolkit/utils 0.2.0-rc.11 → 0.2.0-rc.12
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/README.md +111 -9
- package/dist/archive/archive.d.ts +129 -0
- package/dist/archive/archive.d.ts.map +1 -0
- package/dist/archive/archive.js +223 -0
- package/dist/archive/archive.js.map +1 -0
- package/dist/archive/entries.d.ts +132 -0
- package/dist/archive/entries.d.ts.map +1 -0
- package/dist/archive/entries.js +223 -0
- package/dist/archive/entries.js.map +1 -0
- package/dist/archive/limits.d.ts +39 -0
- package/dist/archive/limits.d.ts.map +1 -0
- package/dist/archive/limits.js +59 -0
- package/dist/archive/limits.js.map +1 -0
- package/dist/archive/records.d.ts +14 -0
- package/dist/archive/records.d.ts.map +1 -0
- package/dist/archive/records.js +3 -0
- package/dist/archive/records.js.map +1 -0
- package/dist/archive/tar-reader.d.ts +18 -0
- package/dist/archive/tar-reader.d.ts.map +1 -0
- package/dist/archive/tar-reader.js +173 -0
- package/dist/archive/tar-reader.js.map +1 -0
- package/dist/archive/zip-reader.d.ts +76 -0
- package/dist/archive/zip-reader.d.ts.map +1 -0
- package/dist/archive/zip-reader.js +376 -0
- package/dist/archive/zip-reader.js.map +1 -0
- package/dist/archive.d.ts +14 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +12 -0
- package/dist/archive.js.map +1 -0
- package/dist/asset-reference.d.ts +17 -1
- package/dist/asset-reference.d.ts.map +1 -1
- package/dist/asset-reference.js +31 -7
- package/dist/asset-reference.js.map +1 -1
- package/dist/dirent-kind.d.ts +13 -40
- package/dist/dirent-kind.d.ts.map +1 -1
- package/dist/dirent-kind.js +21 -69
- package/dist/dirent-kind.js.map +1 -1
- package/dist/entrypoint.d.ts +2 -2
- package/dist/entrypoint.d.ts.map +1 -1
- package/dist/entrypoint.js +4 -3
- package/dist/entrypoint.js.map +1 -1
- package/dist/env-flag.d.ts +2 -0
- package/dist/env-flag.d.ts.map +1 -1
- package/dist/env-flag.js +2 -0
- package/dist/env-flag.js.map +1 -1
- package/dist/errors/confirmed-absent.d.ts +41 -0
- package/dist/errors/confirmed-absent.d.ts.map +1 -0
- package/dist/errors/confirmed-absent.js +43 -0
- package/dist/errors/confirmed-absent.js.map +1 -0
- package/dist/errors/errno-table.d.ts +177 -0
- package/dist/errors/errno-table.d.ts.map +1 -0
- package/dist/errors/errno-table.js +343 -0
- package/dist/errors/errno-table.js.map +1 -0
- package/dist/errors/fs-boundary.d.ts +36 -0
- package/dist/errors/fs-boundary.d.ts.map +1 -0
- package/dist/errors/fs-boundary.js +121 -0
- package/dist/errors/fs-boundary.js.map +1 -0
- package/dist/errors/fs-fault.d.ts +176 -0
- package/dist/errors/fs-fault.d.ts.map +1 -0
- package/dist/errors/fs-fault.js +215 -0
- package/dist/errors/fs-fault.js.map +1 -0
- package/dist/errors/git-errno.d.ts +13 -0
- package/dist/errors/git-errno.d.ts.map +1 -0
- package/dist/errors/git-errno.js +45 -0
- package/dist/errors/git-errno.js.map +1 -0
- package/dist/errors/path-present.d.ts +72 -0
- package/dist/errors/path-present.d.ts.map +1 -0
- package/dist/errors/path-present.js +110 -0
- package/dist/errors/path-present.js.map +1 -0
- package/dist/errors/rag-index-empty.d.ts +16 -0
- package/dist/errors/rag-index-empty.d.ts.map +1 -0
- package/dist/errors/rag-index-empty.js +16 -0
- package/dist/errors/rag-index-empty.js.map +1 -0
- package/dist/errors/suppressed-faults.d.ts +34 -0
- package/dist/errors/suppressed-faults.d.ts.map +1 -0
- package/dist/errors/suppressed-faults.js +66 -0
- package/dist/errors/suppressed-faults.js.map +1 -0
- package/dist/file-crawler.d.ts +13 -18
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +140 -24
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs-utils.d.ts +75 -50
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +148 -158
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +4 -4
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +4 -4
- package/dist/fs.js.map +1 -1
- package/dist/git-snapshot.d.ts +25 -17
- package/dist/git-snapshot.d.ts.map +1 -1
- package/dist/git-snapshot.js +65 -11
- package/dist/git-snapshot.js.map +1 -1
- package/dist/git-tracker.d.ts +11 -3
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +29 -6
- package/dist/git-tracker.js.map +1 -1
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +7 -35
- package/dist/git-utils.js.map +1 -1
- package/dist/git.d.ts +1 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +1 -1
- package/dist/git.js.map +1 -1
- package/dist/gitignore-checker.d.ts.map +1 -1
- package/dist/gitignore-checker.js +5 -4
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/in-order.d.ts +93 -0
- package/dist/in-order.d.ts.map +1 -0
- package/dist/in-order.js +165 -0
- package/dist/in-order.js.map +1 -0
- package/dist/index.d.ts +20 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -4
- package/dist/index.js.map +1 -1
- package/dist/kill-process-tree.d.ts +43 -0
- package/dist/kill-process-tree.d.ts.map +1 -0
- package/dist/kill-process-tree.js +48 -0
- package/dist/kill-process-tree.js.map +1 -0
- package/dist/link-following.d.ts +115 -0
- package/dist/link-following.d.ts.map +1 -0
- package/dist/link-following.js +283 -0
- package/dist/link-following.js.map +1 -0
- package/dist/listing-refusal.d.ts +44 -0
- package/dist/listing-refusal.d.ts.map +1 -1
- package/dist/listing-refusal.js +79 -2
- package/dist/listing-refusal.js.map +1 -1
- package/dist/name-fold.d.ts +25 -0
- package/dist/name-fold.d.ts.map +1 -0
- package/dist/name-fold.js +40 -0
- package/dist/name-fold.js.map +1 -0
- package/dist/path-containment.d.ts +3 -1
- package/dist/path-containment.d.ts.map +1 -1
- package/dist/path-containment.js +13 -7
- package/dist/path-containment.js.map +1 -1
- package/dist/path-utils.d.ts +14 -17
- package/dist/path-utils.d.ts.map +1 -1
- package/dist/path-utils.js +37 -55
- package/dist/path-utils.js.map +1 -1
- package/dist/process.d.ts +2 -0
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +2 -0
- package/dist/process.js.map +1 -1
- package/dist/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +8 -9
- package/dist/project-utils.js.map +1 -1
- package/dist/safe-exec.d.ts +6 -2
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +1 -1
- package/dist/safe-exec.js.map +1 -1
- package/dist/skill-targets.d.ts.map +1 -1
- package/dist/skill-targets.js +3 -1
- package/dist/skill-targets.js.map +1 -1
- package/dist/skill-test/spawn-claude.d.ts +0 -15
- package/dist/skill-test/spawn-claude.d.ts.map +1 -1
- package/dist/skill-test/spawn-claude.js +11 -50
- package/dist/skill-test/spawn-claude.js.map +1 -1
- package/dist/special-file.d.ts +18 -0
- package/dist/special-file.d.ts.map +1 -0
- package/dist/special-file.js +21 -0
- package/dist/special-file.js.map +1 -0
- package/dist/terminating-signals.d.ts +129 -0
- package/dist/terminating-signals.d.ts.map +1 -0
- package/dist/terminating-signals.js +210 -0
- package/dist/terminating-signals.js.map +1 -0
- package/dist/test-helpers.d.ts +9 -5
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +18 -21
- package/dist/test-helpers.js.map +1 -1
- package/dist/test-scratch-guard.d.ts +34 -0
- package/dist/test-scratch-guard.d.ts.map +1 -0
- package/dist/test-scratch-guard.js +45 -0
- package/dist/test-scratch-guard.js.map +1 -0
- package/dist/testing/archive-fixtures.d.ts +147 -0
- package/dist/testing/archive-fixtures.d.ts.map +1 -0
- package/dist/testing/archive-fixtures.js +449 -0
- package/dist/testing/archive-fixtures.js.map +1 -0
- package/dist/testing/executables.d.ts +6 -0
- package/dist/testing/executables.d.ts.map +1 -1
- package/dist/testing/executables.js +20 -9
- package/dist/testing/executables.js.map +1 -1
- package/dist/testing/fault-fs-preload.d.ts +16 -0
- package/dist/testing/fault-fs-preload.d.ts.map +1 -0
- package/dist/testing/fault-fs-preload.js +22 -0
- package/dist/testing/fault-fs-preload.js.map +1 -0
- package/dist/testing/fault-fs.d.ts +115 -0
- package/dist/testing/fault-fs.d.ts.map +1 -0
- package/dist/testing/fault-fs.js +411 -0
- package/dist/testing/fault-fs.js.map +1 -0
- package/dist/testing/fault-spec.d.ts +83 -0
- package/dist/testing/fault-spec.d.ts.map +1 -0
- package/dist/testing/fault-spec.js +132 -0
- package/dist/testing/fault-spec.js.map +1 -0
- package/dist/testing/hostile-tree.d.ts.map +1 -1
- package/dist/testing/hostile-tree.js +3 -4
- package/dist/testing/hostile-tree.js.map +1 -1
- package/dist/testing/platform-gates.d.ts +8 -0
- package/dist/testing/platform-gates.d.ts.map +1 -1
- package/dist/testing/platform-gates.js +24 -0
- package/dist/testing/platform-gates.js.map +1 -1
- package/dist/testing/temp-dir.d.ts +31 -0
- package/dist/testing/temp-dir.d.ts.map +1 -1
- package/dist/testing/temp-dir.js +63 -0
- package/dist/testing/temp-dir.js.map +1 -1
- package/dist/testing/tier-timeout.d.ts +34 -0
- package/dist/testing/tier-timeout.d.ts.map +1 -0
- package/dist/testing/tier-timeout.js +45 -0
- package/dist/testing/tier-timeout.js.map +1 -0
- package/dist/testing/tree-snapshot.d.ts +71 -0
- package/dist/testing/tree-snapshot.d.ts.map +1 -0
- package/dist/testing/tree-snapshot.js +163 -0
- package/dist/testing/tree-snapshot.js.map +1 -0
- package/dist/testing.d.ts +6 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +8 -1
- package/dist/testing.js.map +1 -1
- package/dist/text-file.d.ts +14 -1
- package/dist/text-file.d.ts.map +1 -1
- package/dist/text-file.js +41 -8
- package/dist/text-file.js.map +1 -1
- package/dist/timing-dump.d.ts.map +1 -1
- package/dist/timing-dump.js +2 -10
- package/dist/timing-dump.js.map +1 -1
- package/dist/tree-change/apply-decisions.d.ts +101 -0
- package/dist/tree-change/apply-decisions.d.ts.map +1 -0
- package/dist/tree-change/apply-decisions.js +139 -0
- package/dist/tree-change/apply-decisions.js.map +1 -0
- package/dist/tree-change/apply.d.ts +98 -0
- package/dist/tree-change/apply.d.ts.map +1 -0
- package/dist/tree-change/apply.js +364 -0
- package/dist/tree-change/apply.js.map +1 -0
- package/dist/tree-change/copy-decisions.d.ts +58 -0
- package/dist/tree-change/copy-decisions.d.ts.map +1 -0
- package/dist/tree-change/copy-decisions.js +66 -0
- package/dist/tree-change/copy-decisions.js.map +1 -0
- package/dist/tree-change/copy-tree.d.ts +125 -0
- package/dist/tree-change/copy-tree.d.ts.map +1 -0
- package/dist/tree-change/copy-tree.js +279 -0
- package/dist/tree-change/copy-tree.js.map +1 -0
- package/dist/tree-change/files.d.ts +167 -0
- package/dist/tree-change/files.d.ts.map +1 -0
- package/dist/tree-change/files.js +423 -0
- package/dist/tree-change/files.js.map +1 -0
- package/dist/tree-change/followed-walk.d.ts +54 -0
- package/dist/tree-change/followed-walk.d.ts.map +1 -0
- package/dist/tree-change/followed-walk.js +81 -0
- package/dist/tree-change/followed-walk.js.map +1 -0
- package/dist/tree-change/host-chain.d.ts +48 -0
- package/dist/tree-change/host-chain.d.ts.map +1 -0
- package/dist/tree-change/host-chain.js +85 -0
- package/dist/tree-change/host-chain.js.map +1 -0
- package/dist/tree-change/identity-compare.d.ts +61 -0
- package/dist/tree-change/identity-compare.d.ts.map +1 -0
- package/dist/tree-change/identity-compare.js +115 -0
- package/dist/tree-change/identity-compare.js.map +1 -0
- package/dist/tree-change/identity.d.ts +62 -0
- package/dist/tree-change/identity.d.ts.map +1 -0
- package/dist/tree-change/identity.js +152 -0
- package/dist/tree-change/identity.js.map +1 -0
- package/dist/tree-change/index.d.ts +24 -0
- package/dist/tree-change/index.d.ts.map +1 -0
- package/dist/tree-change/index.js +14 -0
- package/dist/tree-change/index.js.map +1 -0
- package/dist/tree-change/link-chain.d.ts +180 -0
- package/dist/tree-change/link-chain.d.ts.map +1 -0
- package/dist/tree-change/link-chain.js +377 -0
- package/dist/tree-change/link-chain.js.map +1 -0
- package/dist/tree-change/link-containment.d.ts +105 -0
- package/dist/tree-change/link-containment.d.ts.map +1 -0
- package/dist/tree-change/link-containment.js +196 -0
- package/dist/tree-change/link-containment.js.map +1 -0
- package/dist/tree-change/link-walk.d.ts +49 -0
- package/dist/tree-change/link-walk.d.ts.map +1 -0
- package/dist/tree-change/link-walk.js +69 -0
- package/dist/tree-change/link-walk.js.map +1 -0
- package/dist/tree-change/plan.d.ts +206 -0
- package/dist/tree-change/plan.d.ts.map +1 -0
- package/dist/tree-change/plan.js +320 -0
- package/dist/tree-change/plan.js.map +1 -0
- package/dist/tree-change/readable-tree.d.ts +44 -0
- package/dist/tree-change/readable-tree.d.ts.map +1 -0
- package/dist/tree-change/readable-tree.js +52 -0
- package/dist/tree-change/readable-tree.js.map +1 -0
- package/dist/tree-change/rollback-error.d.ts +29 -0
- package/dist/tree-change/rollback-error.d.ts.map +1 -0
- package/dist/tree-change/rollback-error.js +31 -0
- package/dist/tree-change/rollback-error.js.map +1 -0
- package/dist/tree-change/staging-names.d.ts +27 -0
- package/dist/tree-change/staging-names.d.ts.map +1 -0
- package/dist/tree-change/staging-names.js +40 -0
- package/dist/tree-change/staging-names.js.map +1 -0
- package/dist/tree-change/tree-walk.d.ts +81 -0
- package/dist/tree-change/tree-walk.d.ts.map +1 -0
- package/dist/tree-change/tree-walk.js +212 -0
- package/dist/tree-change/tree-walk.js.map +1 -0
- package/dist/win32-namespace.d.ts +24 -0
- package/dist/win32-namespace.d.ts.map +1 -0
- package/dist/win32-namespace.js +31 -0
- package/dist/win32-namespace.js.map +1 -0
- package/dist/yaml/surgical-yaml.d.ts +12 -3
- package/dist/yaml/surgical-yaml.d.ts.map +1 -1
- package/dist/yaml/surgical-yaml.js +16 -6
- package/dist/yaml/surgical-yaml.js.map +1 -1
- package/eslint/README.md +12 -4
- package/eslint/rules/fs-binding-scan.cjs +278 -0
- package/eslint/rules/no-adhoc-errno.cjs +364 -0
- package/eslint/rules/no-bare-symlink-in-tests.cjs +2 -3
- package/eslint/rules/no-blind-catch.cjs +1 -1
- package/eslint/rules/no-destructive-fs.cjs +359 -0
- package/eslint/rules/no-existssync.cjs +70 -0
- package/eslint/rules/no-fragile-entrypoint-guard.cjs +3 -2
- package/eslint/rules/no-link-following-fs.cjs +68 -0
- package/eslint/rules/no-literal-process-exit.cjs +3 -16
- package/eslint/rules/no-literal-test-timeout.cjs +214 -0
- package/eslint/rules/no-raw-signal-listener.cjs +224 -0
- package/eslint/rules/no-stdout-outside-writer.cjs +135 -0
- package/package.json +10 -2
- package/dist/errors/errno.d.ts +0 -51
- package/dist/errors/errno.d.ts.map +0 -1
- package/dist/errors/errno.js +0 -112
- package/dist/errors/errno.js.map +0 -1
- package/eslint/rules/no-fs-promises-cp.cjs +0 -43
package/README.md
CHANGED
|
@@ -28,11 +28,11 @@ The last two columns are the ones that matter when choosing. **"Resolves with ze
|
|
|
28
28
|
| `./text` | `decodeTextContent` — the one bytes-to-text seam: BOM-announced UTF-8/UTF-16LE/UTF-16BE/UTF-32LE/UTF-32BE, BOM stripped, UTF-8 assumed otherwise; reports the encoding, whether it was a BOM fact or an assumption, and how many U+FFFD the decode substituted. `TextTooLargeError` (`TEXT_TOO_LARGE`) when the engine refuses a string that long | **none** | — | **yes** |
|
|
29
29
|
| `./zod` | `ZodTypeNames`, `getZodTypeName`, `isZodType`, `unwrapZodType`, `isZodOptional`, `isZodNullable` | **none** | — | **yes** |
|
|
30
30
|
| `./glob` | `isGlob`, static base extraction, magic remainder | `path` only | — | **yes** |
|
|
31
|
-
| `./fs` | `normalizePath`, `normalizedTmpdir`, `mkdirSyncReal`, `resolveFromImportMeta`, `dynamicImportPath`, `
|
|
32
|
-
| `./testing` | `getTestOutputDir`, `getTestOutputBase`, `setupAsyncTempDirSuite`, `setupSyncTempDirSuite`, `removeScratchDir`, `createTempDir`, `removeTempDir`, `tempDirTracker`, `CANNOT_DENY_READS`, `buildHostileTree`, `HOSTILE_NAMES`, `symlinkCapability`, `createSymlink`, `createSymlinkAsync` | `crypto`, `fs`, `fs/promises`, `os`, `path`, `url` | — | **yes** |
|
|
31
|
+
| `./fs` | `normalizePath`, `normalizedTmpdir`, `mkdirSyncReal`, `resolveFromImportMeta`, `dynamicImportPath`, `fillPathSpellings`, `pathSpellingFrom`, `DirectorySpellingIndex`, `spellingWalkRoot`, `fillRealpaths`, `realpathRowFrom`, `FsLookupCache`, `readTextContent`, `readTextContentSync`, `readDecodableBytes` (refuses a file past `MAX_DECODABLE_BYTES` by `fstat`, before reading it), `exceedsDecodableLength`, `MAX_DECODABLE_BYTES`, `TextTooLargeError` | `buffer`, `fs`, `fs/promises`, `os`, `path`, `url`, `util` | — | **yes** |
|
|
32
|
+
| `./testing` | `getTestOutputDir`, `getTestOutputBase`, `setupAsyncTempDirSuite`, `setupSyncTempDirSuite`, `removeScratchDir`, `createTempDir`, `removeTempDir`, `tempDirTracker`, `scratchTmpdirEnv`, `registerScratchTmpdir`, `CANNOT_DENY_READS`, `tmpdirFoldsCase`, `buildHostileTree`, `HOSTILE_NAMES`, `installFaultFs`, `injectedErrnoError`, `faultRuleOf`, `faultFsSpecOf`, `snapshotTree`, `diffSnapshots`, `subtree`, `symlinkCapability`, `createSymlink`, `createSymlinkAsync`, `atLeastTierTimeout` | `crypto`, `fs`, `fs/promises`, `module`, `os`, `path`, `url` | — | **yes** |
|
|
33
33
|
| `./asset` | `resolveAssetReference` — paths and npm bare specifiers | `fs`, `module`, `os`, `path`, `url` | — | **yes** |
|
|
34
34
|
| `./yaml` | `updateYamlIn`, `verifyConfinedYamlEdit` — byte-surgical YAML edits | **none** | `yaml` | no — needs `yaml` |
|
|
35
|
-
| `./process` | `safeExecSync`, `safeExecResult`, `safeExecFromString`, `isToolAvailable`, `getToolVersion`, `hasShellSyntax`, `CommandExecutionError`, `spawnHardened`, `shouldUseShell`, `windowsShellQuote`, `buildWindowsShellLine`, `resolveShellCommandToken`, `isPathLike`, `makeStdioBlocking`, `describeStdioBlocking` | `child_process`, `path` | `@vibe-validate/git`, `which` | no — needs both |
|
|
35
|
+
| `./process` | `safeExecSync`, `safeExecResult`, `safeExecFromString`, `isToolAvailable`, `getToolVersion`, `hasShellSyntax`, `CommandExecutionError`, `spawnHardened`, `shouldUseShell`, `windowsShellQuote`, `buildWindowsShellLine`, `resolveShellCommandToken`, `isPathLike`, `makeStdioBlocking`, `describeStdioBlocking`, `TERMINATING_SIGNALS`, `isTerminatingSignal`, `reRaiseSignal`, `handleTerminatingSignals`, `REPEAT_SIGNAL_WINDOW_MS`, `killProcessTree` | `child_process`, `path` | `@vibe-validate/git`, `which` | no — needs both |
|
|
36
36
|
| `./git` | `runGit`, `runGitOrThrow`, `gitFindRoot`, `gitLsFiles`, `gitLsOthers`, `isGitIgnored`, `loadGitignoreRules`, `GitTracker`, `gitTreeSnapshot`, `peekGitTreeSnapshot`, `withGitSnapshotCache`, `parseGitUrl`, `isGitUrl`, `nonInteractiveGitOverrides` | `async_hooks`, `fs`, `fs/promises`, `os`, `path`, `url` | `@vibe-validate/git`, `ignore` | no — needs both |
|
|
37
37
|
| `./crawl` | `crawlDirectory`, `crawlDirectorySync`, `crawlPathFilter`, `NEVER_CRAWL_GLOBS`, `BUILD_OUTPUT_GLOBS` | `fs`, `os`, `path`, `url` | `@vibe-validate/git`, `picomatch` | no — needs both |
|
|
38
38
|
| `./skill-test` | `spawnHeadlessClaude`, `assembleClaudeArgs`, `killAllActiveClaudeChildren`, `resolveAuth`, `probeAuthStatus`, `AuthPreflightError`, `applyDeclaredEnv`, `buildForwardedEnv`, `formatForwardedEnvLine`, `isProtectedName`, `protectedEnvNames`, `parseStreamJsonTranscript`, `detectInvocationFromTranscript` | `child_process`, `path`, `stream` | `@vibe-validate/git`, `which` | no — needs both |
|
|
@@ -161,11 +161,9 @@ These return **OS-native** separators, because they resolve real filesystem iden
|
|
|
161
161
|
- `mkdirSyncReal()` - create a directory and return its real path
|
|
162
162
|
- `resolveFromImportMeta()` - resolve paths relative to an `import.meta.url`
|
|
163
163
|
- `dynamicImportPath()` - `import()` an absolute path (works on Windows, which rejects bare absolute paths)
|
|
164
|
-
- `copyDirectory()` - recursive copy that follows a symlinked directory into the tree it points at — contained: a link whose target is outside the source throws `CopyLinkEscapesSourceError`, and a link back into the tree throws `DirectoryWalkRevisitedError`
|
|
165
164
|
- `isUnderRoot(root, candidate)` - the containment verdict a delete/copy/uninstall sink asks, answered by the filesystem: `'inside'` (a strict descendant, realpath-judged), `'outside'` (including the root itself and any symlink that leaves it), or `'absent'` (nothing there yet, and creating it would land inside). Both sides are canonicalized from their deepest existing ancestor, so a root reached through a symlink still contains its members; a refused path throws rather than reading as absent
|
|
166
165
|
- `direntKind(entry)` - what a `Dirent` itself is: `'file' | 'directory' | 'symlink' | 'other'` — the link question asked first, for walks that must not follow (a delete, a size count)
|
|
167
166
|
- `direntKindFollowing(dir, entry)` / `direntKindFollowingSync()` - what a `Dirent` resolves to, one `stat` for a link: `'file' | 'directory' | 'dangling' | 'other'` — for walks over a trusted tree where a link is how the entry got there. Both exist because `isFile()` and `isDirectory()` are BOTH false for a symlink, so a walk that tests only those drops every link silently (`local/dirent-type-needs-symlink-check` refuses that shape)
|
|
168
|
-
- `FollowedWalk` - the cycle guard every following walk holds: `enter(dir)` on the root and each directory recursed into; a directory reached again under a second spelling (a link back into the tree) throws `DirectoryWalkRevisitedError` instead of recursing until the path length runs out
|
|
169
167
|
- `fillPathSpellings(requests, fsCache)` - pass 1 of the case-exact existence check: walk each
|
|
170
168
|
`{ referrer, target }` from the deepest directory the two share, listing every directory on the way
|
|
171
169
|
down exactly once. The only I/O in the pair
|
|
@@ -181,7 +179,15 @@ These return **OS-native** separators, because they resolve real filesystem iden
|
|
|
181
179
|
the root it walks from, for a caller that cannot enumerate its targets up front
|
|
182
180
|
- `FsLookupCache` - per-run memo for `realpath`/`readdir`, sharing in-flight promises. Construct one per
|
|
183
181
|
validation run and let it die with the run — never a module-level singleton, or a long-lived process
|
|
184
|
-
answers from a stale directory listing.
|
|
182
|
+
answers from a stale directory listing. `realpath(p)` answers a `RealpathRow` — `{ path, fault }` — and
|
|
183
|
+
never rejects on a filesystem errno: a path that resolves to nothing, or one the OS declined to resolve,
|
|
184
|
+
is answered from its nearest resolvable ancestor, the second carrying the `RealpathFault`
|
|
185
|
+
(`{ errno, faultClass, error }`) for the judge to report against that one target. `fillRealpaths` /
|
|
186
|
+
`realpathRowFrom(table, p)` read the same rows from a table filled up front (a missing row throws)
|
|
187
|
+
- `realpathNativeSync(p)` / `realpathNative(p)` (main barrel) - the native realpath, forward-slashed, asked
|
|
188
|
+
again of the JS realpath when the volume cannot answer a final-path query
|
|
189
|
+
(`isFinalPathUnanswerableError`: Windows' `ERROR_INVALID_FUNCTION` is `EISDIR`; `ENOTSUP`, `EOPNOTSUPP`).
|
|
190
|
+
Every other error is thrown as it was, for the caller to classify
|
|
185
191
|
|
|
186
192
|
### Processes — `@vibe-agent-toolkit/utils/process`
|
|
187
193
|
|
|
@@ -190,6 +196,8 @@ These return **OS-native** separators, because they resolve real filesystem iden
|
|
|
190
196
|
- `shouldUseShell()` / `windowsShellQuote()` / `buildWindowsShellLine()` - Windows shell-invocation helpers
|
|
191
197
|
- `isToolAvailable()` / `getToolVersion()` / `hasShellSyntax()`
|
|
192
198
|
- `makeStdioBlocking()` - stop `process.exit()` truncating output in a published bin
|
|
199
|
+
- `TERMINATING_SIGNALS` / `isTerminatingSignal()` / `reRaiseSignal()` / `handleTerminatingSignals()` / `REPEAT_SIGNAL_WINDOW_MS` - the signals a process must clean up before dying of, the ONE way to handle them (runs every live guard's cleanup on the first signal, swallows a repeat within `REPEAT_SIGNAL_WINDOW_MS`, ends at once on a later one, otherwise dies of it once the cleanups settle; `local/no-raw-signal-listener` bans raw listeners), and dying of one (status 128+n on POSIX; a fallback exit code on Windows or under PID 1)
|
|
200
|
+
- `killProcessTree()` - SIGKILL a child and everything it started: its process group on POSIX when it leads one, `taskkill /T /F` on Windows, an already-gone target absorbed
|
|
193
201
|
|
|
194
202
|
### Git — `@vibe-agent-toolkit/utils/git`
|
|
195
203
|
|
|
@@ -203,14 +211,38 @@ These return **OS-native** separators, because they resolve real filesystem iden
|
|
|
203
211
|
|
|
204
212
|
**One root finder, on purpose.** `gitFindRoot()` is the only one in the package. There used to be a second name, `findGitRoot()`, whose entire body was `return gitFindRoot(startDir)`; two names for one function guarantees half of all callers pick each. Keeping it off this entry was not enough — it stayed on the `.` barrel, so both names were still one import away — so it has been removed outright. Replace any `findGitRoot(` with `gitFindRoot(`; the behavior is identical.
|
|
205
213
|
|
|
214
|
+
### Archives — `@vibe-agent-toolkit/utils/archive`
|
|
215
|
+
|
|
216
|
+
The ONE module that reads or writes a ZIP or a tarball. `adm-zip` and `tar` are loaded here and nowhere else — by bare name or any subpath, with `import`, `import()`, `require` or `createRequire` (`no-restricted-imports` and `no-restricted-syntax`, with no test-helper exemption), so every lane that opens an archive gets the same answer.
|
|
217
|
+
|
|
218
|
+
- `extractArchive(source, into, { format, limits, omit? })` - (`omit` leaves an entry unwritten — a host's litter beside a skill — while it is still judged) extract a `{ file, side }` or `{ bytes, label }` archive into the directory `into`. The whole entry list is judged BEFORE anything is written, so a refused archive leaves nothing behind; files are written exclusively, never through a link, with mode `0644` or `0755` by the executable bit only. `limits` is required: an omitted bound is the failure
|
|
219
|
+
- `openArchive(source, options)` / `listArchive(source, options)` - the entries without extracting, for a lane that inspects (`read(entry)` inflates one, bounded by its declared size)
|
|
220
|
+
- `archiveEntryRefusal(entries)` - the entry policy alone, pure: a symbolic link, a hard link, a device, a FIFO or an entry of a type the tar reader does not model (sparse, continuation, an unknown type byte — listed as `other`, never dropped); an absolute, drive-rooted, UNC, `..` or NUL-bearing name; a segment Windows cannot write as named (a `:`, one of `<` `>` `"` `|` `?` `*` or a control character, a trailing dot or space, a device name such as `CON` or `com1.txt`), on every host; one name listed twice; a file with an entry beneath it; two names that are one where a volume folds them (`foldedSegment`)
|
|
221
|
+
- `ArchiveRefusedError` / `ARCHIVE_REFUSED_CODE` - the refusal, with its `reason` (`unreadable` — bytes that are no archive of the format, the one reason a lane may pass bytes on; `unsupported` — an archive that parsed and VAT will not take: a ZIP64 ZIP, a directory two readers would read differently, or a tar entry (named) cut short — never passed on by any lane; `link`, `hardlink`, `special`, `unsafe-name`, `name-clash`, `too-many-entries`, `too-large`) and the first offending `entry`; the CLI maps the code to `INPUT_UNREADABLE`. A filesystem fault is an `FsFaultError` instead — the read on the source's side, every write `environment`
|
|
222
|
+
- `ArchiveEntryReadError` / `ARCHIVE_ENTRY_UNREADABLE_CODE` / `isArchiveEntryReadRefusal(error)` (the class on this subpath; the code and the predicate on the main barrel too) - one ZIP member, of an archive whose entry list passed, that does not inflate or holds other than its directory declares: met when the member is READ — `read(entry)`, an extraction writing it, `zipOfArchive` copying it — never at open. `reason` is `unsupported` and `entry` always names the member. Its own code, `ARCHIVE_ENTRY_UNREADABLE`, so the predicate reads the `VatError` brand and that code — never the reason, the entry or the prototype — and a lane that may go on (the org verb's check of a skill's files) skips that one member and reports it, telling it from every refusal of the whole archive. The CLI maps the code to `INPUT_UNREADABLE`
|
|
223
|
+
- `ARCHIVE_LIMITS` (50,000 entries, 256 MiB expanded) / `ARCHIVE_DOWNLOAD_MAX_BYTES` (100 MiB) - the bounds every VAT lane passes. A tarball is inflated through ONE gzip layer, capped; a stream still compressed after it (gzip again, zstd) is refused, never handed to the tar library's own unbounded sniffing. A ZIP's entry count and a name it lists twice (`name-clash`, as in a tarball) are read off its central directory before the ZIP library builds a record; there are two locks on which directory is the archive's. VAT's own: the end record must be the END of the archive (it and exactly the comment it declares), no end-record or ZIP64 signature may stand beside or after it, its two counts must agree, the directory must run exactly from its start to the end record, no byte may stand before the first local header it lists (a self-extracting stub, adjusted or not), each record's offset must hold that entry's own local header (so a directory record cannot stand over an unlisted entry), and no record may hold `0xFFFFFFFF` for a size or its offset (ZIP64) — else `unsupported`. (A member that does not inflate is refused when it is read: `ArchiveEntryReadError`.) And the library's: the entries it builds must be, name for name, the ones VAT read (`sameDirectory`)
|
|
224
|
+
- `archiveEntryPlace(name)` (main barrel) - an entry name as the segments it lands at: a backslash read as a separator on every host, empty and `.` segments dropped. Pure, and loads no archive library: for a rule over entry NAMES (the skill-ZIP layout)
|
|
225
|
+
- `zipOfDirectory(dir)` - the bytes of a ZIP of a directory, for the lanes that package one; every entry made on Unix whatever host writes it, a directory `0755` and a file `0644` or `0755` by the host's execute bit (Windows has none, so every file it writes is `0644`) — never the mode `stat` reports (on Windows a directory `0666`, which a Unix reader could not enter)
|
|
226
|
+
- `zipOfArchive(source, { format, limits, omit? })` - the ZIP VAT writes of an archive it accepts: judged whole as `extractArchive` judges it, then only the entries it lists (minus `omit`), each named by the place it lands at, mode `0644`/`0755`, every entry made on Unix whatever host writes it (so a host-aware reader reads the mode) — what a lane that passes an archive ON sends, so the bytes that leave are exactly what was judged
|
|
227
|
+
- `readArchiveFile(file, side, limits)` - an archive file's bytes, read once (non-blocking open, sized before reading, a fault classified on `side`), for a lane that both judges and sends them as `{ bytes, label }`
|
|
228
|
+
|
|
206
229
|
### Test helpers — `@vibe-agent-toolkit/utils/testing`
|
|
207
230
|
|
|
231
|
+
- `tarFixture(entries, { gzip? })` / `zipFixture(entries, { compress? })` / `archiveEntriesOfDirectory(dir, prefix?)` - an archive written byte by byte with no library, so a test can make the entries no library will write: a link, a hard link, a FIFO, `../x`, a name listed twice, a declared size that lies. `hostileArchiveRows(limits)` is the shared table of archives every extracting lane must refuse, each with the `reason` expected
|
|
232
|
+
|
|
208
233
|
- `setupAsyncTempDirSuite()` / `setupSyncTempDirSuite()` - per-suite temp directories with cleanup
|
|
209
234
|
- `createTempDir()` / `createTempDirAsync()` / `removeTempDir()` / `tempDirTracker()` - the per-call temp-directory primitives; `removeTempDir` refuses, by name, anything not inside the host tmpdir as the filesystem sees it
|
|
235
|
+
- `scratchTmpdirEnv(prefix)` - `{ enter, leave, current }`: make a fresh scratch THE temp directory (`TMPDIR` / `TEMP` / `TMP`) for one test and restore it after — the DESTRUCTIVE-CODE guard for any test that reaches a disposal path, spawned children included. Framework-free: register `enter` / `leave` with the runner's hooks; `registerScratchTmpdir(prefix, { beforeEach, afterEach })` does the registering
|
|
210
236
|
- `CANNOT_DENY_READS` / `PERMISSIONS_ENFORCED` - whether a `chmod 000` denies anything on this host (Windows and uid 0 read everything); route through `skip()` so the skip is visible
|
|
211
|
-
- `
|
|
237
|
+
- `tmpdirFoldsCase()` - whether the OS temp dir's filesystem folds letter case (probed once, not inferred from the platform); gate a case-alias fixture on it
|
|
238
|
+
- `NODE_EXECUTABLE` / `gitExecutable()` / `resolveExecutable(name)` / `findExecutable(name)` (`undefined` instead of a throw, for a test that skips without the tool) / `executableCandidates(name, platform, pathext)` - absolute paths for the binaries a fixture spawns: the running node, and `git` found by walking `PATH` once for the first regular executable file (`PATHEXT` names on Windows) and cached. A fixture spawns these instead of a bare `'git'` or `'node'`, so no writable directory on `PATH` decides what runs.
|
|
212
239
|
- `buildHostileTree(base)` / `hostileTreePerTest(prefix)` / `HOSTILE_NAMES` - the one hostile fixture every sink is tested against: a root with a member, a `..`-named member, a symlink pointing out, one pointing in, a dangling one, an alias link to the root, an unreadable directory, a 200-character name, plus a sibling `victim/secret.txt` outside the root; and the table of names (`../victim`, `..`, `.`, `''`, `/victim`, `C:\victim`, a NUL byte, …) a sink must refuse. Fields the host cannot build are `null`
|
|
240
|
+
- `installFaultFs({ within, faults, rewrites })` / `injectedErrnoError()` - an in-process fs fault injector: patches `node:fs`, `fs.promises` and `FileHandle.prototype` (then `syncBuiltinESMExports`) so a named import, node-tar, adm-zip and write streams are all reached; fails the nth matching call under `within` with a chosen errno (`ENOSPC`, `EACCES`, ...) and can rewrite `stat` results (`ino: 0`, case aliasing). Install BEFORE opening a handle; `cpSync` and native recursive `rmSync` can be failed as a call but not per file inside. `await session.settled()` before `restore()`: a caller's promise can settle while fs work it started goes on (Node's `rm` rejects on its first failed child and leaves the siblings running), and that work would otherwise run under the next session
|
|
241
|
+
- `INJECTED_ERRNOS` - every errno the injector can raise; each has a class in the fs-fault table (a unit test pins it).
|
|
242
|
+
- `faultRuleOf(spec)` / `faultFsSpecOf(json)` and `dist/testing/fault-fs-preload.js` - the injector for a SPAWNED process: `node --import <utils>/dist/testing/fault-fs-preload.js <bin> ...` reads `VAT_FAULT_FS` (`{"within", "faults": [{"family", "op", "pathIncludes", "nth", "errno"}]}`) and installs those faults before the program loads; a spec matches a path by substring, since a predicate cannot cross a process boundary. The preload is never re-exported from `./testing` (importing it IS the installation)
|
|
243
|
+
- `snapshotTree(root, { rewrites, keyPrefix })` / `diffSnapshots(a, b)` / `subtree(s, prefix)` - a comparable picture of a directory tree (kind, mode, sha256, link target) and a diff that names every difference; never follows a link, never opens a special file, fails loudly on an unreadable directory. `rewrites` normalise text (a per-case root, a timestamp) or, with `rewriteBytes`, a binary file (an archive's entry times) before hashing so two runs in different roots compare equal
|
|
213
244
|
- `getTestOutputDir()` / `getTestOutputBase()` - isolated test output paths
|
|
245
|
+
- `atLeastTierTimeout(tierTimeout, budgetMs)` - the timeout a test that needs `budgetMs` passes: `max(tier default, budget)`, so the platform-scaled default is the floor and the measured budget the ceiling — never a literal that shrinks the default on a slow platform, never a multiple that scales an hour-sized budget by the Windows factor. Throws on a non-finite or non-positive input (a config that provided no tier value)
|
|
214
246
|
- `symlinkCapability()` - probes once (memoized per process) whether this host can create symlinks (Windows needs Developer Mode or `SeCreateSymbolicLinkPrivilege`), returning a `SymlinkCapability` token or `null`
|
|
215
247
|
- `createSymlink()` / `createSymlinkAsync()` - the sanctioned way to create a symlink in test code; both require a `SymlinkCapability` from `symlinkCapability()`, so a test cannot reach the raw syscall without first proving the host supports it (or explicitly skipping via vitest's `skip()`)
|
|
216
248
|
|
|
@@ -226,26 +258,96 @@ These are CLI-boundary functions: inner libraries should take a root as a parame
|
|
|
226
258
|
|
|
227
259
|
They are nonetheless on their own [`./project`](#import-narrowly) entry rather than the barrel alone. The entry was briefly withdrawn on the grounds that the functions fit few repos — which is true, and is what the paragraph above says — but that answered the wrong question. What decides whether an *entry* exists is how heavy the only remaining door is, and barrel-only these four once cost five third-party packages to reach while their own code imports nothing but `node:fs` and `node:path`. Publishing the entry is not a claim that the ladder fits you — only that finding out shouldn't cost a dependency graph. The barrel is dependency-free now, which is that same rule applied everywhere rather than a reason to fold `./project` back in.
|
|
228
260
|
|
|
261
|
+
### Ordered and bounded async iteration — `@vibe-agent-toolkit/utils` (barrel only)
|
|
262
|
+
|
|
263
|
+
- `forEachInOrder(items, fn)` / `mapInOrder(items, fn)` - run `fn` one item at a time, each call starting after the previous settles; the first rejection stops the run and later items never start. For when order is the contract: ordered writes, first-refusal-wins errors, a shared memo or sink, a report whose order is its meaning.
|
|
264
|
+
- `everyInOrder(items, fn)` - `Array#every`, in order and awaited: stops at the first `false`. The idiom for a loop that used to `break`.
|
|
265
|
+
- `mapWithConcurrency(items, fn, limit = FS_CONCURRENCY)` - `Promise.all`-shaped with at most `limit` calls in flight, results in input order. For independent work over a population-sized list, where a bare `Promise.all` would open a descriptor per item.
|
|
266
|
+
- `mapConcurrentFailingInOrder(items, fn)` - `mapWithConcurrency`, but every call settles and the rejection of the EARLIEST item by position is rethrown — the error a sequential loop would have raised, for independent read-only work.
|
|
267
|
+
- `promised(work)` - `work()` as a promise: its value resolves, a synchronous throw rejects. `Promise.try` for a Promise-shaped API over a synchronous body, until the Node floor has it.
|
|
268
|
+
|
|
269
|
+
These replace `await` inside a loop, which the repo's ESLint config refuses (`no-await-in-loop`, mirroring Sonar S9382). None is written with `async` or a loop.
|
|
270
|
+
|
|
229
271
|
### Errors — `@vibe-agent-toolkit/utils` (barrel only)
|
|
230
272
|
|
|
231
273
|
- `VatError` - the base of every error VAT throws on purpose: `new VatError(code, message, { cause })`. `code` is a stable `SCREAMING_SNAKE` identity a catch block dispatches on; `name` is taken from the subclass.
|
|
232
274
|
- `isVatError(error, code?)` - the dispatch predicate. Reads a `Symbol.for('vat.error')` brand plus `code`, never the prototype chain, so a `dist` copy of a class still matches a `src` instance. A foreign error carrying a `code` (every `node:fs` errno) answers `false`.
|
|
275
|
+
- `fsFaultOf(error)` / `classifyFsFault(error, { side, action, origin?, shapeFromSource? })` / `withFsFault` / `withFsFaultSync` / `fsBoundary(roots)` - the one errno classifier. `fsFaultOf` walks `cause` (bounded) and answers `{ errno, faultClass, path, dest, syscall }` for a filesystem errno — one the table does not list, on an error a system call raised, is `device`, never `undefined` — `path` and `dest` without the `\\?\` namespace prefix Node may report them with, read once there for every consumer — `undefined` for anything else. `classifyFsFault` returns an `FsFaultError` (code `FS_FAULT`, with `side`, `faultClass`, `errno`, `path`, `origin`, `action`) and passes a non-fs error or an existing `VatError` through untouched. Classes: `absent`, `refused`, `exhausted`, `wrong-type`, `occupied`, `busy`, `unsupported` (EROFS included), `device`. `fsBoundary({ source, destination, environment })` decides the side from the path the OS named, not from the wrapper; `shapeFromSource` attributes a layout fault (`isLayoutFault`: the `wrong-type` and `occupied` classes, and `ENOTDIR` — a file in the way) of a write whose layout an input decided to `source`; every other fault keeps its side. The side x class -> refusal table lives in `schema`, not here. `isFsFaultError(error)` dispatches by brand. A site that resolves a link (or a path through links) and has an answer for one that resolves to nothing asks the one predicate `isUnresolvableLinkError` through one of two catch bodies, chosen by its lane. A WRITE lane (a copy, an install) calls `assertUnresolvableLink(error, ctx)`: it returns when the link resolves to nothing, and otherwise throws the error classified on `ctx` (a non-fs error as it was). A READ/REPORT lane calls `linkResolutionFault(error)`: `undefined` when the link resolves to nothing, the `FsFaultFacts` of any other filesystem errno — a fault for that one target, reported against it while the run goes on — and a non-fs error or a `VatError` rethrown. `FsLookupCache.realpath`, `canonicalizeSync`, link validation and the resources projection's extent, realizations and identity are read lanes. The link validator's existence walk (`DirectorySpellingIndex`) reads a directory listing that failed with an errno the predicate calls unresolvable (`ELOOP`) as absence, so one link gets one answer from the walk and the realpath beside it; `listingFailure` itself keeps `absent` to `ENOENT`/`ENOTDIR`, the crawler's enumeration question.
|
|
276
|
+
- `requireConfirmedAbsent(entry, absentError, ctx, { follows, sideOf })` - believe a probe's "nothing there" (`ENOENT` / `ENOTDIR`) only when the parent's listing agrees: an entry it lists (a link excepted only for a probe that `follows` links, which sees through to a missing target; never for an `lstat`) means the probe's answer was a fault, thrown classified on `sideOf(entry)`; a parent that cannot be listed throws too, on `sideOf(parent)` (`ctx` carries the action and no side). A registry read or a destination probe must never read a refused entry as empty or free.
|
|
277
|
+
- `SideOfPath` / `everywhere(side)` / `sideWithin(root, side, outside)` / `refusedPathOf(error, fallback)` - a fault is charged to the side of the path the OS REFUSED, never to the side of the question the read answered: one probe touches more than its own path (a link walk `lstat`s each component, a confirmed absence lists the parent), so presence probes take a `SideOfPath`. `everywhere` is one side for every path (VAT's staging, one input tree); `sideWithin` puts `root` and what lies beneath it on `side` and every other path on `outside` (the project config, an input, inside the project a build writes into); `refusedPathOf` names the path a refusal acted on, for a caller that classifies a walk's refusal itself.
|
|
278
|
+
- `FsFaultError.path` / `.movedFrom` - a fault is named, and `fsBoundary` sides it, by the path the failing SYSCALL acted on, not by Node's `error.path`: for a refused `symlink` or `link` that is the entry being created (`error.dest` — `error.path` is the target text), and for a refused `rename` it is where the entry could not land, with the entry being moved in `movedFrom` and both in the message
|
|
279
|
+
- `foldedSegment(segment)` - one path segment as the strictest volume VAT writes onto compares it (stream suffix and trailing dots and spaces cut, NFKD, upper- then lower-cased): two names with one fold are one entry somewhere. The archive entry policy and the plugin build's `files:` destinations are judged by it
|
|
280
|
+
- `pathPresent(path, mode, sideOf, absence)` - the one presence predicate: `false` only for an absence (the classifier's `absent` class); every other errno is thrown as an `FsFaultError` on `sideOf(<the path the OS refused>)` — the probe, a walked component, or the parent a confirmation lists (origin `content`). Both choices are the caller's and neither has a default. `mode`: `'entry'` (`lstat`: a dangling link is there — ask before writing) or `'follow'` (`stat`: a dangling link is absent — ask before reading). `absence`: `'confirmed'` (believed only once the parent's listing agrees, via `requireConfirmedAbsent`; a contradicted one is thrown — for a place about to be written, the run's own output, or a part of a package whose absence means "go on without it") or `'probe'` (the probe's word — for a caller that refuses or skips an absent path in its own words). Replaces `existsSync`, which answers `false` for an `EACCES` parent too (`local/no-existssync`).
|
|
281
|
+
- `isCapacityFault({ faultClass })` - whether a fault is the machine or the filesystem giving out (`exhausted`, `busy`, `unsupported`, `device`): never the input's content or layout, and the one to report first among several failures of one operation (an archive extraction whose entries fail after the disk filled). `fsBoundary(...).classify(error, action, fallback)` classifies an error already caught, for a catch that must first decide whether it is a filesystem fault at all.
|
|
282
|
+
- `isLayoutFault({ faultClass, errno })` / `isFileInTheWayError(error)` - whether a write's fault is one an input's layout decided (the only kind `shapeFromSource` moves to `source`), for a consumer judging a promoted fault, such as the CLI fault matrix's I8; `ENOTDIR` alone, a file in the way.
|
|
283
|
+
- `FS_SIDES` / `SOURCE_ORIGINS` - every `FsSide` (`source`, `destination`, `environment`) and every `SourceOrigin` (`argument`, `config`, `content`), as `const` tuples the two types derive from: for a consumer that keys a table by them, such as the schema refusal table's drift test.
|
|
284
|
+
- `FS_FAULT_ERRNOS_BY_CLASS` - the classifier's own table, class to errnos: what a check of anything that lists them (the docs' errno table) reads, so it is the same on every host — `os.constants.errno` is the HOST's list (Windows has no `EDQUOT` or `ESTALE`).
|
|
285
|
+
- Single-errno questions, the only home of an errno literal: `isPathAbsentError`, `isNoSuchEntryError` (`ENOENT` alone, for a site where a path component that is a file must stay loud), `isAlreadyExistsError`, `isUnresolvableLinkError` (the link resolves to nothing: absent, `ELOOP`; every other errno is a fault, the device class included — `UNKNOWN` with it, which libuv `src/win/error.c:172` answers both for a network share that is not there and for credentials the share refused — and so are Windows' ambiguous `EINVAL` and `ENAMETOOLONG`, which libuv answers for several Win32 errors each), `isFinalPathUnanswerableError` (`EISDIR`, `ENOTSUP`, `EOPNOTSUPP`: a volume that cannot answer a native realpath at all), `isInvalidArgumentError`, `isNotARegularFileError`, `isWouldBlockError`, `isTimedOutError` (`ETIMEDOUT`: a spawn given a `timeout` ran out of time), `isProcessGoneError`, `isSymlinkUnsupportedError`, `isRenameContentionError` (`EPERM`/`EBUSY`/`EACCES`: a rename racing a scanner). Whether an error is a filesystem fault at all is `fsFaultOf(e) !== undefined`.
|
|
233
286
|
- `prefixMessageOnce(error, prefix)` - prefix a message in place, once per error object — for a seam a retried item passes through more than once, without re-reading the message to find out.
|
|
234
287
|
- `PathEscapesRootError` (on `./path` too) - what `safePath.joinUnderRoot` throws; recognise it with `isVatError(error, PathEscapesRootError.code)`.
|
|
235
288
|
|
|
236
289
|
Never dispatch on `error.message` — the repo's ESLint config refuses `.message.includes(…)` and friends under `packages/*/src`. Prose is for humans and changes when it is improved; a code changes only when the meaning does.
|
|
237
290
|
|
|
291
|
+
### Entry identity — `@vibe-agent-toolkit/utils` (barrel only)
|
|
292
|
+
|
|
293
|
+
- `sameEntry(a, b)` - `'same' | 'different' | 'unknown'`: are two paths ONE entry on disk (case aliasing on APFS/NTFS, a link and its target, two links to one target)? Judged by a `bigint` `lstat` `dev:ino` (plus a link's target `stat`), or by an NFC-lowercased real path where the filesystem reports `ino` 0. An entry the OS refuses to examine is `unknown`, never `different`; an absent entry is `different`.
|
|
294
|
+
- `isInsideByIdentity(child, ancestor)` - `'inside' | 'outside' | 'unknown'`: does some directory above `child` (which need not exist) answer `sameEntry` to `ancestor`? Strict: an entry is not inside itself.
|
|
295
|
+
- `entryIdentities(entry, side)` - the `Identity[]` an entry answers to (`{ id }` or `{ foldedRealPath }`; `[]` when absent). Throws an `FsFaultError` on the `side` the caller names (`FsSide`) when the OS refuses to examine the entry.
|
|
296
|
+
|
|
297
|
+
### Readable-tree proof and copy — `@vibe-agent-toolkit/utils` (barrel only)
|
|
298
|
+
|
|
299
|
+
One walk serves both, so the proof and the copy can never disagree. Its options (`TreeWalkOptions`): `links` (`LinkPolicy`) — `'follow-contained'` takes a link as what it points at, inside the root only (a link out throws `CopyLinkEscapesSourceError`, `COPY_LINK_ESCAPES_SOURCE`; a link back into the walk throws `DirectoryWalkRevisitedError`, `DIRECTORY_WALK_REVISITED`), `'preserve'` keeps a link as a link and never examines its target — and an optional `filter(relative)` whose excluded entries are never stat'ed, opened or listed. The one special-file policy: a named pipe, socket or device (or a link to one, or one swapped in after the listing — each file is judged by `fstat` on the handle that is then read) is refused unopened as an `FsFaultError` of class `wrong-type` (`EFTYPE`).
|
|
300
|
+
|
|
301
|
+
- `proveTreeReadable(root, { links, filter?, side })` - before a verb writes anything, list every directory and open every file of the directory it is about to read (a root that is not a directory is refused by its listing, `ENOTDIR`, as `copyTree` refuses it); the first refusal, in listing order, is an `FsFaultError` on the `side` the caller names (`ProveTreeReadableOptions`), origin `content`, naming the entry.
|
|
302
|
+
- `refuseEscapingLinks({ root, subtree?, skip?, placement, side, flavor, tree, within, consequence })` / `treeContainment(root, candidate)` / `treeChain(root, path, flavor)` - refuse (`COPY_LINK_ESCAPES_SOURCE`) a tree holding a symbolic link that may not be kept, naming the link. Every directory is walked at its REAL path, so a link is judged where it lives, never through an in-tree alias of its directory. `placement`, `side` and `flavor` are required: `side` is whose filesystem a fault on the walk belongs to, and `flavor` is the path rules the tree's links are read under (`HOST_PATH_FLAVOR` for a real tree). Every link is first walked hop by hop by `lstat` and `readlink` alone (`treeChain`, the one resolver, exported for a caller that must judge a path below a root the same way) — a network, absolute, drive-relative or `\\?\` text anywhere on the chain (held to BOTH flavors' rules, since the tree is untrusted), a climb above the root, or a chain longer than the kernel's hop limit is refused before the kernel resolves anything (a Windows kernel asked to resolve `\\host\share` offers the operator's NTLM credentials to that host); a chain that returns to a link it has already met with the same text pending is a cycle and resolves to nothing, like a dangling link. `in-place` (a tree read where it stands) then refuses a link whose chain resolves outside `root` and accepts one that resolves to nothing; `relocated` (a tree an install copies, link text verbatim) holds each link to two more locks, because where a link leads at the source proves nothing about where its text leads once copied: its text is WALKED (`linkTextRefusal`) and must be relative, never rise above the root, pass through no other link, and end at something that exists; AND the kernel's own resolution must exist and be inside the root — a link that resolves to nothing is the same refusal, not a filesystem fault. It is the one check behind a cloned skill source and a local-directory install, both of which keep a link as a link. The link policies are `follow-contained`, `preserve` and `refuse` (a tree that must hold no link at all — one the archive module extracted); the walk `lstat`s its ROOT, so a root that is itself a link is judged by the same policy as every entry under it
|
|
303
|
+
- `assertLocalChain(path, within, flavor)` / `followedStat(Sync)(path, within)` / `requireFollowedStat(Sync)` / `followedKind(Sync)` / `followedRealpath(Sync)` / `realpathNative(Sync)(path, within)` / `isLinkChainRefusedError` / `untrustedTextRefusal(text)` / `untrustedLinkRefusal(linkPath, text)` / `isWithinUnder(path, root, flavor)` / `win32FoldedName(name)` / `HOST_PATH_FLAVOR` / `LINK_CHAIN_REFUSED_CODE` - `link-following.ts`, the ONE module that asks the kernel to follow a link (enforced by: `local/no-link-following-fs`, which also flags a `symlink` made with no type — Node on Windows follows the target to pick one; its allowlist of operator's-own-path files pins each file's exact count of such calls, asserted both ways). Every follower takes `within`, the tree the path is crawled in: the walk starts at that root's real path (resolved once per process) and reads only what lies beneath it, by `lstat`/`readlink`, on every host; a path that IS the operator's input passes itself and the kernel answers it. Under win32 rules a link text is local only when relative or drive-absolute: every other spelling is a network or device path, and a chain through one (or drive-relative, or past the hop limit) throws `LinkChainRefusedError` (`LINK_CHAIN_REFUSED`, class `refused`, its side from its context in `classifyFsFault`; `fsFaultOf` and `linkResolutionFault` read it as a fault). A probe the OS refuses on the way is thrown raw, never a pass. A chain to nothing is `undefined`/`absent` (or the kernel's `ENOENT`, `ELOOP` for a cycle, from the `require` and realpath forms). `untrustedTextRefusal` / `untrustedLinkRefusal` are the one text rule an untrusted tree's link is held to under both flavors (`network`, `absolute`, `drive-relative`, `unportable` — a NUL, or a component ending in a dot or a space — and `climbs-out`); `isWithinUnder` matches a subpath, ordinal-case-folded under win32 (`win32FoldedName`)
|
|
304
|
+
- `copyTree(source, root, relative, { links, filter?, side, onto })` - copy the directory `source` to `relative` under `root` (`''`: onto `root` itself) by that walk, each file from the handle the walk judged, with its mode; each directory gets its source's mode `| 0o700`, so a read-only source never becomes a copy nothing can fill or remove. Takes the proof's options (`ProveTreeReadableOptions`): a read the OS refuses is a classified fault on the `side` the caller names, as the proof's are; a write failure is the raw errno, for the caller's boundary to classify. `root` is a directory the caller made or its user named: made with its parents when absent, adopted when a real directory, and `EEXIST` when a link (to a directory included) or a file stands AT it; what is ABOVE it is the caller's and is followed, unexamined. Every directory between `root` and the copy must be a real one (made when absent): a link or a file there is a `source` fault (`occupied`) naming it. So a copy INTO a tree that holds copied content — links kept — names that tree's root, and is then never made through one of them.
|
|
305
|
+
- `copyRegularFile(source, root, relative, { side, reading, existing, writing })` - copy ONE regular file to `relative` under `root`, as `copyTree` copies one: opened without blocking and judged by `fstat` on that handle (a special file refused unread), its bytes from that handle, its mode kept. The copy is made as `writeFileUnder` makes a file, through the same code: real directories only (made when absent), an exclusive create, bytes and mode set on that handle — never through a link; `existing` decides a regular file already there. A refused read is a classified fault on `side` (origin `content`) whose action reads `read <reading>`; an entry in the copy's way is a `source` fault (`occupied`) naming it; any other write failure is the raw errno. The one single-file copy (`copyFile` is banned by `local/no-destructive-fs`).
|
|
306
|
+
- `readRegularFile(path)` - a file's bytes through a handle opened without blocking: `EFTYPE` for a special file, `EISDIR` for a directory, raw for the caller to classify.
|
|
307
|
+
- `FollowedWalk` - the cycle guard every other following walk holds, built with the side its tree is on (`new FollowedWalk(side)`; a refused realpath in `enter` is an `FsFaultError` there): `enter(dir)` on the root and each directory recursed into; a directory reached again under a second spelling (a link back into the tree) throws `DirectoryWalkRevisitedError` instead of recursing until the path length runs out.
|
|
308
|
+
|
|
309
|
+
### Tree changes: plan, then apply — `@vibe-agent-toolkit/utils` (barrel only)
|
|
310
|
+
|
|
311
|
+
Every recursive remove, rename or copy of a destination goes through these two calls. A verb describes what it wants as `TreeChange`s — `replace` (a `TreeFill`: `copy` from a source with a link policy and filter, `write` into the empty staged directory with its declared `reads`, or `link` to a target), `replace-file` (its `FileContents`: bytes, or a function called when the file is staged — after every earlier change of the plan has staged, so an archive of a tree the plan stages is a change of that plan) or `remove` (with an optional `keepIfSameAs`) — each with an `Ownership` of what is there (`must-be-free`, `force`, `vat-made` with a `recognise` returning an `OwnershipVerdict`, `vat-state`) and a `label`.
|
|
312
|
+
|
|
313
|
+
- `planTreeChanges(changes)` - takes every destination in its canonical spelling (`safePath.resolve`) and decides every change with no side effect, into a `TreePlan` of `PlannedChange`s (`existing`: `EntryKind`; `action`: `PlannedAction` — `create`, `replace`, `remove`, `keep` or `subsumed`, with a `reason`). A remove aliased (by `sameEntry`, never by name) to a replace or an earlier remove, or inside another change's destination, is `subsumed`; a remove whose `keepIfSameAs` names an entry `same` or `unknown` to it is kept, and so is every remove that holds (or cannot be proven not to hold) a kept entry. A destination whose `lstat` answers absent is believed only when its parent's listing agrees (`requireConfirmedAbsent`). Any other two active changes over one tree are refused `TREE_DESTS_OVERLAP` — the calling verb's defect (by identity; by spelling where the outer destination is absent). `unknown` identity is never a refusal (an `ino`-0 filesystem answers it for every pair). Refusals, coded: `TREE_DEST_OCCUPIED` (`must-be-free` over anything but an empty directory), `TREE_DEST_NOT_OWNED` (`vat-made` disowned, with the recogniser's reason), `TREE_DEST_HOLDS_SOURCE` / `TREE_SOURCE_HOLDS_DEST` (a copy source, or a `write` fill's `reads`, that is or lies inside the destination, or holds it). Every `copy` source is proven readable on the fill's declared `side` (required: an input, VAT's staging, or a copy already in user state). `plan.describe()` is the dry run: one `<action> <label> <dest> [(reason)]` line per change.
|
|
314
|
+
- `applyTreePlan(plan, { afterSwap? })` - stage every new entry beside its destination, park every previous one (`.<base>.vat-staged-<random>.previous`), swap, run `afterSwap`, then remove what was parked. A failure before finalize rolls back so every destination is byte-equal to before (a case-aliased directory back under its on-disk spelling); a parked entry is never deleted on a failure path, and one that cannot be put back makes the error `TREE_ROLLBACK_INCOMPLETE` — `TreeRollbackIncompleteError` (`cause`: the failure; `parked`: where each is; `stranded`: each `TreeRollbackStranded` destination), the one shape of that code, which a caller undoing its own `afterSwap` writes throws too. The primitive's own faults are `FsFaultError`s on side `destination` (a copy source's read stays `source`); a `write` fill's raw errno is `destination` only when it names a path under the destination's parent — an input the callback read is rethrown raw, for the verb's boundary. A thrown failure is never mutated: what a failure path could not clean up (a staged entry, a parent it made, a replaced tree beside a failed remove) is recorded beside it — `suppressedFaultsOf`. A parked replaced tree that cannot be removed is a `TreeChangeWarning` coded `TREE_CLEANUP_INCOMPLETE` in `ApplyResult.warnings`; for a `remove` it is a thrown `destination` fault naming the parked path. Removal makes a read-only tree owner-writable first.
|
|
315
|
+
- `applyTreePlanOrLeftover(plan, { afterSwap? })` - `applyTreePlan` for a verb that reports a change it finished even when what it parked could not then be deleted (an uninstall, a clear): a failure before the commit (every swap made, `afterSwap` run) rolled back and is thrown; one after it is returned as `ApplyOutcome.leftover` (a `destination` fault naming the parked entry), never thrown.
|
|
316
|
+
- `renameFileAtomic(from, to)` - one rename (a file or a tree); under win32 `EPERM` / `EBUSY` / `EACCES` is retried up to 6 tries, backoff 50·2ⁿ ms. Raw errno out.
|
|
317
|
+
- `writeFileUnder(root, relative, contents, { existing, writing })` - write a file VAT composes into a tree it is building (one whose source may have shipped links) without ever writing over or through what is there: each directory component must be a real directory, the file is created exclusively; `existing: 'replace'` removes a regular file of that name first (remove, then create — not one step: a create that then fails leaves no file, and its raw errno), a link or directory always refuses (`FsFaultError`, side `source`, origin `content`, class `occupied`, naming the entry). `copyRegularFile` is built on it.
|
|
318
|
+
- `makeDirectoryUnder(root, relative, writing)` - the directory half of `writeFileUnder`: make `relative` under a tree VAT is building, component by component, adopting real directories and refusing a link or file standing where one goes (a recursive `mkdir` would follow the link out of the tree).
|
|
319
|
+
- `requireTestScratch(root, what)` / `TEST_USER_STATE_UNDER` - the fail-closed guard every resolver of user state under the home directory passes its root through: throws when a test process named the tree user state may be in (the shared vitest setup does, in every tier) and `root` is outside it. Unset, it does nothing.
|
|
320
|
+
- `replaceFile(dest, contents)` - write a temp beside `dest`, then `renameFileAtomic` over it: a failed write leaves `dest` byte-equal. Keeps the file's mode, and refuses a file a write in place would be refused on (a read-only one) before writing anything — the rename alone would replace it; writes through a link, which stays a link. Raw errno out.
|
|
321
|
+
- `withTempDir(prefix, work)` / `disposeTempDir(dir)` - a fresh directory under `normalizedTmpdir()`, always disposed of; the work's error is rethrown unchanged, a disposal failure recorded beside it (`suppressedFaultsOf`). When the work SUCCEEDED nothing is thrown: it returns `TempDirOutcome { value, leftover }` — `leftover` required, `undefined` when the directory went, else the `environment` fault naming the directory, for the verb to report as the one `TREE_CLEANUP_INCOMPLETE` warning beside its finished work. `disposeTempDir` answers `undefined` when gone, else that same leftover fault — and refuses (`TEMP_DIR_OUTSIDE_TMPDIR`, a defect, nothing touched) any directory not strictly under the temp directory, since it makes a read-only tree writable before deleting it.
|
|
322
|
+
- `disposeTempDirSync(dir)` - `disposeTempDir` for a process on its way out (a signal handler, an `exit` listener), where no promise settles: synchronous, the same refusal and the same `undefined`-or-leftover answer, no mode repair, and a short bounded retry of a transient refusal (`EBUSY`/`EPERM` while a just-terminated child lets go on Windows).
|
|
323
|
+
- `disposeTempDirAfterFailure(dir, failure)` - for a temp directory handed back on success and removed only when the work failed: disposes of it and records a disposal fault beside `failure` (`suppressedFaultsOf`), never thrown in its place; the same `TEMP_DIR_OUTSIDE_TMPDIR` refusal as `disposeTempDir`.
|
|
324
|
+
- `recordSuppressedFault(error, fault)` - record `fault` (a leftover found while `error` was being handled) beside `error`, off its cause chain, for `suppressedFaultsOf` to read back; a thrown non-object gets a `process.emitWarning` instead.
|
|
325
|
+
- `suppressedFaultsOf(error)` - the faults recorded against a thrown error, and against every error on its cause chain (bounded, cycle-safe), while each was being handled — a leftover the failure path could not remove — so a wrapper thrown in the failure's place (a rollback-incomplete error, a verb's re-wrap) still carries them. Recorded beside the error, never on its cause chain, so never its classification; a thrown non-object gets a `process.emitWarning` instead. The CLI publishes each as a `TREE_CLEANUP_INCOMPLETE` warning naming the leftover.
|
|
326
|
+
- `isTreeChangeResidue(name)` - whether a directory entry is the primitive's own staged, parked or discarded entry, which a listing beside a destination must skip.
|
|
327
|
+
- `isParkedTreeEntry(name)` - whether a directory entry is a parked previous entry (`….vat-staged-<random>.previous`): the residue a sweep may remove, since a staged entry may be another process's change in flight.
|
|
328
|
+
|
|
238
329
|
### Directory crawling — `@vibe-agent-toolkit/utils/crawl`
|
|
239
330
|
|
|
240
331
|
- `crawlDirectory()` / `crawlDirectorySync()` - gitignore-aware directory walks
|
|
241
332
|
- `NEVER_CRAWL_GLOBS` / `BUILD_OUTPUT_GLOBS` - the standard exclusion sets
|
|
242
333
|
- `UnreadablePolicy` - the REQUIRED `unreadable` option on every crawl (and on `gitLsFiles` /
|
|
243
|
-
`gitLsOthers` under `./git`): `{ refuse: { root, remedy } }` throws `DirectoryListingRefusedError`
|
|
244
|
-
with an adopter-facing, root-relative sentence when a directory cannot be listed
|
|
334
|
+
`gitLsOthers` under `./git`): `{ refuse: { root, remedy, side } }` throws `DirectoryListingRefusedError`
|
|
335
|
+
with an adopter-facing, root-relative sentence when a directory cannot be listed — coded `FS_FAULT`
|
|
336
|
+
and carrying the classified fault (on `side`) as its `cause`. A crawl's required `outputs` — the trees
|
|
337
|
+
the calling verb writes, `[]` for none — is the one declaration of which side a fault is on: a
|
|
338
|
+
directory that is an output, lies inside one or holds one (the base of a project a build writes
|
|
339
|
+
into included) is the destination's, any other is on `side` (`settleCrawlRefusal`, for a caller
|
|
340
|
+
that settles a crawl's refusals itself; `onCrawlOutput(path, outputs)`, for a verb that reads a tree
|
|
341
|
+
it may itself have written earlier in the run — `vat build`'s plugin phase reading `dist/skills` —
|
|
342
|
+
and must ask the same declaration which side that tree is on);
|
|
245
343
|
`{ degrade: (refusal) => … }` keeps walking and hands the gap to you to report. There is no
|
|
246
344
|
default — a shorter list nothing can tell from a complete one is the failure this option exists
|
|
247
345
|
to prevent, so the caller states which answer is honest for its lane, and an omitted policy is
|
|
248
346
|
refused up front by name.
|
|
347
|
+
- `rootListingRefusal(error, dir)` - for a caller that asks whether a scan ROOT exists before
|
|
348
|
+
walking it: the `DirectoryRefusal` a failed `stat`/`readdir` of `dir` stands for, or `undefined`
|
|
349
|
+
when it is simply absent (`ENOENT`/`ENOTDIR`) — never `existsSync`, which calls an untraversable
|
|
350
|
+
parent absent.
|
|
249
351
|
|
|
250
352
|
Glob *pattern inspection* is a separate entry, `./glob`, and stays that way: `./glob` is dependency-free and reaches only `node:path`, whereas crawling reaches the filesystem, `git`, and `picomatch`.
|
|
251
353
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE way VAT reads or extracts an archive — an npm tarball, a skill ZIP, a ZIP fetched from
|
|
3
|
+
* a URL. Every lane goes through here, so every lane has the same answer to the same archive:
|
|
4
|
+
*
|
|
5
|
+
* - The archive is read whole, in memory, bounded by the caller's {@link ArchiveLimits}.
|
|
6
|
+
* - Which archives are extracted is decided from the entry list, before anything is written
|
|
7
|
+
* ({@link archiveEntryRefusal}): only regular files and directories, each named inside the
|
|
8
|
+
* archive, no two of them one name.
|
|
9
|
+
* - What is written is written by VAT, never by an archive library: each directory a plain
|
|
10
|
+
* `mkdir`, each file an exclusive create that no link satisfies (`openFileUnder`), with mode
|
|
11
|
+
* `0644` or `0755` by the archive's executable bit and nothing else of its mode — narrowed by
|
|
12
|
+
* the process's umask, as any file it creates is.
|
|
13
|
+
*
|
|
14
|
+
* So an extracted tree holds no link, no special file and no setuid bit, whatever the archive said.
|
|
15
|
+
*/
|
|
16
|
+
import { type FsSide } from '../errors/fs-fault.js';
|
|
17
|
+
import { type ArchiveEntry, type ArchiveFormat, type ArchiveLimits } from './entries.js';
|
|
18
|
+
/**
|
|
19
|
+
* Where an archive is: a file — on the `side` of the verb that named it, an operator's argument
|
|
20
|
+
* (`source`) or VAT's own download (`environment`) — or bytes already in hand, with the `label`
|
|
21
|
+
* a refusal names them by.
|
|
22
|
+
*/
|
|
23
|
+
export type ArchiveSource = {
|
|
24
|
+
readonly file: string;
|
|
25
|
+
readonly side: FsSide;
|
|
26
|
+
} | {
|
|
27
|
+
readonly bytes: Uint8Array;
|
|
28
|
+
readonly label: string;
|
|
29
|
+
};
|
|
30
|
+
/** What every read of an archive is told. Both are required: an omitted bound is the failure. */
|
|
31
|
+
export interface ArchiveReadOptions {
|
|
32
|
+
readonly format: ArchiveFormat;
|
|
33
|
+
readonly limits: ArchiveLimits;
|
|
34
|
+
}
|
|
35
|
+
/** What an extraction is told: how to read the archive, and which of its entries not to write. */
|
|
36
|
+
export interface ArchiveExtractOptions extends ArchiveReadOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Entries to leave out of what is written (a host's own litter beside a skill). They are still
|
|
39
|
+
* read and still JUDGED: an archive is refused for a link among them like any other.
|
|
40
|
+
*/
|
|
41
|
+
readonly omit?: (entry: ArchiveEntry) => boolean;
|
|
42
|
+
}
|
|
43
|
+
/** An archive read into memory: what it lists, and each file's bytes on request. */
|
|
44
|
+
export interface OpenedArchive {
|
|
45
|
+
/** What a refusal names the archive by: its path, or the label its bytes came with. */
|
|
46
|
+
readonly label: string;
|
|
47
|
+
/** Every entry, in archive order, as the archive spells it. Nothing here has been judged. */
|
|
48
|
+
readonly entries: readonly ArchiveEntry[];
|
|
49
|
+
/**
|
|
50
|
+
* The bytes of one of {@link entries} (a file).
|
|
51
|
+
*
|
|
52
|
+
* @throws ArchiveEntryReadError (`unsupported`, naming the entry) when it does not inflate to the size it declares
|
|
53
|
+
*/
|
|
54
|
+
read(entry: ArchiveEntry): Buffer;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The mode of an extracted file: what the process's own umask gave the file when it was created,
|
|
58
|
+
* never wider — `0644` at most, or `0755` for an executable entry, with execute granted exactly
|
|
59
|
+
* where the umask left read. Under `umask 077` an extracted file is `0600` / `0700`, as a file
|
|
60
|
+
* the user made any other way is.
|
|
61
|
+
*
|
|
62
|
+
* @param created - The mode the exclusive create gave the file
|
|
63
|
+
* @param executable - Whether the archive marked the entry executable
|
|
64
|
+
*/
|
|
65
|
+
export declare function extractedFileMode(created: number, executable: boolean): number;
|
|
66
|
+
/**
|
|
67
|
+
* An archive file's bytes, through a handle opened without blocking (a named pipe is refused,
|
|
68
|
+
* never waited on) and sized by `fstat` on that handle before a byte is read. For a lane that
|
|
69
|
+
* reads an archive ONCE and both judges and passes on those bytes (`{ bytes, label }`).
|
|
70
|
+
*
|
|
71
|
+
* @param file - The archive's path
|
|
72
|
+
* @param side - Whose the file is: an operator's argument (`source`) or VAT's own download (`environment`)
|
|
73
|
+
* @param limits - The bounds the archive will be held to: a file no archive within them can be is refused unread
|
|
74
|
+
* @throws FsFaultError on `side` for a read the OS refuses; ArchiveRefusedError `too-large` for a
|
|
75
|
+
* file no archive within `limits` can be
|
|
76
|
+
*/
|
|
77
|
+
export declare function readArchiveFile(file: string, side: FsSide, limits: ArchiveLimits): Promise<Buffer>;
|
|
78
|
+
/**
|
|
79
|
+
* Read an archive into memory and list it. Nothing is judged: a caller that only inspects an
|
|
80
|
+
* archive (an upload's preflight) sees its links and unsafe names as entries.
|
|
81
|
+
*
|
|
82
|
+
* @param source - The archive
|
|
83
|
+
* @param options - Its `format`, and the `limits` it is held to as it is read
|
|
84
|
+
* @throws FsFaultError on the source's side when the OS refuses the read; ArchiveRefusedError
|
|
85
|
+
* `unreadable` for bytes that are no archive of the format, `too-many-entries` / `too-large`
|
|
86
|
+
* for one past `limits`
|
|
87
|
+
*/
|
|
88
|
+
export declare function openArchive(source: ArchiveSource, options: ArchiveReadOptions): Promise<OpenedArchive>;
|
|
89
|
+
/**
|
|
90
|
+
* Every entry of an archive, in archive order ({@link openArchive}).
|
|
91
|
+
*
|
|
92
|
+
* @throws as {@link openArchive}
|
|
93
|
+
*/
|
|
94
|
+
export declare function listArchive(source: ArchiveSource, options: ArchiveReadOptions): Promise<readonly ArchiveEntry[]>;
|
|
95
|
+
/**
|
|
96
|
+
* Extract an archive into `into`, a directory the caller just made — every entry, or a refusal.
|
|
97
|
+
*
|
|
98
|
+
* The whole entry list is judged first ({@link archiveEntryRefusal}); an archive that passes is
|
|
99
|
+
* written one entry at a time, in archive order, so when this throws nothing is still being
|
|
100
|
+
* written and the caller may dispose of `into`.
|
|
101
|
+
*
|
|
102
|
+
* @param source - The archive
|
|
103
|
+
* @param into - A fresh directory VAT owns (staging, a cache entry): every write is VAT's own scratch
|
|
104
|
+
* @param options - The archive's `format`, the `limits` it is held to, and any entries to `omit`
|
|
105
|
+
* @returns Every entry of the archive, in archive order — an omitted one included
|
|
106
|
+
* @throws ArchiveRefusedError — the input's refusal — for an archive that is unreadable, past
|
|
107
|
+
* `limits`, or holds an entry the policy refuses, with nothing written; ArchiveEntryReadError
|
|
108
|
+
* (`ARCHIVE_ENTRY_UNREADABLE`, the input's too) for a member that does not inflate or holds other
|
|
109
|
+
* than it declares, met as it is written; FsFaultError on the source's side for a read the OS
|
|
110
|
+
* refuses, and side `environment` for a write it refuses
|
|
111
|
+
*/
|
|
112
|
+
export declare function extractArchive(source: ArchiveSource, into: string, options: ArchiveExtractOptions): Promise<readonly ArchiveEntry[]>;
|
|
113
|
+
/**
|
|
114
|
+
* The ZIP VAT writes of an archive it accepts, in memory: the archive judged whole as an extraction
|
|
115
|
+
* judges it, then each entry it lists — but those the caller omits — named by the place it lands
|
|
116
|
+
* at, each file's bytes held to the size it declares, with mode `0644` or `0755` by its executable
|
|
117
|
+
* bit. Nothing else of the archive's bytes: no local entry its directory does not list, no stub
|
|
118
|
+
* before it, no comment, no other mode. What a lane that passes an archive ON sends, so the bytes
|
|
119
|
+
* that leave are exactly the entries that were judged.
|
|
120
|
+
*
|
|
121
|
+
* @param source - The archive
|
|
122
|
+
* @param options - The archive's `format`, the `limits` it is held to, and any entries to `omit`
|
|
123
|
+
* @returns The new archive's bytes
|
|
124
|
+
* @throws as {@link openArchive}; ArchiveRefusedError for an entry the policy refuses;
|
|
125
|
+
* ArchiveEntryReadError (`ARCHIVE_ENTRY_UNREADABLE`) for a member that does not inflate, or does
|
|
126
|
+
* not hold the size it declares
|
|
127
|
+
*/
|
|
128
|
+
export declare function zipOfArchive(source: ArchiveSource, options: ArchiveExtractOptions): Promise<Buffer>;
|
|
129
|
+
//# sourceMappingURL=archive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../../src/archive/archive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAmB,KAAK,MAAM,EAAe,MAAM,uBAAuB,CAAC;AAMlF,OAAO,EAAE,KAAK,YAAY,EAAyB,KAAK,aAAa,EAAE,KAAK,aAAa,EAAqD,MAAM,cAAc,CAAC;AAMnK;;;;GAIG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D,iGAAiG;AACjG,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;CAChC;AAED,kGAAkG;AAClG,MAAM,WAAW,qBAAsB,SAAQ,kBAAkB;IAC/D;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC;CAClD;AAED,oFAAoF;AACpF,MAAM,WAAW,aAAa;IAC5B,uFAAuF;IACvF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,6FAA6F;IAC7F,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IAC1C;;;;OAIG;IACH,IAAI,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAAC;CACnC;AAaD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,GAAG,MAAM,CAG9E;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAUxG;AA8CD;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC,CAE5G;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,SAAS,YAAY,EAAE,CAAC,CAEtH;AAuCD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,SAAS,YAAY,EAAE,CAAC,CAS1I;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAQzG"}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE way VAT reads or extracts an archive — an npm tarball, a skill ZIP, a ZIP fetched from
|
|
3
|
+
* a URL. Every lane goes through here, so every lane has the same answer to the same archive:
|
|
4
|
+
*
|
|
5
|
+
* - The archive is read whole, in memory, bounded by the caller's {@link ArchiveLimits}.
|
|
6
|
+
* - Which archives are extracted is decided from the entry list, before anything is written
|
|
7
|
+
* ({@link archiveEntryRefusal}): only regular files and directories, each named inside the
|
|
8
|
+
* archive, no two of them one name.
|
|
9
|
+
* - What is written is written by VAT, never by an archive library: each directory a plain
|
|
10
|
+
* `mkdir`, each file an exclusive create that no link satisfies (`openFileUnder`), with mode
|
|
11
|
+
* `0644` or `0755` by the archive's executable bit and nothing else of its mode — narrowed by
|
|
12
|
+
* the process's umask, as any file it creates is.
|
|
13
|
+
*
|
|
14
|
+
* So an extracted tree holds no link, no special file and no setuid bit, whatever the archive said.
|
|
15
|
+
*/
|
|
16
|
+
import { classifyFsFault, withFsFault } from '../errors/fs-fault.js';
|
|
17
|
+
import { forEachInOrder } from '../in-order.js';
|
|
18
|
+
import { toForwardSlash } from '../path-core.js';
|
|
19
|
+
import { openForReading } from '../text-file.js';
|
|
20
|
+
import { makeDirectoryUnder, openFileUnder } from '../tree-change/files.js';
|
|
21
|
+
import { ArchiveEntryReadError, ArchiveRefusedError, archiveEntryRefusal, placeOf } from './entries.js';
|
|
22
|
+
import { archiveBytesCap, tooLarge } from './limits.js';
|
|
23
|
+
import { readTar } from './tar-reader.js';
|
|
24
|
+
import { readZip, zipOfEntries } from './zip-reader.js';
|
|
25
|
+
const READERS = {
|
|
26
|
+
tar: readTar,
|
|
27
|
+
zip: readZip,
|
|
28
|
+
};
|
|
29
|
+
/** The widest mode an extracted file gets: the executable bit is all an archive's mode decides. */
|
|
30
|
+
const FILE_MODE = 0o644;
|
|
31
|
+
const EXECUTABLE_MODE = 0o755;
|
|
32
|
+
const PERMISSION_BITS = 0o777;
|
|
33
|
+
const READ_BITS = 0o444;
|
|
34
|
+
/**
|
|
35
|
+
* The mode of an extracted file: what the process's own umask gave the file when it was created,
|
|
36
|
+
* never wider — `0644` at most, or `0755` for an executable entry, with execute granted exactly
|
|
37
|
+
* where the umask left read. Under `umask 077` an extracted file is `0600` / `0700`, as a file
|
|
38
|
+
* the user made any other way is.
|
|
39
|
+
*
|
|
40
|
+
* @param created - The mode the exclusive create gave the file
|
|
41
|
+
* @param executable - Whether the archive marked the entry executable
|
|
42
|
+
*/
|
|
43
|
+
export function extractedFileMode(created, executable) {
|
|
44
|
+
const granted = created & PERMISSION_BITS;
|
|
45
|
+
return executable ? (granted | ((granted & READ_BITS) >> 2)) & EXECUTABLE_MODE : granted & FILE_MODE;
|
|
46
|
+
}
|
|
47
|
+
const labelOf = (source) => ('file' in source ? toForwardSlash(source.file) : source.label);
|
|
48
|
+
/**
|
|
49
|
+
* An archive file's bytes, through a handle opened without blocking (a named pipe is refused,
|
|
50
|
+
* never waited on) and sized by `fstat` on that handle before a byte is read. For a lane that
|
|
51
|
+
* reads an archive ONCE and both judges and passes on those bytes (`{ bytes, label }`).
|
|
52
|
+
*
|
|
53
|
+
* @param file - The archive's path
|
|
54
|
+
* @param side - Whose the file is: an operator's argument (`source`) or VAT's own download (`environment`)
|
|
55
|
+
* @param limits - The bounds the archive will be held to: a file no archive within them can be is refused unread
|
|
56
|
+
* @throws FsFaultError on `side` for a read the OS refuses; ArchiveRefusedError `too-large` for a
|
|
57
|
+
* file no archive within `limits` can be
|
|
58
|
+
*/
|
|
59
|
+
export async function readArchiveFile(file, side, limits) {
|
|
60
|
+
return await withFsFault({ side, origin: 'argument', action: `read the archive ${toForwardSlash(file)}`, path: file }, async () => {
|
|
61
|
+
const handle = await openForReading(file);
|
|
62
|
+
try {
|
|
63
|
+
if ((await handle.stat()).size > archiveBytesCap(limits))
|
|
64
|
+
throw tooLarge(toForwardSlash(file), limits);
|
|
65
|
+
return await handle.readFile();
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
await handle.close();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async function bytesOf(source, limits) {
|
|
73
|
+
if ('file' in source)
|
|
74
|
+
return await readArchiveFile(source.file, source.side, limits);
|
|
75
|
+
if (source.bytes.byteLength > archiveBytesCap(limits))
|
|
76
|
+
throw tooLarge(source.label, limits);
|
|
77
|
+
return Buffer.from(source.bytes.buffer, source.bytes.byteOffset, source.bytes.byteLength);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* One record's bytes, held to the size its entry declared. An entry that does not inflate, or does
|
|
81
|
+
* not hold what it declares, is in an archive that parsed: `unsupported` — never `unreadable`, which
|
|
82
|
+
* means bytes that are no archive at all, the one refusal a lane may pass on.
|
|
83
|
+
*/
|
|
84
|
+
function bytesOfRecord(archive, record) {
|
|
85
|
+
let data;
|
|
86
|
+
try {
|
|
87
|
+
data = record.read();
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
throw new ArchiveEntryReadError(archive, record.name, `does not inflate (${String(error)})`, { cause: error });
|
|
91
|
+
}
|
|
92
|
+
if (data.length !== record.size) {
|
|
93
|
+
throw new ArchiveEntryReadError(archive, record.name, `declares ${record.size} bytes and holds ${data.length}`);
|
|
94
|
+
}
|
|
95
|
+
return data;
|
|
96
|
+
}
|
|
97
|
+
async function openRecords(source, options) {
|
|
98
|
+
const label = labelOf(source);
|
|
99
|
+
const records = READERS[options.format](await bytesOf(source, options.limits), label, options.limits);
|
|
100
|
+
const byEntry = new Map(records.map((record) => [record, record]));
|
|
101
|
+
return {
|
|
102
|
+
label,
|
|
103
|
+
entries: records,
|
|
104
|
+
records,
|
|
105
|
+
read(entry) {
|
|
106
|
+
const record = byEntry.get(entry);
|
|
107
|
+
if (record === undefined)
|
|
108
|
+
throw new TypeError(`"${entry.name}" is not an entry of ${label}`);
|
|
109
|
+
return bytesOfRecord(label, record);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Read an archive into memory and list it. Nothing is judged: a caller that only inspects an
|
|
115
|
+
* archive (an upload's preflight) sees its links and unsafe names as entries.
|
|
116
|
+
*
|
|
117
|
+
* @param source - The archive
|
|
118
|
+
* @param options - Its `format`, and the `limits` it is held to as it is read
|
|
119
|
+
* @throws FsFaultError on the source's side when the OS refuses the read; ArchiveRefusedError
|
|
120
|
+
* `unreadable` for bytes that are no archive of the format, `too-many-entries` / `too-large`
|
|
121
|
+
* for one past `limits`
|
|
122
|
+
*/
|
|
123
|
+
export async function openArchive(source, options) {
|
|
124
|
+
return await openRecords(source, options);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Every entry of an archive, in archive order ({@link openArchive}).
|
|
128
|
+
*
|
|
129
|
+
* @throws as {@link openArchive}
|
|
130
|
+
*/
|
|
131
|
+
export async function listArchive(source, options) {
|
|
132
|
+
return (await openRecords(source, options)).entries;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* An archive read and judged whole ({@link archiveEntryRefusal}): what an extraction writes, and
|
|
136
|
+
* what a re-zip writes, are decided by the one entry policy before a byte of either is written.
|
|
137
|
+
*
|
|
138
|
+
* @throws as {@link openArchive}; ArchiveRefusedError for an entry the policy refuses
|
|
139
|
+
*/
|
|
140
|
+
async function openJudged(source, options) {
|
|
141
|
+
const opened = await openRecords(source, options);
|
|
142
|
+
const refusal = archiveEntryRefusal(opened.entries);
|
|
143
|
+
if (refusal !== undefined)
|
|
144
|
+
throw new ArchiveRefusedError(opened.label, refusal);
|
|
145
|
+
return opened;
|
|
146
|
+
}
|
|
147
|
+
/** The records of a judged archive to write: every one but those the caller omits. */
|
|
148
|
+
const keptRecords = (opened, options) => options.omit === undefined ? opened.records : opened.records.filter((record) => options.omit?.(record) !== true);
|
|
149
|
+
/** Write one accepted entry under `into`: a directory made plainly, a file created exclusively with only its executable bit kept. */
|
|
150
|
+
async function writeRecord(archive, into, record) {
|
|
151
|
+
const relative = placeOf(record.name).join('/');
|
|
152
|
+
const writing = `the entry ${record.name} of ${archive}`;
|
|
153
|
+
if (record.kind === 'directory') {
|
|
154
|
+
// The archive's own root (`./`) is the directory the caller made.
|
|
155
|
+
if (relative !== '')
|
|
156
|
+
await makeDirectoryUnder(into, relative, writing);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const data = bytesOfRecord(archive, record);
|
|
160
|
+
const handle = await openFileUnder(into, relative, { existing: 'refuse', writing });
|
|
161
|
+
try {
|
|
162
|
+
await handle.writeFile(data);
|
|
163
|
+
// On the handle, never by path: nothing swapped in after the create is examined or chmod-ed through.
|
|
164
|
+
await handle.chmod(extractedFileMode((await handle.stat()).mode, record.executable));
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
await handle.close();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Extract an archive into `into`, a directory the caller just made — every entry, or a refusal.
|
|
172
|
+
*
|
|
173
|
+
* The whole entry list is judged first ({@link archiveEntryRefusal}); an archive that passes is
|
|
174
|
+
* written one entry at a time, in archive order, so when this throws nothing is still being
|
|
175
|
+
* written and the caller may dispose of `into`.
|
|
176
|
+
*
|
|
177
|
+
* @param source - The archive
|
|
178
|
+
* @param into - A fresh directory VAT owns (staging, a cache entry): every write is VAT's own scratch
|
|
179
|
+
* @param options - The archive's `format`, the `limits` it is held to, and any entries to `omit`
|
|
180
|
+
* @returns Every entry of the archive, in archive order — an omitted one included
|
|
181
|
+
* @throws ArchiveRefusedError — the input's refusal — for an archive that is unreadable, past
|
|
182
|
+
* `limits`, or holds an entry the policy refuses, with nothing written; ArchiveEntryReadError
|
|
183
|
+
* (`ARCHIVE_ENTRY_UNREADABLE`, the input's too) for a member that does not inflate or holds other
|
|
184
|
+
* than it declares, met as it is written; FsFaultError on the source's side for a read the OS
|
|
185
|
+
* refuses, and side `environment` for a write it refuses
|
|
186
|
+
*/
|
|
187
|
+
export async function extractArchive(source, into, options) {
|
|
188
|
+
const opened = await openJudged(source, options);
|
|
189
|
+
try {
|
|
190
|
+
// In order: a directory before what goes in it, and the first failure the only one.
|
|
191
|
+
await forEachInOrder(keptRecords(opened, options), (record) => writeRecord(opened.label, into, record));
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
throw classifyFsFault(error, { side: 'environment', action: `extract ${opened.label}`, path: into });
|
|
195
|
+
}
|
|
196
|
+
return opened.entries;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The ZIP VAT writes of an archive it accepts, in memory: the archive judged whole as an extraction
|
|
200
|
+
* judges it, then each entry it lists — but those the caller omits — named by the place it lands
|
|
201
|
+
* at, each file's bytes held to the size it declares, with mode `0644` or `0755` by its executable
|
|
202
|
+
* bit. Nothing else of the archive's bytes: no local entry its directory does not list, no stub
|
|
203
|
+
* before it, no comment, no other mode. What a lane that passes an archive ON sends, so the bytes
|
|
204
|
+
* that leave are exactly the entries that were judged.
|
|
205
|
+
*
|
|
206
|
+
* @param source - The archive
|
|
207
|
+
* @param options - The archive's `format`, the `limits` it is held to, and any entries to `omit`
|
|
208
|
+
* @returns The new archive's bytes
|
|
209
|
+
* @throws as {@link openArchive}; ArchiveRefusedError for an entry the policy refuses;
|
|
210
|
+
* ArchiveEntryReadError (`ARCHIVE_ENTRY_UNREADABLE`) for a member that does not inflate, or does
|
|
211
|
+
* not hold the size it declares
|
|
212
|
+
*/
|
|
213
|
+
export async function zipOfArchive(source, options) {
|
|
214
|
+
const opened = await openJudged(source, options);
|
|
215
|
+
return zipOfEntries(keptRecords(opened, options).flatMap((record) => {
|
|
216
|
+
const place = placeOf(record.name).join('/');
|
|
217
|
+
// The archive's own root (`./`) is no entry of the new one.
|
|
218
|
+
if (place === '')
|
|
219
|
+
return [];
|
|
220
|
+
return [{ place, data: record.kind === 'directory' ? undefined : bytesOfRecord(opened.label, record), executable: record.executable }];
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=archive.js.map
|