@vibe-agent-toolkit/utils 0.2.0-rc.1 → 0.2.0-rc.3
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 +6 -3
- 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 +680 -0
- package/dist/crawl-timing.d.ts.map +1 -0
- package/dist/crawl-timing.js +809 -0
- package/dist/crawl-timing.js.map +1 -0
- package/dist/file-crawler.d.ts +22 -0
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +31 -7
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs-utils.d.ts +17 -0
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +69 -0
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +3 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +5 -1
- 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 +181 -0
- package/dist/git-snapshot.d.ts.map +1 -0
- package/dist/git-snapshot.js +232 -0
- package/dist/git-snapshot.js.map +1 -0
- package/dist/git-tracker.d.ts +94 -1
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +135 -3
- package/dist/git-tracker.js.map +1 -1
- package/dist/git-utils.d.ts +38 -0
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +103 -80
- package/dist/git-utils.js.map +1 -1
- package/dist/git.d.ts +2 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +2 -1
- package/dist/git.js.map +1 -1
- package/dist/gitignore-checker.d.ts.map +1 -1
- package/dist/gitignore-checker.js +6 -3
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -1
- package/dist/index.js.map +1 -1
- package/dist/link-auth/expand-macro.d.ts.map +1 -1
- package/dist/link-auth/expand-macro.js +7 -1
- package/dist/link-auth/expand-macro.js.map +1 -1
- package/dist/link-auth/resolve-token.d.ts.map +1 -1
- package/dist/link-auth/resolve-token.js +15 -1
- package/dist/link-auth/resolve-token.js.map +1 -1
- package/dist/path-core.d.ts +1 -1
- package/dist/path-core.js +1 -1
- package/dist/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +6 -3
- package/dist/project-utils.js.map +1 -1
- package/dist/safe-exec.d.ts +38 -10
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +88 -19
- package/dist/safe-exec.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 +28 -0
- package/dist/skill-test/spawn-claude.d.ts.map +1 -1
- package/dist/skill-test/spawn-claude.js +51 -2
- 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 +47 -3
- package/dist/skill-test/transcript.js.map +1 -1
- package/dist/test-helpers.d.ts +185 -13
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +274 -27
- package/dist/test-helpers.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 +348 -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 +53 -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 +119 -0
- package/dist/timing-dump.d.ts.map +1 -0
- package/dist/timing-dump.js +164 -0
- package/dist/timing-dump.js.map +1 -0
- package/eslint/README.md +105 -3
- package/eslint/index.cjs +41 -0
- package/eslint/index.d.cts +7 -4
- package/eslint/rules/no-bare-symlink-in-tests.cjs +200 -0
- package/eslint/rules/no-raw-text-decode.cjs +296 -0
- package/eslint/rules/no-self-package-import.cjs +157 -0
- package/package.json +9 -2
package/dist/git-tracker.d.ts
CHANGED
|
@@ -56,6 +56,12 @@ export declare class GitTracker {
|
|
|
56
56
|
private readonly activeSet;
|
|
57
57
|
/** Absolute paths of every directory that contains at least one active-set file. */
|
|
58
58
|
private readonly activeAncestors;
|
|
59
|
+
/**
|
|
60
|
+
* Lowercased absolute path → the root-relative path spelled the way git
|
|
61
|
+
* spelled it. Built from the very same `git ls-files` output `activeSet`
|
|
62
|
+
* comes from, so it costs one extra `Map` and no extra git invocation.
|
|
63
|
+
*/
|
|
64
|
+
private readonly indexPaths;
|
|
59
65
|
private initialized;
|
|
60
66
|
private activeSetPopulated;
|
|
61
67
|
/** Whether `git ls-files` actually answered during {@link initialize}. */
|
|
@@ -72,8 +78,60 @@ export declare class GitTracker {
|
|
|
72
78
|
* With `includeUntracked: false`, only tracked files are pre-populated.
|
|
73
79
|
* Untracked non-ignored files will miss the cache and fall through to
|
|
74
80
|
* `git check-ignore` via {@link isIgnored}.
|
|
81
|
+
*
|
|
82
|
+
* ## This is the one bracket in the crawl-timing seam's `shared` stratum
|
|
83
|
+
*
|
|
84
|
+
* The `git ls-files` spawn below is preparation BOTH crawlers consume and
|
|
85
|
+
* NEITHER owns — the incumbent link walk and the projection's contributors are
|
|
86
|
+
* each handed a tracker by their caller — so it is charged to
|
|
87
|
+
* {@link CRAWL_SHARED_GIT_TRACKER_ID}, in a stratum belonging to no arm.
|
|
88
|
+
*
|
|
89
|
+
* The bracket is here, inside the class, and not at the six sites that build a
|
|
90
|
+
* tracker, for the reason `crawl-timing.ts` gives about `ResourceRegistry`: six
|
|
91
|
+
* copies are six chances to disagree, and a seventh site added later would
|
|
92
|
+
* silently go uncharged. Here, every caller is covered by construction — which
|
|
93
|
+
* includes `@vibe-agent-toolkit/discovery`, a package that could not have filed
|
|
94
|
+
* a row from its own call site at all, since it depends on `utils` alone.
|
|
95
|
+
*
|
|
96
|
+
* The early return above is deliberately OUTSIDE it: a re-entrant call does no
|
|
97
|
+
* work, and charging it would inflate `calls` with questions rather than
|
|
98
|
+
* spawns.
|
|
75
99
|
*/
|
|
76
100
|
initialize(options?: GitTrackerInitOptions): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* The active set read off a snapshot this process has ALREADY taken — or
|
|
103
|
+
* `null` to say "ask git yourself".
|
|
104
|
+
*
|
|
105
|
+
* ## Why this is the same question, not a similar one
|
|
106
|
+
*
|
|
107
|
+
* `git ls-files --cached --others --exclude-standard` returns
|
|
108
|
+
* `tracked ∪ (untracked ∧ ¬ignored)`. A snapshot is `git add --all` **without**
|
|
109
|
+
* `--force` staged into a throwaway index, whose membership is that same set
|
|
110
|
+
* by construction — the exclusion of ignored paths is `--force`'s absence in
|
|
111
|
+
* both cases. So where a snapshot is already in hand, the spawn this method
|
|
112
|
+
* replaces would re-derive a set the process is already holding.
|
|
113
|
+
*
|
|
114
|
+
* It is deliberately a PEEK ({@link peekGitTreeSnapshot}) and never a take: a
|
|
115
|
+
* snapshot costs a `git add --all`, so causing one to avoid an `ls-files`
|
|
116
|
+
* would be a large loss dressed as a saving. On the incumbent walk no snapshot
|
|
117
|
+
* is ever taken, the peek misses, and this tracker spawns exactly as it always
|
|
118
|
+
* has.
|
|
119
|
+
*
|
|
120
|
+
* ## The one divergence, and why it is an improvement
|
|
121
|
+
*
|
|
122
|
+
* `--cached` reads the REAL index, so a tracked file deleted from the working
|
|
123
|
+
* tree is still listed. `git add --all` stages that deletion, so the snapshot
|
|
124
|
+
* omits it. The set is therefore not identical — it is the same set minus
|
|
125
|
+
* paths that do not exist, which is what {@link isIgnoredByActiveSet} already
|
|
126
|
+
* documents the active set to be ("it can only ever contain paths that
|
|
127
|
+
* EXIST"). The snapshot-sourced set honours that sentence more exactly than
|
|
128
|
+
* the spawn does.
|
|
129
|
+
*
|
|
130
|
+
* @param includeUntracked - The caller's requested membership
|
|
131
|
+
* @returns Root-relative paths in `git ls-files` shape, or `null` when no
|
|
132
|
+
* snapshot is available or the request is one a snapshot cannot answer
|
|
133
|
+
*/
|
|
134
|
+
private activePathsFromOpenSnapshot;
|
|
77
135
|
/**
|
|
78
136
|
* Did git actually answer, or is this tracker an empty shell?
|
|
79
137
|
*
|
|
@@ -139,8 +197,21 @@ export declare class GitTracker {
|
|
|
139
197
|
* possible per-path spawn.
|
|
140
198
|
*
|
|
141
199
|
* @param absolutePath - Absolute path to check
|
|
200
|
+
* @param knownToExist - The caller's own answer to the existence question, when
|
|
201
|
+
* it has already asked. Supplying it skips this method's `existsSync`, which
|
|
202
|
+
* is otherwise paid once per path that is absent from the active set — i.e.
|
|
203
|
+
* once per ignored path, and the projection's `filesystem` extent enumerates
|
|
204
|
+
* all of them (11,108 calls on an 8,496-path adopter tree).
|
|
205
|
+
*
|
|
206
|
+
* **It must mean what `existsSync` means: `stat` succeeds, following
|
|
207
|
+
* symlinks.** A caller holding only an `lstat` result has a DIFFERENT fact —
|
|
208
|
+
* `lstat` succeeds on a dangling symlink where `existsSync` returns false —
|
|
209
|
+
* and must narrow it to `exists && symlinkResolves !== false` rather than
|
|
210
|
+
* pass the `lstat` boolean through, or dangling symlinks silently stop
|
|
211
|
+
* falling back to `git check-ignore` and start reporting as ignored.
|
|
212
|
+
* Omit it and nothing changes.
|
|
142
213
|
*/
|
|
143
|
-
isIgnoredByActiveSet(absolutePath: string): boolean;
|
|
214
|
+
isIgnoredByActiveSet(absolutePath: string, knownToExist?: boolean): boolean;
|
|
144
215
|
private isWithinProjectRoot;
|
|
145
216
|
/**
|
|
146
217
|
* Check if a file is ignored by git.
|
|
@@ -151,6 +222,28 @@ export declare class GitTracker {
|
|
|
151
222
|
* @returns true if file is gitignored, false otherwise
|
|
152
223
|
*/
|
|
153
224
|
isIgnored(filePath: string): boolean;
|
|
225
|
+
/**
|
|
226
|
+
* The spelling git records for a path, or `null` if git has no record of it.
|
|
227
|
+
*
|
|
228
|
+
* This is the casing oracle, not another ignore check. On a case-insensitive
|
|
229
|
+
* filesystem `docs/Readme.md` and `docs/README.md` are one inode with two
|
|
230
|
+
* spellings, and Node's two `realpath` implementations disagree about which
|
|
231
|
+
* one they hand back — so anything that derives an identity from a path needs
|
|
232
|
+
* a single authoritative spelling, and git's is it wherever git has one.
|
|
233
|
+
*
|
|
234
|
+
* The lookup key is lowercased, which is the point: the caller asks with
|
|
235
|
+
* whatever casing it observed and gets back the casing git holds.
|
|
236
|
+
*
|
|
237
|
+
* Answers only from the pre-populated set — never spawns. A path git does not
|
|
238
|
+
* know (untracked-and-ignored, non-existent, outside the project root, or any
|
|
239
|
+
* path at all when `git ls-files` did not answer) returns `null`, and the
|
|
240
|
+
* caller falls back to the on-disk casing.
|
|
241
|
+
*
|
|
242
|
+
* @param absolutePath - Absolute path to look up
|
|
243
|
+
* @returns Root-relative, forward-slashed path as git spells it — relative to
|
|
244
|
+
* THIS tracker's project root — or `null` when git has no record of it
|
|
245
|
+
*/
|
|
246
|
+
indexPathFor(absolutePath: string): string | null;
|
|
154
247
|
/**
|
|
155
248
|
* Get cache statistics.
|
|
156
249
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-tracker.d.ts","sourceRoot":"","sources":["../src/git-tracker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"git-tracker.d.ts","sourceRoot":"","sources":["../src/git-tracker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAcH;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAS;IAC/C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmC;IACzD,6FAA6F;IAC7F,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;IACpD,oFAAoF;IACpF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA0B;IAC1D;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAC7D,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,kBAAkB,CAAS;IACnC,0EAA0E;IAC1E,OAAO,CAAC,WAAW,CAAS;gBAEhB,WAAW,EAAE,MAAM;IAK/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACG,UAAU,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiChE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACH,OAAO,CAAC,2BAA2B;IA6BnC;;;;;;;;;;;;;OAaG;IACH,QAAQ,IAAI,OAAO;IAInB;;;;;;;OAOG;IACH,OAAO,CAAC,mBAAmB;IAwB3B;;;;;;;;;;;OAWG;IACH,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAQlD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAyCG;IACH,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,OAAO,GAAG,OAAO;IA2B3E,OAAO,CAAC,mBAAmB;IAQ3B;;;;;;;OAOG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAqBpC;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAIjD;;OAEG;IACH,QAAQ,IAAI;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,mBAAmB,EAAE,MAAM,CAAA;KAAE;IAQrF;;OAEG;IACH,KAAK,IAAI,IAAI;CASd"}
|
package/dist/git-tracker.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync } from 'node:fs';
|
|
8
8
|
import { dirname } from 'node:path';
|
|
9
|
+
import { CRAWL_SHARED_GIT_TRACKER_ID, crawlTimingStart, recordSharedPass, } from './crawl-timing.js';
|
|
10
|
+
import { peekGitTreeSnapshot } from './git-snapshot.js';
|
|
9
11
|
import { gitLsFiles, isGitIgnored } from './git-utils.js';
|
|
10
12
|
import { safePath, toForwardSlash } from './path-utils.js';
|
|
11
13
|
/**
|
|
@@ -42,6 +44,12 @@ export class GitTracker {
|
|
|
42
44
|
activeSet = new Set();
|
|
43
45
|
/** Absolute paths of every directory that contains at least one active-set file. */
|
|
44
46
|
activeAncestors = new Set();
|
|
47
|
+
/**
|
|
48
|
+
* Lowercased absolute path → the root-relative path spelled the way git
|
|
49
|
+
* spelled it. Built from the very same `git ls-files` output `activeSet`
|
|
50
|
+
* comes from, so it costs one extra `Map` and no extra git invocation.
|
|
51
|
+
*/
|
|
52
|
+
indexPaths = new Map();
|
|
45
53
|
initialized = false;
|
|
46
54
|
activeSetPopulated = false;
|
|
47
55
|
/** Whether `git ls-files` actually answered during {@link initialize}. */
|
|
@@ -61,13 +69,32 @@ export class GitTracker {
|
|
|
61
69
|
* With `includeUntracked: false`, only tracked files are pre-populated.
|
|
62
70
|
* Untracked non-ignored files will miss the cache and fall through to
|
|
63
71
|
* `git check-ignore` via {@link isIgnored}.
|
|
72
|
+
*
|
|
73
|
+
* ## This is the one bracket in the crawl-timing seam's `shared` stratum
|
|
74
|
+
*
|
|
75
|
+
* The `git ls-files` spawn below is preparation BOTH crawlers consume and
|
|
76
|
+
* NEITHER owns — the incumbent link walk and the projection's contributors are
|
|
77
|
+
* each handed a tracker by their caller — so it is charged to
|
|
78
|
+
* {@link CRAWL_SHARED_GIT_TRACKER_ID}, in a stratum belonging to no arm.
|
|
79
|
+
*
|
|
80
|
+
* The bracket is here, inside the class, and not at the six sites that build a
|
|
81
|
+
* tracker, for the reason `crawl-timing.ts` gives about `ResourceRegistry`: six
|
|
82
|
+
* copies are six chances to disagree, and a seventh site added later would
|
|
83
|
+
* silently go uncharged. Here, every caller is covered by construction — which
|
|
84
|
+
* includes `@vibe-agent-toolkit/discovery`, a package that could not have filed
|
|
85
|
+
* a row from its own call site at all, since it depends on `utils` alone.
|
|
86
|
+
*
|
|
87
|
+
* The early return above is deliberately OUTSIDE it: a re-entrant call does no
|
|
88
|
+
* work, and charging it would inflate `calls` with questions rather than
|
|
89
|
+
* spawns.
|
|
64
90
|
*/
|
|
65
91
|
async initialize(options) {
|
|
66
92
|
if (this.initialized) {
|
|
67
93
|
return;
|
|
68
94
|
}
|
|
95
|
+
const startedAt = crawlTimingStart();
|
|
69
96
|
const includeUntracked = options?.includeUntracked ?? true;
|
|
70
|
-
const files = gitLsFiles({
|
|
97
|
+
const files = this.activePathsFromOpenSnapshot(includeUntracked) ?? gitLsFiles({
|
|
71
98
|
cwd: this.projectRoot,
|
|
72
99
|
...(includeUntracked ? { includeUntracked: true } : {}),
|
|
73
100
|
});
|
|
@@ -76,12 +103,78 @@ export class GitTracker {
|
|
|
76
103
|
const absolutePath = safePath.resolve(this.projectRoot, relativePath);
|
|
77
104
|
this.cache.set(absolutePath, false); // false = not ignored
|
|
78
105
|
this.activeSet.add(absolutePath);
|
|
106
|
+
this.indexPaths.set(absolutePath.toLowerCase(), toForwardSlash(relativePath));
|
|
79
107
|
}
|
|
80
108
|
this.populateAncestorSet();
|
|
81
109
|
}
|
|
82
110
|
this.gitAnswered = files !== null;
|
|
83
111
|
this.activeSetPopulated = includeUntracked && files !== null;
|
|
84
112
|
this.initialized = true;
|
|
113
|
+
// After the state above is settled, so a throw from the seam could never
|
|
114
|
+
// leave a half-initialized tracker; and charged even when git did not answer,
|
|
115
|
+
// because a failed `git ls-files` still spawned a process and still cost the
|
|
116
|
+
// command the time it took to fail.
|
|
117
|
+
recordSharedPass(CRAWL_SHARED_GIT_TRACKER_ID, startedAt);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The active set read off a snapshot this process has ALREADY taken — or
|
|
121
|
+
* `null` to say "ask git yourself".
|
|
122
|
+
*
|
|
123
|
+
* ## Why this is the same question, not a similar one
|
|
124
|
+
*
|
|
125
|
+
* `git ls-files --cached --others --exclude-standard` returns
|
|
126
|
+
* `tracked ∪ (untracked ∧ ¬ignored)`. A snapshot is `git add --all` **without**
|
|
127
|
+
* `--force` staged into a throwaway index, whose membership is that same set
|
|
128
|
+
* by construction — the exclusion of ignored paths is `--force`'s absence in
|
|
129
|
+
* both cases. So where a snapshot is already in hand, the spawn this method
|
|
130
|
+
* replaces would re-derive a set the process is already holding.
|
|
131
|
+
*
|
|
132
|
+
* It is deliberately a PEEK ({@link peekGitTreeSnapshot}) and never a take: a
|
|
133
|
+
* snapshot costs a `git add --all`, so causing one to avoid an `ls-files`
|
|
134
|
+
* would be a large loss dressed as a saving. On the incumbent walk no snapshot
|
|
135
|
+
* is ever taken, the peek misses, and this tracker spawns exactly as it always
|
|
136
|
+
* has.
|
|
137
|
+
*
|
|
138
|
+
* ## The one divergence, and why it is an improvement
|
|
139
|
+
*
|
|
140
|
+
* `--cached` reads the REAL index, so a tracked file deleted from the working
|
|
141
|
+
* tree is still listed. `git add --all` stages that deletion, so the snapshot
|
|
142
|
+
* omits it. The set is therefore not identical — it is the same set minus
|
|
143
|
+
* paths that do not exist, which is what {@link isIgnoredByActiveSet} already
|
|
144
|
+
* documents the active set to be ("it can only ever contain paths that
|
|
145
|
+
* EXIST"). The snapshot-sourced set honours that sentence more exactly than
|
|
146
|
+
* the spawn does.
|
|
147
|
+
*
|
|
148
|
+
* @param includeUntracked - The caller's requested membership
|
|
149
|
+
* @returns Root-relative paths in `git ls-files` shape, or `null` when no
|
|
150
|
+
* snapshot is available or the request is one a snapshot cannot answer
|
|
151
|
+
*/
|
|
152
|
+
activePathsFromOpenSnapshot(includeUntracked) {
|
|
153
|
+
// A snapshot cannot express the tracked-ONLY set: `add --all` stages
|
|
154
|
+
// untracked-not-ignored files too, and nothing in the result marks which
|
|
155
|
+
// entries were already tracked. Declining is the only correct answer.
|
|
156
|
+
if (!includeUntracked)
|
|
157
|
+
return null;
|
|
158
|
+
const snapshot = peekGitTreeSnapshot(this.projectRoot);
|
|
159
|
+
if (snapshot === undefined)
|
|
160
|
+
return null;
|
|
161
|
+
const relativePaths = [];
|
|
162
|
+
for (const entry of snapshot.entries) {
|
|
163
|
+
// A snapshot covers the whole REPOSITORY, which may be an ancestor of this
|
|
164
|
+
// tracker's root — `git ls-files` run at that root scopes its listing to
|
|
165
|
+
// it, so anything above is not this tracker's business and must not enter
|
|
166
|
+
// the active set. Everything below is kept verbatim, symlinks and
|
|
167
|
+
// submodule gitlinks included, because `--cached` lists those too.
|
|
168
|
+
const relativePath = toForwardSlash(safePath.relative(this.normalizedProjectRoot, entry.absolutePath));
|
|
169
|
+
// A leading `..` SEGMENT, not a `../` prefix: compared as a path segment,
|
|
170
|
+
// a file legitimately named `..hidden` is kept rather than silently
|
|
171
|
+
// dropped as if it were an escape.
|
|
172
|
+
if (relativePath.length === 0 || relativePath.split('/')[0] === '..') {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
relativePaths.push(relativePath);
|
|
176
|
+
}
|
|
177
|
+
return relativePaths;
|
|
85
178
|
}
|
|
86
179
|
/**
|
|
87
180
|
* Did git actually answer, or is this tracker an empty shell?
|
|
@@ -175,8 +268,21 @@ export class GitTracker {
|
|
|
175
268
|
* possible per-path spawn.
|
|
176
269
|
*
|
|
177
270
|
* @param absolutePath - Absolute path to check
|
|
271
|
+
* @param knownToExist - The caller's own answer to the existence question, when
|
|
272
|
+
* it has already asked. Supplying it skips this method's `existsSync`, which
|
|
273
|
+
* is otherwise paid once per path that is absent from the active set — i.e.
|
|
274
|
+
* once per ignored path, and the projection's `filesystem` extent enumerates
|
|
275
|
+
* all of them (11,108 calls on an 8,496-path adopter tree).
|
|
276
|
+
*
|
|
277
|
+
* **It must mean what `existsSync` means: `stat` succeeds, following
|
|
278
|
+
* symlinks.** A caller holding only an `lstat` result has a DIFFERENT fact —
|
|
279
|
+
* `lstat` succeeds on a dangling symlink where `existsSync` returns false —
|
|
280
|
+
* and must narrow it to `exists && symlinkResolves !== false` rather than
|
|
281
|
+
* pass the `lstat` boolean through, or dangling symlinks silently stop
|
|
282
|
+
* falling back to `git check-ignore` and start reporting as ignored.
|
|
283
|
+
* Omit it and nothing changes.
|
|
178
284
|
*/
|
|
179
|
-
isIgnoredByActiveSet(absolutePath) {
|
|
285
|
+
isIgnoredByActiveSet(absolutePath, knownToExist) {
|
|
180
286
|
if (!this.activeSetPopulated) {
|
|
181
287
|
return this.isIgnored(absolutePath);
|
|
182
288
|
}
|
|
@@ -191,7 +297,8 @@ export class GitTracker {
|
|
|
191
297
|
// Absent from the active set. That means "ignored" only for a path that is
|
|
192
298
|
// actually there; otherwise the set has no opinion and git must be asked.
|
|
193
299
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- caller-supplied path, read-only existence probe
|
|
194
|
-
|
|
300
|
+
const present = knownToExist ?? existsSync(normalized);
|
|
301
|
+
if (!present) {
|
|
195
302
|
return this.isIgnored(absolutePath);
|
|
196
303
|
}
|
|
197
304
|
return true;
|
|
@@ -228,6 +335,30 @@ export class GitTracker {
|
|
|
228
335
|
this.cache.set(cacheKey, ignored);
|
|
229
336
|
return ignored;
|
|
230
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* The spelling git records for a path, or `null` if git has no record of it.
|
|
340
|
+
*
|
|
341
|
+
* This is the casing oracle, not another ignore check. On a case-insensitive
|
|
342
|
+
* filesystem `docs/Readme.md` and `docs/README.md` are one inode with two
|
|
343
|
+
* spellings, and Node's two `realpath` implementations disagree about which
|
|
344
|
+
* one they hand back — so anything that derives an identity from a path needs
|
|
345
|
+
* a single authoritative spelling, and git's is it wherever git has one.
|
|
346
|
+
*
|
|
347
|
+
* The lookup key is lowercased, which is the point: the caller asks with
|
|
348
|
+
* whatever casing it observed and gets back the casing git holds.
|
|
349
|
+
*
|
|
350
|
+
* Answers only from the pre-populated set — never spawns. A path git does not
|
|
351
|
+
* know (untracked-and-ignored, non-existent, outside the project root, or any
|
|
352
|
+
* path at all when `git ls-files` did not answer) returns `null`, and the
|
|
353
|
+
* caller falls back to the on-disk casing.
|
|
354
|
+
*
|
|
355
|
+
* @param absolutePath - Absolute path to look up
|
|
356
|
+
* @returns Root-relative, forward-slashed path as git spells it — relative to
|
|
357
|
+
* THIS tracker's project root — or `null` when git has no record of it
|
|
358
|
+
*/
|
|
359
|
+
indexPathFor(absolutePath) {
|
|
360
|
+
return this.indexPaths.get(safePath.resolve(absolutePath).toLowerCase()) ?? null;
|
|
361
|
+
}
|
|
231
362
|
/**
|
|
232
363
|
* Get cache statistics.
|
|
233
364
|
*/
|
|
@@ -245,6 +376,7 @@ export class GitTracker {
|
|
|
245
376
|
this.cache.clear();
|
|
246
377
|
this.activeSet.clear();
|
|
247
378
|
this.activeAncestors.clear();
|
|
379
|
+
this.indexPaths.clear();
|
|
248
380
|
this.initialized = false;
|
|
249
381
|
this.activeSetPopulated = false;
|
|
250
382
|
this.gitAnswered = false;
|
package/dist/git-tracker.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-tracker.js","sourceRoot":"","sources":["../src/git-tracker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAqB3D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,OAAO,UAAU;IACJ,WAAW,CAAS;IACpB,qBAAqB,CAAS;IAC9B,KAAK,GAAyB,IAAI,GAAG,EAAE,CAAC;IACzD,6FAA6F;IAC5E,SAAS,GAAgB,IAAI,GAAG,EAAE,CAAC;IACpD,oFAAoF;IACnE,eAAe,GAAgB,IAAI,GAAG,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"git-tracker.js","sourceRoot":"","sources":["../src/git-tracker.ts"],"names":[],"mappings":"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"}
|
package/dist/git-utils.d.ts
CHANGED
|
@@ -43,6 +43,44 @@ export declare function gitLsFiles(options: {
|
|
|
43
43
|
patterns?: string[];
|
|
44
44
|
includeUntracked?: boolean;
|
|
45
45
|
}): string[] | null;
|
|
46
|
+
/**
|
|
47
|
+
* List paths git does NOT track, with each wholly-untracked directory collapsed
|
|
48
|
+
* to a single entry.
|
|
49
|
+
*
|
|
50
|
+
* This is the **prune list**, not a file list, and the distinction is the whole
|
|
51
|
+
* value. `--others --ignored --exclude-standard` alone returns every ignored file
|
|
52
|
+
* individually: measured on an 8,496-path adopter working tree that is 533,557
|
|
53
|
+
* paths in 1.19 s — worse than the crawl it was meant to replace, because
|
|
54
|
+
* `.turbo/cache` alone contributed 418,518 of them. Adding `--directory`
|
|
55
|
+
* collapses each wholly-ignored directory to one entry: **369 entries in 60 ms**.
|
|
56
|
+
* A caller can then decide per directory whether to descend, and skip a
|
|
57
|
+
* half-million paths by name without ever entering them.
|
|
58
|
+
*
|
|
59
|
+
* A collapsed directory entry is returned with a trailing `/`, exactly as git
|
|
60
|
+
* spells it. That is how a caller tells "this whole subtree" from "this one
|
|
61
|
+
* file", so it is deliberately not normalized away here.
|
|
62
|
+
*
|
|
63
|
+
* @param options - Configuration options
|
|
64
|
+
* @param options.cwd - Working directory inside the repository
|
|
65
|
+
* @param options.ignored - Restrict to ignored paths. Off, the listing is
|
|
66
|
+
* untracked-but-not-ignored paths — which is the only way to see an EMPTY
|
|
67
|
+
* untracked directory, since a directory with no files in it is invisible to
|
|
68
|
+
* `ls-files` and to any tree object
|
|
69
|
+
* @param options.directory - Collapse a wholly-untracked directory to one entry
|
|
70
|
+
* @returns Paths relative to the git root, or null if git did not answer
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* // The prune list: where the ignored territory is, without enumerating it.
|
|
75
|
+
* const prune = gitLsOthers({ cwd: root, ignored: true, directory: true });
|
|
76
|
+
* // → ['dist/', 'node_modules/', '.turbo/', 'notes.local.md']
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export declare function gitLsOthers(options: {
|
|
80
|
+
cwd: string;
|
|
81
|
+
ignored?: boolean;
|
|
82
|
+
directory?: boolean;
|
|
83
|
+
}): string[] | null;
|
|
46
84
|
/**
|
|
47
85
|
* Check if a file path is ignored by git
|
|
48
86
|
*
|
package/dist/git-utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-utils.d.ts","sourceRoot":"","sources":["../src/git-utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"git-utils.d.ts","sourceRoot":"","sources":["../src/git-utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAUH;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAmB3D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,MAAM,EAAE,GAAG,IAAI,CAiClB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,GAAG,MAAM,EAAE,GAAG,IAAI,CAuBlB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAE,MAAsB,GAAG,OAAO,CAiDnF"}
|
package/dist/git-utils.js
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
* Centralized git command wrapper.
|
|
3
3
|
* All git commands should go through this module for consistency and testability.
|
|
4
4
|
*/
|
|
5
|
-
import { spawnSync } from 'node:child_process';
|
|
6
5
|
import { existsSync } from 'node:fs';
|
|
7
6
|
import { dirname, parse } from 'node:path';
|
|
8
|
-
import which from 'which';
|
|
9
7
|
import { lookupGitRoot, rememberGitRoot } from './git-root-cache.js';
|
|
8
|
+
import { runGit } from './git-run.js';
|
|
10
9
|
import { safePath } from './path-utils.js';
|
|
11
10
|
/**
|
|
12
11
|
* Find the git repository root by walking up from the given directory.
|
|
@@ -62,44 +61,86 @@ export function gitFindRoot(startDir) {
|
|
|
62
61
|
* ```
|
|
63
62
|
*/
|
|
64
63
|
export function gitLsFiles(options) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
64
|
+
// -z emits NUL-separated, UNQUOTED paths regardless of byte content. Without
|
|
65
|
+
// it, git quotes any path containing non-ASCII bytes (wraps it in double
|
|
66
|
+
// quotes with octal escapes, e.g. `café.md` -> `"caf\303\251.md"`), which is
|
|
67
|
+
// unusable to any exact-string lookup against the real filename.
|
|
68
|
+
const args = ['ls-files', '-z'];
|
|
69
|
+
// Include untracked files that aren't gitignored
|
|
70
|
+
if (options.includeUntracked) {
|
|
71
|
+
args.push('--cached', '--others', '--exclude-standard');
|
|
72
|
+
}
|
|
73
|
+
// Add patterns if provided
|
|
74
|
+
if (options.patterns && options.patterns.length > 0) {
|
|
75
|
+
args.push('--', ...options.patterns);
|
|
76
|
+
}
|
|
77
|
+
// `trim: false` because the output is NUL-delimited: git sorts by byte value,
|
|
78
|
+
// so a path beginning with a space sorts FIRST and a trim would silently
|
|
79
|
+
// rename it to a path that does not exist.
|
|
80
|
+
const result = runGit(args, { cwd: options.cwd, trim: false });
|
|
81
|
+
// Every failure means the same thing to this caller — no listing. That covers
|
|
82
|
+
// "not a repository" (128), git missing entirely, and a listing too large for
|
|
83
|
+
// the buffer, which `ok` folds in because a truncated answer here is files
|
|
84
|
+
// silently missing rather than a short list.
|
|
85
|
+
if (!result.ok) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
// NUL-separated (from -z above), so no path can ever need quote-unescaping —
|
|
89
|
+
// a trailing NUL just produces one empty string at the end, dropped below.
|
|
90
|
+
return result.stdout.split('\0').filter((line) => line.length > 0);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* List paths git does NOT track, with each wholly-untracked directory collapsed
|
|
94
|
+
* to a single entry.
|
|
95
|
+
*
|
|
96
|
+
* This is the **prune list**, not a file list, and the distinction is the whole
|
|
97
|
+
* value. `--others --ignored --exclude-standard` alone returns every ignored file
|
|
98
|
+
* individually: measured on an 8,496-path adopter working tree that is 533,557
|
|
99
|
+
* paths in 1.19 s — worse than the crawl it was meant to replace, because
|
|
100
|
+
* `.turbo/cache` alone contributed 418,518 of them. Adding `--directory`
|
|
101
|
+
* collapses each wholly-ignored directory to one entry: **369 entries in 60 ms**.
|
|
102
|
+
* A caller can then decide per directory whether to descend, and skip a
|
|
103
|
+
* half-million paths by name without ever entering them.
|
|
104
|
+
*
|
|
105
|
+
* A collapsed directory entry is returned with a trailing `/`, exactly as git
|
|
106
|
+
* spells it. That is how a caller tells "this whole subtree" from "this one
|
|
107
|
+
* file", so it is deliberately not normalized away here.
|
|
108
|
+
*
|
|
109
|
+
* @param options - Configuration options
|
|
110
|
+
* @param options.cwd - Working directory inside the repository
|
|
111
|
+
* @param options.ignored - Restrict to ignored paths. Off, the listing is
|
|
112
|
+
* untracked-but-not-ignored paths — which is the only way to see an EMPTY
|
|
113
|
+
* untracked directory, since a directory with no files in it is invisible to
|
|
114
|
+
* `ls-files` and to any tree object
|
|
115
|
+
* @param options.directory - Collapse a wholly-untracked directory to one entry
|
|
116
|
+
* @returns Paths relative to the git root, or null if git did not answer
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* // The prune list: where the ignored territory is, without enumerating it.
|
|
121
|
+
* const prune = gitLsOthers({ cwd: root, ignored: true, directory: true });
|
|
122
|
+
* // → ['dist/', 'node_modules/', '.turbo/', 'notes.local.md']
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export function gitLsOthers(options) {
|
|
126
|
+
// `--exclude-standard` is unconditional: `--ignored` is rejected outright by
|
|
127
|
+
// git without an exclude source, and without it the not-ignored listing would
|
|
128
|
+
// report every ignored path as merely untracked — the exact inversion of what
|
|
129
|
+
// either caller asked for.
|
|
130
|
+
const args = ['ls-files', '-z', '--others', '--exclude-standard'];
|
|
131
|
+
if (options.ignored) {
|
|
132
|
+
args.push('--ignored');
|
|
133
|
+
}
|
|
134
|
+
if (options.directory) {
|
|
135
|
+
args.push('--directory');
|
|
98
136
|
}
|
|
99
|
-
|
|
100
|
-
|
|
137
|
+
// See `gitLsFiles`: NUL-delimited output must not be trimmed, or a path
|
|
138
|
+
// beginning with a space — which git sorts FIRST — comes back renamed.
|
|
139
|
+
const result = runGit(args, { cwd: options.cwd, trim: false });
|
|
140
|
+
if (!result.ok) {
|
|
101
141
|
return null;
|
|
102
142
|
}
|
|
143
|
+
return result.stdout.split('\0').filter((line) => line.length > 0);
|
|
103
144
|
}
|
|
104
145
|
/**
|
|
105
146
|
* Check if a file path is ignored by git
|
|
@@ -137,51 +178,33 @@ export function isGitIgnored(filePath, cwd = process.cwd()) {
|
|
|
137
178
|
if (gitFindRoot(cwd) === null) {
|
|
138
179
|
return false;
|
|
139
180
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const ancestorResult = spawnSync(gitPath, [...checkIgnoreArgs, current], {
|
|
164
|
-
cwd,
|
|
165
|
-
encoding: 'utf-8',
|
|
166
|
-
stdio: 'pipe',
|
|
167
|
-
shell: false,
|
|
168
|
-
});
|
|
169
|
-
if (ancestorResult.status === 0) {
|
|
170
|
-
return true;
|
|
171
|
-
}
|
|
172
|
-
// If this ancestor check also fails fatally, keep walking up
|
|
173
|
-
// If it returns 1 (not ignored), the parent is tracked — stop walking
|
|
174
|
-
if (ancestorResult.status === 1) {
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
current = dirname(current);
|
|
181
|
+
const checkIgnoreArgs = ['check-ignore', '-q'];
|
|
182
|
+
// git check-ignore returns exit code 0 if file is ignored, 1 if not
|
|
183
|
+
const result = runGit([...checkIgnoreArgs, filePath], { cwd });
|
|
184
|
+
if (result.status === 0) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
// Exit code 128 = fatal error (e.g., path beyond a symbolic link).
|
|
188
|
+
// Walk up ancestor directories to check if a parent is gitignored.
|
|
189
|
+
// Example: data/ is gitignored, data/symlink/deep/file.md fails with 128,
|
|
190
|
+
// but checking data/ directly returns 0.
|
|
191
|
+
if (result.status !== 1) {
|
|
192
|
+
const resolvedCwd = safePath.resolve(cwd);
|
|
193
|
+
const resolvedFile = safePath.resolve(cwd, filePath);
|
|
194
|
+
let current = dirname(resolvedFile);
|
|
195
|
+
while (current !== resolvedCwd && !current.endsWith(parse(current).root)) {
|
|
196
|
+
const ancestorResult = runGit([...checkIgnoreArgs, current], { cwd });
|
|
197
|
+
if (ancestorResult.status === 0) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
// If this ancestor check also fails fatally, keep walking up
|
|
201
|
+
// If it returns 1 (not ignored), the parent is tracked — stop walking
|
|
202
|
+
if (ancestorResult.status === 1) {
|
|
203
|
+
break;
|
|
178
204
|
}
|
|
205
|
+
current = dirname(current);
|
|
179
206
|
}
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
// If git is not available or other error, assume not ignored
|
|
184
|
-
return false;
|
|
185
207
|
}
|
|
208
|
+
return false;
|
|
186
209
|
}
|
|
187
210
|
//# sourceMappingURL=git-utils.js.map
|