@vibe-agent-toolkit/utils 0.2.0-rc.1 → 0.2.0-rc.10
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 +69 -23
- package/dist/asset-reference.js +3 -11
- package/dist/asset-reference.js.map +1 -1
- package/dist/compare-code-units.d.ts +28 -0
- package/dist/compare-code-units.d.ts.map +1 -0
- package/dist/compare-code-units.js +33 -0
- package/dist/compare-code-units.js.map +1 -0
- package/dist/crawl-timing.d.ts +657 -0
- package/dist/crawl-timing.d.ts.map +1 -0
- package/dist/crawl-timing.js +765 -0
- package/dist/crawl-timing.js.map +1 -0
- package/dist/crawl.d.ts +2 -4
- package/dist/crawl.d.ts.map +1 -1
- package/dist/crawl.js +2 -4
- package/dist/crawl.js.map +1 -1
- package/dist/dirent-kind.d.ts +83 -0
- package/dist/dirent-kind.d.ts.map +1 -0
- package/dist/dirent-kind.js +129 -0
- package/dist/dirent-kind.js.map +1 -0
- package/dist/entrypoint.d.ts +54 -0
- package/dist/entrypoint.d.ts.map +1 -0
- package/dist/entrypoint.js +71 -0
- package/dist/entrypoint.js.map +1 -0
- package/dist/env-flag.d.ts +51 -0
- package/dist/env-flag.d.ts.map +1 -0
- package/dist/env-flag.js +62 -0
- package/dist/env-flag.js.map +1 -0
- package/dist/errors/errno.d.ts +51 -0
- package/dist/errors/errno.d.ts.map +1 -0
- package/dist/errors/errno.js +112 -0
- package/dist/errors/errno.js.map +1 -0
- package/dist/errors/vat-error.d.ts +60 -0
- package/dist/errors/vat-error.d.ts.map +1 -0
- package/dist/errors/vat-error.js +86 -0
- package/dist/errors/vat-error.js.map +1 -0
- package/dist/file-crawler.d.ts +60 -0
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +133 -29
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs/file-hash.d.ts.map +1 -1
- package/dist/fs/file-hash.js +0 -1
- package/dist/fs/file-hash.js.map +1 -1
- package/dist/fs-utils.d.ts +422 -191
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +534 -204
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +5 -2
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +9 -7
- package/dist/fs.js.map +1 -1
- package/dist/git-run.d.ts +167 -0
- package/dist/git-run.d.ts.map +1 -0
- package/dist/git-run.js +154 -0
- package/dist/git-run.js.map +1 -0
- package/dist/git-snapshot.d.ts +199 -0
- package/dist/git-snapshot.d.ts.map +1 -0
- package/dist/git-snapshot.js +250 -0
- package/dist/git-snapshot.js.map +1 -0
- package/dist/git-tracker.d.ts +166 -3
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +250 -22
- package/dist/git-tracker.js.map +1 -1
- package/dist/git-utils.d.ts +84 -1
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +208 -82
- package/dist/git-utils.js.map +1 -1
- package/dist/git.d.ts +13 -4
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +12 -4
- package/dist/git.js.map +1 -1
- package/dist/gitignore-checker.d.ts.map +1 -1
- package/dist/gitignore-checker.js +15 -6
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/glob/glob-pattern.js +5 -5
- package/dist/glob/glob-pattern.js.map +1 -1
- package/dist/index.d.ts +32 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +95 -50
- package/dist/index.js.map +1 -1
- package/dist/listing-refusal.d.ts +112 -0
- package/dist/listing-refusal.d.ts.map +1 -0
- package/dist/listing-refusal.js +105 -0
- package/dist/listing-refusal.js.map +1 -0
- package/dist/path-containment.d.ts +64 -0
- package/dist/path-containment.d.ts.map +1 -0
- package/dist/path-containment.js +111 -0
- package/dist/path-containment.js.map +1 -0
- package/dist/path-core.d.ts +113 -18
- package/dist/path-core.d.ts.map +1 -1
- package/dist/path-core.js +139 -23
- package/dist/path-core.js.map +1 -1
- package/dist/path-utils.d.ts.map +1 -1
- package/dist/path-utils.js +42 -47
- package/dist/path-utils.js.map +1 -1
- package/dist/path.d.ts +1 -1
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +1 -1
- package/dist/path.js.map +1 -1
- package/dist/process.d.ts +7 -0
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +7 -0
- package/dist/process.js.map +1 -1
- package/dist/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +15 -9
- package/dist/project-utils.js.map +1 -1
- package/dist/project.d.ts +7 -5
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +7 -5
- package/dist/project.js.map +1 -1
- package/dist/safe-exec.d.ts +40 -11
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +111 -25
- package/dist/safe-exec.js.map +1 -1
- package/dist/skill-targets.d.ts +2 -2
- package/dist/skill-targets.js +2 -2
- package/dist/skill-test/auth-resolver.d.ts +10 -2
- package/dist/skill-test/auth-resolver.d.ts.map +1 -1
- package/dist/skill-test/auth-resolver.js +26 -11
- package/dist/skill-test/auth-resolver.js.map +1 -1
- package/dist/skill-test/index.d.ts +1 -1
- package/dist/skill-test/index.d.ts.map +1 -1
- package/dist/skill-test/index.js +1 -1
- package/dist/skill-test/index.js.map +1 -1
- package/dist/skill-test/spawn-claude.d.ts +30 -1
- package/dist/skill-test/spawn-claude.d.ts.map +1 -1
- package/dist/skill-test/spawn-claude.js +71 -5
- package/dist/skill-test/spawn-claude.js.map +1 -1
- package/dist/skill-test/transcript.d.ts +41 -2
- package/dist/skill-test/transcript.d.ts.map +1 -1
- package/dist/skill-test/transcript.js +55 -10
- package/dist/skill-test/transcript.js.map +1 -1
- package/dist/spawn-hardened.js +0 -1
- package/dist/spawn-hardened.js.map +1 -1
- package/dist/stdio-blocking.js +13 -14
- package/dist/stdio-blocking.js.map +1 -1
- package/dist/test-helpers.d.ts +135 -100
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +224 -146
- package/dist/test-helpers.js.map +1 -1
- package/dist/testing/executables.d.ts +32 -0
- package/dist/testing/executables.d.ts.map +1 -0
- package/dist/testing/executables.js +70 -0
- package/dist/testing/executables.js.map +1 -0
- package/dist/testing/hostile-tree.d.ts +79 -0
- package/dist/testing/hostile-tree.d.ts.map +1 -0
- package/dist/testing/hostile-tree.js +164 -0
- package/dist/testing/hostile-tree.js.map +1 -0
- package/dist/testing/platform-gates.d.ts +31 -0
- package/dist/testing/platform-gates.d.ts.map +1 -0
- package/dist/testing/platform-gates.js +31 -0
- package/dist/testing/platform-gates.js.map +1 -0
- package/dist/testing/temp-dir.d.ts +255 -0
- package/dist/testing/temp-dir.d.ts.map +1 -0
- package/dist/testing/temp-dir.js +363 -0
- package/dist/testing/temp-dir.js.map +1 -0
- package/dist/testing.d.ts +94 -3
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +119 -3
- package/dist/testing.js.map +1 -1
- package/dist/text-content.d.ts +197 -0
- package/dist/text-content.d.ts.map +1 -0
- package/dist/text-content.js +354 -0
- package/dist/text-content.js.map +1 -0
- package/dist/text-file.d.ts +45 -0
- package/dist/text-file.d.ts.map +1 -0
- package/dist/text-file.js +51 -0
- package/dist/text-file.js.map +1 -0
- package/dist/text.d.ts +16 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +16 -0
- package/dist/text.js.map +1 -0
- package/dist/timing-dump.d.ts +136 -0
- package/dist/timing-dump.d.ts.map +1 -0
- package/dist/timing-dump.js +229 -0
- package/dist/timing-dump.js.map +1 -0
- package/dist/windows-shell.d.ts +0 -1
- package/dist/windows-shell.d.ts.map +1 -1
- package/dist/windows-shell.js +0 -1
- package/dist/windows-shell.js.map +1 -1
- package/dist/zod-introspection.js +2 -2
- package/dist/zod-introspection.js.map +1 -1
- package/eslint/README.md +223 -49
- package/eslint/index.cjs +111 -97
- package/eslint/index.d.cts +16 -6
- package/eslint/rules/callee-name.cjs +23 -0
- package/eslint/rules/commands-import-boundary.cjs +148 -0
- package/eslint/rules/dead-import.cjs +1 -1
- package/eslint/rules/dirent-type-needs-symlink-check.cjs +282 -0
- package/eslint/rules/eslint-rule-factory.cjs +5 -2
- package/eslint/rules/exempt-path-matcher.cjs +2 -1
- package/eslint/rules/explicit-zod-strictness.cjs +120 -0
- package/eslint/rules/no-bare-dynamic-import-path.cjs +5 -1
- package/eslint/rules/no-bare-executable-spawn.cjs +70 -0
- package/eslint/rules/no-bare-symlink-in-tests.cjs +213 -0
- package/eslint/rules/no-blind-catch.cjs +171 -0
- package/eslint/rules/no-child-process-execSync.cjs +8 -0
- package/eslint/rules/no-decaying-referent.cjs +217 -0
- package/eslint/rules/no-dotdot-containment.cjs +160 -0
- package/eslint/rules/no-file-url-string-concat.cjs +4 -1
- package/eslint/rules/no-fragile-entrypoint-guard.cjs +228 -0
- package/eslint/rules/no-fs-mkdirSync.cjs +8 -0
- package/eslint/rules/no-fs-promises-cp.cjs +7 -0
- package/eslint/rules/no-fs-realpathSync.cjs +8 -0
- package/eslint/rules/no-hardcoded-path-split.cjs +29 -18
- package/eslint/rules/no-io-in-unit-tier.cjs +129 -0
- package/eslint/rules/no-literal-process-exit.cjs +129 -0
- package/eslint/rules/no-manual-path-normalize.cjs +136 -107
- package/eslint/rules/no-os-tmpdir.cjs +8 -0
- package/eslint/rules/no-path-operations-in-comparisons.cjs +7 -2
- package/eslint/rules/no-path-sep-in-strings.cjs +7 -2
- package/eslint/rules/no-path-startswith.cjs +7 -2
- package/eslint/rules/no-process-exit-in-phase.cjs +127 -0
- package/eslint/rules/no-raw-node-path.cjs +563 -0
- package/eslint/rules/no-raw-text-decode.cjs +305 -0
- package/eslint/rules/no-registry-count-pin.cjs +255 -0
- package/eslint/rules/no-self-package-import.cjs +168 -0
- package/eslint/rules/no-test-scoped-functions.cjs +10 -3
- package/eslint/rules/no-unix-shell-commands.cjs +6 -2
- package/eslint/rules/no-unsafe-root-join.cjs +23 -2
- package/eslint/rules/no-url-pathname-for-fs.cjs +5 -1
- package/eslint/rules/no-version-literal.cjs +151 -0
- package/eslint/rules/prefer-startswith-over-regex.cjs +9 -0
- package/eslint/rules/require-justified-skip.cjs +8 -2
- package/eslint/rules/safe-import.cjs +1 -1
- package/package.json +16 -16
- package/dist/link-auth/build-headers.d.ts +0 -34
- package/dist/link-auth/build-headers.d.ts.map +0 -1
- package/dist/link-auth/build-headers.js +0 -58
- package/dist/link-auth/build-headers.js.map +0 -1
- package/dist/link-auth/expand-macro.d.ts +0 -38
- package/dist/link-auth/expand-macro.d.ts.map +0 -1
- package/dist/link-auth/expand-macro.js +0 -133
- package/dist/link-auth/expand-macro.js.map +0 -1
- package/dist/link-auth/macros.yaml +0 -50
- package/dist/link-auth/resolve-token.d.ts +0 -83
- package/dist/link-auth/resolve-token.d.ts.map +0 -1
- package/dist/link-auth/resolve-token.js +0 -101
- package/dist/link-auth/resolve-token.js.map +0 -1
- package/dist/link-auth/resolve.d.ts +0 -102
- package/dist/link-auth/resolve.d.ts.map +0 -1
- package/dist/link-auth/resolve.js +0 -66
- package/dist/link-auth/resolve.js.map +0 -1
- package/dist/link-auth/rewrite.d.ts +0 -52
- package/dist/link-auth/rewrite.d.ts.map +0 -1
- package/dist/link-auth/rewrite.js +0 -102
- package/dist/link-auth/rewrite.js.map +0 -1
- package/dist/link-auth/select-provider.d.ts +0 -30
- package/dist/link-auth/select-provider.d.ts.map +0 -1
- package/dist/link-auth/select-provider.js +0 -55
- package/dist/link-auth/select-provider.js.map +0 -1
- package/dist/link-auth/template.d.ts +0 -40
- package/dist/link-auth/template.d.ts.map +0 -1
- package/dist/link-auth/template.js +0 -89
- package/dist/link-auth/template.js.map +0 -1
- package/dist/link-auth/transforms.d.ts +0 -46
- package/dist/link-auth/transforms.d.ts.map +0 -1
- package/dist/link-auth/transforms.js +0 -52
- package/dist/link-auth/transforms.js.map +0 -1
- package/dist/template-entry.d.ts +0 -10
- package/dist/template-entry.d.ts.map +0 -1
- package/dist/template-entry.js +0 -10
- package/dist/template-entry.js.map +0 -1
- package/dist/template.d.ts +0 -7
- package/dist/template.d.ts.map +0 -1
- package/dist/template.js +0 -18
- package/dist/template.js.map +0 -1
- package/eslint/rules/no-path-join.cjs +0 -14
- package/eslint/rules/no-path-relative.cjs +0 -14
- package/eslint/rules/no-path-resolve.cjs +0 -14
- package/eslint/rules/path-function-rule-factory.cjs +0 -496
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errno predicates — the two questions every catch site asks of an OS error,
|
|
3
|
+
* answered by the `code` on the error or down its `cause` chain.
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ This module imports NOTHING. It is the leaf that `fs-utils.ts`,
|
|
6
|
+
* `path-utils.ts`, `dirent-kind.ts`, `path-containment.ts` and the rest of the
|
|
7
|
+
* package share; it lived in `fs-utils.ts` until that put three of them in an
|
|
8
|
+
* import cycle (`fs-utils → path-utils → fs-utils` and two more), and a leaf
|
|
9
|
+
* is the one place a shared predicate cannot re-open one. (The cycle was once
|
|
10
|
+
* blamed for a platform-dependent knip verdict on these two names; breaking it
|
|
11
|
+
* changed nothing there — the cause was a missing knip entry, see
|
|
12
|
+
* `docs/contributing/traps.md`, "A subpath module's re-exports flap by platform".)
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Errno codes meaning "the filesystem refused this path", as opposed to a defect
|
|
16
|
+
* in our own code.
|
|
17
|
+
*
|
|
18
|
+
* Shared because two lanes need the same answer and must not drift: `vat audit`
|
|
19
|
+
* decides whether to degrade a scan over a tree it does not own, and the skill
|
|
20
|
+
* packager decides whether a `files:` match is copyable. A second, independently
|
|
21
|
+
* written list is how those two come to disagree about what counts as the
|
|
22
|
+
* environment's fault.
|
|
23
|
+
*
|
|
24
|
+
* The set is deliberately broad. An earlier, "conservative" version omitted
|
|
25
|
+
* `ENOTSUP` — the errno of the very issue this was written for — along with
|
|
26
|
+
* `EEXIST`, which an ordinary two-entry `files:` config reaches with no
|
|
27
|
+
* permissions involved at all. Both escaped raw. Every code here means the OS
|
|
28
|
+
* refused a syscall on a path; none of them can be produced by a type error or a
|
|
29
|
+
* logic bug in our own code, which is the only distinction the callers need.
|
|
30
|
+
*
|
|
31
|
+
* `EIO` and `EBUSY` are included even though they can indicate failing hardware:
|
|
32
|
+
* neither caller *swallows* anything, each reports the path and the OS message,
|
|
33
|
+
* so a dying disk surfaces once per affected path. Aborting the run instead would
|
|
34
|
+
* report less. `ENOENT` is included because a bulk scan races real filesystems —
|
|
35
|
+
* an entry listed by `readdir` can be gone by the time it is opened.
|
|
36
|
+
*/
|
|
37
|
+
const FILESYSTEM_ACCESS_ERRNOS = new Set([
|
|
38
|
+
// Permission and ownership
|
|
39
|
+
'EACCES', 'EPERM', 'EROFS',
|
|
40
|
+
// Presence and shape
|
|
41
|
+
'ENOENT', 'EEXIST', 'ENOTDIR', 'EISDIR', 'ENOTEMPTY', 'ELOOP', 'ENAMETOOLONG',
|
|
42
|
+
// Capability of the object or filesystem
|
|
43
|
+
'ENOTSUP', 'EOPNOTSUPP', 'EXDEV', 'ETXTBSY', 'EINVAL',
|
|
44
|
+
// Resource exhaustion and transient device state
|
|
45
|
+
'ENOSPC', 'EDQUOT', 'EMFILE', 'ENFILE', 'EIO', 'EBUSY', 'EAGAIN',
|
|
46
|
+
// Network filesystems
|
|
47
|
+
'ESTALE', 'ETIMEDOUT', 'EHOSTDOWN', 'ENETDOWN',
|
|
48
|
+
// Windows surfaces this for reparse points and some network paths
|
|
49
|
+
'UNKNOWN',
|
|
50
|
+
]);
|
|
51
|
+
/**
|
|
52
|
+
* Whether `error` is the filesystem refusing a path rather than a bug.
|
|
53
|
+
*
|
|
54
|
+
* Deliberately NOT `error instanceof Error`: the point of every caller is to
|
|
55
|
+
* degrade on a hostile tree, and a `TypeError` from a validator is not that.
|
|
56
|
+
* Treating one as environmental turns a real defect into a warning about
|
|
57
|
+
* whichever file it happened on — which makes a tool quietest exactly when it is
|
|
58
|
+
* most wrong.
|
|
59
|
+
*
|
|
60
|
+
* Walks `cause`, because the errno is routinely re-wrapped on its way up. The CLI
|
|
61
|
+
* config loader turns a read failure into `new Error('Failed to load config: …')`;
|
|
62
|
+
* without following the chain the predicate answered "not a filesystem error" for
|
|
63
|
+
* a plain `EACCES`, and an unreadable config aborted a whole `vat audit` run. Any
|
|
64
|
+
* layer that adds context to an OS error defeats a `code`-only check, so the check
|
|
65
|
+
* cannot be `code`-only.
|
|
66
|
+
*/
|
|
67
|
+
export function isFilesystemAccessError(error) {
|
|
68
|
+
return hasErrnoCode(error, (code) => FILESYSTEM_ACCESS_ERRNOS.has(code));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether `error` means **there is nothing at this path** — `ENOENT`, or
|
|
72
|
+
* `ENOTDIR` for a path whose component turned out to be a file — and nothing
|
|
73
|
+
* else.
|
|
74
|
+
*
|
|
75
|
+
* This is the narrowing a `try { stat(p) } catch { return null }` is rewritten
|
|
76
|
+
* to under the `no-blind-catch` lint rule: the sentinel stands for *absent*,
|
|
77
|
+
* so only an absence may produce it; a refusal (`EACCES`, `EPERM`, `ELOOP`) or
|
|
78
|
+
* a bug (`TypeError`) is rethrown and stays loud.
|
|
79
|
+
*
|
|
80
|
+
* ⚠️ Deliberately NOT {@link isFilesystemAccessError}. That predicate answers
|
|
81
|
+
* "is this the environment's fault?" and to answer it groups `ENOENT` with
|
|
82
|
+
* `EACCES` — the exact conflation that once turned an unreadable directory
|
|
83
|
+
* into an empty one. The two questions have two predicates on purpose; see
|
|
84
|
+
* also {@link listingFailure}, which makes the same split for `readdir`.
|
|
85
|
+
*
|
|
86
|
+
* Walks `cause` for the same reason its sibling does: the errno is routinely
|
|
87
|
+
* re-wrapped on its way up.
|
|
88
|
+
*/
|
|
89
|
+
export function isPathAbsentError(error) {
|
|
90
|
+
return hasErrnoCode(error, (code) => code === 'ENOENT' || code === 'ENOTDIR');
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Whether any string `code` on `error` or down its `cause` chain satisfies
|
|
94
|
+
* `accept`. Bounded: a malformed `cause` chain must not become an infinite
|
|
95
|
+
* loop inside an error path, which is the worst place to hang.
|
|
96
|
+
*/
|
|
97
|
+
function hasErrnoCode(error, accept) {
|
|
98
|
+
for (let current = error, depth = 0; depth < 10; depth++) {
|
|
99
|
+
if (typeof current !== 'object' || current === null)
|
|
100
|
+
return false;
|
|
101
|
+
if ('code' in current) {
|
|
102
|
+
const { code } = current;
|
|
103
|
+
if (typeof code === 'string' && accept(code))
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (!('cause' in current))
|
|
107
|
+
return false;
|
|
108
|
+
current = current.cause;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=errno.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errno.js","sourceRoot":"","sources":["../../src/errors/errno.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,wBAAwB,GAAwB,IAAI,GAAG,CAAC;IAC5D,2BAA2B;IAC3B,QAAQ,EAAE,OAAO,EAAE,OAAO;IAC1B,qBAAqB;IACrB,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc;IAC7E,yCAAyC;IACzC,SAAS,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ;IACrD,iDAAiD;IACjD,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ;IAChE,sBAAsB;IACtB,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU;IAC9C,kEAAkE;IAClE,SAAS;CACV,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAc;IACpD,OAAO,YAAY,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,wBAAwB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;AAC3E,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,OAAO,YAAY,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,SAAS,CAAC,CAAC;AAChF,CAAC;AAED;;;;GAIG;AACH,SAAS,YAAY,CAAC,KAAc,EAAE,MAAiC;IACrE,KAAK,IAAI,OAAO,GAAY,KAAK,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC;QAClE,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAClE,IAAI,MAAM,IAAI,OAAO,EAAE,CAAC;YACtB,MAAM,EAAE,IAAI,EAAE,GAAG,OAA4B,CAAC;YAC9C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAC;QAC5D,CAAC;QACD,IAAI,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC;YAAE,OAAO,KAAK,CAAC;QACxC,OAAO,GAAI,OAA8B,CAAC,KAAK,CAAC;IAClD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The base class of every error VAT throws on purpose.
|
|
3
|
+
*
|
|
4
|
+
* `code` is the one field a catch block may dispatch on. A message is prose
|
|
5
|
+
* for a human and changes whenever the prose improves; a code is a contract
|
|
6
|
+
* and changes only when the meaning does. Three packages used to recognise a
|
|
7
|
+
* root-escape by `error.message.startsWith('safePath.joinUnderRoot:')`, which
|
|
8
|
+
* held exactly until someone reworded the sentence — the ESLint restriction
|
|
9
|
+
* on `.message.includes(…)` exists so nobody writes the fourth.
|
|
10
|
+
*
|
|
11
|
+
* The brand is a registry symbol rather than the class identity because the
|
|
12
|
+
* class identity is not stable across the one boundary VAT crosses every day:
|
|
13
|
+
* a `dist` copy of a class never `instanceof`-matches a `src` instance, and
|
|
14
|
+
* `packages/cli` imports the same modules from both during tests.
|
|
15
|
+
* `Symbol.for` is realm-global, so {@link isVatError} answers the same on both
|
|
16
|
+
* sides.
|
|
17
|
+
*/
|
|
18
|
+
declare const VAT_ERROR_BRAND: unique symbol;
|
|
19
|
+
/** An error VAT threw on purpose, with a stable machine-readable `code`. */
|
|
20
|
+
export declare class VatError extends Error {
|
|
21
|
+
/** Stable identity for dispatch — `SCREAMING_SNAKE`, never read from prose. */
|
|
22
|
+
readonly code: string;
|
|
23
|
+
readonly [VAT_ERROR_BRAND] = true;
|
|
24
|
+
/**
|
|
25
|
+
* @param code - The stable identity a catch block dispatches on
|
|
26
|
+
* @param message - What went wrong, for a human
|
|
27
|
+
* @param options - `cause`, as on a native Error
|
|
28
|
+
*/
|
|
29
|
+
constructor(code: string, message: string, options?: ErrorOptions);
|
|
30
|
+
toString(): string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether `error` is a VAT error — optionally one carrying exactly `code`.
|
|
34
|
+
*
|
|
35
|
+
* Reads the brand and the `code` field, never the prototype chain, so an
|
|
36
|
+
* instance produced by another copy of this module (a `dist` build beside a
|
|
37
|
+
* `src` import) still answers yes. A foreign error that happens to carry a
|
|
38
|
+
* `code` — every `node:fs` errno does — answers no: the brand is the claim.
|
|
39
|
+
*
|
|
40
|
+
* @param error - Anything a catch block received
|
|
41
|
+
* @param code - When given, the exact code required
|
|
42
|
+
*/
|
|
43
|
+
export declare function isVatError(error: unknown, code?: string): error is VatError;
|
|
44
|
+
/**
|
|
45
|
+
* Prefix an error's message IN PLACE, exactly once per error object.
|
|
46
|
+
*
|
|
47
|
+
* In place rather than re-wrapped because the error's class is what its
|
|
48
|
+
* catch blocks dispatch on (`GradingNonceError`, `RateLimitSignal`, …), and a
|
|
49
|
+
* wrapper would flatten every class into one. Once, because a retried item or
|
|
50
|
+
* a cached error object passes through the same seam more than once and must
|
|
51
|
+
* not accumulate prefixes. "Once" is remembered per object, not detected by
|
|
52
|
+
* re-reading the message — the previous check was `message.startsWith(prefix)`,
|
|
53
|
+
* which is a dispatch on prose by another name.
|
|
54
|
+
*
|
|
55
|
+
* @param error - Anything a catch block received; a non-Error is left alone
|
|
56
|
+
* @param prefix - Text to put before the message
|
|
57
|
+
*/
|
|
58
|
+
export declare function prefixMessageOnce(error: unknown, prefix: string): void;
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=vat-error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vat-error.d.ts","sourceRoot":"","sources":["../../src/errors/vat-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,QAAA,MAAM,eAAe,EAAE,OAAO,MAAgC,CAAC;AAE/D,4EAA4E;AAC5E,qBAAa,QAAS,SAAQ,KAAK;IACjC,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,CAAC,eAAe,CAAC,QAAQ;IAElC;;;;OAIG;gBACS,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;IAMxD,QAAQ,IAAI,MAAM;CAG5B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,IAAI,QAAQ,CAM3E;AAUD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAItE"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The base class of every error VAT throws on purpose.
|
|
3
|
+
*
|
|
4
|
+
* `code` is the one field a catch block may dispatch on. A message is prose
|
|
5
|
+
* for a human and changes whenever the prose improves; a code is a contract
|
|
6
|
+
* and changes only when the meaning does. Three packages used to recognise a
|
|
7
|
+
* root-escape by `error.message.startsWith('safePath.joinUnderRoot:')`, which
|
|
8
|
+
* held exactly until someone reworded the sentence — the ESLint restriction
|
|
9
|
+
* on `.message.includes(…)` exists so nobody writes the fourth.
|
|
10
|
+
*
|
|
11
|
+
* The brand is a registry symbol rather than the class identity because the
|
|
12
|
+
* class identity is not stable across the one boundary VAT crosses every day:
|
|
13
|
+
* a `dist` copy of a class never `instanceof`-matches a `src` instance, and
|
|
14
|
+
* `packages/cli` imports the same modules from both during tests.
|
|
15
|
+
* `Symbol.for` is realm-global, so {@link isVatError} answers the same on both
|
|
16
|
+
* sides.
|
|
17
|
+
*/
|
|
18
|
+
const VAT_ERROR_BRAND = Symbol.for('vat.error');
|
|
19
|
+
/** An error VAT threw on purpose, with a stable machine-readable `code`. */
|
|
20
|
+
export class VatError extends Error {
|
|
21
|
+
/** Stable identity for dispatch — `SCREAMING_SNAKE`, never read from prose. */
|
|
22
|
+
code;
|
|
23
|
+
[VAT_ERROR_BRAND] = true;
|
|
24
|
+
/**
|
|
25
|
+
* @param code - The stable identity a catch block dispatches on
|
|
26
|
+
* @param message - What went wrong, for a human
|
|
27
|
+
* @param options - `cause`, as on a native Error
|
|
28
|
+
*/
|
|
29
|
+
constructor(code, message, options) {
|
|
30
|
+
super(message, options);
|
|
31
|
+
this.name = new.target.name;
|
|
32
|
+
this.code = code;
|
|
33
|
+
}
|
|
34
|
+
toString() {
|
|
35
|
+
return `${this.name} [${this.code}]: ${this.message}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Whether `error` is a VAT error — optionally one carrying exactly `code`.
|
|
40
|
+
*
|
|
41
|
+
* Reads the brand and the `code` field, never the prototype chain, so an
|
|
42
|
+
* instance produced by another copy of this module (a `dist` build beside a
|
|
43
|
+
* `src` import) still answers yes. A foreign error that happens to carry a
|
|
44
|
+
* `code` — every `node:fs` errno does — answers no: the brand is the claim.
|
|
45
|
+
*
|
|
46
|
+
* @param error - Anything a catch block received
|
|
47
|
+
* @param code - When given, the exact code required
|
|
48
|
+
*/
|
|
49
|
+
export function isVatError(error, code) {
|
|
50
|
+
if (typeof error !== 'object' || error === null)
|
|
51
|
+
return false;
|
|
52
|
+
if (!(VAT_ERROR_BRAND in error) || error[VAT_ERROR_BRAND] !== true)
|
|
53
|
+
return false;
|
|
54
|
+
const declared = error.code;
|
|
55
|
+
if (typeof declared !== 'string')
|
|
56
|
+
return false;
|
|
57
|
+
return code === undefined || declared === code;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Marks an error {@link prefixMessageOnce} has already prefixed. A registry
|
|
61
|
+
* symbol on the error itself rather than a module-local `WeakSet`: under the
|
|
62
|
+
* same src/dist duplication that justifies the `Symbol.for` brand above, a
|
|
63
|
+
* per-module set would make "once" mean "once per copy of this module".
|
|
64
|
+
*/
|
|
65
|
+
const PREFIXED = Symbol.for('vat.error.prefixed');
|
|
66
|
+
/**
|
|
67
|
+
* Prefix an error's message IN PLACE, exactly once per error object.
|
|
68
|
+
*
|
|
69
|
+
* In place rather than re-wrapped because the error's class is what its
|
|
70
|
+
* catch blocks dispatch on (`GradingNonceError`, `RateLimitSignal`, …), and a
|
|
71
|
+
* wrapper would flatten every class into one. Once, because a retried item or
|
|
72
|
+
* a cached error object passes through the same seam more than once and must
|
|
73
|
+
* not accumulate prefixes. "Once" is remembered per object, not detected by
|
|
74
|
+
* re-reading the message — the previous check was `message.startsWith(prefix)`,
|
|
75
|
+
* which is a dispatch on prose by another name.
|
|
76
|
+
*
|
|
77
|
+
* @param error - Anything a catch block received; a non-Error is left alone
|
|
78
|
+
* @param prefix - Text to put before the message
|
|
79
|
+
*/
|
|
80
|
+
export function prefixMessageOnce(error, prefix) {
|
|
81
|
+
if (!(error instanceof Error) || PREFIXED in error)
|
|
82
|
+
return;
|
|
83
|
+
Object.defineProperty(error, PREFIXED, { value: true, enumerable: false });
|
|
84
|
+
error.message = `${prefix}${error.message}`;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=vat-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vat-error.js","sourceRoot":"","sources":["../../src/errors/vat-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,eAAe,GAAkB,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;AAE/D,4EAA4E;AAC5E,MAAM,OAAO,QAAS,SAAQ,KAAK;IACjC,+EAA+E;IACtE,IAAI,CAAS;IACb,CAAC,eAAe,CAAC,GAAG,IAAI,CAAC;IAElC;;;;OAIG;IACH,YAAY,IAAY,EAAE,OAAe,EAAE,OAAsB;QAC/D,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAEQ,QAAQ;QACf,OAAO,GAAG,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACxD,CAAC;CACF;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU,CAAC,KAAc,EAAE,IAAa;IACtD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,IAAI,CAAC,CAAC,eAAe,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC,eAAe,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACjF,MAAM,QAAQ,GAAI,KAA4B,CAAC,IAAI,CAAC;IACpD,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC/C,OAAO,IAAI,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI,CAAC;AACjD,CAAC;AAED;;;;;GAKG;AACH,MAAM,QAAQ,GAAkB,MAAM,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;AAEjE;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAc,EAAE,MAAc;IAC9D,IAAI,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,IAAI,QAAQ,IAAI,KAAK;QAAE,OAAO;IAC3D,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3E,KAAK,CAAC,OAAO,GAAG,GAAG,MAAM,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC;AAC9C,CAAC"}
|
package/dist/file-crawler.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { type UnreadablePolicy } from './listing-refusal.js';
|
|
2
|
+
export type { DirectoryRefusal } from './fs-utils.js';
|
|
3
|
+
export { DirectoryListingRefusedError, type RefuseListingContext, refusedListingMessage, settleRefusal, type UnreadablePolicy, } from './listing-refusal.js';
|
|
1
4
|
/**
|
|
2
5
|
* Options for directory crawling
|
|
3
6
|
*/
|
|
@@ -10,6 +13,22 @@ export interface CrawlOptions {
|
|
|
10
13
|
exclude?: string[];
|
|
11
14
|
/** Follow symbolic links (default: false) */
|
|
12
15
|
followSymlinks?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Observer for every symbolic link the WALK met and did not follow — called
|
|
18
|
+
* once per link that the include/exclude patterns admit, with the path in the
|
|
19
|
+
* same form a result would carry ({@link CrawlOptions.absolute}).
|
|
20
|
+
*
|
|
21
|
+
* An observer rather than a policy, and optional for that reason: declining a
|
|
22
|
+
* link is already decided by `followSymlinks: false`, and a caller that does not
|
|
23
|
+
* ask is told nothing it relied on. It exists for the caller that must not let
|
|
24
|
+
* a declined link vanish without a record — the projection's filesystem extent,
|
|
25
|
+
* which turns each one into a condition row.
|
|
26
|
+
*
|
|
27
|
+
* Walk route only, and ENFORCED: passing it without `respectGitignore: false`
|
|
28
|
+
* throws. The `git ls-files` route never declines a link (see the KNOWN
|
|
29
|
+
* DIVERGENCE block below), so it would have none to report.
|
|
30
|
+
*/
|
|
31
|
+
onSymlinkNotFollowed?: (path: string) => void;
|
|
13
32
|
/** Return absolute paths in results (default: true) */
|
|
14
33
|
absolute?: boolean;
|
|
15
34
|
/** Only return files (not directories) - default: true */
|
|
@@ -35,6 +54,25 @@ export interface CrawlOptions {
|
|
|
35
54
|
* committed yet"; `respectGitignore: false` is not, and costs the whole walk.
|
|
36
55
|
*/
|
|
37
56
|
includeUntracked?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* What to do with a directory the walk could not LIST. Required, with no
|
|
59
|
+
* default: see {@link UnreadablePolicy} for the two answers and why the
|
|
60
|
+
* caller — not the crawler — is the one who knows which is honest here.
|
|
61
|
+
*
|
|
62
|
+
* A directory that VANISHED between being enumerated and being listed
|
|
63
|
+
* (`ENOENT` / `ENOTDIR`) is not a refusal: it is no longer in the population
|
|
64
|
+
* and is skipped without a call.
|
|
65
|
+
*
|
|
66
|
+
* **Both routes settle it.** The `git ls-files` route walks the working tree
|
|
67
|
+
* whenever {@link CrawlOptions.includeUntracked} is set (`--others`), and a
|
|
68
|
+
* directory git could not open arrives here too — read off git's stderr,
|
|
69
|
+
* where it is the only trace: git exits 0 and lists fewer files. A directory
|
|
70
|
+
* the `exclude` patterns drop is not reported on either route, because the
|
|
71
|
+
* walk never lists one. The tracked-only listing (`includeUntracked: false`)
|
|
72
|
+
* opens no directory at all — the index names every member — so it has no
|
|
73
|
+
* gap to report.
|
|
74
|
+
*/
|
|
75
|
+
unreadable: UnreadablePolicy;
|
|
38
76
|
}
|
|
39
77
|
/**
|
|
40
78
|
* Directories no VAT crawl should ever walk into. THE canonical list — any lane
|
|
@@ -81,6 +119,28 @@ export declare const NEVER_CRAWL_GLOBS: readonly ["**/node_modules/**", "**/.git
|
|
|
81
119
|
* two lists — a lane spreads whichever ones apply.
|
|
82
120
|
*/
|
|
83
121
|
export declare const BUILD_OUTPUT_GLOBS: readonly ["**/dist/**"];
|
|
122
|
+
/**
|
|
123
|
+
* THE include/exclude decision, compiled once — "is this base-relative path a
|
|
124
|
+
* member of the crawl?"
|
|
125
|
+
*
|
|
126
|
+
* Extracted because it is asked from three places that must never drift: the
|
|
127
|
+
* `git ls-files` branch below, the manual walk below it, and — since the
|
|
128
|
+
* projection-backed lane landed — a population that arrives already enumerated
|
|
129
|
+
* and still has to be narrowed to what the caller's globs admit. A lane that
|
|
130
|
+
* approximated this with its own picomatch call would be measuring the corpus
|
|
131
|
+
* with a different matcher than the one that ships, which is how a "clean" A/B
|
|
132
|
+
* comes to compare two different questions.
|
|
133
|
+
*
|
|
134
|
+
* The `+ '/'` retry is the load-bearing subtlety, not a defensive extra: a
|
|
135
|
+
* directory-shaped exclude like `docs/legacy/**` does not match the bare path
|
|
136
|
+
* `docs/legacy`, so without it an excluded directory is admitted as a member
|
|
137
|
+
* whenever a caller enumerates directories as well as files.
|
|
138
|
+
*
|
|
139
|
+
* @param include - Include globs, base-relative, forward-slashed
|
|
140
|
+
* @param exclude - Exclude globs, same basis; an empty list excludes nothing
|
|
141
|
+
* @returns A predicate over base-relative, forward-slashed paths
|
|
142
|
+
*/
|
|
143
|
+
export declare function crawlPathFilter(include: readonly string[], exclude: readonly string[]): (relativePath: string) => boolean;
|
|
84
144
|
/**
|
|
85
145
|
* Crawl a directory tree and return matching files (async)
|
|
86
146
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-crawler.d.ts","sourceRoot":"","sources":["../src/file-crawler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"file-crawler.d.ts","sourceRoot":"","sources":["../src/file-crawler.ts"],"names":[],"mappings":"AAUA,OAAO,EAA0C,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAGrG,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAItD,OAAO,EACL,4BAA4B,EAC5B,KAAK,oBAAoB,EACzB,qBAAqB,EACrB,aAAa,EACb,KAAK,gBAAgB,GACtB,MAAM,sBAAsB,CAAC;AAE9B;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,6CAA6C;IAC7C,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;;;;;;;;OAcG;IACH,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,uDAAuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;;;;OAUG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;;;;;;;;;;;OAiBG;IACH,UAAU,EAAE,gBAAgB,CAAC;CAC9B;AAkBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,iBAAiB,sJAQpB,CAAC;AAEX;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,yBAA0B,CAAC;AAI1D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAQnC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAE7E;AAkBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,EAAE,CA4TlE"}
|
package/dist/file-crawler.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import picomatch from 'picomatch';
|
|
3
|
+
import { directoryRefusalFor, listingFailure, } from './fs-utils.js';
|
|
3
4
|
import { gitFindRoot, gitLsFiles } from './git-utils.js';
|
|
5
|
+
import { requireUnreadablePolicy, settleRefusal } from './listing-refusal.js';
|
|
4
6
|
import { toForwardSlash, safePath } from './path-utils.js';
|
|
7
|
+
// The refusal vocabulary lives in `listing-refusal.ts` so `git-utils.ts` can
|
|
8
|
+
// share it without importing this module (which imports that one). Re-exported
|
|
9
|
+
// here because `./crawl` is where every caller of the walk already looks.
|
|
10
|
+
export { DirectoryListingRefusedError, refusedListingMessage, settleRefusal, } from './listing-refusal.js';
|
|
5
11
|
/**
|
|
6
12
|
* Glob options shared by every pattern this module compiles.
|
|
7
13
|
*
|
|
@@ -71,6 +77,35 @@ export const NEVER_CRAWL_GLOBS = [
|
|
|
71
77
|
*/
|
|
72
78
|
export const BUILD_OUTPUT_GLOBS = ['**/dist/**'];
|
|
73
79
|
const DEFAULT_EXCLUDE = [...NEVER_CRAWL_GLOBS, ...BUILD_OUTPUT_GLOBS];
|
|
80
|
+
/**
|
|
81
|
+
* THE include/exclude decision, compiled once — "is this base-relative path a
|
|
82
|
+
* member of the crawl?"
|
|
83
|
+
*
|
|
84
|
+
* Extracted because it is asked from three places that must never drift: the
|
|
85
|
+
* `git ls-files` branch below, the manual walk below it, and — since the
|
|
86
|
+
* projection-backed lane landed — a population that arrives already enumerated
|
|
87
|
+
* and still has to be narrowed to what the caller's globs admit. A lane that
|
|
88
|
+
* approximated this with its own picomatch call would be measuring the corpus
|
|
89
|
+
* with a different matcher than the one that ships, which is how a "clean" A/B
|
|
90
|
+
* comes to compare two different questions.
|
|
91
|
+
*
|
|
92
|
+
* The `+ '/'` retry is the load-bearing subtlety, not a defensive extra: a
|
|
93
|
+
* directory-shaped exclude like `docs/legacy/**` does not match the bare path
|
|
94
|
+
* `docs/legacy`, so without it an excluded directory is admitted as a member
|
|
95
|
+
* whenever a caller enumerates directories as well as files.
|
|
96
|
+
*
|
|
97
|
+
* @param include - Include globs, base-relative, forward-slashed
|
|
98
|
+
* @param exclude - Exclude globs, same basis; an empty list excludes nothing
|
|
99
|
+
* @returns A predicate over base-relative, forward-slashed paths
|
|
100
|
+
*/
|
|
101
|
+
export function crawlPathFilter(include, exclude) {
|
|
102
|
+
const isIncluded = picomatch([...include], PICOMATCH_OPTIONS);
|
|
103
|
+
const isExcluded = exclude.length > 0 ? picomatch([...exclude], PICOMATCH_OPTIONS) : () => false;
|
|
104
|
+
return (relativePath) => {
|
|
105
|
+
const normalized = toForwardSlash(relativePath);
|
|
106
|
+
return isIncluded(normalized) && !isExcluded(normalized) && !isExcluded(`${normalized}/`);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
74
109
|
/**
|
|
75
110
|
* Crawl a directory tree and return matching files (async)
|
|
76
111
|
*
|
|
@@ -90,6 +125,21 @@ const DEFAULT_EXCLUDE = [...NEVER_CRAWL_GLOBS, ...BUILD_OUTPUT_GLOBS];
|
|
|
90
125
|
export async function crawlDirectory(options) {
|
|
91
126
|
return crawlDirectorySync(options);
|
|
92
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Refuse a symlink observer on the `git ls-files` route.
|
|
130
|
+
*
|
|
131
|
+
* That route admits links as members and declines none, so the observer would
|
|
132
|
+
* compile, be accepted, and never be called. Refused up front rather than only
|
|
133
|
+
* when a git root happens to be found, so the contract is static.
|
|
134
|
+
*
|
|
135
|
+
* @param observer - The caller's `onSymlinkNotFollowed`
|
|
136
|
+
* @param respectGitignore - Whether the git route may be taken
|
|
137
|
+
*/
|
|
138
|
+
function requireWalkForSymlinkObserver(observer, respectGitignore) {
|
|
139
|
+
if (observer !== undefined && respectGitignore) {
|
|
140
|
+
throw new TypeError('crawlDirectory: onSymlinkNotFollowed requires respectGitignore: false — the git ls-files route declines no link');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
93
143
|
/**
|
|
94
144
|
* Crawl a directory tree and return matching files (synchronous)
|
|
95
145
|
*
|
|
@@ -107,17 +157,36 @@ export async function crawlDirectory(options) {
|
|
|
107
157
|
* });
|
|
108
158
|
*/
|
|
109
159
|
export function crawlDirectorySync(options) {
|
|
110
|
-
const { baseDir, include = ['**/*'], exclude = DEFAULT_EXCLUDE, followSymlinks = false, absolute = true, filesOnly = true, respectGitignore = true, includeUntracked = false, } = options;
|
|
160
|
+
const { baseDir, include = ['**/*'], exclude = DEFAULT_EXCLUDE, followSymlinks = false, absolute = true, filesOnly = true, respectGitignore = true, includeUntracked = false, unreadable, onSymlinkNotFollowed, } = options;
|
|
161
|
+
requireUnreadablePolicy(unreadable, 'crawlDirectory');
|
|
162
|
+
requireWalkForSymlinkObserver(onSymlinkNotFollowed, respectGitignore);
|
|
111
163
|
const picoOptions = PICOMATCH_OPTIONS;
|
|
112
164
|
// Resolve base directory to absolute path
|
|
113
165
|
const resolvedBaseDir = safePath.resolve(baseDir);
|
|
166
|
+
// Compiled once, ahead of the route choice, because BOTH routes ask it: the
|
|
167
|
+
// walk before it lists a directory, the git route before it reports one git
|
|
168
|
+
// could not list. One matcher is what keeps "is this directory excluded?"
|
|
169
|
+
// answered the same way on both sides of the fork.
|
|
170
|
+
const isExcluded = exclude.length > 0 ? picomatch(exclude, picoOptions) : () => false;
|
|
171
|
+
/**
|
|
172
|
+
* Check if a path should be excluded based on patterns
|
|
173
|
+
*/
|
|
174
|
+
function shouldExclude(normalizedPath) {
|
|
175
|
+
// Check explicit exclude patterns
|
|
176
|
+
return isExcluded(normalizedPath) || isExcluded(normalizedPath + '/');
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* A refused listing surfaces under the caller's policy — thrown, or handed
|
|
180
|
+
* over. Never a silent skip, on either route.
|
|
181
|
+
*/
|
|
182
|
+
function raiseRefusal(refusal) {
|
|
183
|
+
settleRefusal(unreadable, refusal);
|
|
184
|
+
}
|
|
114
185
|
// Ensure base directory exists
|
|
115
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- baseDir is from controlled config, not user input
|
|
116
186
|
if (!fs.existsSync(resolvedBaseDir)) {
|
|
117
187
|
throw new Error(`Base directory does not exist: ${resolvedBaseDir}`);
|
|
118
188
|
}
|
|
119
189
|
// Ensure base directory is actually a directory
|
|
120
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- resolved path validated above
|
|
121
190
|
const baseStat = fs.statSync(resolvedBaseDir);
|
|
122
191
|
if (!baseStat.isDirectory()) {
|
|
123
192
|
throw new Error(`Base path is not a directory: ${resolvedBaseDir}`);
|
|
@@ -132,6 +201,24 @@ export function crawlDirectorySync(options) {
|
|
|
132
201
|
const gitFiles = gitLsFiles({
|
|
133
202
|
cwd: resolvedBaseDir,
|
|
134
203
|
includeUntracked,
|
|
204
|
+
// The SAME decision the walk makes, in the same order: a directory
|
|
205
|
+
// outside this crawl's base or dropped by `exclude` is never listed by
|
|
206
|
+
// the walk, so a refusal on it is not this crawl's gap; anything else
|
|
207
|
+
// is, and goes where the walk's would go.
|
|
208
|
+
// `degrade` from git's side only: the refusal is filtered to this
|
|
209
|
+
// crawl's territory and then settled under the CALLER's policy, which
|
|
210
|
+
// may well be `refuse`.
|
|
211
|
+
unreadable: {
|
|
212
|
+
degrade: (refusal) => {
|
|
213
|
+
// Strictly beneath the base: a repository is often an ancestor of
|
|
214
|
+
// the crawl, and git names every refusal in the whole worktree.
|
|
215
|
+
if (!toForwardSlash(refusal.directory).startsWith(`${toForwardSlash(resolvedBaseDir)}/`))
|
|
216
|
+
return;
|
|
217
|
+
if (shouldExclude(toForwardSlash(safePath.relative(resolvedBaseDir, refusal.directory))))
|
|
218
|
+
return;
|
|
219
|
+
raiseRefusal(refusal);
|
|
220
|
+
},
|
|
221
|
+
},
|
|
135
222
|
});
|
|
136
223
|
if (gitFiles !== null) {
|
|
137
224
|
// ⚠️ KNOWN DIVERGENCE: this branch ignores `followSymlinks`.
|
|
@@ -156,14 +243,9 @@ export function crawlDirectorySync(options) {
|
|
|
156
243
|
// entry, not a drive-by fix here.
|
|
157
244
|
//
|
|
158
245
|
// Git ls-files succeeded - filter using glob patterns
|
|
159
|
-
const
|
|
160
|
-
const isExcluded = exclude.length > 0 ? picomatch(exclude, picoOptions) : () => false;
|
|
246
|
+
const isMember = crawlPathFilter(include, exclude);
|
|
161
247
|
return gitFiles
|
|
162
|
-
.filter((relativePath) =>
|
|
163
|
-
const normalizedPath = toForwardSlash(relativePath);
|
|
164
|
-
// Check both include and exclude patterns
|
|
165
|
-
return isIncluded(normalizedPath) && !isExcluded(normalizedPath) && !isExcluded(normalizedPath + '/');
|
|
166
|
-
})
|
|
248
|
+
.filter((relativePath) => isMember(relativePath))
|
|
167
249
|
.map((relativePath) => {
|
|
168
250
|
// git ls-files returns paths relative to cwd
|
|
169
251
|
return absolute ? safePath.resolve(resolvedBaseDir, relativePath) : relativePath;
|
|
@@ -175,7 +257,6 @@ export function crawlDirectorySync(options) {
|
|
|
175
257
|
// Fall back to manual directory crawling (not in git repo or git ls-files failed)
|
|
176
258
|
// Compile glob patterns using picomatch
|
|
177
259
|
const isIncluded = picomatch(include, picoOptions);
|
|
178
|
-
const isExcluded = exclude.length > 0 ? picomatch(exclude, picoOptions) : () => false;
|
|
179
260
|
const results = [];
|
|
180
261
|
/**
|
|
181
262
|
* Real paths of directories already walked, maintained ONLY when following
|
|
@@ -194,13 +275,6 @@ export function crawlDirectorySync(options) {
|
|
|
194
275
|
* directory cannot be reached twice, so the default path pays no `realpath`.
|
|
195
276
|
*/
|
|
196
277
|
const visitedRealDirs = new Set();
|
|
197
|
-
/**
|
|
198
|
-
* Check if a path should be excluded based on patterns
|
|
199
|
-
*/
|
|
200
|
-
function shouldExclude(normalizedPath) {
|
|
201
|
-
// Check explicit exclude patterns
|
|
202
|
-
return isExcluded(normalizedPath) || isExcluded(normalizedPath + '/');
|
|
203
|
-
}
|
|
204
278
|
/**
|
|
205
279
|
* Add a path to results if it matches include patterns
|
|
206
280
|
*/
|
|
@@ -209,24 +283,49 @@ export function crawlDirectorySync(options) {
|
|
|
209
283
|
results.push(absolute ? fullPath : relativePath);
|
|
210
284
|
}
|
|
211
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* What a failed `readdirSync` / `statSync` means for the walk.
|
|
288
|
+
*
|
|
289
|
+
* Absence (`ENOENT` / `ENOTDIR`) is an entry that vanished between being
|
|
290
|
+
* enumerated by its parent and being asked about itself — not in the
|
|
291
|
+
* population, nothing to report. Anything else is a refusal, and a refusal
|
|
292
|
+
* is a GAP: handed to the caller if it asked, thrown otherwise. Never a
|
|
293
|
+
* silent `return` — that is the shorter list this walk used to hand back.
|
|
294
|
+
*
|
|
295
|
+
* @param error - What the filesystem threw
|
|
296
|
+
* @param target - The directory (or link) it was asked about
|
|
297
|
+
* @returns True when the entry is simply gone and the walk should move on
|
|
298
|
+
*/
|
|
299
|
+
function reportOrSkip(error, target) {
|
|
300
|
+
const listing = listingFailure(error);
|
|
301
|
+
if (listing.outcome === 'absent')
|
|
302
|
+
return true;
|
|
303
|
+
if (listing.outcome === 'unreadable')
|
|
304
|
+
raiseRefusal(directoryRefusalFor(listing, target));
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
212
307
|
/**
|
|
213
308
|
* Record a directory as walked, reporting whether it had already been seen.
|
|
214
309
|
*
|
|
215
310
|
* Identity is `realpathSync.native`, not the traversal path: two names for
|
|
216
311
|
* one directory must collide here or the alias is enumerated twice. A
|
|
217
|
-
* directory whose real path cannot be read is
|
|
218
|
-
*
|
|
219
|
-
*
|
|
312
|
+
* directory whose real path cannot be read is not descended into — refusing
|
|
313
|
+
* to walk something we cannot identify is the safe side of a guard whose
|
|
314
|
+
* whole job is bounding traversal — but it is NOT silently "already walked":
|
|
315
|
+
* a directory that vanished is skipped like any other absence, and one the
|
|
316
|
+
* OS refused to canonicalise is a gap, surfaced under the caller's policy
|
|
317
|
+
* exactly as a refused listing is (see `reportOrSkip`).
|
|
220
318
|
*
|
|
221
319
|
* @param dir - Directory about to be walked
|
|
222
|
-
* @returns True when this directory
|
|
320
|
+
* @returns True when this directory must not be walked (seen before, gone, or refused)
|
|
223
321
|
*/
|
|
224
322
|
function alreadyWalked(dir) {
|
|
225
323
|
let realPath;
|
|
226
324
|
try {
|
|
227
325
|
realPath = fs.realpathSync.native(dir);
|
|
228
326
|
}
|
|
229
|
-
catch {
|
|
327
|
+
catch (error) {
|
|
328
|
+
reportOrSkip(error, dir);
|
|
230
329
|
return true;
|
|
231
330
|
}
|
|
232
331
|
if (visitedRealDirs.has(realPath)) {
|
|
@@ -240,16 +339,20 @@ export function crawlDirectorySync(options) {
|
|
|
240
339
|
*/
|
|
241
340
|
function processSymlink(fullPath, normalizedPath, relativePath) {
|
|
242
341
|
if (!followSymlinks) {
|
|
342
|
+
if (onSymlinkNotFollowed !== undefined && isIncluded(normalizedPath)) {
|
|
343
|
+
onSymlinkNotFollowed(absolute ? fullPath : relativePath);
|
|
344
|
+
}
|
|
243
345
|
return;
|
|
244
346
|
}
|
|
245
347
|
// Resolve symlink and check if it's a directory or file
|
|
246
348
|
let targetStat;
|
|
247
349
|
try {
|
|
248
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- path constructed from validated baseDir + entries
|
|
249
350
|
targetStat = fs.statSync(fullPath);
|
|
250
351
|
}
|
|
251
|
-
catch {
|
|
252
|
-
//
|
|
352
|
+
catch (error) {
|
|
353
|
+
// A broken symlink is absence and is skipped; a target the OS refused to
|
|
354
|
+
// stat is a gap, and goes the same way a refused listing does.
|
|
355
|
+
reportOrSkip(error, fullPath);
|
|
253
356
|
return;
|
|
254
357
|
}
|
|
255
358
|
if (targetStat.isDirectory()) {
|
|
@@ -285,11 +388,12 @@ export function crawlDirectorySync(options) {
|
|
|
285
388
|
}
|
|
286
389
|
let entries;
|
|
287
390
|
try {
|
|
288
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- path constructed from validated baseDir, recursively walking
|
|
289
391
|
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
290
392
|
}
|
|
291
|
-
catch {
|
|
292
|
-
//
|
|
393
|
+
catch (error) {
|
|
394
|
+
// 🚨 Not a silent skip. A directory that refused to be listed is a gap in
|
|
395
|
+
// the population this walk defines — see `reportOrSkip`.
|
|
396
|
+
reportOrSkip(error, currentDir);
|
|
293
397
|
return;
|
|
294
398
|
}
|
|
295
399
|
// Two passes, not one: `readdirSync` order is filesystem-defined (see
|